@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.0
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 +54 -0
- package/README.md +1 -1
- package/dist/cli.js +4659 -4261
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +316 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +118 -755
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +5 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/usage-cli.ts +10 -1
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +209 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +157 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +210 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1892 -12226
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
|
@@ -114,16 +114,19 @@ export class UiHelpers {
|
|
|
114
114
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
115
115
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
116
116
|
const useDim = options?.dim ?? true;
|
|
117
|
-
|
|
117
|
+
// Resolve the dim color lazily so a later theme change re-shapes the line
|
|
118
|
+
// instead of leaving the palette that was active when it was presented.
|
|
119
|
+
const styleFn = useDim ? (t: string) => theme.fg("dim", t) : undefined;
|
|
118
120
|
|
|
119
121
|
if (last && secondLast && last === this.ctx.lastStatusText && secondLast === this.ctx.lastStatusSpacer) {
|
|
120
|
-
this.ctx.lastStatusText.
|
|
122
|
+
this.ctx.lastStatusText.setStyleFn(styleFn);
|
|
123
|
+
this.ctx.lastStatusText.setText(message);
|
|
121
124
|
this.ctx.ui.requestRender();
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
const spacer = new Spacer(1);
|
|
126
|
-
const text = new Text(
|
|
129
|
+
const text = new Text(message, 1, 0).setStyleFn(styleFn);
|
|
127
130
|
this.ctx.present([spacer, text]);
|
|
128
131
|
this.ctx.lastStatusSpacer = spacer;
|
|
129
132
|
this.ctx.lastStatusText = text;
|
|
@@ -714,11 +717,13 @@ export class UiHelpers {
|
|
|
714
717
|
}
|
|
715
718
|
|
|
716
719
|
showError(errorMessage: string): void {
|
|
717
|
-
|
|
720
|
+
const text = new Text(`Error: ${errorMessage}`, 1, 0).setStyleFn(t => theme.fg("error", t));
|
|
721
|
+
this.ctx.present([new Spacer(1), text]);
|
|
718
722
|
}
|
|
719
723
|
|
|
720
724
|
showWarning(warningMessage: string): void {
|
|
721
|
-
|
|
725
|
+
const text = new Text(`Warning: ${warningMessage}`, 1, 0).setStyleFn(t => theme.fg("warning", t));
|
|
726
|
+
this.ctx.present([new Spacer(1), text]);
|
|
722
727
|
}
|
|
723
728
|
|
|
724
729
|
showNewVersionNotification(newVersion: string): void {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
5
|
+
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
6
|
+
|
|
7
|
+
/** Reads a plan from a local URL or cwd-relative filesystem path. */
|
|
8
|
+
export async function readPlanFile(
|
|
9
|
+
planFilePath: string,
|
|
10
|
+
options: { localProtocolOptions: LocalProtocolOptions; cwd: string },
|
|
11
|
+
): Promise<string | null> {
|
|
12
|
+
const resolvedPath = planFilePath.startsWith("local:")
|
|
13
|
+
? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), options.localProtocolOptions)
|
|
14
|
+
: resolveToCwd(planFilePath, options.cwd);
|
|
15
|
+
try {
|
|
16
|
+
return await Bun.file(resolvedPath).text();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (isEnoent(error)) return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Lists session-local plan files from newest to oldest. */
|
|
24
|
+
export async function listPlanFiles(options: { localProtocolOptions: LocalProtocolOptions }): Promise<string[]> {
|
|
25
|
+
const localRoot = resolveLocalUrlToPath("local://", options.localProtocolOptions);
|
|
26
|
+
try {
|
|
27
|
+
const entries = await fs.promises.readdir(localRoot, { withFileTypes: true });
|
|
28
|
+
const plans = await Promise.all(
|
|
29
|
+
entries
|
|
30
|
+
.filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
|
|
31
|
+
.map(async entry => {
|
|
32
|
+
const stat = await fs.promises.stat(path.join(localRoot, entry.name)).catch(() => null);
|
|
33
|
+
return { url: `local://${entry.name}`, mtime: stat?.mtimeMs ?? 0 };
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Prompt-cache benchmark stable prefix.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Cache benchmark suffix {{variant}}.
|
|
@@ -59,6 +59,6 @@ Rules are local constraints. You MUST read `rule://<name>` when working in that
|
|
|
59
59
|
{{/if}}
|
|
60
60
|
{{#if secretsEnabled}}
|
|
61
61
|
<redacted-content>
|
|
62
|
-
Some values in tool output are redacted for security. They appear as `#
|
|
62
|
+
Some values in tool output are redacted for security. They appear as placeholder tokens such as `#HASH#`, `#HASH:CASE#`, or `#NAME_HASH:CASE#` (uppercase-alphanumeric digest, optional case hint, optional friendly-name prefix). These are **not errors** — they are intentional placeholders for sensitive values (API keys, passwords, tokens). Treat them as opaque strings. NEVER attempt to decode, fix, or report them as problems.
|
|
63
63
|
</redacted-content>
|
|
64
64
|
{{/if}}
|
|
@@ -40,7 +40,14 @@ Working directory layout (sorted by mtime, recent first; depth ≤ 3):
|
|
|
40
40
|
</workspace-tree>
|
|
41
41
|
{{/if}}
|
|
42
42
|
{{/if}}
|
|
43
|
-
|
|
43
|
+
{{#if additionalWorkspaceRoots.length}}
|
|
44
|
+
<workspace-roots>
|
|
45
|
+
This session also spans the additional directories below. This list is the CURRENT workspace state and supersedes any workspace change mentioned earlier in the conversation. Use absolute paths under these roots to `read`/`grep`/`glob`/`edit` them. Manage the set with `/add-dir` and `/remove-dir`; `/dirs` lists them.
|
|
46
|
+
{{#each additionalWorkspaceRoots}}
|
|
47
|
+
- {{this}}
|
|
48
|
+
{{/each}}
|
|
49
|
+
</workspace-roots>
|
|
50
|
+
{{/if}}
|
|
44
51
|
Today is {{date}}, and the current working directory is '{{cwd}}'.
|
|
45
52
|
|
|
46
53
|
<critical>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Your yield was recorded, but {{count}} background job{{#if multiple}}s{{/if}} you own {{#if multiple}}are{{else}}is{{/if}} still running: {{jobs}}.
|
|
2
|
+
|
|
3
|
+
This run completes only after these jobs settle AND you submit a fresh `yield` that accounts for their results. Job results arrive as follow-up messages; a result that arrives after your yield supersedes it — your current yield will NOT be accepted as the final report. Decide now:
|
|
4
|
+
- Need the results? Wait for them (`hub` op:"wait"), then submit a fresh `yield` that incorporates them.
|
|
5
|
+
- Job no longer needed? Cancel it (`hub` op:"cancel", ids:[…]) and re-yield.
|
|
6
|
+
- Otherwise stand by; when each result arrives, submit a fresh `yield` (repeat your report unchanged if the result does not affect it).
|
|
@@ -101,7 +101,7 @@ Use tools whenever they improve correctness, completeness, or grounding.
|
|
|
101
101
|
# Tool I/O
|
|
102
102
|
- Prefer relative paths for `path`-like fields.
|
|
103
103
|
{{#if intentTracing}}- Most tools take `{{intentField}}`: a concise intent, present participle, 2–6 words, no period, capitalized.{{/if}}
|
|
104
|
-
{{#if secretsEnabled}}- Redacted `#
|
|
104
|
+
{{#if secretsEnabled}}- Redacted `#HASH#`, `#HASH:CASE#`, or `#NAME_HASH:CASE#` tokens in output are opaque strings.{{/if}}
|
|
105
105
|
{{#has tools "inspect_image"}}- Image tasks: prefer `{{toolRefs.inspect_image}}` over `{{toolRefs.read}}` to spare session context.{{/has}}
|
|
106
106
|
|
|
107
107
|
# Specialized Tools
|
|
@@ -28,56 +28,64 @@ For independent per-item chains (review → verify, fetch → extract → score)
|
|
|
28
28
|
|
|
29
29
|
**Python (`eval`, Python backend):**
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
```python
|
|
32
|
+
DIMENSIONS = [{"key": "bugs", "prompt": "…"}, {"key": "perf", "prompt": "…"}]
|
|
33
|
+
def review_and_verify(d):
|
|
34
|
+
found = agent(d["prompt"], label=f"review:{d['key']}", schema=FINDINGS_SCHEMA)
|
|
35
|
+
return parallel([lambda f=f: {**f, "verdict": agent(
|
|
36
|
+
f"Refute if you can (default refuted when unsure): {f['title']}",
|
|
37
|
+
label=f"verify:{f['file']}", schema=VERDICT_SCHEMA)} for f in found["findings"]])
|
|
38
|
+
phase("Review")
|
|
39
|
+
results = parallel([lambda d=d: review_and_verify(d) for d in DIMENSIONS])
|
|
40
|
+
confirmed = [f for group in results for f in group if f["verdict"]["is_real"]]
|
|
41
|
+
```
|
|
40
42
|
|
|
41
43
|
**JavaScript (`eval`, JavaScript backend):**
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
```js
|
|
46
|
+
const DIMENSIONS = [{ key: "bugs", prompt: "…" }, { key: "perf", prompt: "…" }];
|
|
47
|
+
async function reviewAndVerify(d) {
|
|
48
|
+
const found = await agent(d.prompt, {
|
|
49
|
+
label: `review:${d.key}`,
|
|
50
|
+
schema: FINDINGS_SCHEMA,
|
|
51
|
+
});
|
|
52
|
+
return await parallel(found.findings.map((f) => async () => ({
|
|
53
|
+
...f,
|
|
54
|
+
verdict: await agent(
|
|
55
|
+
`Refute if you can (default refuted when unsure): ${f.title}`,
|
|
56
|
+
{ label: `verify:${f.file}`, schema: VERDICT_SCHEMA },
|
|
57
|
+
),
|
|
58
|
+
})));
|
|
59
|
+
}
|
|
60
|
+
phase("Review");
|
|
61
|
+
const results = await parallel(DIMENSIONS.map((d) => async () => reviewAndVerify(d)));
|
|
62
|
+
const confirmed = results.flat().filter((f) => f.verdict.is_real);
|
|
63
|
+
```
|
|
60
64
|
Reach for `pipeline()` only when a stage genuinely needs ALL of the previous stage first — dedup/merge across the whole set, early-exit on zero, or "compare against the other findings" — because its inter-stage barrier makes every item wait for the slowest peer:
|
|
61
65
|
|
|
62
66
|
**Python (`eval`, Python backend):**
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
```python
|
|
69
|
+
phase("Find")
|
|
70
|
+
found = parallel([lambda d=d: agent(d["prompt"], schema=FINDINGS_SCHEMA) for d in DIMENSIONS])
|
|
71
|
+
findings = dedupe([f for r in found for f in r["findings"]]) # needs everything at once
|
|
72
|
+
phase("Verify")
|
|
73
|
+
verdicts = parallel([lambda f=f: agent(verify_prompt(f), schema=VERDICT_SCHEMA) for f in findings])
|
|
74
|
+
```
|
|
69
75
|
|
|
70
76
|
**JavaScript (`eval`, JavaScript backend):**
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
))
|
|
78
|
+
```js
|
|
79
|
+
phase("Find");
|
|
80
|
+
const found = await parallel(DIMENSIONS.map((d) => async () =>
|
|
81
|
+
await agent(d.prompt, { schema: FINDINGS_SCHEMA }),
|
|
82
|
+
));
|
|
83
|
+
const findings = dedupe(found.flatMap((r) => r.findings)); // needs everything at once
|
|
84
|
+
phase("Verify");
|
|
85
|
+
const verdicts = await parallel(findings.map((f) => async () =>
|
|
86
|
+
await agent(verifyPrompt(f), { schema: VERDICT_SCHEMA }),
|
|
87
|
+
));
|
|
88
|
+
```
|
|
81
89
|
Use ordinary code between calls to flatten/map/filter; don't add a barrier just for that. Nested `parallel()` pools each cap independently, so keep total fan-out sane.
|
|
82
90
|
</structure>
|
|
83
91
|
|
|
@@ -12,6 +12,8 @@ Completing tasks out of phase order can move this pointer **back** to an earlier
|
|
|
12
12
|
|`start`|`task`|Mark in progress|
|
|
13
13
|
|`done`|`task` or `phase`|Mark completed|
|
|
14
14
|
|`drop`|`task` or `phase`|Mark abandoned|
|
|
15
|
+
|`block`|`task` or `phase`, optional `reason`|Mark **blocked** — open but waiting on external input; excluded from the stop-time incomplete-todo reminder|
|
|
16
|
+
|`unblock`|`task` or `phase`|Return a blocked task to `pending`|
|
|
15
17
|
|`rm`|`task` or `phase` (optional)|Remove task or phase; omit both to clear|
|
|
16
18
|
|`append`|`phase`, `items: string[]`|Append tasks to `phase`; lazily creates phase|
|
|
17
19
|
|`view`|—|Read-only: echo list|
|
|
@@ -23,7 +25,7 @@ Completing tasks out of phase order can move this pointer **back** to an earlier
|
|
|
23
25
|
## Rules
|
|
24
26
|
- Mark tasks done immediately after finishing. Complete phases in order.
|
|
25
27
|
- NEVER make a todo call your turn's only tool call — batch it with the real work: `init` with the first reads/edits, each `done`/`start` with the next action. Solo todo turns waste a round trip.
|
|
26
|
-
-
|
|
28
|
+
- Waiting on something you can't act on (a user decision, another agent, an external service)? `block` the task (optional `reason`) — it stays in the tracker but won't trip the stop reminder; `unblock` when it's actionable again. If the blocker is itself agent-actionable, `append` an unblocking task instead.
|
|
27
29
|
- Keep `task`/`phase` strings stable once introduced.
|
|
28
30
|
- Lost the exact task text? `view` echoes the list — NEVER guess from memory.
|
|
29
31
|
|
|
@@ -14,13 +14,23 @@
|
|
|
14
14
|
* - ensureLive during an in-flight park either cancels the park (session still
|
|
15
15
|
* live) or waits for detach+park and then revives.
|
|
16
16
|
* - Concurrent ensureLive/park operations coalesce per id.
|
|
17
|
+
*
|
|
18
|
+
* Every adoption, park, and revival is bound to the exact {@link AgentRef} it
|
|
19
|
+
* started from, so stale async work (a late finalizer, a cancelled initializer,
|
|
20
|
+
* a superseded revive) can never clobber a newer same-id ref.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
20
24
|
import type { AgentSession } from "../session/agent-session";
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type AgentRef,
|
|
27
|
+
type AgentRefExpectation,
|
|
28
|
+
AgentRegistry,
|
|
29
|
+
MAIN_AGENT_ID,
|
|
30
|
+
type RegistryEvent,
|
|
31
|
+
} from "./agent-registry";
|
|
22
32
|
|
|
23
|
-
export type AgentReviver = () => Promise<AgentSession>;
|
|
33
|
+
export type AgentReviver = (expected: AgentRef) => Promise<AgentSession>;
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
36
|
* Builds a reviver for a `parked` ref restored from disk (Agent Hub scan,
|
|
@@ -39,12 +49,15 @@ export interface AdoptOptions {
|
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
interface AdoptedAgent {
|
|
52
|
+
ref: AgentRef;
|
|
42
53
|
idleTtlMs: number;
|
|
43
54
|
revive?: AgentReviver;
|
|
44
55
|
timer?: NodeJS.Timeout;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
interface ParkInFlight {
|
|
59
|
+
/** The exact ref this park was started for. */
|
|
60
|
+
ref: AgentRef;
|
|
48
61
|
/** Resolves when the park attempt finishes (success, cancel, or dispose error). */
|
|
49
62
|
promise: Promise<void>;
|
|
50
63
|
/** Cancel before the session is detached. Returns true if cancel took effect. */
|
|
@@ -55,6 +68,11 @@ interface ParkInFlight {
|
|
|
55
68
|
detached: boolean;
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
interface RevivingAgent {
|
|
72
|
+
ref: AgentRef;
|
|
73
|
+
promise: Promise<AgentSession>;
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
export class AgentLifecycleManager {
|
|
59
77
|
static #global: AgentLifecycleManager | undefined;
|
|
60
78
|
|
|
@@ -85,12 +103,13 @@ export class AgentLifecycleManager {
|
|
|
85
103
|
readonly #registry: AgentRegistry;
|
|
86
104
|
readonly #adopted = new Map<string, AdoptedAgent>();
|
|
87
105
|
/**
|
|
88
|
-
* In-flight park attempts
|
|
89
|
-
* detached; after detach, ensureLive
|
|
106
|
+
* In-flight park attempts, each bound to the ref it started from. A park is
|
|
107
|
+
* cancelable until the live session is detached; after detach, ensureLive
|
|
108
|
+
* waits for the park and revives.
|
|
90
109
|
*/
|
|
91
110
|
readonly #parks = new Map<string, ParkInFlight>();
|
|
92
|
-
/** In-flight revives, so concurrent {@link ensureLive} calls coalesce. */
|
|
93
|
-
readonly #revivals = new Map<string,
|
|
111
|
+
/** In-flight revives, bound to the parked ref that initiated them, so concurrent {@link ensureLive} calls coalesce. */
|
|
112
|
+
readonly #revivals = new Map<string, RevivingAgent>();
|
|
94
113
|
#unsubscribe: (() => void) | undefined;
|
|
95
114
|
#persistedReviverFactory: PersistedSubagentReviverFactory | undefined;
|
|
96
115
|
/** TTL applied when a cold-revived ref is adopted on demand. */
|
|
@@ -115,23 +134,29 @@ export class AgentLifecycleManager {
|
|
|
115
134
|
/**
|
|
116
135
|
* Take ownership of a finished subagent. Caller has already set registry
|
|
117
136
|
* status to "idle". Arms the TTL timer (idleTtlMs <= 0 adopts without one).
|
|
137
|
+
* When `expected` is given, the adoption is refused if the id no longer
|
|
138
|
+
* resolves to that ref (or that ref's session).
|
|
118
139
|
*/
|
|
119
|
-
adopt(id: string, opts: AdoptOptions): void {
|
|
140
|
+
adopt(id: string, opts: AdoptOptions, expected?: AgentRefExpectation): void {
|
|
120
141
|
if (id === MAIN_AGENT_ID) return;
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
const ref = this.#registry.get(id);
|
|
143
|
+
if (!ref || (expected !== undefined && ref !== expected && ref.session !== expected)) {
|
|
144
|
+
logger.warn("AgentLifecycleManager.adopt: unknown or replaced agent id", { id });
|
|
123
145
|
return;
|
|
124
146
|
}
|
|
125
147
|
const existing = this.#adopted.get(id);
|
|
126
148
|
clearTimeout(existing?.timer);
|
|
127
|
-
const adopted: AdoptedAgent = { idleTtlMs: opts.idleTtlMs, revive: opts.revive };
|
|
149
|
+
const adopted: AdoptedAgent = { ref, idleTtlMs: opts.idleTtlMs, revive: opts.revive };
|
|
128
150
|
this.#adopted.set(id, adopted);
|
|
129
151
|
this.#armTimer(id, adopted);
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
/** True if the id is adopted (parked or live). */
|
|
133
|
-
has(id: string): boolean {
|
|
134
|
-
|
|
154
|
+
/** True if the id is adopted (parked or live) — and, when `expected` is given, still bound to that ref. */
|
|
155
|
+
has(id: string, expected?: AgentRefExpectation): boolean {
|
|
156
|
+
const adopted = this.#adopted.get(id);
|
|
157
|
+
return Boolean(
|
|
158
|
+
adopted && (expected === undefined || adopted.ref === expected || adopted.ref.session === expected),
|
|
159
|
+
);
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/**
|
|
@@ -147,11 +172,14 @@ export class AgentLifecycleManager {
|
|
|
147
172
|
/**
|
|
148
173
|
* True while {@link park} is disposing this agent's session (lets dispose
|
|
149
174
|
* hooks distinguish park from teardown). False once the park is cancelled
|
|
150
|
-
* by ensureLive or after detach+dispose completes.
|
|
175
|
+
* by ensureLive or after detach+dispose completes. When `expected` is
|
|
176
|
+
* given, only a park bound to that ref (or its session) counts.
|
|
151
177
|
*/
|
|
152
|
-
isParking(id: string): boolean {
|
|
178
|
+
isParking(id: string, expected?: AgentRefExpectation): boolean {
|
|
153
179
|
const park = this.#parks.get(id);
|
|
154
|
-
return Boolean(
|
|
180
|
+
return Boolean(
|
|
181
|
+
park && !park.cancelled && (expected === undefined || park.ref === expected || park.ref.session === expected),
|
|
182
|
+
);
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
/**
|
|
@@ -170,7 +198,8 @@ export class AgentLifecycleManager {
|
|
|
170
198
|
const adopted = this.#adopted.get(id);
|
|
171
199
|
if (!adopted) return;
|
|
172
200
|
const ref = this.#registry.get(id);
|
|
173
|
-
|
|
201
|
+
if (!ref || adopted.ref !== ref) return;
|
|
202
|
+
const session = ref.session;
|
|
174
203
|
if (!session) return;
|
|
175
204
|
|
|
176
205
|
if (adopted.timer) {
|
|
@@ -180,6 +209,7 @@ export class AgentLifecycleManager {
|
|
|
180
209
|
|
|
181
210
|
let cancelled = false;
|
|
182
211
|
const park: ParkInFlight = {
|
|
212
|
+
ref,
|
|
183
213
|
promise: undefined as unknown as Promise<void>,
|
|
184
214
|
cancel: () => {
|
|
185
215
|
// Cancel only before detach — once detached the old session is already
|
|
@@ -200,16 +230,16 @@ export class AgentLifecycleManager {
|
|
|
200
230
|
await Promise.resolve();
|
|
201
231
|
if (cancelled) return;
|
|
202
232
|
|
|
203
|
-
// Re-check liveness: release/unregister may have raced us.
|
|
233
|
+
// Re-check liveness: release/unregister/replace may have raced us.
|
|
204
234
|
const live = this.#registry.get(id);
|
|
205
|
-
if (!live
|
|
206
|
-
if (
|
|
235
|
+
if (live !== ref || !live.session || live.session !== session) return;
|
|
236
|
+
if (this.#adopted.get(id)?.ref !== ref) return;
|
|
207
237
|
|
|
208
238
|
// Commit: detach + parked *before* dispose so callers never see a
|
|
209
239
|
// dying session via ref.session / idle status.
|
|
210
240
|
park.detached = true;
|
|
211
|
-
this.#registry.detachSession(id);
|
|
212
|
-
this.#registry.setStatus(id, "parked");
|
|
241
|
+
this.#registry.detachSession(id, ref);
|
|
242
|
+
this.#registry.setStatus(id, "parked", ref);
|
|
213
243
|
|
|
214
244
|
try {
|
|
215
245
|
await session.dispose();
|
|
@@ -238,16 +268,16 @@ export class AgentLifecycleManager {
|
|
|
238
268
|
async ensureLive(id: string): Promise<AgentSession> {
|
|
239
269
|
const park = this.#parks.get(id);
|
|
240
270
|
if (park) {
|
|
241
|
-
const
|
|
271
|
+
const parked = this.#registry.get(id);
|
|
242
272
|
// Cancel if the live session is still attached — keep it instead of
|
|
243
273
|
// thrashing dispose + revive.
|
|
244
|
-
if (
|
|
274
|
+
if (parked?.session && !park.detached && park.cancel()) {
|
|
245
275
|
await park.promise;
|
|
246
276
|
const kept = this.#registry.get(id)?.session;
|
|
247
277
|
if (kept) {
|
|
248
278
|
// Park cleared the idle timer; re-arm so TTL park still works.
|
|
249
279
|
const adopted = this.#adopted.get(id);
|
|
250
|
-
if (adopted && ref.status === "idle") this.#armTimer(id, adopted);
|
|
280
|
+
if (adopted && adopted.ref === parked && parked.status === "idle") this.#armTimer(id, adopted);
|
|
251
281
|
return kept;
|
|
252
282
|
}
|
|
253
283
|
} else {
|
|
@@ -265,13 +295,14 @@ export class AgentLifecycleManager {
|
|
|
265
295
|
}
|
|
266
296
|
if (ref.session) return ref.session;
|
|
267
297
|
const inflight = this.#revivals.get(id);
|
|
268
|
-
if (inflight) return inflight;
|
|
298
|
+
if (inflight?.ref === ref) return inflight.promise;
|
|
269
299
|
const revival = this.#resolveAndRevive(id, ref);
|
|
270
|
-
|
|
300
|
+
const pending: RevivingAgent = { ref, promise: revival };
|
|
301
|
+
this.#revivals.set(id, pending);
|
|
271
302
|
try {
|
|
272
303
|
return await revival;
|
|
273
304
|
} finally {
|
|
274
|
-
this.#revivals.delete(id);
|
|
305
|
+
if (this.#revivals.get(id) === pending) this.#revivals.delete(id);
|
|
275
306
|
}
|
|
276
307
|
}
|
|
277
308
|
|
|
@@ -283,53 +314,72 @@ export class AgentLifecycleManager {
|
|
|
283
314
|
* when the agent is not revivable or no reviver can be produced.
|
|
284
315
|
*/
|
|
285
316
|
async #resolveAndRevive(id: string, ref: AgentRef): Promise<AgentSession> {
|
|
286
|
-
let
|
|
317
|
+
let adoption = this.#adopted.get(id);
|
|
318
|
+
let revive = adoption?.ref === ref ? adoption.revive : undefined;
|
|
287
319
|
let coldAdopted = false;
|
|
288
320
|
if (!revive && ref.status === "parked" && ref.sessionFile && this.#persistedReviverFactory) {
|
|
289
321
|
revive = await this.#persistedReviverFactory(ref);
|
|
290
322
|
if (revive) {
|
|
291
|
-
|
|
323
|
+
adoption = { ref, idleTtlMs: this.#persistedReviveTtlMs, revive };
|
|
324
|
+
this.#adopted.set(id, adoption);
|
|
292
325
|
coldAdopted = true;
|
|
293
326
|
}
|
|
294
327
|
}
|
|
295
|
-
if (
|
|
328
|
+
if (this.#registry.get(id) !== ref) {
|
|
329
|
+
throw new Error(`Agent "${id}" changed while its persisted session was being prepared.`);
|
|
330
|
+
}
|
|
331
|
+
if (ref.status !== "parked" || !revive || !adoption) {
|
|
296
332
|
throw new Error(
|
|
297
333
|
`Agent "${id}" is ${ref.status} and cannot be revived${revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
|
|
298
334
|
);
|
|
299
335
|
}
|
|
300
336
|
try {
|
|
301
|
-
return await this.#revive(id, revive, ref
|
|
337
|
+
return await this.#revive(id, revive, ref, adoption);
|
|
302
338
|
} catch (error) {
|
|
303
339
|
// A failed cold revive (stale ctx, missing cwd, bad MCP) must not leave a
|
|
304
340
|
// poisoned reviver stuck in #adopted — drop it so a later ensureLive
|
|
305
341
|
// rebuilds via the factory (which may have fresher context by then).
|
|
306
|
-
if (coldAdopted) this.#adopted.delete(id);
|
|
342
|
+
if (coldAdopted && this.#adopted.get(id) === adoption) this.#adopted.delete(id);
|
|
307
343
|
throw error;
|
|
308
344
|
}
|
|
309
345
|
}
|
|
310
346
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Hard removal: dispose if live, unregister from registry, drop timers.
|
|
349
|
+
* When `expected` is given, only a ref matching it is released; a stale
|
|
350
|
+
* release can never take down a newer same-id ref. Returns true when a
|
|
351
|
+
* matching ref was released.
|
|
352
|
+
*/
|
|
353
|
+
async release(id: string, expected?: AgentRefExpectation): Promise<boolean> {
|
|
313
354
|
const adopted = this.#adopted.get(id);
|
|
314
|
-
|
|
315
|
-
|
|
355
|
+
const current = this.#registry.get(id);
|
|
356
|
+
const currentMatches =
|
|
357
|
+
current && (expected === undefined || current === expected || current.session === expected);
|
|
358
|
+
const adoptedMatches =
|
|
359
|
+
adopted && (expected === undefined || adopted.ref === expected || adopted.ref.session === expected);
|
|
360
|
+
const ref = currentMatches ? current : adoptedMatches ? adopted.ref : undefined;
|
|
361
|
+
if (!ref) return false;
|
|
362
|
+
if (adopted?.ref === ref) {
|
|
363
|
+
clearTimeout(adopted.timer);
|
|
364
|
+
this.#adopted.delete(id);
|
|
365
|
+
}
|
|
316
366
|
|
|
317
367
|
const park = this.#parks.get(id);
|
|
318
|
-
if (park) {
|
|
368
|
+
if (park && park.ref === ref) {
|
|
319
369
|
// Prefer cancel when the session is still live so release owns dispose.
|
|
320
370
|
if (!park.detached) park.cancel();
|
|
321
371
|
await park.promise;
|
|
322
372
|
}
|
|
323
373
|
|
|
324
|
-
|
|
325
|
-
if (ref?.session) {
|
|
374
|
+
if (this.#registry.get(id) === ref && ref.session) {
|
|
326
375
|
try {
|
|
327
376
|
await ref.session.dispose();
|
|
328
377
|
} catch (error) {
|
|
329
378
|
logger.warn("AgentLifecycleManager.release: session dispose failed", { id, error: String(error) });
|
|
330
379
|
}
|
|
331
380
|
}
|
|
332
|
-
this.#registry.unregister(id);
|
|
381
|
+
this.#registry.unregister(id, ref);
|
|
382
|
+
return true;
|
|
333
383
|
}
|
|
334
384
|
|
|
335
385
|
/** Teardown everything (process exit / main session dispose). */
|
|
@@ -343,11 +393,31 @@ export class AgentLifecycleManager {
|
|
|
343
393
|
this.#persistedReviverFactory = undefined;
|
|
344
394
|
}
|
|
345
395
|
|
|
346
|
-
async #revive(id: string, revive: AgentReviver,
|
|
347
|
-
const session = await revive();
|
|
348
|
-
this.#registry.
|
|
396
|
+
async #revive(id: string, revive: AgentReviver, ref: AgentRef, adopted: AdoptedAgent): Promise<AgentSession> {
|
|
397
|
+
const session = await revive(ref);
|
|
398
|
+
let liveRef = this.#registry.get(id);
|
|
399
|
+
if (liveRef === ref) {
|
|
400
|
+
if (!this.#registry.attachSession(id, session, ref.sessionFile, ref)) {
|
|
401
|
+
await session.dispose();
|
|
402
|
+
throw new Error(`Agent "${id}" changed before its persisted session could attach.`);
|
|
403
|
+
}
|
|
404
|
+
liveRef = ref;
|
|
405
|
+
} else if (
|
|
406
|
+
!liveRef ||
|
|
407
|
+
liveRef.session !== session ||
|
|
408
|
+
liveRef.kind !== ref.kind ||
|
|
409
|
+
liveRef.parentId !== ref.parentId ||
|
|
410
|
+
liveRef.sessionFile !== ref.sessionFile
|
|
411
|
+
) {
|
|
412
|
+
await session.dispose();
|
|
413
|
+
throw new Error(`Agent "${id}" was replaced while its persisted session was reviving.`);
|
|
414
|
+
}
|
|
415
|
+
adopted.ref = liveRef;
|
|
349
416
|
// Emits status_changed → "idle", which re-arms the TTL timer below.
|
|
350
|
-
this.#registry.setStatus(id, "idle")
|
|
417
|
+
if (!this.#registry.setStatus(id, "idle", liveRef)) {
|
|
418
|
+
await session.dispose();
|
|
419
|
+
throw new Error(`Agent "${id}" changed before its persisted session became idle.`);
|
|
420
|
+
}
|
|
351
421
|
return session;
|
|
352
422
|
}
|
|
353
423
|
|
|
@@ -364,7 +434,7 @@ export class AgentLifecycleManager {
|
|
|
364
434
|
|
|
365
435
|
#onRegistryEvent(event: RegistryEvent): void {
|
|
366
436
|
const adopted = this.#adopted.get(event.ref.id);
|
|
367
|
-
if (!adopted) return;
|
|
437
|
+
if (!adopted || adopted.ref !== event.ref) return;
|
|
368
438
|
if (event.type === "removed") {
|
|
369
439
|
clearTimeout(adopted.timer);
|
|
370
440
|
this.#adopted.delete(event.ref.id);
|