@oh-my-pi/pi-coding-agent 17.0.8 → 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 +87 -1
- package/README.md +1 -1
- package/dist/cli.js +5495 -5021
- 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/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +30 -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 +350 -50
- 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/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -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 +18 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/mcp/tool-bridge.d.ts +7 -3
- package/dist/types/mcp/types.d.ts +6 -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/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +8 -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/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- 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 +120 -750
- 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-loader.d.ts +6 -4
- 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/task/types.d.ts +14 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- 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 +8 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- 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/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +95 -1
- package/package.json +13 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- 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/grievances-cli.ts +2 -2
- package/src/cli/usage-cli.ts +11 -2
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +50 -2
- package/src/config/model-resolver.ts +0 -1
- 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 +250 -54
- 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/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
- 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/registry-helpers.ts +40 -0
- 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 +52 -9
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +44 -11
- package/src/main.ts +9 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/render.ts +94 -35
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/mcp/tool-bridge.ts +107 -11
- package/src/mcp/types.ts +7 -0
- 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 +27 -10
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +28 -3
- package/src/modes/components/settings-selector.ts +178 -6
- 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.test.ts +63 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +70 -22
- 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 +80 -41
- 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 +160 -38
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- 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/goals/guided-goal-system.md +24 -3
- 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/task.md +12 -2
- 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 +255 -139
- 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 +1910 -12213
- 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.test.ts +224 -1
- package/src/session/session-context.ts +47 -5
- 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 +10 -51
- 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 +25 -4
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +288 -70
- package/src/task/index.ts +148 -111
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +4 -1
- package/src/task/types.ts +16 -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/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +12 -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/report-tool-issue.ts +79 -28
- 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/firecrawl.ts +46 -13
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +8 -1
package/src/vibe/runtime.ts
CHANGED
|
@@ -24,7 +24,9 @@ import { registerArtifactsDir } from "../internal-urls/registry-helpers";
|
|
|
24
24
|
import { MCPManager } from "../mcp/manager";
|
|
25
25
|
import vibeTurnResultTemplate from "../prompts/tools/vibe-turn-result.md" with { type: "text" };
|
|
26
26
|
import { AgentLifecycleManager } from "../registry/agent-lifecycle";
|
|
27
|
-
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
27
|
+
import { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
28
|
+
import type { SessionEntry } from "../session/session-entries";
|
|
29
|
+
import { SessionManager, SessionPersistenceIndeterminateError } from "../session/session-manager";
|
|
28
30
|
import { getBundledAgent } from "../task/agents";
|
|
29
31
|
import { type ExecutorOptions, runSubagentFollowUpTurn, runSubprocess } from "../task/executor";
|
|
30
32
|
import { generateTaskName } from "../task/name-generator";
|
|
@@ -68,6 +70,82 @@ const TRACE_LINE_MAX = 120;
|
|
|
68
70
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
69
71
|
/** Response text cap inside a delivered turn result; full output stays at agent://<id>. */
|
|
70
72
|
const RESPONSE_PREVIEW_MAX = 6000;
|
|
73
|
+
/** Grace period for abort-aware turns before teardown detaches a stuck provider/tool call. */
|
|
74
|
+
const CANCELLED_TURN_SETTLE_GRACE_MS = 250;
|
|
75
|
+
|
|
76
|
+
const VIBE_LIFECYCLE_CUSTOM_TYPE = "vibe-session-lifecycle";
|
|
77
|
+
const VIBE_LIFECYCLE_VERSION = 1;
|
|
78
|
+
|
|
79
|
+
export interface VibeOwnerScope {
|
|
80
|
+
ownerId: string;
|
|
81
|
+
parentSessionId: string;
|
|
82
|
+
parentSessionFile: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface VibeParentSession {
|
|
86
|
+
getAgentId?: () => string | null;
|
|
87
|
+
getSessionId?: () => string | null;
|
|
88
|
+
getSessionFile: () => string | null;
|
|
89
|
+
sessionManager?: ToolSession["sessionManager"] &
|
|
90
|
+
Partial<
|
|
91
|
+
Pick<SessionManager, "appendModeChange" | "appendEntriesAtomically" | "recoverPersistenceFromCurrentState">
|
|
92
|
+
>;
|
|
93
|
+
asyncJobManager?: AsyncJobManager;
|
|
94
|
+
settings: ToolSession["settings"];
|
|
95
|
+
getActiveModelString?: () => string | undefined;
|
|
96
|
+
getModelString?: () => string | undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type VibeTombstoneReason = "explicit-kill" | "mode-exit" | "spawn-failed" | "unrecoverable";
|
|
100
|
+
|
|
101
|
+
interface VibeLifecycleBase {
|
|
102
|
+
version: typeof VIBE_LIFECYCLE_VERSION;
|
|
103
|
+
id: string;
|
|
104
|
+
ownerId: string;
|
|
105
|
+
parentSessionId: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface VibeSpawnLifecycleEvent extends VibeLifecycleBase {
|
|
109
|
+
action: "spawn";
|
|
110
|
+
cli: VibeCli;
|
|
111
|
+
agent: string;
|
|
112
|
+
childSessionFile: string;
|
|
113
|
+
createdAt: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface VibeTurnLifecycleEvent extends VibeLifecycleBase {
|
|
117
|
+
action: "turn-started" | "turn-settled";
|
|
118
|
+
turn: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface VibeTombstoneLifecycleEvent extends VibeLifecycleBase {
|
|
122
|
+
action: "tombstone";
|
|
123
|
+
reason: VibeTombstoneReason;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface VibeTombstoneRevocationEvent extends VibeLifecycleBase {
|
|
127
|
+
action: "tombstone-revoked";
|
|
128
|
+
reason: "mode-exit";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type VibeLifecycleEvent =
|
|
132
|
+
| VibeSpawnLifecycleEvent
|
|
133
|
+
| VibeTurnLifecycleEvent
|
|
134
|
+
| VibeTombstoneLifecycleEvent
|
|
135
|
+
| VibeTombstoneRevocationEvent;
|
|
136
|
+
|
|
137
|
+
interface VibeRestoreCandidate {
|
|
138
|
+
spawn: VibeSpawnLifecycleEvent;
|
|
139
|
+
turnCount: number;
|
|
140
|
+
lastActivityAt: number;
|
|
141
|
+
inFlight: boolean;
|
|
142
|
+
tombstoneReason?: VibeTombstoneReason;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ResolvedVibeWorker {
|
|
146
|
+
agent: AgentDefinition;
|
|
147
|
+
modelOverride?: string | string[];
|
|
148
|
+
}
|
|
71
149
|
|
|
72
150
|
interface VibeTurn {
|
|
73
151
|
jobId: string;
|
|
@@ -83,6 +161,9 @@ interface VibeRecord {
|
|
|
83
161
|
id: string;
|
|
84
162
|
cli: VibeCli;
|
|
85
163
|
ownerId: string;
|
|
164
|
+
parentSessionId: string;
|
|
165
|
+
parentSessionFile: string | null;
|
|
166
|
+
childSessionFile?: string;
|
|
86
167
|
agent: AgentDefinition;
|
|
87
168
|
modelOverride?: string | string[];
|
|
88
169
|
state: VibeSessionState;
|
|
@@ -107,6 +188,10 @@ interface VibeRecord {
|
|
|
107
188
|
queue: string[];
|
|
108
189
|
turnCount: number;
|
|
109
190
|
killed: boolean;
|
|
191
|
+
/** True while a parent switch is detaching this process-local record without terminating it. */
|
|
192
|
+
suspended: boolean;
|
|
193
|
+
/** True only after a terminal lifecycle event has durably flushed. */
|
|
194
|
+
terminalPersisted: boolean;
|
|
110
195
|
}
|
|
111
196
|
|
|
112
197
|
/**
|
|
@@ -172,6 +257,87 @@ function firstLine(text: string, max = 100): string {
|
|
|
172
257
|
return oneLineLabel(text, max);
|
|
173
258
|
}
|
|
174
259
|
|
|
260
|
+
function scopeKey(scope: VibeOwnerScope, id: string): string {
|
|
261
|
+
return `${scope.parentSessionId}\0${scope.parentSessionFile ?? ""}\0${scope.ownerId}\0${id}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function matchesScope(record: VibeRecord, scope: VibeOwnerScope): boolean {
|
|
265
|
+
return (
|
|
266
|
+
record.ownerId === scope.ownerId &&
|
|
267
|
+
record.parentSessionId === scope.parentSessionId &&
|
|
268
|
+
record.parentSessionFile === scope.parentSessionFile
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function objectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
273
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
274
|
+
return value as Record<string, unknown>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseLifecycleEvent(value: unknown): VibeLifecycleEvent | undefined {
|
|
278
|
+
const data = objectRecord(value);
|
|
279
|
+
if (!data || data.version !== VIBE_LIFECYCLE_VERSION) return undefined;
|
|
280
|
+
if (typeof data.id !== "string" || !data.id) return undefined;
|
|
281
|
+
if (typeof data.ownerId !== "string" || !data.ownerId) return undefined;
|
|
282
|
+
if (typeof data.parentSessionId !== "string" || !data.parentSessionId) return undefined;
|
|
283
|
+
const base: VibeLifecycleBase = {
|
|
284
|
+
version: VIBE_LIFECYCLE_VERSION,
|
|
285
|
+
id: data.id,
|
|
286
|
+
ownerId: data.ownerId,
|
|
287
|
+
parentSessionId: data.parentSessionId,
|
|
288
|
+
};
|
|
289
|
+
if (data.action === "spawn") {
|
|
290
|
+
const cli = data.cli === "fast" || data.cli === "good" ? data.cli : undefined;
|
|
291
|
+
if (!cli || typeof data.agent !== "string" || typeof data.childSessionFile !== "string") return undefined;
|
|
292
|
+
if (typeof data.createdAt !== "number" || !Number.isFinite(data.createdAt)) return undefined;
|
|
293
|
+
return {
|
|
294
|
+
...base,
|
|
295
|
+
action: "spawn",
|
|
296
|
+
cli,
|
|
297
|
+
agent: data.agent,
|
|
298
|
+
childSessionFile: data.childSessionFile,
|
|
299
|
+
createdAt: data.createdAt,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (data.action === "turn-started" || data.action === "turn-settled") {
|
|
303
|
+
if (typeof data.turn !== "number" || !Number.isInteger(data.turn) || data.turn < 1) return undefined;
|
|
304
|
+
return { ...base, action: data.action, turn: data.turn };
|
|
305
|
+
}
|
|
306
|
+
if (data.action === "tombstone") {
|
|
307
|
+
const reason = data.reason;
|
|
308
|
+
if (
|
|
309
|
+
reason !== "explicit-kill" &&
|
|
310
|
+
reason !== "mode-exit" &&
|
|
311
|
+
reason !== "spawn-failed" &&
|
|
312
|
+
reason !== "unrecoverable"
|
|
313
|
+
) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
return { ...base, action: "tombstone", reason };
|
|
317
|
+
}
|
|
318
|
+
if (data.action === "tombstone-revoked" && data.reason === "mode-exit") {
|
|
319
|
+
return { ...base, action: "tombstone-revoked", reason: "mode-exit" };
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Child ids claimed by any valid Vibe spawn event, independent of current parent scope. */
|
|
325
|
+
export function persistedVibeChildIds(entries: Iterable<SessionEntry>): Set<string> {
|
|
326
|
+
const ids = new Set<string>();
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (entry.type !== "custom" || entry.customType !== VIBE_LIFECYCLE_CUSTOM_TYPE) continue;
|
|
329
|
+
const event = parseLifecycleEvent(entry.data);
|
|
330
|
+
if (
|
|
331
|
+
event?.action === "spawn" &&
|
|
332
|
+
/^[A-Za-z0-9_-]+$/.test(event.id) &&
|
|
333
|
+
event.childSessionFile === `${event.id}.jsonl`
|
|
334
|
+
) {
|
|
335
|
+
ids.add(event.id);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return ids;
|
|
339
|
+
}
|
|
340
|
+
|
|
175
341
|
/** Merge the monitor's rolling `recentTools` window (newest first) into the per-turn trace (oldest first). */
|
|
176
342
|
function mergeTrace(turn: VibeTurn, progress: AgentProgress): void {
|
|
177
343
|
turn.toolCount = progress.toolCount;
|
|
@@ -188,10 +354,28 @@ function mergeTrace(turn: VibeTurn, progress: AgentProgress): void {
|
|
|
188
354
|
/** Thrown from a turn job body so the job manager marks the job failed while carrying the formatted result. */
|
|
189
355
|
export class VibeTurnError extends Error {}
|
|
190
356
|
|
|
357
|
+
async function awaitCancelledTurnJobs(jobs: ReadonlySet<AsyncJob>): Promise<void> {
|
|
358
|
+
if (jobs.size === 0) return;
|
|
359
|
+
const settled = Promise.allSettled([...jobs].map(job => job.promise)).then(() => true);
|
|
360
|
+
const timeout = Promise.withResolvers<false>();
|
|
361
|
+
const timer = setTimeout(() => timeout.resolve(false), CANCELLED_TURN_SETTLE_GRACE_MS);
|
|
362
|
+
timer.unref();
|
|
363
|
+
try {
|
|
364
|
+
if (!(await Promise.race([settled, timeout.promise]))) {
|
|
365
|
+
logger.warn("vibe: detached cancelled turn that did not settle within teardown grace period", {
|
|
366
|
+
jobCount: jobs.size,
|
|
367
|
+
graceMs: CANCELLED_TURN_SETTLE_GRACE_MS,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
191
375
|
/**
|
|
192
|
-
* Process-global registry of vibe worker sessions, scoped
|
|
193
|
-
*
|
|
194
|
-
*
|
|
376
|
+
* Process-global registry of vibe worker sessions, scoped by both owner agent
|
|
377
|
+
* id and stable parent session id. Persisted lifecycle events rebuild idle
|
|
378
|
+
* records after a process restart; live turn jobs remain process-local.
|
|
195
379
|
*/
|
|
196
380
|
export class VibeSessionRegistry {
|
|
197
381
|
static #global: VibeSessionRegistry | undefined;
|
|
@@ -218,6 +402,8 @@ export class VibeSessionRegistry {
|
|
|
218
402
|
id: record.id,
|
|
219
403
|
cli: record.cli ?? "fast",
|
|
220
404
|
ownerId: record.ownerId,
|
|
405
|
+
parentSessionId: "test-parent-session",
|
|
406
|
+
parentSessionFile: null,
|
|
221
407
|
agent: getBundledAgent("sonic")!,
|
|
222
408
|
state: record.state ?? "running",
|
|
223
409
|
createdAt: Date.now(),
|
|
@@ -225,10 +411,182 @@ export class VibeSessionRegistry {
|
|
|
225
411
|
queue: [],
|
|
226
412
|
turnCount: 0,
|
|
227
413
|
killed: false,
|
|
414
|
+
suspended: false,
|
|
415
|
+
terminalPersisted: false,
|
|
228
416
|
});
|
|
229
417
|
}
|
|
230
418
|
|
|
231
419
|
readonly #records = new Map<string, VibeRecord>();
|
|
420
|
+
readonly #terminationTails = new Map<string, Promise<void>>();
|
|
421
|
+
readonly #terminatedScopes = new Set<string>();
|
|
422
|
+
|
|
423
|
+
ownerScope(session: VibeParentSession): VibeOwnerScope {
|
|
424
|
+
const parentSessionId = session.getSessionId?.();
|
|
425
|
+
if (!parentSessionId) {
|
|
426
|
+
throw new ToolError("Vibe sessions require a stable parent session id.");
|
|
427
|
+
}
|
|
428
|
+
const parentSessionFile = session.getSessionFile();
|
|
429
|
+
return {
|
|
430
|
+
ownerId: session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
431
|
+
parentSessionId,
|
|
432
|
+
parentSessionFile: parentSessionFile ? path.resolve(parentSessionFile) : null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Re-open spawn admission after an explicit Vibe-mode entry. */
|
|
437
|
+
activateScope(scope: VibeOwnerScope): void {
|
|
438
|
+
this.#terminatedScopes.delete(scopeKey(scope, ""));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async #withTerminationLock<T>(scope: VibeOwnerScope, operation: () => Promise<T>): Promise<T> {
|
|
442
|
+
const key = scopeKey(scope, "");
|
|
443
|
+
const predecessor = this.#terminationTails.get(key) ?? Promise.resolve();
|
|
444
|
+
const released = Promise.withResolvers<void>();
|
|
445
|
+
const tail = predecessor.then(() => released.promise);
|
|
446
|
+
this.#terminationTails.set(key, tail);
|
|
447
|
+
await predecessor;
|
|
448
|
+
try {
|
|
449
|
+
return await operation();
|
|
450
|
+
} finally {
|
|
451
|
+
released.resolve();
|
|
452
|
+
if (this.#terminationTails.get(key) === tail) this.#terminationTails.delete(key);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#resolveWorker(session: VibeParentSession, cli: VibeCli): ResolvedVibeWorker {
|
|
457
|
+
const agentName = VIBE_CLI_AGENT[cli];
|
|
458
|
+
const agent = getBundledAgent(agentName);
|
|
459
|
+
if (!agent) {
|
|
460
|
+
throw new ToolError(`Bundled agent "${agentName}" for vibe cli "${cli}" is unavailable.`);
|
|
461
|
+
}
|
|
462
|
+
const agentModelOverrides = session.settings.get("task.agentModelOverrides");
|
|
463
|
+
return {
|
|
464
|
+
agent,
|
|
465
|
+
modelOverride: resolveAgentModelPatterns({
|
|
466
|
+
settingsOverride: agentModelOverrides[agentName],
|
|
467
|
+
agentModel: agent.model,
|
|
468
|
+
settings: session.settings,
|
|
469
|
+
activeModelPattern: session.getActiveModelString?.(),
|
|
470
|
+
fallbackModelPattern: session.getModelString?.(),
|
|
471
|
+
}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async #appendLifecycleEvent(
|
|
476
|
+
session: VibeParentSession,
|
|
477
|
+
event: VibeLifecycleEvent,
|
|
478
|
+
expectedParentSessionFile: string | null,
|
|
479
|
+
): Promise<boolean> {
|
|
480
|
+
if (!expectedParentSessionFile || !session.sessionManager) return false;
|
|
481
|
+
const matchesCurrentScope = (): boolean => {
|
|
482
|
+
const currentSessionFile = session.getSessionFile();
|
|
483
|
+
return (
|
|
484
|
+
session.getSessionId?.() === event.parentSessionId &&
|
|
485
|
+
(session.getAgentId?.() ?? MAIN_AGENT_ID) === event.ownerId &&
|
|
486
|
+
currentSessionFile !== null &&
|
|
487
|
+
path.resolve(currentSessionFile) === expectedParentSessionFile
|
|
488
|
+
);
|
|
489
|
+
};
|
|
490
|
+
if (!matchesCurrentScope()) return false;
|
|
491
|
+
await session.sessionManager.ensureOnDisk();
|
|
492
|
+
if (!matchesCurrentScope()) return false;
|
|
493
|
+
session.sessionManager.appendCustomEntry(VIBE_LIFECYCLE_CUSTOM_TYPE, event);
|
|
494
|
+
await session.sessionManager.flush();
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#eventBase(record: VibeRecord): VibeLifecycleBase {
|
|
499
|
+
return {
|
|
500
|
+
version: VIBE_LIFECYCLE_VERSION,
|
|
501
|
+
id: record.id,
|
|
502
|
+
ownerId: record.ownerId,
|
|
503
|
+
parentSessionId: record.parentSessionId,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async #appendTombstone(
|
|
508
|
+
session: VibeParentSession,
|
|
509
|
+
record: VibeRecord,
|
|
510
|
+
reason: VibeTombstoneReason,
|
|
511
|
+
): Promise<boolean> {
|
|
512
|
+
return this.#appendLifecycleEvent(
|
|
513
|
+
session,
|
|
514
|
+
{
|
|
515
|
+
...this.#eventBase(record),
|
|
516
|
+
action: "tombstone",
|
|
517
|
+
reason,
|
|
518
|
+
},
|
|
519
|
+
record.parentSessionFile,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#hasInMemoryTombstone(session: VibeParentSession, record: VibeRecord): boolean {
|
|
524
|
+
let terminalReason: VibeTombstoneReason | undefined;
|
|
525
|
+
for (const entry of session.sessionManager?.getEntries() ?? []) {
|
|
526
|
+
if (entry.type !== "custom" || entry.customType !== VIBE_LIFECYCLE_CUSTOM_TYPE) continue;
|
|
527
|
+
const event = parseLifecycleEvent(entry.data);
|
|
528
|
+
if (
|
|
529
|
+
!event ||
|
|
530
|
+
event.id !== record.id ||
|
|
531
|
+
event.ownerId !== record.ownerId ||
|
|
532
|
+
event.parentSessionId !== record.parentSessionId
|
|
533
|
+
) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (event.action === "tombstone") terminalReason = event.reason;
|
|
537
|
+
else if (event.action === "tombstone-revoked" && terminalReason === "mode-exit") terminalReason = undefined;
|
|
538
|
+
}
|
|
539
|
+
return terminalReason !== undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async #persistModeExit(
|
|
543
|
+
session: VibeParentSession,
|
|
544
|
+
scope: VibeOwnerScope,
|
|
545
|
+
records: readonly VibeRecord[],
|
|
546
|
+
): Promise<void> {
|
|
547
|
+
const pending = records.filter(record => !record.terminalPersisted);
|
|
548
|
+
const sessionManager = session.sessionManager;
|
|
549
|
+
if (!sessionManager) {
|
|
550
|
+
if (pending.some(record => record.childSessionFile)) {
|
|
551
|
+
throw new ToolError("Vibe mode exit cannot persist worker tombstones without the parent session manager.");
|
|
552
|
+
}
|
|
553
|
+
for (const record of pending) record.terminalPersisted = true;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const currentScope = this.ownerScope(session);
|
|
557
|
+
if (
|
|
558
|
+
currentScope.ownerId !== scope.ownerId ||
|
|
559
|
+
currentScope.parentSessionId !== scope.parentSessionId ||
|
|
560
|
+
currentScope.parentSessionFile !== scope.parentSessionFile
|
|
561
|
+
) {
|
|
562
|
+
throw new ToolError("Vibe parent session changed before mode exit could be persisted.");
|
|
563
|
+
}
|
|
564
|
+
const parentSessionFile = currentScope.parentSessionFile;
|
|
565
|
+
const persistedPending = pending.filter(record => record.childSessionFile !== undefined);
|
|
566
|
+
for (const record of persistedPending) {
|
|
567
|
+
if (
|
|
568
|
+
!parentSessionFile ||
|
|
569
|
+
path.resolve(parentSessionFile.slice(0, -6), `${record.id}.jsonl`) !== record.childSessionFile
|
|
570
|
+
) {
|
|
571
|
+
throw new ToolError(`Vibe session "${record.id}" changed parent scope before termination.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const appendEntriesAtomically = sessionManager.appendEntriesAtomically;
|
|
575
|
+
if (!appendEntriesAtomically) {
|
|
576
|
+
throw new ToolError("Vibe mode exit requires atomic parent-session persistence.");
|
|
577
|
+
}
|
|
578
|
+
await appendEntriesAtomically.call(sessionManager, () => {
|
|
579
|
+
for (const record of persistedPending) {
|
|
580
|
+
sessionManager.appendCustomEntry(VIBE_LIFECYCLE_CUSTOM_TYPE, {
|
|
581
|
+
...this.#eventBase(record),
|
|
582
|
+
action: "tombstone",
|
|
583
|
+
reason: "mode-exit",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
sessionManager.appendModeChange?.("none");
|
|
587
|
+
});
|
|
588
|
+
for (const record of pending) record.terminalPersisted = true;
|
|
589
|
+
}
|
|
232
590
|
|
|
233
591
|
#manager(session: ToolSession): AsyncJobManager {
|
|
234
592
|
const manager = session.asyncJobManager;
|
|
@@ -238,10 +596,10 @@ export class VibeSessionRegistry {
|
|
|
238
596
|
return manager;
|
|
239
597
|
}
|
|
240
598
|
|
|
241
|
-
#record(
|
|
242
|
-
const record = this.#records.get(id.trim());
|
|
243
|
-
if (!record || record
|
|
244
|
-
const roster = this
|
|
599
|
+
#record(scope: VibeOwnerScope, id: string): VibeRecord {
|
|
600
|
+
const record = this.#records.get(scopeKey(scope, id.trim()));
|
|
601
|
+
if (!record || !matchesScope(record, scope)) {
|
|
602
|
+
const roster = this.#listIds(scope);
|
|
245
603
|
throw new ToolError(
|
|
246
604
|
`Unknown vibe session "${id}".${roster.length > 0 ? ` Active sessions: ${roster.join(", ")}` : " No sessions — spawn one with vibe_spawn."}`,
|
|
247
605
|
);
|
|
@@ -249,10 +607,30 @@ export class VibeSessionRegistry {
|
|
|
249
607
|
return record;
|
|
250
608
|
}
|
|
251
609
|
|
|
252
|
-
|
|
610
|
+
#registeredAgent(record: VibeRecord): AgentRef | undefined {
|
|
611
|
+
const ref = AgentRegistry.global().get(record.id);
|
|
612
|
+
if (ref?.kind !== "sub" || ref.parentId !== record.ownerId) return undefined;
|
|
613
|
+
if (record.childSessionFile && ref.sessionFile !== record.childSessionFile) return undefined;
|
|
614
|
+
return ref;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#listIds(scope: VibeOwnerScope): string[] {
|
|
253
618
|
const ids: string[] = [];
|
|
254
619
|
for (const record of this.#records.values()) {
|
|
255
|
-
if (record
|
|
620
|
+
if (matchesScope(record, scope) && record.state !== "dead") ids.push(record.id);
|
|
621
|
+
}
|
|
622
|
+
return ids;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
listIds(session: ToolSession): string[] {
|
|
626
|
+
return this.#listIds(this.ownerScope(session));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Ids of every live worker owned by `ownerId`, across all parent scopes (status-line tok/s aggregation). */
|
|
630
|
+
listIdsByOwner(ownerId: string): string[] {
|
|
631
|
+
const ids: string[] = [];
|
|
632
|
+
for (const record of this.#records.values()) {
|
|
633
|
+
if (record.ownerId === ownerId && record.state !== "dead") ids.push(record.id);
|
|
256
634
|
}
|
|
257
635
|
return ids;
|
|
258
636
|
}
|
|
@@ -263,11 +641,12 @@ export class VibeSessionRegistry {
|
|
|
263
641
|
* tool, and streamed text tail. All strings are one-line sanitized here so
|
|
264
642
|
* renderers can print them verbatim.
|
|
265
643
|
*/
|
|
266
|
-
screens(
|
|
644
|
+
screens(session: ToolSession, ids?: string[]): VibeScreenSnapshot[] {
|
|
645
|
+
const scope = this.ownerScope(session);
|
|
267
646
|
const wanted = ids?.length ? new Set(ids.map(id => id.trim())) : undefined;
|
|
268
647
|
const records: VibeRecord[] = [];
|
|
269
648
|
for (const record of this.#records.values()) {
|
|
270
|
-
if (record
|
|
649
|
+
if (!matchesScope(record, scope)) continue;
|
|
271
650
|
if (wanted && !wanted.has(record.id)) continue;
|
|
272
651
|
records.push(record);
|
|
273
652
|
}
|
|
@@ -296,51 +675,272 @@ export class VibeSessionRegistry {
|
|
|
296
675
|
}));
|
|
297
676
|
}
|
|
298
677
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
678
|
+
#persistedIds(session: VibeParentSession, scope: VibeOwnerScope): Set<string> {
|
|
679
|
+
const ids = new Set<string>();
|
|
680
|
+
for (const entry of session.sessionManager?.getEntries() ?? []) {
|
|
681
|
+
if (entry.type !== "custom" || entry.customType !== VIBE_LIFECYCLE_CUSTOM_TYPE) continue;
|
|
682
|
+
const event = parseLifecycleEvent(entry.data);
|
|
683
|
+
if (event?.ownerId === scope.ownerId && event.parentSessionId === scope.parentSessionId) ids.add(event.id);
|
|
684
|
+
}
|
|
685
|
+
for (const record of this.#records.values()) {
|
|
686
|
+
if (matchesScope(record, scope)) ids.add(record.id);
|
|
307
687
|
}
|
|
688
|
+
return ids;
|
|
689
|
+
}
|
|
308
690
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
691
|
+
async #resolvePersistedChild(
|
|
692
|
+
parentSessionFile: string,
|
|
693
|
+
spawn: VibeSpawnLifecycleEvent,
|
|
694
|
+
options?: { requireAgentMatch?: boolean },
|
|
695
|
+
): Promise<string | undefined> {
|
|
696
|
+
if (options?.requireAgentMatch !== false && spawn.agent !== VIBE_CLI_AGENT[spawn.cli]) return undefined;
|
|
697
|
+
if (!/^[A-Za-z0-9_-]+$/.test(spawn.id) || spawn.childSessionFile !== `${spawn.id}.jsonl`) return undefined;
|
|
698
|
+
const artifactsDir = path.resolve(parentSessionFile.slice(0, -6));
|
|
699
|
+
const childSessionFile = path.resolve(artifactsDir, spawn.childSessionFile);
|
|
700
|
+
const relative = path.relative(artifactsDir, childSessionFile);
|
|
701
|
+
if (!relative || path.isAbsolute(relative) || relative.startsWith(`..${path.sep}`) || relative === "..") {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const persisted = await SessionManager.peekSessionInit(childSessionFile);
|
|
706
|
+
return persisted?.init ? childSessionFile : undefined;
|
|
707
|
+
} catch {
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async #markTerminalRef(
|
|
713
|
+
id: string,
|
|
714
|
+
ownerId: string,
|
|
715
|
+
childSessionFile: string,
|
|
716
|
+
expected?: AgentRef | null,
|
|
717
|
+
): Promise<void> {
|
|
718
|
+
const registry = AgentRegistry.global();
|
|
719
|
+
const existing = registry.get(id);
|
|
720
|
+
if (expected !== undefined && existing !== undefined && existing !== expected) return;
|
|
721
|
+
if (
|
|
722
|
+
existing &&
|
|
723
|
+
(existing.kind !== "sub" || existing.parentId !== ownerId || existing.sessionFile !== childSessionFile)
|
|
724
|
+
) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (existing?.status === "aborted" && !existing.session) return;
|
|
728
|
+
if (existing && !registry.setStatus(id, "aborted", existing)) return;
|
|
729
|
+
if (existing && AgentLifecycleManager.global().has(id, existing)) {
|
|
730
|
+
await AgentLifecycleManager.global().release(id, existing);
|
|
731
|
+
} else if (existing?.session) {
|
|
732
|
+
await existing.session.dispose();
|
|
733
|
+
}
|
|
734
|
+
const current = registry.get(id);
|
|
735
|
+
if (current && current !== existing) return;
|
|
736
|
+
if (current) registry.unregister(id, current);
|
|
737
|
+
registry.register({
|
|
738
|
+
id,
|
|
739
|
+
displayName: id,
|
|
740
|
+
kind: "sub",
|
|
741
|
+
parentId: ownerId,
|
|
742
|
+
session: null,
|
|
743
|
+
sessionFile: childSessionFile,
|
|
744
|
+
status: "aborted",
|
|
316
745
|
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Reconcile resumable and terminal workers from the persisted parent journal. */
|
|
749
|
+
async rehydrate(session: VibeParentSession): Promise<number> {
|
|
750
|
+
const sessionFile = session.getSessionFile();
|
|
751
|
+
const sessionManager = session.sessionManager;
|
|
752
|
+
if (!sessionFile || !sessionManager) return 0;
|
|
753
|
+
const scope = this.ownerScope(session);
|
|
754
|
+
const allSpawns = new Map<string, VibeSpawnLifecycleEvent>();
|
|
755
|
+
const terminalIntents = new Map<string, VibeTombstoneReason>();
|
|
756
|
+
for (const entry of sessionManager.getEntries()) {
|
|
757
|
+
if (entry.type !== "custom" || entry.customType !== VIBE_LIFECYCLE_CUSTOM_TYPE) continue;
|
|
758
|
+
const event = parseLifecycleEvent(entry.data);
|
|
759
|
+
if (!event || event.ownerId !== scope.ownerId || event.parentSessionId !== scope.parentSessionId) continue;
|
|
760
|
+
if (event.action === "spawn") allSpawns.set(event.id, event);
|
|
761
|
+
else if (event.action === "tombstone") terminalIntents.set(event.id, event.reason);
|
|
762
|
+
else if (event.action === "tombstone-revoked" && terminalIntents.get(event.id) === "mode-exit") {
|
|
763
|
+
terminalIntents.delete(event.id);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const candidates = new Map<string, VibeRestoreCandidate>();
|
|
768
|
+
for (const entry of sessionManager.getBranch()) {
|
|
769
|
+
if (entry.type !== "custom" || entry.customType !== VIBE_LIFECYCLE_CUSTOM_TYPE) continue;
|
|
770
|
+
const event = parseLifecycleEvent(entry.data);
|
|
771
|
+
if (!event || event.ownerId !== scope.ownerId || event.parentSessionId !== scope.parentSessionId) continue;
|
|
772
|
+
const eventTime = Date.parse(entry.timestamp);
|
|
773
|
+
if (event.action === "spawn") {
|
|
774
|
+
candidates.set(event.id, {
|
|
775
|
+
spawn: event,
|
|
776
|
+
turnCount: 0,
|
|
777
|
+
lastActivityAt: Number.isFinite(eventTime) ? eventTime : event.createdAt,
|
|
778
|
+
inFlight: false,
|
|
779
|
+
});
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const candidate = candidates.get(event.id);
|
|
783
|
+
if (!candidate) continue;
|
|
784
|
+
candidate.lastActivityAt = Number.isFinite(eventTime) ? eventTime : candidate.lastActivityAt;
|
|
785
|
+
if (event.action === "turn-started" && event.turn >= candidate.turnCount) {
|
|
786
|
+
candidate.turnCount = event.turn;
|
|
787
|
+
candidate.inFlight = true;
|
|
788
|
+
} else if (event.action === "turn-settled" && event.turn >= candidate.turnCount) {
|
|
789
|
+
candidate.turnCount = event.turn;
|
|
790
|
+
candidate.inFlight = false;
|
|
791
|
+
} else if (event.action === "tombstone") {
|
|
792
|
+
candidate.tombstoneReason = event.reason;
|
|
793
|
+
} else if (event.action === "tombstone-revoked" && candidate.tombstoneReason === "mode-exit") {
|
|
794
|
+
candidate.tombstoneReason = undefined;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
for (const id of terminalIntents.keys()) {
|
|
799
|
+
const spawn = allSpawns.get(id);
|
|
800
|
+
if (!spawn) continue;
|
|
801
|
+
const childSessionFile = await this.#resolvePersistedChild(sessionFile, spawn, { requireAgentMatch: false });
|
|
802
|
+
if (!childSessionFile) continue;
|
|
803
|
+
await this.#markTerminalRef(id, scope.ownerId, childSessionFile);
|
|
804
|
+
this.#records.delete(scopeKey(scope, id));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
let restored = 0;
|
|
808
|
+
for (const candidate of candidates.values()) {
|
|
809
|
+
const { spawn } = candidate;
|
|
810
|
+
if (candidate.tombstoneReason || terminalIntents.has(spawn.id) || candidate.turnCount < 1) continue;
|
|
811
|
+
const childSessionFile = await this.#resolvePersistedChild(sessionFile, spawn);
|
|
812
|
+
if (!childSessionFile) continue;
|
|
813
|
+
const key = scopeKey(scope, spawn.id);
|
|
814
|
+
if (this.#records.has(key)) continue;
|
|
815
|
+
const existing = AgentRegistry.global().get(spawn.id);
|
|
816
|
+
const existingIsResumable =
|
|
817
|
+
existing?.kind === "sub" &&
|
|
818
|
+
existing.parentId === scope.ownerId &&
|
|
819
|
+
existing.sessionFile === childSessionFile &&
|
|
820
|
+
(existing.status === "idle" || existing.status === "parked");
|
|
821
|
+
const blockedByCollision = Boolean(existing && !existingIsResumable);
|
|
822
|
+
const { agent, modelOverride } = this.#resolveWorker(session, spawn.cli);
|
|
823
|
+
if (!existing) {
|
|
824
|
+
AgentRegistry.global().register({
|
|
825
|
+
id: spawn.id,
|
|
826
|
+
displayName: spawn.id,
|
|
827
|
+
kind: "sub",
|
|
828
|
+
parentId: scope.ownerId,
|
|
829
|
+
session: null,
|
|
830
|
+
sessionFile: childSessionFile,
|
|
831
|
+
status: "parked",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
this.#records.set(key, {
|
|
835
|
+
id: spawn.id,
|
|
836
|
+
cli: spawn.cli,
|
|
837
|
+
ownerId: scope.ownerId,
|
|
838
|
+
parentSessionId: scope.parentSessionId,
|
|
839
|
+
parentSessionFile: scope.parentSessionFile,
|
|
840
|
+
childSessionFile,
|
|
841
|
+
agent,
|
|
842
|
+
modelOverride,
|
|
843
|
+
state: "idle",
|
|
844
|
+
createdAt: spawn.createdAt,
|
|
845
|
+
lastActivityAt: candidate.lastActivityAt,
|
|
846
|
+
lastActivity: blockedByCollision
|
|
847
|
+
? "blocked by an agent id collision"
|
|
848
|
+
: candidate.inFlight
|
|
849
|
+
? `turn ${candidate.turnCount} interrupted by process restart`
|
|
850
|
+
: undefined,
|
|
851
|
+
queue: [],
|
|
852
|
+
turnCount: candidate.turnCount,
|
|
853
|
+
killed: false,
|
|
854
|
+
suspended: false,
|
|
855
|
+
terminalPersisted: false,
|
|
856
|
+
});
|
|
857
|
+
restored++;
|
|
858
|
+
}
|
|
859
|
+
return restored;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Spawn a persistent worker session and start its first turn in the background. */
|
|
863
|
+
async spawn(session: ToolSession, args: { cli: VibeCli; name?: string; prompt: string }): Promise<VibeSpawnOutcome> {
|
|
864
|
+
const scope = this.ownerScope(session);
|
|
865
|
+
return this.#withTerminationLock(scope, () => this.#spawnLocked(session, scope, args));
|
|
866
|
+
}
|
|
317
867
|
|
|
868
|
+
async #spawnLocked(
|
|
869
|
+
session: ToolSession,
|
|
870
|
+
scope: VibeOwnerScope,
|
|
871
|
+
args: { cli: VibeCli; name?: string; prompt: string },
|
|
872
|
+
): Promise<VibeSpawnOutcome> {
|
|
873
|
+
if (this.#terminatedScopes.has(scopeKey(scope, ""))) {
|
|
874
|
+
throw new ToolError("Vibe mode has exited; enter Vibe mode again before spawning a worker.");
|
|
875
|
+
}
|
|
876
|
+
const manager = this.#manager(session);
|
|
877
|
+
const { agent, modelOverride } = this.#resolveWorker(session, args.cli);
|
|
318
878
|
if (!session.agentOutputManager) {
|
|
319
879
|
session.agentOutputManager = new AgentOutputManager(session.getArtifactsDir ?? (() => null));
|
|
320
880
|
}
|
|
881
|
+
const reservedIds = this.#persistedIds(session, scope);
|
|
882
|
+
for (const ref of AgentRegistry.global().list()) reservedIds.add(ref.id);
|
|
883
|
+
await session.agentOutputManager.reserve(reservedIds);
|
|
321
884
|
const requestedName = args.name?.replace(/[^A-Za-z0-9_-]+/g, "").slice(0, 48);
|
|
322
885
|
const id = await session.agentOutputManager.allocate(requestedName || generateTaskName());
|
|
323
|
-
|
|
886
|
+
const parentSessionFile = scope.parentSessionFile;
|
|
887
|
+
const childSessionName = `${id}.jsonl`;
|
|
888
|
+
const childSessionFile = parentSessionFile
|
|
889
|
+
? path.resolve(parentSessionFile.slice(0, -6), childSessionName)
|
|
890
|
+
: undefined;
|
|
891
|
+
const createdAt = Date.now();
|
|
324
892
|
const record: VibeRecord = {
|
|
325
893
|
id,
|
|
326
894
|
cli: args.cli,
|
|
327
|
-
ownerId:
|
|
895
|
+
ownerId: scope.ownerId,
|
|
896
|
+
parentSessionId: scope.parentSessionId,
|
|
897
|
+
parentSessionFile,
|
|
898
|
+
childSessionFile,
|
|
328
899
|
agent,
|
|
329
900
|
modelOverride,
|
|
330
901
|
state: "starting",
|
|
331
|
-
createdAt
|
|
332
|
-
lastActivityAt:
|
|
902
|
+
createdAt,
|
|
903
|
+
lastActivityAt: createdAt,
|
|
333
904
|
queue: [],
|
|
334
905
|
turnCount: 0,
|
|
335
906
|
killed: false,
|
|
907
|
+
suspended: false,
|
|
908
|
+
terminalPersisted: false,
|
|
336
909
|
};
|
|
337
|
-
|
|
338
|
-
|
|
910
|
+
const key = scopeKey(scope, id);
|
|
911
|
+
this.#records.set(key, record);
|
|
912
|
+
let spawnPersisted = false;
|
|
339
913
|
try {
|
|
914
|
+
if (childSessionFile) {
|
|
915
|
+
spawnPersisted = await this.#appendLifecycleEvent(
|
|
916
|
+
session,
|
|
917
|
+
{
|
|
918
|
+
...this.#eventBase(record),
|
|
919
|
+
action: "spawn",
|
|
920
|
+
cli: args.cli,
|
|
921
|
+
agent: agent.name,
|
|
922
|
+
childSessionFile: childSessionName,
|
|
923
|
+
createdAt,
|
|
924
|
+
},
|
|
925
|
+
record.parentSessionFile,
|
|
926
|
+
);
|
|
927
|
+
if (!spawnPersisted) throw new ToolError("Vibe parent session changed before the worker could start.");
|
|
928
|
+
}
|
|
340
929
|
const jobId = this.#registerTurnJob(session, manager, record, args.prompt, { first: true });
|
|
341
930
|
return { id, jobId };
|
|
342
931
|
} catch (error) {
|
|
343
|
-
|
|
932
|
+
record.killed = true;
|
|
933
|
+
record.state = "dead";
|
|
934
|
+
record.lastActivityAt = Date.now();
|
|
935
|
+
record.lastActivity = "spawn failed";
|
|
936
|
+
if (childSessionFile) {
|
|
937
|
+
// A rejected terminal write leaves this dead record in the map so mode exit can retry it.
|
|
938
|
+
record.terminalPersisted = await this.#appendTombstone(session, record, "spawn-failed");
|
|
939
|
+
if (!record.terminalPersisted) {
|
|
940
|
+
throw new ToolError("Vibe parent session changed before spawn failure could be persisted.");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
this.#records.delete(key);
|
|
344
944
|
throw error;
|
|
345
945
|
}
|
|
346
946
|
}
|
|
@@ -351,16 +951,20 @@ export class VibeSessionRegistry {
|
|
|
351
951
|
* background turn immediately.
|
|
352
952
|
*/
|
|
353
953
|
async send(session: ToolSession, args: { session: string; message: string }): Promise<VibeSendOutcome> {
|
|
354
|
-
const
|
|
355
|
-
const record = this.#record(
|
|
954
|
+
const scope = this.ownerScope(session);
|
|
955
|
+
const record = this.#record(scope, args.session);
|
|
356
956
|
if (record.state === "dead") {
|
|
357
957
|
throw new ToolError(`Vibe session "${record.id}" is dead. Spawn a new one with vibe_spawn.`);
|
|
358
958
|
}
|
|
359
959
|
const message = args.message.trim();
|
|
360
960
|
if (!message) throw new ToolError("Message must not be empty.");
|
|
961
|
+
const registered = this.#registeredAgent(record);
|
|
962
|
+
if (AgentRegistry.global().get(record.id) && !registered) {
|
|
963
|
+
throw new ToolError(`Vibe session "${record.id}" no longer resolves to this parent session.`);
|
|
964
|
+
}
|
|
361
965
|
|
|
362
966
|
if (record.turn) {
|
|
363
|
-
const live =
|
|
967
|
+
const live = registered?.session;
|
|
364
968
|
if (live?.isStreaming) {
|
|
365
969
|
await live.steer(message);
|
|
366
970
|
record.lastActivityAt = Date.now();
|
|
@@ -371,6 +975,10 @@ export class VibeSessionRegistry {
|
|
|
371
975
|
return { id: record.id, mode: "queued" };
|
|
372
976
|
}
|
|
373
977
|
|
|
978
|
+
if (!registered || (registered.status !== "idle" && registered.status !== "parked")) {
|
|
979
|
+
throw new ToolError(`Vibe session "${record.id}" no longer resolves to this parent session.`);
|
|
980
|
+
}
|
|
981
|
+
|
|
374
982
|
const manager = this.#manager(session);
|
|
375
983
|
const jobId = this.#registerTurnJob(session, manager, record, message, { first: false });
|
|
376
984
|
return { id: record.id, mode: "turn", jobId };
|
|
@@ -386,14 +994,14 @@ export class VibeSessionRegistry {
|
|
|
386
994
|
session: ToolSession,
|
|
387
995
|
args: { sessions?: string[]; timeoutMs?: number; signal?: AbortSignal },
|
|
388
996
|
): Promise<VibeWaitOutcome> {
|
|
389
|
-
const
|
|
997
|
+
const scope = this.ownerScope(session);
|
|
390
998
|
const manager = this.#manager(session);
|
|
391
999
|
// Named sessions are watched regardless of state (a just-settled turn is
|
|
392
1000
|
// reported from its retained job); the no-args form watches every
|
|
393
1001
|
// session with a turn actually in flight.
|
|
394
1002
|
const watched = args.sessions?.length
|
|
395
|
-
? args.sessions.map(id => this.#record(
|
|
396
|
-
: [...this.#records.values()].filter(record => record
|
|
1003
|
+
? args.sessions.map(id => this.#record(scope, id))
|
|
1004
|
+
: [...this.#records.values()].filter(record => matchesScope(record, scope) && record.turn !== undefined);
|
|
397
1005
|
|
|
398
1006
|
// Snapshot each watched turn's job at entry: #finishTurn installs a
|
|
399
1007
|
// queued follow-up turn inside the settling job's callback (before that
|
|
@@ -461,41 +1069,179 @@ export class VibeSessionRegistry {
|
|
|
461
1069
|
return { settled, stillRunning, timedOut: waited && settled.length === 0 };
|
|
462
1070
|
}
|
|
463
1071
|
|
|
464
|
-
/**
|
|
1072
|
+
/** Detach one parent's process-local workers without tombstoning their persisted conversations. */
|
|
1073
|
+
async suspendScope(scope: VibeOwnerScope, manager?: AsyncJobManager): Promise<number> {
|
|
1074
|
+
const records = [...this.#records.values()].filter(record => matchesScope(record, scope));
|
|
1075
|
+
const teardown = records.map(record => ({
|
|
1076
|
+
record,
|
|
1077
|
+
ref: this.#registeredAgent(record),
|
|
1078
|
+
job: record.turn && manager ? manager.getJob(record.turn.jobId) : undefined,
|
|
1079
|
+
}));
|
|
1080
|
+
for (const { record } of teardown) {
|
|
1081
|
+
record.suspended = true;
|
|
1082
|
+
record.queue.length = 0;
|
|
1083
|
+
record.state = "dead";
|
|
1084
|
+
record.lastActivityAt = Date.now();
|
|
1085
|
+
record.lastActivity = "suspended for parent-session switch";
|
|
1086
|
+
this.#records.delete(scopeKey(scope, record.id));
|
|
1087
|
+
if (record.turn && manager) manager.cancel(record.turn.jobId, { ownerId: record.ownerId });
|
|
1088
|
+
}
|
|
1089
|
+
for (const { record, ref } of teardown) {
|
|
1090
|
+
if (!ref) continue;
|
|
1091
|
+
try {
|
|
1092
|
+
await AgentLifecycleManager.global().release(record.id, ref);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
logger.warn("vibe: failed to detach worker session", {
|
|
1095
|
+
id: record.id,
|
|
1096
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
await awaitCancelledTurnJobs(new Set(teardown.flatMap(entry => (entry.job ? [entry.job] : []))));
|
|
1101
|
+
return records.length;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Terminate one worker; a tombstone failure still tears it down before reconciliation and error delivery. */
|
|
465
1105
|
async kill(session: ToolSession, id: string): Promise<VibeKillOutcome> {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
1106
|
+
const scope = this.ownerScope(session);
|
|
1107
|
+
return this.#withTerminationLock(scope, () => {
|
|
1108
|
+
const record = this.#record(scope, id);
|
|
1109
|
+
return this.#killRecord(record, session.asyncJobManager, session, "explicit-kill");
|
|
1110
|
+
});
|
|
469
1111
|
}
|
|
470
1112
|
|
|
471
|
-
/** Kill every session
|
|
472
|
-
async killAll(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1113
|
+
/** Kill every live session in one parent scope after durably recording the complete mode-exit intent. */
|
|
1114
|
+
async killAll(session: VibeParentSession, ownerScope?: VibeOwnerScope): Promise<number> {
|
|
1115
|
+
const scope = ownerScope ?? this.ownerScope(session);
|
|
1116
|
+
return this.#withTerminationLock(scope, async () => {
|
|
1117
|
+
try {
|
|
1118
|
+
const killed = await this.#killAllLocked(session, scope);
|
|
1119
|
+
this.#terminatedScopes.add(scopeKey(scope, ""));
|
|
1120
|
+
return killed;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error instanceof SessionPersistenceIndeterminateError) {
|
|
1123
|
+
this.#terminatedScopes.add(scopeKey(scope, ""));
|
|
1124
|
+
}
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async #killAllLocked(session: VibeParentSession, scope: VibeOwnerScope): Promise<number> {
|
|
1131
|
+
const records = [...this.#records.values()].filter(
|
|
1132
|
+
record => matchesScope(record, scope) && !(record.state === "dead" && record.terminalPersisted),
|
|
1133
|
+
);
|
|
1134
|
+
if (records.length === 0) {
|
|
1135
|
+
const entries = session.sessionManager?.getBranch() ?? [];
|
|
1136
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
1137
|
+
const entry = entries[index];
|
|
1138
|
+
if (entry.type !== "mode_change") continue;
|
|
1139
|
+
if (entry.mode === "none") return 0;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
await this.#persistModeExit(session, scope, records);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (error instanceof SessionPersistenceIndeterminateError) {
|
|
1147
|
+
for (const record of records) {
|
|
1148
|
+
await this.#killRecord(record, session.asyncJobManager, session, "mode-exit", false);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
throw error;
|
|
1152
|
+
}
|
|
1153
|
+
for (const record of records) {
|
|
1154
|
+
await this.#killRecord(record, session.asyncJobManager, session, "mode-exit");
|
|
478
1155
|
}
|
|
479
|
-
return
|
|
1156
|
+
return records.length;
|
|
480
1157
|
}
|
|
481
1158
|
|
|
482
|
-
async #killRecord(
|
|
1159
|
+
async #killRecord(
|
|
1160
|
+
record: VibeRecord,
|
|
1161
|
+
manager: AsyncJobManager | undefined,
|
|
1162
|
+
session: VibeParentSession,
|
|
1163
|
+
reason: VibeTombstoneReason,
|
|
1164
|
+
persistTerminal = true,
|
|
1165
|
+
): Promise<VibeKillOutcome> {
|
|
1166
|
+
const registered = this.#registeredAgent(record);
|
|
1167
|
+
const settlingJobs = new Set<AsyncJob>();
|
|
1168
|
+
if (record.turn && manager) {
|
|
1169
|
+
const job = manager.getJob(record.turn.jobId);
|
|
1170
|
+
if (job) settlingJobs.add(job);
|
|
1171
|
+
}
|
|
1172
|
+
let persistenceError: unknown;
|
|
1173
|
+
if (persistTerminal && !record.terminalPersisted) {
|
|
1174
|
+
try {
|
|
1175
|
+
if (record.killed) {
|
|
1176
|
+
const recover = session.sessionManager?.recoverPersistenceFromCurrentState;
|
|
1177
|
+
if (!recover) throw new ToolError("Vibe tombstone recovery requires parent-session persistence.");
|
|
1178
|
+
await recover.call(session.sessionManager);
|
|
1179
|
+
}
|
|
1180
|
+
if (!this.#hasInMemoryTombstone(session, record) && record.childSessionFile) {
|
|
1181
|
+
if (!(await this.#appendTombstone(session, record, reason))) {
|
|
1182
|
+
throw new ToolError(`Vibe session "${record.id}" changed parent scope before termination.`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
record.terminalPersisted = true;
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
persistenceError = error;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
483
1190
|
record.killed = true;
|
|
484
1191
|
record.queue.length = 0;
|
|
485
1192
|
let cancelledTurn = false;
|
|
486
1193
|
if (record.turn && manager) {
|
|
1194
|
+
const job = manager.getJob(record.turn.jobId);
|
|
1195
|
+
if (job) settlingJobs.add(job);
|
|
487
1196
|
cancelledTurn = manager.cancel(record.turn.jobId, { ownerId: record.ownerId });
|
|
488
1197
|
}
|
|
489
1198
|
record.state = "dead";
|
|
490
1199
|
record.lastActivityAt = Date.now();
|
|
491
1200
|
record.lastActivity = "killed";
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1201
|
+
if (registered) {
|
|
1202
|
+
try {
|
|
1203
|
+
await AgentLifecycleManager.global().release(record.id, registered);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
logger.warn("vibe: failed to release worker session", {
|
|
1206
|
+
id: record.id,
|
|
1207
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
await awaitCancelledTurnJobs(settlingJobs);
|
|
1212
|
+
const terminalRef = registered ?? this.#registeredAgent(record) ?? null;
|
|
1213
|
+
if (record.childSessionFile) {
|
|
1214
|
+
try {
|
|
1215
|
+
const persisted = await SessionManager.peekSessionInit(record.childSessionFile);
|
|
1216
|
+
if (persisted?.init)
|
|
1217
|
+
await this.#markTerminalRef(record.id, record.ownerId, record.childSessionFile, terminalRef);
|
|
1218
|
+
} catch {
|
|
1219
|
+
// A turn killed before session initialization has no readable history to retain.
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (persistenceError) {
|
|
1223
|
+
let finalPersistenceError = persistenceError;
|
|
1224
|
+
const recover = session.sessionManager?.recoverPersistenceFromCurrentState;
|
|
1225
|
+
if (recover) {
|
|
1226
|
+
try {
|
|
1227
|
+
await recover.call(session.sessionManager);
|
|
1228
|
+
if (!this.#hasInMemoryTombstone(session, record) && record.childSessionFile) {
|
|
1229
|
+
if (!(await this.#appendTombstone(session, record, reason))) {
|
|
1230
|
+
throw new ToolError(`Vibe session "${record.id}" changed parent scope before termination.`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
record.terminalPersisted = true;
|
|
1234
|
+
} catch (recoveryError) {
|
|
1235
|
+
if (recoveryError instanceof SessionPersistenceIndeterminateError) {
|
|
1236
|
+
finalPersistenceError = recoveryError;
|
|
1237
|
+
}
|
|
1238
|
+
logger.warn("vibe: failed to reconcile explicit tombstone persistence", {
|
|
1239
|
+
id: record.id,
|
|
1240
|
+
error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
throw finalPersistenceError;
|
|
499
1245
|
}
|
|
500
1246
|
return { id: record.id, cancelledTurn };
|
|
501
1247
|
}
|
|
@@ -601,6 +1347,18 @@ export class VibeSessionRegistry {
|
|
|
601
1347
|
record.turnCount = turnIndex;
|
|
602
1348
|
record.lastActivityAt = Date.now();
|
|
603
1349
|
try {
|
|
1350
|
+
const turnStartedPersisted = await this.#appendLifecycleEvent(
|
|
1351
|
+
session,
|
|
1352
|
+
{
|
|
1353
|
+
...this.#eventBase(record),
|
|
1354
|
+
action: "turn-started",
|
|
1355
|
+
turn: turnIndex,
|
|
1356
|
+
},
|
|
1357
|
+
record.parentSessionFile,
|
|
1358
|
+
);
|
|
1359
|
+
if (record.childSessionFile && !turnStartedPersisted) {
|
|
1360
|
+
throw new ToolError(`Vibe session "${record.id}" changed parent scope before its turn started.`);
|
|
1361
|
+
}
|
|
604
1362
|
const result = options.first
|
|
605
1363
|
? await runSubprocess(await this.#buildSpawnOptions(session, record, message, signal, onProgress))
|
|
606
1364
|
: await runSubagentFollowUpTurn({
|
|
@@ -613,10 +1371,10 @@ export class VibeSessionRegistry {
|
|
|
613
1371
|
eventBus: session.eventBus,
|
|
614
1372
|
artifactsDir: session.getSessionFile()?.slice(0, -6),
|
|
615
1373
|
});
|
|
616
|
-
return this.#settleTurn(session, manager, record, turn, ownJobId, turnIndex, result);
|
|
1374
|
+
return await this.#settleTurn(session, manager, record, turn, ownJobId, turnIndex, result);
|
|
617
1375
|
} catch (error) {
|
|
618
1376
|
if (error instanceof VibeTurnError) throw error;
|
|
619
|
-
this.#finishTurn(session, manager, record, ownJobId);
|
|
1377
|
+
await this.#finishTurn(session, manager, record, ownJobId);
|
|
620
1378
|
const reason = error instanceof Error ? error.message : String(error);
|
|
621
1379
|
record.lastActivity = firstLine(`turn failed: ${reason}`);
|
|
622
1380
|
throw new VibeTurnError(
|
|
@@ -632,19 +1390,41 @@ export class VibeSessionRegistry {
|
|
|
632
1390
|
}
|
|
633
1391
|
|
|
634
1392
|
/** Post-turn bookkeeping shared by success and failure paths: clear the in-flight turn, flush the queue. */
|
|
635
|
-
#finishTurn(
|
|
1393
|
+
async #finishTurn(
|
|
1394
|
+
session: ToolSession,
|
|
1395
|
+
manager: AsyncJobManager,
|
|
1396
|
+
record: VibeRecord,
|
|
1397
|
+
settledJobId: string,
|
|
1398
|
+
): Promise<void> {
|
|
636
1399
|
record.lastJobId = settledJobId;
|
|
637
1400
|
record.turn = undefined;
|
|
638
1401
|
record.live = undefined;
|
|
639
1402
|
record.lastActivityAt = Date.now();
|
|
640
|
-
if (record.killed) {
|
|
1403
|
+
if (record.killed || record.suspended) {
|
|
1404
|
+
record.state = "dead";
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
// Only an idle/parked ref with this parent's exact child file is resumable.
|
|
1408
|
+
const registered = this.#registeredAgent(record);
|
|
1409
|
+
record.state = registered && (registered.status === "idle" || registered.status === "parked") ? "idle" : "dead";
|
|
1410
|
+
if (record.state === "dead") {
|
|
1411
|
+
record.terminalPersisted = await this.#appendTombstone(session, record, "unrecoverable");
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const settledPersisted = await this.#appendLifecycleEvent(
|
|
1415
|
+
session,
|
|
1416
|
+
{
|
|
1417
|
+
...this.#eventBase(record),
|
|
1418
|
+
action: "turn-settled",
|
|
1419
|
+
turn: record.turnCount,
|
|
1420
|
+
},
|
|
1421
|
+
record.parentSessionFile,
|
|
1422
|
+
);
|
|
1423
|
+
if (record.childSessionFile && !settledPersisted) {
|
|
641
1424
|
record.state = "dead";
|
|
642
1425
|
return;
|
|
643
1426
|
}
|
|
644
|
-
|
|
645
|
-
// to continue — mark the record dead so sends fail with clear guidance.
|
|
646
|
-
record.state = AgentRegistry.global().get(record.id) ? "idle" : "dead";
|
|
647
|
-
if (record.state === "dead" || record.queue.length === 0) return;
|
|
1427
|
+
if (record.queue.length === 0) return;
|
|
648
1428
|
const nextMessage = record.queue.splice(0, record.queue.length).join("\n\n");
|
|
649
1429
|
try {
|
|
650
1430
|
this.#registerTurnJob(session, manager, record, nextMessage, { first: false });
|
|
@@ -659,7 +1439,7 @@ export class VibeSessionRegistry {
|
|
|
659
1439
|
}
|
|
660
1440
|
|
|
661
1441
|
/** Format a settled turn into the self-delivering result text (activity trace + response). */
|
|
662
|
-
#settleTurn(
|
|
1442
|
+
async #settleTurn(
|
|
663
1443
|
session: ToolSession,
|
|
664
1444
|
manager: AsyncJobManager,
|
|
665
1445
|
record: VibeRecord,
|
|
@@ -667,8 +1447,8 @@ export class VibeSessionRegistry {
|
|
|
667
1447
|
settledJobId: string,
|
|
668
1448
|
turnIndex: number,
|
|
669
1449
|
result: SingleResult,
|
|
670
|
-
): string {
|
|
671
|
-
this.#finishTurn(session, manager, record, settledJobId);
|
|
1450
|
+
): Promise<string> {
|
|
1451
|
+
await this.#finishTurn(session, manager, record, settledJobId);
|
|
672
1452
|
const failed = result.exitCode !== 0 || result.aborted === true;
|
|
673
1453
|
const status = result.aborted ? "aborted" : failed ? "failed" : "completed";
|
|
674
1454
|
record.lastActivity = firstLine(
|
|
@@ -742,7 +1522,7 @@ export class VibeSessionRegistry {
|
|
|
742
1522
|
* computed identically to the main session's rate.
|
|
743
1523
|
*/
|
|
744
1524
|
export function aggregateVibeWorkerTokensPerSecond(ownerId: string): number | null {
|
|
745
|
-
const ids = VibeSessionRegistry.global().
|
|
1525
|
+
const ids = VibeSessionRegistry.global().listIdsByOwner(ownerId);
|
|
746
1526
|
if (ids.length === 0) return null;
|
|
747
1527
|
let total = 0;
|
|
748
1528
|
let any = false;
|