From 42fccf73306f1b8d0d0651ddbb9753d26b29feb6 Mon Sep 17 00:00:00 2001 From: elkaix Date: Thu, 10 Sep 2026 14:22:30 -0400 Subject: [PATCH 01/12] fix(desktop): fall back to the default macOS update manifest on an empty channel argument The release workflow passes the channel manifest positionally, so an unresolved channel output reaches the script as an empty string rather than a missing argument. The nullish coalescing default did not catch it, and joining an empty name onto the distribution directory made the notarization step read the directory itself and fail with EISDIR. --- .../desktop/scripts/finalize-mac-artifacts.ts | 8 +++++- .../tests/finalize-mac-artifacts.spec.ts | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/desktop/scripts/finalize-mac-artifacts.ts b/apps/desktop/scripts/finalize-mac-artifacts.ts index 8acdccf6..a742d018 100644 --- a/apps/desktop/scripts/finalize-mac-artifacts.ts +++ b/apps/desktop/scripts/finalize-mac-artifacts.ts @@ -153,7 +153,13 @@ export function finalizeMacArtifacts(options: FinalizeMacArtifactsOptions): void .sort() if (dmgs.length === 0) throw new Error(`No DMG artifacts found in ${options.distDir}`) - const manifestName = options.manifestName ?? DEFAULT_MAC_MANIFEST + // The workflow always passes the channel manifest positionally, so an unresolved + // channel output arrives as an empty string rather than a missing argument; `??` + // would keep it and read the distribution directory itself. + const manifestName = + options.manifestName === undefined || options.manifestName === '' + ? DEFAULT_MAC_MANIFEST + : options.manifestName const metadataPath = join(options.distDir, manifestName) let metadata = readFileSync(metadataPath, 'utf8') const credentialArgs = buildNotarytoolArguments(options.env) diff --git a/apps/desktop/tests/finalize-mac-artifacts.spec.ts b/apps/desktop/tests/finalize-mac-artifacts.spec.ts index cb24ebba..0015990c 100644 --- a/apps/desktop/tests/finalize-mac-artifacts.spec.ts +++ b/apps/desktop/tests/finalize-mac-artifacts.spec.ts @@ -158,6 +158,32 @@ sha512: old expect(readFileSync(join(distDir, 'nightly-mac.yml'), 'utf8')).toContain(`sha512: ${checksum}`) }) + it('falls back to the default manifest when the channel argument arrives empty', () => { + const distDir = mkdtempSync(join(tmpdir(), 'pythinker-mac-artifacts-')) + directories.push(distDir) + const filename = 'Pythinker-0.1.3-arm64.dmg' + const dmg = Buffer.from('empty argument dmg fixture') + writeFileSync(join(distDir, filename), dmg) + writeFileSync(join(distDir, 'latest-mac.yml'), `files: + - url: ${filename} + sha512: old + size: 1 +path: ${filename} +sha512: old +`) + + finalizeMacArtifacts({ + distDir, + env: { APPLE_KEYCHAIN_PROFILE: 'pythinker-notary' }, + log: () => {}, + manifestName: '', + runCommand: () => ({ status: 0, stderr: '', stdout: '{"status":"Accepted"}' }), + }) + + const checksum = createHash('sha512').update(dmg).digest('base64') + expect(readFileSync(join(distDir, 'latest-mac.yml'), 'utf8')).toContain(`sha512: ${checksum}`) + }) + it('prints and rejects a non-accepted notarytool result before stapling', () => { const distDir = mkdtempSync(join(tmpdir(), 'pythinker-mac-artifacts-')) directories.push(distDir) From 6933ef998ae3f1194489af770a7f01208d54329f Mon Sep 17 00:00:00 2001 From: elkaix Date: Thu, 10 Sep 2026 14:22:30 -0400 Subject: [PATCH 02/12] refactor(agent-core-v2)!: remove the ${now} system prompt template variable The variable baked a single timestamp into the rendered system prompt, which then went stale for the rest of the session. The current date already reaches the agent through its own context reminder, so the placeholder now renders verbatim like any other unknown variable. --- .changeset/remove-now-template-variable.md | 5 +++++ docs/customization/agents.md | 1 - .../src/app/agentProfileCatalog/agentProfileCatalog.ts | 1 - .../src/app/agentProfileCatalog/profile-shared.ts | 1 - .../test/app/agentProfileCatalog/profile-shared.test.ts | 6 +++--- 5 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 .changeset/remove-now-template-variable.md diff --git a/.changeset/remove-now-template-variable.md b/.changeset/remove-now-template-variable.md new file mode 100644 index 00000000..377cda9c --- /dev/null +++ b/.changeset/remove-now-template-variable.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": major +--- + +Remove the `${now}` variable from custom system prompt templates. Delete `${now}` from your `SYSTEM.md` and agent files — the agent still receives the current date. diff --git a/docs/customization/agents.md b/docs/customization/agents.md index 33d27dd9..2e29d351 100644 --- a/docs/customization/agents.md +++ b/docs/customization/agents.md @@ -153,7 +153,6 @@ Like the body of a regular agent file, SYSTEM.md is rendered as a template each | `${cwd_listing}` | Listing of the working directory | | `${os}` | Operating system kind | | `${shell}` | Shell name and path, for example `bash (\`/bin/bash\`)` | -| `${now}` | Current time in ISO format | | `${additional_dirs_info}` | Additional directories added to the workspace; empty when there are none | | `${base_prompt}` | The default system prompt. Inside `SYSTEM.md` itself this is the built-in default; inside an agent file it is the effective default — the built-in default, or your `SYSTEM.md` override when present | | `${plugin_sections}` | A complete Plugin Instructions block contributed by enabled plugins; empty when no enabled plugin contributes instructions | diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts b/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts index 2f4ab1e3..6ff7be2b 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts @@ -23,7 +23,6 @@ export interface AgentProfileContext { readonly osKind?: string; readonly shellName?: string; readonly shellPath?: string; - readonly now?: string; readonly skills?: string; readonly skillActive?: boolean; readonly pluginSections?: string; diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/profile-shared.ts b/packages/agent-core-v2/src/app/agentProfileCatalog/profile-shared.ts index c6ae69a5..3efe25fe 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/profile-shared.ts +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/profile-shared.ts @@ -138,7 +138,6 @@ export function systemPromptVars( os: context.osKind ?? '', windows_notes: context.osKind === 'Windows' ? `\n\n${WINDOWS_NOTES}\n\n` : '', shell: shellName.length > 0 ? `${shellName} (\`${shellPath}\`)` : '', - now: context.now ?? new Date().toISOString(), cwd: context.cwd ?? '', cwd_listing: context.cwdListing ?? '', agents_md: context.agentsMd ?? '', diff --git a/packages/agent-core-v2/test/app/agentProfileCatalog/profile-shared.test.ts b/packages/agent-core-v2/test/app/agentProfileCatalog/profile-shared.test.ts index 9ae20b61..e5654df4 100644 --- a/packages/agent-core-v2/test/app/agentProfileCatalog/profile-shared.test.ts +++ b/packages/agent-core-v2/test/app/agentProfileCatalog/profile-shared.test.ts @@ -159,14 +159,14 @@ describe('renderPromptTemplateResult', () => { ); }); - it('renders ${now} in custom prompt templates', () => { + it('keeps ${now} verbatim as an unknown placeholder', () => { const result = renderPromptTemplateResult( 'date=${now} agents=${agents_md}', - { cwd: '/work', now: '2026-07-29T00:30:00.000Z', agentsMd: 'AGENTS' }, + { cwd: '/work', agentsMd: 'AGENTS' }, { skillActive: true }, ); - expect(result.text).toBe('date=2026-07-29T00:30:00.000Z agents=AGENTS'); + expect(result.text).toBe('date=${now} agents=AGENTS'); expect(result.environment).toEqual({ cwd: '/work' }); }); From 9beed0450d591fe88f17ced97012e1ec743c8c9a Mon Sep 17 00:00:00 2001 From: elkaix Date: Thu, 10 Sep 2026 14:47:32 -0400 Subject: [PATCH 03/12] feat!: graduate remote control, storage and the subagent model pool out of experimental Remote Control no longer needs an opt-in: the rc subcommand, the --remote-control option and the slash command are always available. The subagent model pool is likewise unconditional, so [secondary_model] takes effect with no environment variable. The two storage kill switches move from experimental flags to a [database] config section with base and search keys, both defaulting to true. Their environment variables are renamed accordingly, and the search backend is now chosen once the configuration is ready rather than at construction time. --- .../graduate-database-and-remote-control.md | 5 ++ .changeset/graduate-subagent-model-pool.md | 5 ++ apps/pythinker-code/src/cli/sub/web/index.ts | 5 +- .../src/cli/sub/web/remote-control.ts | 12 --- apps/pythinker-code/src/cli/sub/web/run.ts | 17 +--- apps/pythinker-code/src/main.ts | 2 +- .../src/native/search-worker.ts | 4 +- .../src/tui/commands/registry.ts | 3 +- apps/pythinker-code/test/cli/options.test.ts | 8 +- .../test/cli/web/remote-control.test.ts | 16 ---- apps/pythinker-code/test/cli/web/web.test.ts | 49 ++--------- .../test/tui/commands/registry.test.ts | 4 +- .../test/tui/commands/resolve.test.ts | 4 +- docs/configuration/config-files.md | 13 ++- docs/configuration/env-vars.md | 3 +- docs/guides/remote-control.md | 4 - docs/reference/slash-commands.md | 2 +- .../agent-core-v2/docs/config-manifest.toml | 17 +++- .../src/agent/tools/agent/agentTool.ts | 3 +- .../src/app/config/configService.ts | 2 +- .../src/app/remoteControl/flag.ts | 16 ---- .../sessionIndex/sessionIndexMirrorService.ts | 9 +- .../app/sessionIndex/sessionIndexService.ts | 8 +- .../agentDynamicWorkflowTool.ts | 4 +- .../skill/catalog/builtin/update-config.md | 2 +- packages/agent-core-v2/src/index.ts | 4 +- .../src/persistence/backends/minidb/flag.ts | 13 --- .../src/persistence/configSection.ts | 42 +++++++++ .../src/session/subagent/configSection.ts | 24 ++---- .../src/session/subagent/flag.ts | 15 ---- .../subagent/subagentModelPolicyService.ts | 9 +- .../subagentModelsValidationService.ts | 4 +- .../test/app/config/config.test.ts | 66 +++++--------- .../agent-core-v2/test/app/config/stubs.ts | 8 ++ .../app/sessionIndex/sessionIndex.test.ts | 29 ++++--- .../sessionIndex/sessionIndexMirror.test.ts | 13 +-- .../dynamic_workflow/dynamic_workflow.test.ts | 2 +- .../features/tower/tools/spawnTool.test.ts | 4 - packages/agent-core-v2/test/harness/agent.ts | 17 ++-- .../test/session/subagent/routing.test.ts | 14 +-- .../test/session/subagent/spawn.test.ts | 18 +--- .../subagentModelPolicyService.test.ts | 47 +--------- .../subagent/subagentModelsValidation.test.ts | 9 +- packages/agent-core-v2/test/setup.ts | 2 +- packages/agent-core-v2/test/tool/tool.test.ts | 60 ++++--------- .../agent-gateway/src/search/searchService.ts | 86 ++++++++++--------- .../test/search/searchRoute.test.ts | 2 +- .../test/search/searchService.test.ts | 30 ++++--- packages/agent-gateway/test/sessions.test.ts | 2 +- packages/agent-gateway/test/setup.ts | 4 +- .../test/subagentModelPolicy.test.ts | 22 ++--- packages/node-sdk/test/list-sessions.test.ts | 10 +-- 52 files changed, 297 insertions(+), 476 deletions(-) create mode 100644 .changeset/graduate-database-and-remote-control.md create mode 100644 .changeset/graduate-subagent-model-pool.md delete mode 100644 packages/agent-core-v2/src/app/remoteControl/flag.ts delete mode 100644 packages/agent-core-v2/src/persistence/backends/minidb/flag.ts create mode 100644 packages/agent-core-v2/src/persistence/configSection.ts diff --git a/.changeset/graduate-database-and-remote-control.md b/.changeset/graduate-database-and-remote-control.md new file mode 100644 index 00000000..74dc7cea --- /dev/null +++ b/.changeset/graduate-database-and-remote-control.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": major +--- + +Remote Control is always available — `pythinker rc`, `pythinker web --remote-control` and `/remote-control` no longer need an experimental flag. Session indexing and global search move to the new `[database]` section: set `PYTHINKER_CODE_PERSISTENCE_MINIDB_READMODEL` (was `PYTHINKER_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL`) and `PYTHINKER_CODE_SEARCH_WORKER` (was `PYTHINKER_CODE_EXPERIMENTAL_SEARCH_WORKER`), or `[database] base` and `[database] search` in `config.toml`. diff --git a/.changeset/graduate-subagent-model-pool.md b/.changeset/graduate-subagent-model-pool.md new file mode 100644 index 00000000..d95e5cd5 --- /dev/null +++ b/.changeset/graduate-subagent-model-pool.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": major +--- + +The subagent model pool is always available. Remove `PYTHINKER_CODE_EXPERIMENTAL_SECONDARY_MODEL` from your environment — it no longer does anything, and `[secondary_model]` takes effect with no opt-in. diff --git a/apps/pythinker-code/src/cli/sub/web/index.ts b/apps/pythinker-code/src/cli/sub/web/index.ts index 4eec0fd0..b364f84d 100644 --- a/apps/pythinker-code/src/cli/sub/web/index.ts +++ b/apps/pythinker-code/src/cli/sub/web/index.ts @@ -15,7 +15,6 @@ import type { Command } from 'commander'; import { registerDeprecatedServerCommand } from './deprecated-server'; import { registerRotateTokenCommand } from './rotate-token'; import { buildWebCommand } from './run'; -import { isRemoteControlEnabled } from './remote-control'; export function registerWebCommand(program: Command): void { const web = buildWebCommand( @@ -26,10 +25,10 @@ export function registerWebCommand(program: Command): void { registerRotateTokenCommand(web); buildWebCommand( program - .command('rc', { hidden: !isRemoteControlEnabled() }) + .command('rc') .alias('remote') .description( - 'Run the local Pythinker server and open the web UI through Remote Control (experimental).', + 'Run the local Pythinker server and open the web UI through Remote Control.', ), { forceRemoteControl: true }, ); diff --git a/apps/pythinker-code/src/cli/sub/web/remote-control.ts b/apps/pythinker-code/src/cli/sub/web/remote-control.ts index e4e5a178..2ccfab71 100644 --- a/apps/pythinker-code/src/cli/sub/web/remote-control.ts +++ b/apps/pythinker-code/src/cli/sub/web/remote-control.ts @@ -13,8 +13,6 @@ import { acquireRemoteControlLock } from './remote-control-lock'; export const REMOTE_CONTROL_RELAY_ORIGIN = 'https://code-rc.pythinker.com'; -export const REMOTE_CONTROL_FLAG_ENV = 'PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL'; - export const REMOTE_CONTROL_RELAY_ENV = 'PYTHINKER_CODE_REMOTE_CONTROL_RELAY'; export const REMOTE_CONTROL_RELAY_KEY_ENV = 'PYTHINKER_CODE_REMOTE_CONTROL_RELAY_KEY'; @@ -55,16 +53,6 @@ export function resolveRelayKey( return candidate; } -const TRUTHY_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']); - -export function isRemoteControlEnabled( - env: Readonly> = process.env, -): boolean { - const truthy = (key: string): boolean => - TRUTHY_ENV_VALUES.has((env[key] ?? '').trim().toLowerCase()); - return truthy('PYTHINKER_CODE_EXPERIMENTAL_FLAG') || truthy(REMOTE_CONTROL_FLAG_ENV); -} - const MAX_HTTP_HEADER_BYTES = 64 * 1024; const MAX_HTTP_REQUEST_BYTES = 10 * 1024 * 1024; const HTTP_REQUEST_TIMEOUT_MS = 30_000; diff --git a/apps/pythinker-code/src/cli/sub/web/run.ts b/apps/pythinker-code/src/cli/sub/web/run.ts index 4abff9fe..41441877 100644 --- a/apps/pythinker-code/src/cli/sub/web/run.ts +++ b/apps/pythinker-code/src/cli/sub/web/run.ts @@ -44,8 +44,6 @@ import { formatHostForUrl, type NetworkAddress } from './networks'; import { formatRemoteControlOutput, formatRemoteControlStatus, - isRemoteControlEnabled, - REMOTE_CONTROL_FLAG_ENV, resolveRelayKey, resolveRelayOrigin, startRemoteControl, @@ -181,23 +179,21 @@ export function buildWebCommand( withServerOptions.addOption( new Option( '--rc, --remote-control', - 'Expose the web UI through Pythinker Remote Control (experimental).', - ) - .default(false) - .hideHelp(!isRemoteControlEnabled()), + 'Expose the web UI through Pythinker Remote Control.', + ).default(false), ); } withServerOptions.addOption( new Option( '--relay-key ', 'Secret the Remote Control relay requires. Defaults to $PYTHINKER_CODE_REMOTE_CONTROL_RELAY_KEY.', - ).hideHelp(!isRemoteControlEnabled()), + ), ); withServerOptions.addOption( new Option( '--relay-origin ', 'Remote Control relay to tunnel through. Defaults to $PYTHINKER_CODE_REMOTE_CONTROL_RELAY.', - ).hideHelp(!isRemoteControlEnabled()), + ), ); return withServerOptions .option('--no-open', 'Do not open the web UI in the default browser.', true) @@ -218,11 +214,6 @@ export async function handleWebCommand( deps: WebCommandDeps = DEFAULT_WEB_COMMAND_DEPS, ): Promise { const parsed = parseServerOptions(opts); - if (opts.remoteControl === true && !isRemoteControlEnabled()) { - throw new Error( - `--remote-control is experimental: set ${REMOTE_CONTROL_FLAG_ENV}=1 (or PYTHINKER_CODE_EXPERIMENTAL_FLAG=1) to enable it.`, - ); - } if (opts.remoteControl === true && parsed.dangerousBypassAuth) { throw new Error('--remote-control cannot be combined with --dangerous-bypass-auth.'); } diff --git a/apps/pythinker-code/src/main.ts b/apps/pythinker-code/src/main.ts index 8c089d8a..db08c458 100644 --- a/apps/pythinker-code/src/main.ts +++ b/apps/pythinker-code/src/main.ts @@ -165,7 +165,7 @@ function bootstrap(): void { ); // Same pattern for the global-search worker: extracted from the SEA blob so // the search index runs off the main thread; a failure leaves the search - // surface degraded (the `search_worker` flag restores the inline host). + // surface degraded ([database] search = false restores the inline host). const searchWorkerInstall = installKapSearchWorker(); startupTrace( searchWorkerInstall.status === 'installed' diff --git a/apps/pythinker-code/src/native/search-worker.ts b/apps/pythinker-code/src/native/search-worker.ts index c94b84a9..a1c25c3a 100644 --- a/apps/pythinker-code/src/native/search-worker.ts +++ b/apps/pythinker-code/src/native/search-worker.ts @@ -36,8 +36,8 @@ function errorCode(error: unknown): string { /** * Install the SEA-bundled global-search worker without making optional * extraction fatal. Without it the search service resolves no worker entry - * inside the single-file binary and reports the index as degraded; the - * `search_worker` experimental flag restores the in-process host. + * inside the single-file binary and reports the index as degraded; + * `[database] search = false` restores the in-process host. */ export function installKapSearchWorker( options: NativeAssetOptions = {}, diff --git a/apps/pythinker-code/src/tui/commands/registry.ts b/apps/pythinker-code/src/tui/commands/registry.ts index 4031c361..a9176e10 100644 --- a/apps/pythinker-code/src/tui/commands/registry.ts +++ b/apps/pythinker-code/src/tui/commands/registry.ts @@ -448,10 +448,9 @@ export const BUILTIN_SLASH_COMMANDS = [ { name: 'remote-control', aliases: ['rc'], - description: 'Open the current session through Pythinker Remote Control (experimental)', + description: 'Open the current session through Pythinker Remote Control', priority: 40, availability: 'always', - experimentalFlag: 'remote-control', }, { name: 'exit', diff --git a/apps/pythinker-code/test/cli/options.test.ts b/apps/pythinker-code/test/cli/options.test.ts index 77b1ec29..533343d0 100644 --- a/apps/pythinker-code/test/cli/options.test.ts +++ b/apps/pythinker-code/test/cli/options.test.ts @@ -5,7 +5,7 @@ * Run: pnpm -C apps/pythinker-code exec vitest run test/cli/options.test.ts */ -import { describe, expect, it, onTestFinished, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createProgram } from '#/cli/commands'; import type { CLIOptions } from '#/cli/options'; @@ -574,11 +574,6 @@ describe('CLI options parsing', () => { }); it('registers the visible sub-commands', () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '0'); - onTestFinished(() => { - vi.unstubAllEnvs(); - }); const program = createProgram( '0.0.0', () => {}, @@ -597,6 +592,7 @@ describe('CLI options parsing', () => { 'session', 'acp', 'web', + 'rc', 'server', 'doctor', 'vis', diff --git a/apps/pythinker-code/test/cli/web/remote-control.test.ts b/apps/pythinker-code/test/cli/web/remote-control.test.ts index 69f06c65..cae51c1b 100644 --- a/apps/pythinker-code/test/cli/web/remote-control.test.ts +++ b/apps/pythinker-code/test/cli/web/remote-control.test.ts @@ -13,7 +13,6 @@ import { filterForwardRequestHeaders, formatRemoteControlOutput, formatRemoteControlStatus, - isRemoteControlEnabled, parseRawHttpRequest, rewriteRemoteControlResponse, startRemoteControl, @@ -30,21 +29,6 @@ afterEach(async () => { while (cleanups.length > 0) await cleanups.pop()!(); }); -describe('Remote Control experimental flag', () => { - it('is off unless the per-feature env or the master switch is truthy', () => { - expect(isRemoteControlEnabled({})).toBe(false); - expect(isRemoteControlEnabled({ PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL: '0' })).toBe(false); - expect(isRemoteControlEnabled({ PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL: '1' })).toBe(true); - expect(isRemoteControlEnabled({ PYTHINKER_CODE_EXPERIMENTAL_FLAG: 'true' })).toBe(true); - expect( - isRemoteControlEnabled({ - PYTHINKER_CODE_EXPERIMENTAL_FLAG: '0', - PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL: 'yes', - }), - ).toBe(true); - }); -}); - describe('Remote Control URLs', () => { it('builds the public device entry without a local token', () => { const url = buildRemoteControlUrl('device/one'); diff --git a/apps/pythinker-code/test/cli/web/web.test.ts b/apps/pythinker-code/test/cli/web/web.test.ts index 2de97f96..92c3899a 100644 --- a/apps/pythinker-code/test/cli/web/web.test.ts +++ b/apps/pythinker-code/test/cli/web/web.test.ts @@ -465,7 +465,6 @@ describe('`pythinker web` opens the browser', () => { }); it('passes the resolved relay origin to the tunnel', async () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '1'); const { handleWebCommand } = await import('#/cli/sub/web/run'); const { runner } = makeRunner(); const { stdout, stderr } = makeIo(); @@ -507,7 +506,6 @@ describe('`pythinker web` opens the browser', () => { }); it('refuses to start Remote Control without a relay key', async () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '1'); vi.stubEnv('PYTHINKER_CODE_REMOTE_CONTROL_RELAY_KEY', ''); const { handleWebCommand } = await import('#/cli/sub/web/run'); const { runner } = makeRunner(); @@ -531,7 +529,6 @@ describe('`pythinker web` opens the browser', () => { }); it('rejects Remote Control on a non-loopback host', async () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '1'); const { handleWebCommand } = await import('#/cli/sub/web/run'); const { runner } = makeRunner(); const { stdout, stderr } = makeIo(); @@ -544,32 +541,11 @@ describe('`pythinker web` opens the browser', () => { ).rejects.toThrow('--remote-control requires a loopback host.'); }); - it('rejects --remote-control while the experimental flag is off', async () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '0'); - const { handleWebCommand } = await import('#/cli/sub/web/run'); - const { runner } = makeRunner(); - const { stdout, stderr } = makeIo(); - - await expect( - handleWebCommand( - { remoteControl: true, open: false }, - { startServerForeground: runner, openUrl: vi.fn(), stdout, stderr }, - ), - ).rejects.toThrow('--remote-control is experimental:'); - }); - - it('hides --remote-control from help unless the experimental flag is on', () => { - const remoteControlOption = () => - makeProgram() - .commands.find((command) => command.name() === 'web')! - .options.find((option) => option.long === '--remote-control'); - - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '0'); - expect(remoteControlOption()?.hidden).toBe(true); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '1'); - expect(remoteControlOption()?.hidden).toBe(false); + it('shows --remote-control in help', () => { + const remoteControlOption = makeProgram() + .commands.find((command) => command.name() === 'web')! + .options.find((option) => option.long === '--remote-control'); + expect(remoteControlOption?.hidden).toBeFalsy(); }); }); @@ -1240,17 +1216,11 @@ describe('pythinker rc', () => { expect(longs).not.toContain('--remote-control'); }); - it('hides `rc` from help unless the experimental flag is on', () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '0'); - expect(makeProgram().helpInformation()).not.toContain('rc|remote'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '1'); + it('shows `rc` in help', () => { expect(makeProgram().helpInformation()).toContain('rc|remote'); }); it('forces Remote Control for both `rc` and `remote`', async () => { - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('PYTHINKER_CODE_EXPERIMENTAL_REMOTE_CONTROL', '0'); for (const name of ['rc', 'remote']) { const program = makeProgram(); let stderr = ''; @@ -1262,14 +1232,13 @@ describe('pythinker rc', () => { .spyOn(process, 'exit') .mockImplementation(() => undefined as never); try { - await program.parseAsync(['node', 'pythinker', name]); + await program.parseAsync(['node', 'pythinker', name, '--host', '0.0.0.0']); } finally { errSpy.mockRestore(); exitSpy.mockRestore(); } - // The flag-off experimental error proves remoteControl was forced before - // the runner could start. - expect(stderr).toContain('--remote-control is experimental:'); + // The loopback check only runs when remoteControl was forced on. + expect(stderr).toContain('--remote-control requires a loopback host.'); } }); }); diff --git a/apps/pythinker-code/test/tui/commands/registry.test.ts b/apps/pythinker-code/test/tui/commands/registry.test.ts index 00902a59..a3fa17ef 100644 --- a/apps/pythinker-code/test/tui/commands/registry.test.ts +++ b/apps/pythinker-code/test/tui/commands/registry.test.ts @@ -233,10 +233,10 @@ describe('built-in slash command registry', () => { expect(resolveSlashCommandAvailability(command!, 'Ship feature X')).toBe('always'); }); - it('gates remote-control behind the remote-control experiment, always available', () => { + it('exposes remote-control ungated and always available', () => { const command = findBuiltInSlashCommand('remote-control'); expect(command).toBeDefined(); - expect((command as PythinkerSlashCommand).experimentalFlag).toBe('remote-control'); + expect((command as PythinkerSlashCommand).experimentalFlag).toBeUndefined(); expect(resolveSlashCommandAvailability(command!, '')).toBe('always'); }); }); diff --git a/apps/pythinker-code/test/tui/commands/resolve.test.ts b/apps/pythinker-code/test/tui/commands/resolve.test.ts index 21b446a0..81f337f4 100644 --- a/apps/pythinker-code/test/tui/commands/resolve.test.ts +++ b/apps/pythinker-code/test/tui/commands/resolve.test.ts @@ -65,9 +65,7 @@ describe('resolveSlashCommandInput', () => { }); - it('gates /remote-control behind the remote-control experimental flag', () => { - expect(resolve('/rc')).toEqual({ kind: 'message', input: '/rc' }); - setExperimentalFeatures([{ id: 'remote-control', enabled: true }]); + it('resolves /remote-control without any experimental flag', () => { expect(resolve('/rc')).toMatchObject({ kind: 'builtin', name: 'remote-control' }); expect(resolve('/remote-control')).toMatchObject({ kind: 'builtin', name: 'remote-control' }); }); diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md index e75dd5a9..bca96831 100644 --- a/docs/configuration/config-files.md +++ b/docs/configuration/config-files.md @@ -216,7 +216,7 @@ Subagents inherit the model the main agent is running by default. The `[secondar ### Subagent model pool -Secondary-model routing is enabled by default in every launch mode, including the interactive TUI. Set `PYTHINKER_CODE_EXPERIMENTAL_SECONDARY_MODEL=false` to disable it. While routing is disabled, the pool keys stay inert: subagents inherit the caller's model and session startup skips the pool validation. +The pool is always available and needs no opt-in; with no `[secondary_model]` keys configured, subagents simply inherit the caller's model. The minimal configuration is one line — a lone `default_model` is a pool with a single entry: @@ -465,6 +465,17 @@ Like the `tools` / `disallowedTools` fields of an agent file, this section shape `max_edge_px` can be overridden by the `PYTHINKER_IMAGE_MAX_EDGE_PX` environment variable and `read_byte_budget` by `PYTHINKER_IMAGE_READ_BYTE_BUDGET`; both take higher priority than `config.toml`. +## `database` + +`database` controls the embedded storage engines behind session indexing and global search. Both keys default to `true` and act as kill switches that fall back to the legacy behavior when set to `false`. + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `base` | `boolean` | `true` | Use the minidb-backed read model for session indexing; `false` falls back to reading session metadata directly | +| `search` | `boolean` | `true` | Run the global search index in a dedicated worker thread; `false` runs it in the server process | + +`base` can be overridden by the `PYTHINKER_CODE_PERSISTENCE_MINIDB_READMODEL` environment variable and `search` by `PYTHINKER_CODE_SEARCH_WORKER`; both take higher priority than `config.toml`. +