@oh-my-pi/pi-coding-agent 17.3.3 → 17.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/{CHANGELOG-s5t1xdmy.md → CHANGELOG-tt9k4jpr.md} +81 -0
- package/dist/cli.js +3724 -3705
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/markit/converters/pdf/index.d.ts +2 -1
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/tools/read-pdf.d.ts +15 -0
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +3 -4
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -16
- package/scripts/build-binary.ts +0 -2
- package/scripts/bundle-dist.ts +0 -1
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/read-cli.ts +2 -0
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +9 -5
- package/src/extensibility/extensions/runner.ts +124 -21
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +16 -10
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/markit/NOTICE +8 -8
- package/src/markit/converters/pdf/index.ts +14 -126
- package/src/mcp/client.ts +8 -9
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/read-pdf.ts +135 -0
- package/src/tools/read.ts +63 -37
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/external-editor.ts +24 -5
- package/src/utils/markit.ts +6 -44
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
- package/dist/types/markit/converters/pdf/columns.d.ts +0 -35
- package/dist/types/markit/converters/pdf/extract.d.ts +0 -10
- package/dist/types/markit/converters/pdf/grid.d.ts +0 -25
- package/dist/types/markit/converters/pdf/headers.d.ts +0 -24
- package/dist/types/markit/converters/pdf/render.d.ts +0 -24
- package/dist/types/markit/converters/pdf/types.d.ts +0 -75
- package/dist/types/tools/read-pdf-images.d.ts +0 -12
- package/dist/types/utils/mupdf-wasm-embed.d.ts +0 -1
- package/scripts/embed-mupdf-wasm.ts +0 -67
- package/src/markit/converters/pdf/columns.ts +0 -103
- package/src/markit/converters/pdf/extract.ts +0 -598
- package/src/markit/converters/pdf/grid.ts +0 -780
- package/src/markit/converters/pdf/headers.ts +0 -106
- package/src/markit/converters/pdf/render.ts +0 -501
- package/src/markit/converters/pdf/types.ts +0 -84
- package/src/tools/read-pdf-images.ts +0 -250
- package/src/utils/mupdf-wasm-embed.ts +0 -12
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
logger,
|
|
51
51
|
postmortem,
|
|
52
52
|
prompt,
|
|
53
|
+
sanitizeText,
|
|
53
54
|
setProjectDir,
|
|
54
55
|
} from "@oh-my-pi/pi-utils";
|
|
55
56
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
@@ -390,16 +391,20 @@ export interface InteractiveModeOptions {
|
|
|
390
391
|
/**
|
|
391
392
|
* Anchored live-region container for the HUD/status rows between the transcript
|
|
392
393
|
* and the editor (working loader, todo + subagent HUDs, transient notification
|
|
393
|
-
* panels). While it has content every row is live: it reports a seam at 0
|
|
394
|
-
* engine never commits these anchored,
|
|
395
|
-
* scrollback — otherwise stale duplicates pile
|
|
396
|
-
* terminals once the loader sits below a tall HUD. The transcript's own seam,
|
|
394
|
+
* panels). While it has content every row is live: it reports a seam at 0 and
|
|
395
|
+
* pins that live region so the engine never commits these anchored,
|
|
396
|
+
* rebuilt-in-place rows to native scrollback — otherwise stale duplicates pile
|
|
397
|
+
* up above the live copy on short terminals once the loader sits below a tall HUD. The transcript's own seam,
|
|
397
398
|
* when present, sits higher and wins (topmost-seam merge in TUI.render).
|
|
398
399
|
*/
|
|
399
400
|
class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion {
|
|
400
401
|
getNativeScrollbackLiveRegionStart(): number | undefined {
|
|
401
402
|
return this.children.length > 0 ? 0 : undefined;
|
|
402
403
|
}
|
|
404
|
+
|
|
405
|
+
isNativeScrollbackLiveRegionPinned(): boolean {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
403
408
|
}
|
|
404
409
|
|
|
405
410
|
/**
|
|
@@ -814,7 +819,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
814
819
|
this.editor.onAutocompleteUpdate = () => {
|
|
815
820
|
this.ui.requestRender();
|
|
816
821
|
};
|
|
817
|
-
this.editor.setShimmerRepaintHandler(() => this.ui.
|
|
822
|
+
this.editor.setShimmerRepaintHandler(() => this.ui.requestDirectWrite(this.editor));
|
|
818
823
|
this.#syncEditorMaxHeight();
|
|
819
824
|
this.#resizeHandler = () => {
|
|
820
825
|
this.#syncEditorMaxHeight();
|
|
@@ -1108,6 +1113,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1108
1113
|
// before initHooksAndCustomTools/#reconcileModeFromSession/#enterPlanMode —
|
|
1109
1114
|
// all of which can reach setSessionName during init.
|
|
1110
1115
|
this.#eventBusUnsubscribers.push(
|
|
1116
|
+
this.sessionManager.onPersistenceError(error => {
|
|
1117
|
+
const detail = truncateToWidth(
|
|
1118
|
+
replaceTabs(sanitizeText(error.message)).replace(/[\r\n]+/g, " "),
|
|
1119
|
+
TRUNCATE_LENGTHS.LINE,
|
|
1120
|
+
);
|
|
1121
|
+
this.showWarning(
|
|
1122
|
+
`Session persistence failed: ${detail}. Unsaved entries remain in memory; persistence will retry on the next entry.`,
|
|
1123
|
+
);
|
|
1124
|
+
}),
|
|
1111
1125
|
this.sessionManager.onSessionNameChanged(() => {
|
|
1112
1126
|
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1113
1127
|
this.#handleSessionAccentInputsChanged();
|
|
@@ -1177,6 +1191,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1177
1191
|
|
|
1178
1192
|
this.#eventBusUnsubscribers.push(
|
|
1179
1193
|
this.session.subscribe(event => {
|
|
1194
|
+
if (event.type === "model_changed") {
|
|
1195
|
+
this.#updateWelcomeModel();
|
|
1196
|
+
}
|
|
1180
1197
|
void this.#handleGoalSessionEvent(event);
|
|
1181
1198
|
}),
|
|
1182
1199
|
onStatusLineSessionAccentChanged(() => {
|
|
@@ -1184,6 +1201,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1184
1201
|
this.#handleSessionAccentInputsChanged();
|
|
1185
1202
|
}),
|
|
1186
1203
|
);
|
|
1204
|
+
// Resync the welcome banner to the live model: init-time reconciliations
|
|
1205
|
+
// (#reconcileModeFromSession, #enterPlanMode for plan.defaultOnStartup)
|
|
1206
|
+
// can change the model before this subscription exists, so the
|
|
1207
|
+
// model_changed events they emit are never observed by the handler above.
|
|
1208
|
+
this.#updateWelcomeModel();
|
|
1187
1209
|
this.#eventBusUnsubscribers.push(
|
|
1188
1210
|
onModelRolesChanged(() => {
|
|
1189
1211
|
void this.#reapplyPlanModeModelOnRoleChange();
|
|
@@ -3509,10 +3531,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3509
3531
|
if (!this.vibeModeEnabled) {
|
|
3510
3532
|
return;
|
|
3511
3533
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3534
|
+
// Tear down with the queued-message drain suppressed: aborting the active
|
|
3535
|
+
// turn would otherwise let a queued user steer/follow-up restart on the
|
|
3536
|
+
// still-live Vibe tools before this teardown removes them (issue #8326).
|
|
3537
|
+
let killed = 0;
|
|
3538
|
+
await this.session.runModeExitTeardown(async () => {
|
|
3539
|
+
if (this.session.isStreaming) {
|
|
3540
|
+
await this.session.abort();
|
|
3541
|
+
}
|
|
3542
|
+
killed = await VibeSessionRegistry.global().killAll(this.#vibeParentSession(), this.#vibeModeOwnerScope);
|
|
3543
|
+
await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
|
|
3544
|
+
this.session.setVibeModeState(undefined);
|
|
3545
|
+
});
|
|
3516
3546
|
this.vibeModeEnabled = false;
|
|
3517
3547
|
this.#vibeModePreviousTools = undefined;
|
|
3518
3548
|
this.#vibeModeOwnerScope = undefined;
|
|
@@ -4252,7 +4282,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4252
4282
|
nextEditor.onAutocompleteUpdate = () => {
|
|
4253
4283
|
this.ui.requestRender();
|
|
4254
4284
|
};
|
|
4255
|
-
nextEditor.setShimmerRepaintHandler(() => this.ui.
|
|
4285
|
+
nextEditor.setShimmerRepaintHandler(() => this.ui.requestDirectWrite(nextEditor));
|
|
4256
4286
|
nextEditor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
|
|
4257
4287
|
nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
4258
4288
|
if (this.historyStorage) {
|
|
@@ -4430,6 +4460,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4430
4460
|
);
|
|
4431
4461
|
}
|
|
4432
4462
|
|
|
4463
|
+
#updateWelcomeModel(): void {
|
|
4464
|
+
if (!this.#welcomeComponent) {
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
this.#welcomeComponent.setModel(this.session.model?.name ?? "Unknown", this.session.model?.provider ?? "Unknown");
|
|
4469
|
+
this.ui.requestRender();
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4433
4472
|
#updateWelcomeLspServers(): void {
|
|
4434
4473
|
if (!this.#welcomeComponent) {
|
|
4435
4474
|
return;
|
|
@@ -353,6 +353,13 @@ export class RpcClient {
|
|
|
353
353
|
// failures are reaped by the readyPromise catch below; established
|
|
354
354
|
// workers are reaped here so pending requests cannot hang indefinitely.
|
|
355
355
|
if (!readySettled) {
|
|
356
|
+
// Stdout can close before the exit reaper finishes draining stderr.
|
|
357
|
+
// child.exited settles only after the stderr tail is complete (for
|
|
358
|
+
// nonzero exits), so give it a bounded head start: the exit watcher
|
|
359
|
+
// below was registered first and rejects with the real stderr text
|
|
360
|
+
// instead of an empty "Stderr:" (flaked under full-suite load).
|
|
361
|
+
await Promise.race([child.exited.catch(() => {}), Bun.sleep(250)]);
|
|
362
|
+
if (readySettled) return;
|
|
356
363
|
readySettled = true;
|
|
357
364
|
readyReject(new Error(`Agent output stream ended before ready. Stderr: ${child.peekStderr()}`));
|
|
358
365
|
return;
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"thinkingText": "warmGray",
|
|
34
34
|
"selectedBg": "selectedBg",
|
|
35
35
|
"userMessageBg": "userMsgBg",
|
|
36
|
-
"userMessageText": "",
|
|
36
|
+
"userMessageText": "forestFloor",
|
|
37
37
|
"customMessageBg": "customMsgBg",
|
|
38
|
-
"customMessageText": "",
|
|
38
|
+
"customMessageText": "forestFloor",
|
|
39
39
|
"customMessageLabel": "#5a7a52",
|
|
40
40
|
"toolPendingBg": "toolPendingBg",
|
|
41
41
|
"toolSuccessBg": "toolSuccessBg",
|
|
@@ -39,7 +39,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
39
39
|
"| `Tab` | Path completion / accept autocomplete |",
|
|
40
40
|
`| \`${appKey(bindings, "app.interrupt")}\` | Cancel autocomplete / interrupt active work |`,
|
|
41
41
|
`| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
|
|
42
|
-
`| \`${appKey(bindings, "app.exit")}\` | Exit (
|
|
42
|
+
`| \`${appKey(bindings, "app.exit")}\` | Exit (saves current prompt as draft) |`,
|
|
43
43
|
`| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
|
|
44
44
|
`| \`${appKey(bindings, "app.display.reset")}\` | Reset terminal display |`,
|
|
45
45
|
`| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
|
|
@@ -47,7 +47,6 @@ Additional workspace directories. This CURRENT workspace state supersedes worksp
|
|
|
47
47
|
{{/each}}
|
|
48
48
|
</workspace-roots>
|
|
49
49
|
{{/if}}
|
|
50
|
-
Today: {{date}}; current working directory: '{{cwd}}'.
|
|
51
50
|
|
|
52
51
|
<critical>
|
|
53
52
|
- Each response MUST advance the task; completion only stopping condition.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
Checkpoint
|
|
2
|
-
Context: branch summary and retained report below.
|
|
3
|
-
NEVER call `rewind` again for this checkpoint; continue from retained report.
|
|
1
|
+
Checkpoint called and rewound. Report retained below. Need explore again → new `checkpoint`.
|
|
4
2
|
|
|
5
3
|
Report:
|
|
6
4
|
{{report}}
|
|
@@ -21,6 +21,7 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
21
21
|
|
|
22
22
|
- `app.path` → NEVER tamper with a real desktop app (no stealth patches).
|
|
23
23
|
- `app.relay: true` → drive the user's own Chrome tabs via the omp browser relay (auto-started; needs the OMP Browser Relay extension installed). `app.target` picks a tab by URL/title substring; without it the visible tab is adopted without stealing focus.
|
|
24
|
+
- `close` releases the named tool session. It closes tool-owned headless pages and owned cmux surfaces, but NEVER closes pages in CDP-connected or relay browsers. Spawned-browser pages remain open unless `kill: true` terminates their process.
|
|
24
25
|
- Selectors: CSS + puppeteer `aria/…`, `text/…`, `xpath/…`, `pierce/…`. Playwright-only pseudos (`:has-text()`, `:visible`) are REJECTED.
|
|
25
26
|
</instruction>
|
|
26
27
|
|
|
@@ -1,13 +1 @@
|
|
|
1
|
-
End active checkpoint; rewind context to it, replacing intermediate exploration with your report.
|
|
2
|
-
|
|
3
|
-
Call immediately after investigative work started by `checkpoint`.
|
|
4
|
-
|
|
5
|
-
Requirements:
|
|
6
|
-
- `report` MUST be concise, factual, actionable; include key findings, decisions, unresolved risks.
|
|
7
|
-
- AVOID raw scratch logs unless essential.
|
|
8
|
-
- MUST call before yielding if checkpoint active.
|
|
9
|
-
|
|
10
|
-
Behavior:
|
|
11
|
-
- No active checkpoint → error. Checkpoint already rewound → continue from retained report; NEVER retry.
|
|
12
|
-
- Success → session rewinds, retains your report as context, closes checkpoint.
|
|
13
|
-
- Successful rewind final for that checkpoint; repeat calls error.
|
|
1
|
+
End the active checkpoint; rewind context to it, replacing intermediate exploration with your report.
|
|
@@ -174,6 +174,40 @@ export class AgentLifecycleManager {
|
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Reclaim a provably-dead parked corpse so a fresh spawn can reuse its id.
|
|
179
|
+
* Refuses live, adopted, in-flight, or cold-revivable refs. For a parked ref
|
|
180
|
+
* restored from disk, the persisted factory is consulted before removal
|
|
181
|
+
* because cold revivers are created lazily by {@link ensureLive}.
|
|
182
|
+
*
|
|
183
|
+
* Only refs in the registry this manager owns are touched; the transcript
|
|
184
|
+
* stays readable at `history://<id>`. Returns true when the corpse was
|
|
185
|
+
* unregistered.
|
|
186
|
+
*/
|
|
187
|
+
async reclaimDeadCorpse(id: string, expected: AgentRef): Promise<boolean> {
|
|
188
|
+
const ref = this.#registry.get(id);
|
|
189
|
+
if (ref !== expected || ref.status !== "parked" || ref.session) return false;
|
|
190
|
+
if (this.#adopted.has(id) || this.#parks.has(id) || this.#revivals.has(id)) return false;
|
|
191
|
+
|
|
192
|
+
const persistedFactory = ref.sessionFile ? this.#persistedReviverFactory : undefined;
|
|
193
|
+
if (persistedFactory) {
|
|
194
|
+
try {
|
|
195
|
+
if (await persistedFactory(ref)) return false;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.warn("AgentLifecycleManager.reclaimDeadCorpse: persisted reviver probe failed", {
|
|
198
|
+
id,
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
});
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
// The factory awaited I/O; another lifecycle operation may now own or
|
|
204
|
+
// have replaced this ref. Revalidate every reclaim invariant.
|
|
205
|
+
if (this.#registry.get(id) !== ref || ref.status !== "parked" || ref.session) return false;
|
|
206
|
+
if (this.#adopted.has(id) || this.#parks.has(id) || this.#revivals.has(id)) return false;
|
|
207
|
+
}
|
|
208
|
+
return this.#registry.unregister(id, ref);
|
|
209
|
+
}
|
|
210
|
+
|
|
177
211
|
/**
|
|
178
212
|
* True when this manager owns `registry` — i.e. its adopt/park/revive state
|
|
179
213
|
* describes that registry's refs. Lets a caller holding a specific registry
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* revival) and are only removed on explicit release/teardown.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
12
13
|
import type { AgentSession } from "../session/agent-session";
|
|
13
14
|
import { oneLineLabel } from "../task/types";
|
|
14
15
|
|
|
@@ -135,6 +136,11 @@ export class AgentRegistry {
|
|
|
135
136
|
return expected === undefined || ref === expected || ref.session === expected;
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
#rejectStatusUpdate(id: string, status: AgentStatus, reason: string): false {
|
|
140
|
+
logger.debug("Agent registry status update rejected", { id, status, reason });
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
138
144
|
register(input: RegisterInput): AgentRef {
|
|
139
145
|
const now = Date.now();
|
|
140
146
|
const ref: AgentRef = {
|
|
@@ -181,10 +187,15 @@ export class AgentRegistry {
|
|
|
181
187
|
|
|
182
188
|
setStatus(id: string, status: AgentStatus, expected?: AgentRefExpectation): boolean {
|
|
183
189
|
const ref = this.#refs.get(id);
|
|
184
|
-
if (!ref
|
|
190
|
+
if (!ref) return this.#rejectStatusUpdate(id, status, "missing-ref");
|
|
191
|
+
if (!this.#matchesExpected(ref, expected)) {
|
|
192
|
+
return this.#rejectStatusUpdate(id, status, "session-ownership-changed");
|
|
193
|
+
}
|
|
185
194
|
// `aborted` is terminal: delayed progress/revival work from the killed
|
|
186
195
|
// generation must never transition the tombstone back to a live status.
|
|
187
|
-
if (ref.status === "aborted")
|
|
196
|
+
if (ref.status === "aborted") {
|
|
197
|
+
return status === "aborted" || this.#rejectStatusUpdate(id, status, "aborted-is-terminal");
|
|
198
|
+
}
|
|
188
199
|
if (ref.status === status) return true;
|
|
189
200
|
ref.status = status;
|
|
190
201
|
// Activity describes current work; it is meaningless once the agent
|
|
@@ -270,6 +281,20 @@ export class AgentRegistry {
|
|
|
270
281
|
);
|
|
271
282
|
}
|
|
272
283
|
|
|
284
|
+
/** Whether a ref's claimed running state is corroborated by its attached live session. */
|
|
285
|
+
isRunning(ref: AgentRef): boolean {
|
|
286
|
+
if (ref.status !== "running") return false;
|
|
287
|
+
return ref.session?.isStreaming === true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Mirror a session's authoritative run-state notifications into its owned registry ref. */
|
|
291
|
+
syncSessionStatus(id: string, session: AgentSession): () => void {
|
|
292
|
+
const unsubscribe = session.subscribeRunState(status => {
|
|
293
|
+
this.setStatus(id, status, session);
|
|
294
|
+
});
|
|
295
|
+
return unsubscribe;
|
|
296
|
+
}
|
|
297
|
+
|
|
273
298
|
onChange(listener: RegistryListener): () => void {
|
|
274
299
|
this.#listeners.add(listener);
|
|
275
300
|
return () => this.#listeners.delete(listener);
|
|
@@ -25,6 +25,8 @@ interface PersistedAgentMetadata {
|
|
|
25
25
|
createdAt?: number;
|
|
26
26
|
lastActivity?: number;
|
|
27
27
|
history?: AgentHistorySummary;
|
|
28
|
+
/** True when the file is only a SessionManager header (no session_init, no messages). */
|
|
29
|
+
incomplete?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface PersistedTranscript {
|
|
@@ -244,6 +246,8 @@ async function readPersistedAgentMetadata(sessionFile: string): Promise<Persiste
|
|
|
244
246
|
let createdAt: number | undefined;
|
|
245
247
|
let activity: string | undefined;
|
|
246
248
|
let history: AgentHistorySummary = {};
|
|
249
|
+
let hasSessionInit = false;
|
|
250
|
+
let hasConversation = false;
|
|
247
251
|
try {
|
|
248
252
|
await visitEntriesFromFileStream(
|
|
249
253
|
sessionFile,
|
|
@@ -264,7 +268,12 @@ async function readPersistedAgentMetadata(sessionFile: string): Promise<Persiste
|
|
|
264
268
|
}
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
271
|
+
if (record.type === "message" || record.type === "custom_message") {
|
|
272
|
+
hasConversation = true;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
267
275
|
if (record.type !== "session_init") return;
|
|
276
|
+
hasSessionInit = true;
|
|
268
277
|
createdAt ??= timestampOf(record.timestamp);
|
|
269
278
|
if (typeof record.task === "string") activity = summarizePersistedTask(record.task);
|
|
270
279
|
const inferred = typeof record.systemPrompt === "string" ? inferBundledAgent(record.systemPrompt) : {};
|
|
@@ -290,6 +299,7 @@ async function readPersistedAgentMetadata(sessionFile: string): Promise<Persiste
|
|
|
290
299
|
activity,
|
|
291
300
|
createdAt: createdAt ?? file?.birthtimeMs,
|
|
292
301
|
lastActivity: file?.mtimeMs,
|
|
302
|
+
incomplete: !hasSessionInit && !hasConversation,
|
|
293
303
|
history: {
|
|
294
304
|
...history,
|
|
295
305
|
...(hasOutput ? { outputPath } : {}),
|
|
@@ -425,26 +435,36 @@ async function registerPersistedSubagentsFromDir(
|
|
|
425
435
|
if (!registry.get(id)) {
|
|
426
436
|
const metadata = await readPersistedAgentMetadata(sessionFile);
|
|
427
437
|
if (!shouldContinue()) return;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
438
|
+
// Metadata reads yield. A spawn may claim the id while this scan is
|
|
439
|
+
// inspecting the file; never replace that live generation with a
|
|
440
|
+
// transcript-derived parked ref.
|
|
441
|
+
const unclaimed = !registry.get(id);
|
|
442
|
+
// SessionManager.open writes title+session before createAgentSession
|
|
443
|
+
// claims the id. Parking that stub makes the spawn's expectedAgentRef:null
|
|
444
|
+
// CAS fail with "already owned by another session generation".
|
|
445
|
+
if (unclaimed && metadata.incomplete && !tombstoned) continue;
|
|
446
|
+
if (unclaimed) {
|
|
447
|
+
registry.register({
|
|
448
|
+
id,
|
|
449
|
+
displayName: id,
|
|
450
|
+
kind: "sub",
|
|
451
|
+
parentId: parentId ?? MAIN_AGENT_ID,
|
|
452
|
+
session: null,
|
|
453
|
+
sessionFile,
|
|
454
|
+
activity: metadata.activity,
|
|
455
|
+
createdAt: metadata.createdAt,
|
|
456
|
+
lastActivity: metadata.lastActivity,
|
|
457
|
+
history: metadata.history,
|
|
458
|
+
status: tombstoned ? "aborted" : "parked",
|
|
459
|
+
});
|
|
460
|
+
const ref = registry.get(id);
|
|
461
|
+
transcripts.push({
|
|
462
|
+
id,
|
|
463
|
+
sessionFile,
|
|
464
|
+
createdAt: ref?.createdAt,
|
|
465
|
+
lastActivity: ref?.lastActivity,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
448
468
|
}
|
|
449
469
|
await registerPersistedSubagentsFromDir(
|
|
450
470
|
registry,
|
package/src/sdk.ts
CHANGED
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
|
|
137
137
|
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
|
138
138
|
import type { AuthStorage } from "./session/auth-storage";
|
|
139
|
+
import { withDateCwdReminder } from "./session/date-cwd-reminder";
|
|
139
140
|
import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
|
|
140
141
|
import {
|
|
141
142
|
type CustomMessage,
|
|
@@ -222,6 +223,8 @@ import { ttsTool } from "./tools/tts";
|
|
|
222
223
|
import { resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
223
224
|
import { EventBus } from "./utils/event-bus";
|
|
224
225
|
import { normalizeProviderContextImagesForModel } from "./utils/image-loading";
|
|
226
|
+
import { formatLocalCalendarDate } from "./utils/local-date";
|
|
227
|
+
import { normalizePromptPath } from "./utils/prompt-path";
|
|
225
228
|
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
226
229
|
import { VibeSessionRegistry } from "./vibe/runtime";
|
|
227
230
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
@@ -3034,6 +3037,20 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3034
3037
|
options.expectedAgentRef === undefined
|
|
3035
3038
|
? agentRegistry.register(registrationInput)
|
|
3036
3039
|
: agentRegistry.registerIfAvailable(registrationInput, options.expectedAgentRef);
|
|
3040
|
+
if (!registeredAgentRef && options.expectedAgentRef === null) {
|
|
3041
|
+
// A fresh spawn collided with an existing id. If that id is held by a
|
|
3042
|
+
// provably-dead parked corpse — no live session, no reviver — reclaim it
|
|
3043
|
+
// so this new generation can take the id instead of failing forever at
|
|
3044
|
+
// construction. Without this, one such corpse (isolated-run park,
|
|
3045
|
+
// interrupted construction) poisons the id for the whole process (#8490).
|
|
3046
|
+
// The reclaim is gated by the lifecycle owner and only touches the
|
|
3047
|
+
// registry it manages; the corpse's transcript stays at history://.
|
|
3048
|
+
const stale = agentRegistry.get(resolvedAgentId);
|
|
3049
|
+
const lifecycle = AgentLifecycleManager.global();
|
|
3050
|
+
if (stale && lifecycle.manages(agentRegistry) && (await lifecycle.reclaimDeadCorpse(resolvedAgentId, stale))) {
|
|
3051
|
+
registeredAgentRef = agentRegistry.registerIfAvailable(registrationInput, null);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3037
3054
|
if (!registeredAgentRef) {
|
|
3038
3055
|
throw new Error(`Agent "${resolvedAgentId}" is already owned by another session generation.`);
|
|
3039
3056
|
}
|
|
@@ -3130,7 +3147,15 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3130
3147
|
let transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
|
|
3131
3148
|
if (snapcompactInline) transformed = await snapcompactInline.transform(transformed, transformModel);
|
|
3132
3149
|
transformed = clampProviderContextImages(transformed, transformModel);
|
|
3133
|
-
|
|
3150
|
+
transformed = await normalizeProviderContextImagesForModel(transformed, transformModel);
|
|
3151
|
+
// Keep per-request volatility out of the system prompt: the date/cwd
|
|
3152
|
+
// reminder rides on the first user turn so open-weight providers keep
|
|
3153
|
+
// their tool-schema prefix cache (#7404).
|
|
3154
|
+
return withDateCwdReminder(
|
|
3155
|
+
transformed,
|
|
3156
|
+
formatLocalCalendarDate(),
|
|
3157
|
+
normalizePromptPath(sessionManager.getCwd()),
|
|
3158
|
+
);
|
|
3134
3159
|
};
|
|
3135
3160
|
const onPayload = async (payload: unknown, model?: Model) => {
|
|
3136
3161
|
return await extensionRunner.emitBeforeProviderRequest(payload, model);
|
|
@@ -3771,7 +3796,12 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3771
3796
|
transformProviderContext: async (context, transformModel) => {
|
|
3772
3797
|
let transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
|
|
3773
3798
|
transformed = clampProviderContextImages(transformed, transformModel);
|
|
3774
|
-
|
|
3799
|
+
transformed = await normalizeProviderContextImagesForModel(transformed, transformModel);
|
|
3800
|
+
return withDateCwdReminder(
|
|
3801
|
+
transformed,
|
|
3802
|
+
formatLocalCalendarDate(),
|
|
3803
|
+
normalizePromptPath(sessionManager.getCwd()),
|
|
3804
|
+
);
|
|
3775
3805
|
},
|
|
3776
3806
|
thinkingBudgets: agent.thinkingBudgets,
|
|
3777
3807
|
temperature: agent.temperature,
|
|
@@ -211,8 +211,6 @@ export interface AgentSessionConfig {
|
|
|
211
211
|
toolNames: string[],
|
|
212
212
|
tools: Map<string, AgentTool>,
|
|
213
213
|
) => Promise<{ systemPrompt: string[]; xdevCatalogNames?: readonly string[] }>;
|
|
214
|
-
/** Local calendar date provider used by prompt-cache invalidation. */
|
|
215
|
-
getLocalCalendarDate?: () => string;
|
|
216
214
|
/** Tools mounted under `xd://`, for `/tools` display. */
|
|
217
215
|
getXdevToolEntries?: () => Array<{ name: string; summary: string }>;
|
|
218
216
|
/** `xd://` presentation state backed by the canonical tool map. */
|