Skip to content

Commit f0affc9

Browse files
committed
merge fix/reconcile-rows-2026-09-10 into fix/reconcile-rows-b-2026-09-10
Keep the stacked PR current with the base branch tip.
2 parents c12e7b3 + 2c125ad commit f0affc9

11 files changed

Lines changed: 154 additions & 16 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": minor
3+
---
4+
5+
Add the `PYTHINKER_CODE_PERMISSION_MODE_REMINDER` environment variable: set it to a false value or an empty value to stop injecting the auto permission-mode reminders into the model context.

apps/pythinker-code/src/cli/v2/run-v2-print.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,14 @@ export async function runV2Print(
207207
try {
208208
await restorePermission();
209209
} finally {
210-
if (telemetryService !== undefined) {
211-
await raceWithTimeout(telemetryService.shutdown(), CLI_SHUTDOWN_TIMEOUT_MS);
210+
try {
211+
if (telemetryService !== undefined) {
212+
await raceWithTimeout(telemetryService.shutdown(), CLI_SHUTDOWN_TIMEOUT_MS);
213+
}
214+
} finally {
215+
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }).catch(() => {});
216+
app.dispose();
212217
}
213-
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }).catch(() => {});
214-
app.dispose();
215218
}
216219
})());
217220
await raceWithTimeout(pending, PROMPT_CLEANUP_TIMEOUT_MS);

apps/pythinker-code/test/tui/commands/experiments.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function makeHost() {
4444
getExperimentalFeatures: vi.fn(async () => [
4545
feature({ enabled: false, source: 'config', configValue: false }),
4646
]),
47-
reloadSession: vi.fn(async () => session),
47+
reloadSession: vi.fn(async () => ({ ...session, id: 'ses-experiments-reloaded' })),
4848
},
4949
session,
5050
refreshSlashCommandAutocomplete: vi.fn(),
@@ -95,7 +95,7 @@ describe('experimental feature command handlers', () => {
9595
expect(host.harness.reloadSession).toHaveBeenCalledWith({ id: host.session.id });
9696
expect(host.session.reloadSession).not.toHaveBeenCalled();
9797
expect(host.reloadCurrentSessionView).toHaveBeenCalledWith(
98-
host.session,
98+
expect.objectContaining({ id: 'ses-experiments-reloaded' }),
9999
'Experimental features updated. Session reloaded.',
100100
);
101101
expect(host.mountEditorReplacement).not.toHaveBeenCalled();

apps/pythinker-code/test/tui/commands/reload.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ auto_install = false
8484
});
8585
expect(session.reloadSession).not.toHaveBeenCalled();
8686
expect(host.reloadCurrentSessionView).toHaveBeenCalledWith(
87-
session,
87+
{ ...session, id: 'ses-1-reloaded' },
8888
'Session reloaded.',
8989
);
9090
expect(host.harness.getConfig).toHaveBeenCalledWith({ reload: true });
@@ -207,7 +207,7 @@ function makeHost({
207207
state,
208208
session,
209209
harness: {
210-
reloadSession: vi.fn(async () => session),
210+
reloadSession: vi.fn(async () => ({ ...session, id: 'ses-1-reloaded' })),
211211
getConfig: vi.fn(async () => ({
212212
models: {
213213
fresh: { provider: 'test', model: 'fresh-model', maxContextSize: 1000 },

apps/pythinker-code/test/tui/pythinker-tui-message-flow.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ function makeHarness(session = makeSession(), overrides: Record<string, unknown>
243243
createSession: vi.fn(async () => session),
244244
resumeSession: vi.fn(async () => session),
245245
forkSession: vi.fn(async () => session),
246-
reloadSession: vi.fn(async () => session),
246+
reloadSession: vi.fn(async () => ({ ...session, id: 'ses-1-reloaded' })),
247247
listSessions: vi.fn(async () => []),
248248
exportSession: vi.fn(async () => ({
249249
zipPath: '/tmp/fake-session.zip',
@@ -2072,6 +2072,11 @@ command = "vim"
20722072
expect(driver.state.appState.theme).toBe('light');
20732073
});
20742074
expect(session.reloadSession).not.toHaveBeenCalled();
2075+
await vi.waitFor(() => {
2076+
expect((driver as unknown as { session?: { id: string } }).session?.id).toBe(
2077+
'ses-1-reloaded',
2078+
);
2079+
});
20752080
expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'reload' });
20762081
const transcript = stripSgr(renderTranscript(driver));
20772082
expect(transcript).toContain('hello before reload');

docs/configuration/env-vars.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
141141
| `PYTHINKER_CODE_BUILTIN_PRODUCT_SKILLS` | Whether the built-in skills documenting Pythinker Code itself are offered to the model; takes higher priority than `builtin_product_skills` in `config.toml` (default enabled) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
142142
| `PYTHINKER_CODE_TUI_FULL_SCREEN` | Control the fullscreen TUI with a fixed prompt dock, scrollable transcript, mouse text selection, clickable links, transcript search, and a clickable jump-to-bottom control. Fullscreen is enabled by default | `0` restores the legacy inline UI; unset or any other value keeps fullscreen enabled |
143143
| `PYTHINKER_CODE_DANGEROUS_COMMAND_GUARD` | Override [`[permission].dangerous_command_guard`](./config-files.md#permission). The guard asks before dangerous or unanalyzable `Bash` commands in interactive modes and blocks them in Auto mode | `true` or `false`; default `true` |
144+
| `PYTHINKER_CODE_PERMISSION_MODE_REMINDER` | Stop injecting the automatic permission-mode reminder (the context note that explains Auto mode) into the model context; set to a false value or an empty value to disable | Truthy keeps the reminder enabled; falsy: `0`/`false`/`no`/`off` disables it; an empty value disables it |
144145
| `PYTHINKER_CODE_EXPERIMENTAL_SUBAGENT_FORK` | Enable the experimental `fork` parameter on the `Agent` and `AgentDynamicWorkflow` tools, letting the model start a subagent with a snapshot of the calling agent's conversation history instead of an empty context; the master `PYTHINKER_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
145146
| `PYTHINKER_CODE_EXPERIMENTAL_TOWER` | Enable the experimental [`/tower`](../reference/slash-commands.md#modes--run-control) command for workspace-wide subagent coordination; the master `PYTHINKER_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
146147
| `PYTHINKER_CODE_SEARCH_WORKER` | Run the global search index in a dedicated worker thread; takes higher priority than `[database] search` in `config.toml` (default `true`) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |

packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { PermissionMode } from '#/agent/permissionPolicy/types';
22
import { Service } from '#/_base/di/service';
3+
import { parseBooleanEnv } from '#/_base/utils/env';
4+
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
35
import { LifecycleScope } from '#/app/scopes';
46
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
57
import { Emitter, type Event } from '#/_base/event';
@@ -20,6 +22,8 @@ import {
2022
PermissionSetMode,
2123
} from './permissionModeOps';
2224

25+
export const PERMISSION_MODE_REMINDER_ENV = 'PYTHINKER_CODE_PERMISSION_MODE_REMINDER';
26+
2327
export class AgentPermissionModeService extends Service implements IAgentPermissionModeService {
2428
declare readonly _serviceBrand: undefined;
2529

@@ -32,15 +36,19 @@ export class AgentPermissionModeService extends Service implements IAgentPermiss
3236
@IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService,
3337
@ITelemetryService private readonly telemetry: ITelemetryService,
3438
@IAgentStateService private readonly agentState: IAgentStateService,
39+
@IBootstrapService bootstrap: IBootstrapService,
3540
) {
3641
super();
3742
this.agentState.contributeState(permissionModeKey);
3843
this.agentState.contributeState(permissionModeConfiguredKey);
39-
this._register(
40-
activateReminderWhenReady(this.agentLifecycle, this.scopeContext, (reminder) =>
41-
new PermissionModeInjection(this, reminder, this.agentState),
42-
),
43-
);
44+
const reminderEnv = bootstrap.getEnv(PERMISSION_MODE_REMINDER_ENV);
45+
if (reminderEnv?.trim() !== '' && parseBooleanEnv(reminderEnv) !== false) {
46+
this._register(
47+
activateReminderWhenReady(this.agentLifecycle, this.scopeContext, (reminder) =>
48+
new PermissionModeInjection(this, reminder, this.agentState),
49+
),
50+
);
51+
}
4452
}
4553

4654
get mode(): PermissionMode {

packages/agent-core-v2/test/agent/permissionMode/permissionMode.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import type { ReminderRuntime } from '#/features/reminder/reminderAgentRuntime';
77
import type { ContextInjectionProvider } from '#/features/reminder/types';
88
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
99
import { lifecycleWithReminder } from '../../features/reminder/stubs';
10+
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
11+
import { stubBootstrap } from '../../app/bootstrap/stubs';
1012
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
1113
import { PermissionModeInjection } from '#/agent/permissionMode/injection/permissionModeInjection';
12-
import { AgentPermissionModeService } from '#/agent/permissionMode/permissionModeService';
14+
import {
15+
AgentPermissionModeService,
16+
PERMISSION_MODE_REMINDER_ENV,
17+
} from '#/agent/permissionMode/permissionModeService';
1318
import { permissionModeKey } from '#/agent/permissionMode/permissionModeOps';
1419
import type { PermissionMode } from '#/agent/permissionPolicy/types';
1520
import { IAgentStateService } from '#/agent/state/agentState';
@@ -57,15 +62,18 @@ let log: IAppendLogStore;
5762
let dispatcher: IEventDispatcher;
5863
let svc: IAgentPermissionModeService;
5964
let reminderLive = false;
65+
let bootstrapEnv: NodeJS.ProcessEnv;
6066

6167
beforeEach(() => {
6268
registeredInjection = undefined;
6369
reminderLive = false;
70+
bootstrapEnv = {};
6471
disposables = new DisposableStore();
6572
ix = disposables.add(new TestInstantiationService());
6673
ix.stub(IFileSystemStorageService, new InMemoryStorageService());
6774
ix.set(IAppendLogStore, new SyncDescriptor(AppendLogStore));
6875
ix.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub));
76+
ix.stub(IBootstrapService, stubBootstrap('/tmp/pythinker-home', bootstrapEnv));
6977
ix.set(IAgentStateService, new AgentStateService());
7078
ix.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService));
7179
log = ix.get(IAppendLogStore);
@@ -243,4 +251,74 @@ describe('AgentPermissionModeService (wire-backed)', () => {
243251
expect(written[0]).toMatchObject({ type: 'metadata' });
244252
expect(written.slice(1)).toEqual([{ type: 'permission.set_mode', mode: 'auto' }]);
245253
});
254+
it('skips the auto-mode reminder injection when the reminder env is disabled', () => {
255+
registeredInjection = undefined;
256+
const ix2 = disposables.add(new TestInstantiationService());
257+
ix2.stub(IFileSystemStorageService, new InMemoryStorageService());
258+
ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore));
259+
ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub));
260+
ix2.stub(
261+
IBootstrapService,
262+
stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '0' }),
263+
);
264+
ix2.set(IAgentStateService, new AgentStateService());
265+
ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService));
266+
registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-no-reminder'), {
267+
log: ix2.get(IAppendLogStore),
268+
});
269+
registerTestEventDispatcher(ix2);
270+
271+
const svc2 = ix2.get(IAgentPermissionModeService);
272+
273+
expect(registeredInjection).toBeUndefined();
274+
svc2.setMode('auto');
275+
expect(svc2.mode).toBe('auto');
276+
expect(registeredInjection).toBeUndefined();
277+
});
278+
it('skips the auto-mode reminder injection when the reminder env is set to an empty value', () => {
279+
registeredInjection = undefined;
280+
const ix2 = disposables.add(new TestInstantiationService());
281+
ix2.stub(IFileSystemStorageService, new InMemoryStorageService());
282+
ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore));
283+
ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub));
284+
ix2.stub(
285+
IBootstrapService,
286+
stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '' }),
287+
);
288+
ix2.set(IAgentStateService, new AgentStateService());
289+
ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService));
290+
registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-empty-reminder'), {
291+
log: ix2.get(IAppendLogStore),
292+
});
293+
registerTestEventDispatcher(ix2);
294+
295+
const svc2 = ix2.get(IAgentPermissionModeService);
296+
297+
expect(registeredInjection).toBeUndefined();
298+
svc2.setMode('auto');
299+
expect(svc2.mode).toBe('auto');
300+
expect(registeredInjection).toBeUndefined();
301+
});
302+
303+
it('keeps the auto-mode reminder injection when the env override enables it explicitly', () => {
304+
registeredInjection = undefined;
305+
const ix2 = disposables.add(new TestInstantiationService());
306+
ix2.stub(IFileSystemStorageService, new InMemoryStorageService());
307+
ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore));
308+
ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub));
309+
ix2.stub(
310+
IBootstrapService,
311+
stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '1' }),
312+
);
313+
ix2.set(IAgentStateService, new AgentStateService());
314+
ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService));
315+
registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-reminder-on'), {
316+
log: ix2.get(IAppendLogStore),
317+
});
318+
registerTestEventDispatcher(ix2);
319+
320+
ix2.get(IAgentPermissionModeService);
321+
322+
expect((registeredInjection as { readonly name: string } | undefined)?.name).toBe('permission_mode');
323+
});
246324
});

packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ describe('AgentLifecycleService', () => {
265265
_serviceBrand: undefined,
266266
homeDir: '/tmp/pythinker-agentLifecycle-home',
267267
cwd: '/tmp/pythinker-agentLifecycle-home',
268+
getEnv: () => undefined,
268269
} as unknown as IBootstrapService);
269270
ix.stub(ISessionWorkspaceContext, {
270271
_serviceBrand: undefined,

packages/agent-gateway/src/lib/promptMedia.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export async function resolvePromptMediaFiles(
224224
content.push({
225225
type: 'text',
226226
text: persisted === null
227-
? buildUnsupportedImageNotice(effectiveMime)
227+
? buildUnsupportedImageNotice(effectiveMime, name)
228228
: buildAttachedFileNotice(name, effectiveMime, bytes.length, persisted),
229229
});
230230
if (persisted !== null) {

0 commit comments

Comments
 (0)