@gajae-code/coding-agent 0.16.7 → 0.17.1
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 +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -142,6 +142,13 @@ export interface SessionIndexEvent {
|
|
|
142
142
|
endpointFileId?: string;
|
|
143
143
|
lifecycleRequestId?: string;
|
|
144
144
|
terminalUncertain?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Distinguishes the terminal-uncertain claim written by a forced stop of a
|
|
147
|
+
* stale-endpoint session from the one written by the fail-closed tail of an
|
|
148
|
+
* ordinary teardown. Only the forced claim releases the session's worktree;
|
|
149
|
+
* see `worktreeOccupant`.
|
|
150
|
+
*/
|
|
151
|
+
forcedStaleRelease?: boolean;
|
|
145
152
|
/** OS process incarnation (C1); absent on legacy v1/v2 events. */
|
|
146
153
|
hostIncarnation?: string;
|
|
147
154
|
/** Present on host_heartbeat checkpoints (C2). */
|
|
@@ -164,6 +171,8 @@ export interface IndexedSession {
|
|
|
164
171
|
indexSeq: number;
|
|
165
172
|
lifecycleRequestId?: string;
|
|
166
173
|
terminalUncertain?: boolean;
|
|
174
|
+
/** True only for a terminal-uncertain claim written by a forced stale-worktree release. */
|
|
175
|
+
forcedStaleRelease?: boolean;
|
|
167
176
|
hostIncarnation?: string;
|
|
168
177
|
identityProvenance: SessionIdentityProvenance;
|
|
169
178
|
activity?: SessionActivity;
|
|
@@ -598,6 +607,7 @@ function projectIdentity(
|
|
|
598
607
|
endpointFileId: latest.endpointFileId,
|
|
599
608
|
lifecycleRequestId: latest.lifecycleRequestId,
|
|
600
609
|
terminalUncertain,
|
|
610
|
+
...(latest.forcedStaleRelease === true ? { forcedStaleRelease: true } : {}),
|
|
601
611
|
indexSeq: latest.indexSeq,
|
|
602
612
|
hostIncarnation: latest.hostIncarnation,
|
|
603
613
|
masterRole: latest.masterRole,
|
|
@@ -879,21 +889,27 @@ function compactEvents(events: SessionIndexEvent[], policy: ResolvedRetentionPol
|
|
|
879
889
|
}
|
|
880
890
|
if (policy.maxRows >= 1 && kept.length > policy.maxRows) {
|
|
881
891
|
const keptLatest = new Map<string, SessionIndexEvent>();
|
|
892
|
+
const rowsBySession = new Map<string, number>();
|
|
882
893
|
for (const event of kept) {
|
|
883
894
|
const previous = keptLatest.get(event.sessionId);
|
|
884
895
|
if (previous === undefined || event.indexSeq > previous.indexSeq) keptLatest.set(event.sessionId, event);
|
|
896
|
+
rowsBySession.set(event.sessionId, (rowsBySession.get(event.sessionId) ?? 0) + 1);
|
|
885
897
|
}
|
|
886
898
|
const anchorSession = kept.find(event => event.indexSeq === maxIndexSeq)?.sessionId;
|
|
887
899
|
const candidates = [...keptLatest.entries()]
|
|
888
900
|
.filter(([sessionId]) => sessionId !== anchorSession)
|
|
889
901
|
.filter(([, latest]) => !(policy.tombstoneRule === "retain" && latest.type === "session_deleted"))
|
|
890
902
|
.sort((a, b) => a[1].indexSeq - b[1].indexSeq);
|
|
891
|
-
let
|
|
903
|
+
let remaining = kept.length;
|
|
904
|
+
const evicted = new Set<string>();
|
|
892
905
|
for (const [sessionId] of candidates) {
|
|
893
|
-
if (
|
|
894
|
-
|
|
906
|
+
if (remaining <= policy.maxRows) break;
|
|
907
|
+
evicted.add(sessionId);
|
|
908
|
+
remaining -= rowsBySession.get(sessionId)!;
|
|
895
909
|
}
|
|
896
|
-
|
|
910
|
+
// Evict whole sessions in the same order without repeatedly copying the
|
|
911
|
+
// surviving history for every victim while rotation holds the index lock.
|
|
912
|
+
return kept.filter(event => !evicted.has(event.sessionId));
|
|
897
913
|
}
|
|
898
914
|
return kept;
|
|
899
915
|
}
|
|
@@ -1233,8 +1249,59 @@ export class SessionIndex {
|
|
|
1233
1249
|
// captured here (refresh() does this via #refreshUnderLock).
|
|
1234
1250
|
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1235
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Parse/checksum history before contending for the global lock (#5438). Under
|
|
1254
|
+
* the lock, prove both file identities and the exact prepared bytes still
|
|
1255
|
+
* match. Growth is safe only after comparing the whole prefix, not merely
|
|
1256
|
+
* its size/mtime: a same-inode rewrite followed by append must replay.
|
|
1257
|
+
* There is no optimistic retry loop; replacement or corruption falls back
|
|
1258
|
+
* to the authoritative locked replay and existing quarantine repair.
|
|
1259
|
+
*/
|
|
1260
|
+
async #replayPreparedAppendUnderLock(scan: SessionIndexScan, prior: SessionIndexChangeStamp): Promise<void> {
|
|
1261
|
+
const stamp = await readIndexChangeStamp(this.#agentDir);
|
|
1262
|
+
if (
|
|
1263
|
+
scan.diagnosis.status !== "healthy" ||
|
|
1264
|
+
stamp.snapshot.exists !== prior.snapshot.exists ||
|
|
1265
|
+
stamp.snapshot.ino !== prior.snapshot.ino ||
|
|
1266
|
+
stamp.log.exists !== prior.log.exists ||
|
|
1267
|
+
stamp.log.ino !== prior.log.ino
|
|
1268
|
+
) {
|
|
1269
|
+
await this.#replayUnderLock();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const read = async (file: string): Promise<Buffer | undefined> => {
|
|
1273
|
+
try {
|
|
1274
|
+
return await fs.readFile(file);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
1277
|
+
throw error;
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
const [snapshot, log] = await Promise.all([read(snapshotFor(this.#agentDir)), read(logFor(this.#agentDir))]);
|
|
1281
|
+
const sameSnapshot =
|
|
1282
|
+
snapshot === undefined
|
|
1283
|
+
? scan.snapshotContents === undefined
|
|
1284
|
+
: scan.snapshotContents !== undefined && snapshot.equals(scan.snapshotContents);
|
|
1285
|
+
const samePrefix =
|
|
1286
|
+
log === undefined
|
|
1287
|
+
? scan.logContents === undefined
|
|
1288
|
+
: scan.logContents !== undefined &&
|
|
1289
|
+
log.length >= scan.logContents.length &&
|
|
1290
|
+
log.subarray(0, scan.logContents.length).equals(scan.logContents);
|
|
1291
|
+
if (!sameSnapshot || !samePrefix) {
|
|
1292
|
+
await this.#replayUnderLock();
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
this.#adoptScan(scan);
|
|
1296
|
+
if ((log?.length ?? 0) > this.#logOffset) await this.#tailUnderLock(this.indexSeq, true, true);
|
|
1297
|
+
}
|
|
1236
1298
|
async #replayUnderLock(): Promise<void> {
|
|
1237
1299
|
const scan = await this.#scan();
|
|
1300
|
+
this.#adoptScan(scan);
|
|
1301
|
+
await this.#writeAuditUnderLock();
|
|
1302
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1303
|
+
}
|
|
1304
|
+
#adoptScan(scan: SessionIndexScan): void {
|
|
1238
1305
|
if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
|
|
1239
1306
|
this.#events = [...scan.snapshotEvents, ...scan.validLogEvents];
|
|
1240
1307
|
this.#warnings = [];
|
|
@@ -1245,8 +1312,6 @@ export class SessionIndex {
|
|
|
1245
1312
|
this.#corruptSuffix = scan.diagnosis.status === "corrupt";
|
|
1246
1313
|
if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
|
|
1247
1314
|
if (this.#corruptSuffix) this.#warnings.push("Corrupt session index entry; replay truncated");
|
|
1248
|
-
await this.#writeAuditUnderLock();
|
|
1249
|
-
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1250
1315
|
}
|
|
1251
1316
|
/** Seed the audit dedupe set once, then append records for newly-rejected events. */
|
|
1252
1317
|
async #writeAuditUnderLock(): Promise<void> {
|
|
@@ -1472,7 +1537,7 @@ export class SessionIndex {
|
|
|
1472
1537
|
await this.#replayUnderLock();
|
|
1473
1538
|
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
1474
1539
|
}
|
|
1475
|
-
async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true): Promise<void> {
|
|
1540
|
+
async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true, strictSequence = false): Promise<void> {
|
|
1476
1541
|
let data: Buffer;
|
|
1477
1542
|
try {
|
|
1478
1543
|
const handle = await fs.open(logFor(this.#agentDir), "r");
|
|
@@ -1508,10 +1573,19 @@ export class SessionIndex {
|
|
|
1508
1573
|
corrupt = true;
|
|
1509
1574
|
continue;
|
|
1510
1575
|
}
|
|
1511
|
-
|
|
1576
|
+
// Valid JSON need not be an event object. In particular, null must
|
|
1577
|
+
// reach replay/repair rather than throw while reading its fields.
|
|
1578
|
+
if (event === null || typeof event !== "object" || Array.isArray(event)) {
|
|
1579
|
+
corrupt = true;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (corrupt || (!strictSequence && event.indexSeq <= snapshotSeq)) continue;
|
|
1512
1583
|
const { checksum, ...unsigned } = event;
|
|
1513
1584
|
if (checksum !== sessionIndexChecksum(unsigned) || event.indexSeq !== this.indexSeq + 1) corrupt = true;
|
|
1514
|
-
else
|
|
1585
|
+
else {
|
|
1586
|
+
this.#events.push(event);
|
|
1587
|
+
if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
|
|
1588
|
+
}
|
|
1515
1589
|
}
|
|
1516
1590
|
if (hasUnterminatedSuffix) corrupt = true;
|
|
1517
1591
|
if (corrupt) {
|
|
@@ -1563,8 +1637,10 @@ export class SessionIndex {
|
|
|
1563
1637
|
input.pid > 0
|
|
1564
1638
|
)
|
|
1565
1639
|
ownIncarnation = input.processIncarnation ?? processIncarnation(input.pid);
|
|
1640
|
+
const preparedStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1641
|
+
const preparedScan = await this.#scan();
|
|
1566
1642
|
return await withSessionIndexLock("append", this.#agentDir, async () => {
|
|
1567
|
-
await this.#
|
|
1643
|
+
await this.#replayPreparedAppendUnderLock(preparedScan, preparedStamp);
|
|
1568
1644
|
if (this.#corruptSuffix) {
|
|
1569
1645
|
// A corrupt suffix used to hard-fail every append until a human ran
|
|
1570
1646
|
// `gjc gc --repair-session-index` — but the writers that corrupt the
|
|
@@ -1596,14 +1672,36 @@ export class SessionIndex {
|
|
|
1596
1672
|
if (ownIncarnation !== undefined && unsigned.hostIncarnation === undefined)
|
|
1597
1673
|
unsigned.hostIncarnation = ownIncarnation;
|
|
1598
1674
|
const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1675
|
+
const serialized = JSON.stringify(event);
|
|
1676
|
+
// Publish to memory only after the complete append and fsync succeed.
|
|
1677
|
+
// No need to reread/checksum the row this lock holder just wrote.
|
|
1678
|
+
await appendSync(logFor(this.#agentDir), serialized);
|
|
1679
|
+
// Keep caller-owned input/return objects separate from durable state.
|
|
1680
|
+
this.#events.push(JSON.parse(serialized) as SessionIndexEvent);
|
|
1681
|
+
this.#logOffset += Buffer.byteLength(serialized) + 1;
|
|
1682
|
+
if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
|
|
1683
|
+
await this.#writeAuditUnderLock();
|
|
1684
|
+
if (this.#logOffset >= ROTATE_BYTES) await this.#rotate();
|
|
1685
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1602
1686
|
return event;
|
|
1603
1687
|
});
|
|
1604
1688
|
});
|
|
1605
1689
|
}
|
|
1606
|
-
async unregisterIfCurrent(
|
|
1690
|
+
async unregisterIfCurrent(
|
|
1691
|
+
expected: Pick<
|
|
1692
|
+
SessionIndexEvent,
|
|
1693
|
+
| "sessionId"
|
|
1694
|
+
| "locator"
|
|
1695
|
+
| "endpointGeneration"
|
|
1696
|
+
| "pid"
|
|
1697
|
+
| "indexSeq"
|
|
1698
|
+
| "endpointMtimeMs"
|
|
1699
|
+
| "endpointFileId"
|
|
1700
|
+
| "lifecycleRequestId"
|
|
1701
|
+
| "processIncarnation"
|
|
1702
|
+
| "hostIncarnation"
|
|
1703
|
+
>,
|
|
1704
|
+
): Promise<boolean> {
|
|
1607
1705
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1608
1706
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
1609
1707
|
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
@@ -1632,6 +1730,7 @@ export class SessionIndex {
|
|
|
1632
1730
|
session.endpointGeneration === expected.endpointGeneration &&
|
|
1633
1731
|
session.pid === expected.pid &&
|
|
1634
1732
|
session.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
1733
|
+
session.endpointFileId === expected.endpointFileId &&
|
|
1635
1734
|
session.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
1636
1735
|
session.processIncarnation === expected.processIncarnation &&
|
|
1637
1736
|
(session.hostIncarnation ?? session.processIncarnation) ===
|
|
@@ -1678,6 +1777,7 @@ export class SessionIndex {
|
|
|
1678
1777
|
: { processIncarnation: expected.processIncarnation }),
|
|
1679
1778
|
...(expected.hostIncarnation === undefined ? {} : { hostIncarnation: expected.hostIncarnation }),
|
|
1680
1779
|
...(expected.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: expected.endpointMtimeMs }),
|
|
1780
|
+
...(expected.endpointFileId === undefined ? {} : { endpointFileId: expected.endpointFileId }),
|
|
1681
1781
|
...(expected.lifecycleRequestId === undefined
|
|
1682
1782
|
? {}
|
|
1683
1783
|
: { lifecycleRequestId: expected.lifecycleRequestId }),
|
|
@@ -1747,15 +1847,23 @@ export class SessionIndex {
|
|
|
1747
1847
|
);
|
|
1748
1848
|
}
|
|
1749
1849
|
async #rotate(): Promise<void> {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1850
|
+
// Every caller has already replayed/refreshed under this same lock.
|
|
1851
|
+
// Replaying before snapshot and again after rotation needlessly parses
|
|
1852
|
+
// the entire history multiple times in the append critical section.
|
|
1853
|
+
const events = compactEvents(this.#events, this.#policy);
|
|
1854
|
+
await replaceAtomically(
|
|
1855
|
+
snapshotFor(this.#agentDir),
|
|
1856
|
+
JSON.stringify({ version: SESSION_INDEX_SNAPSHOT_VERSION, indexSeq: this.indexSeq, events }),
|
|
1857
|
+
);
|
|
1858
|
+
await replaceAtomically(logFor(this.#agentDir), "");
|
|
1859
|
+
this.#events = events;
|
|
1753
1860
|
this.#logOffset = 0;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1861
|
+
this.#corruptSuffix = false;
|
|
1862
|
+
this.#warnings = [];
|
|
1863
|
+
for (const event of events) {
|
|
1864
|
+
if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
|
|
1865
|
+
}
|
|
1866
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1759
1867
|
}
|
|
1760
1868
|
|
|
1761
1869
|
listSessions(): SessionList {
|
|
@@ -2002,6 +2110,7 @@ export class SessionIndex {
|
|
|
2002
2110
|
| "processIncarnation"
|
|
2003
2111
|
| "hostIncarnation"
|
|
2004
2112
|
| "endpointMtimeMs"
|
|
2113
|
+
| "endpointFileId"
|
|
2005
2114
|
| "lifecycleRequestId"
|
|
2006
2115
|
>,
|
|
2007
2116
|
): { type: "host_unregistered" | "session_closed" | "session_deleted"; indexSeq: number } | undefined {
|
|
@@ -2023,6 +2132,7 @@ export class SessionIndex {
|
|
|
2023
2132
|
event.type === "session_closed" ||
|
|
2024
2133
|
event.type === "session_deleted") &&
|
|
2025
2134
|
(event.hostIncarnation ?? event.processIncarnation) === expectedIncarnation &&
|
|
2135
|
+
event.endpointFileId === expected.endpointFileId &&
|
|
2026
2136
|
(supersededAt === undefined || event.indexSeq < supersededAt) &&
|
|
2027
2137
|
!followsApplicableTombstone(
|
|
2028
2138
|
this.#events,
|
|
@@ -2071,6 +2181,7 @@ export class SessionIndex {
|
|
|
2071
2181
|
| "processIncarnation"
|
|
2072
2182
|
| "hostIncarnation"
|
|
2073
2183
|
| "endpointMtimeMs"
|
|
2184
|
+
| "endpointFileId"
|
|
2074
2185
|
| "lifecycleRequestId"
|
|
2075
2186
|
>,
|
|
2076
2187
|
): number | undefined {
|
|
@@ -2088,6 +2199,7 @@ export class SessionIndex {
|
|
|
2088
2199
|
return this.#events.findLast(
|
|
2089
2200
|
event =>
|
|
2090
2201
|
event.type === "session_closed" &&
|
|
2202
|
+
event.endpointFileId === expected.endpointFileId &&
|
|
2091
2203
|
(supersededAt === undefined || event.indexSeq < supersededAt) &&
|
|
2092
2204
|
!followsApplicableTombstone(
|
|
2093
2205
|
this.#events,
|
|
@@ -2236,6 +2348,7 @@ export class SessionIndex {
|
|
|
2236
2348
|
| "lifecycleRequestId"
|
|
2237
2349
|
| "hostIncarnation"
|
|
2238
2350
|
| "processIncarnation"
|
|
2351
|
+
| "endpointFileId"
|
|
2239
2352
|
>,
|
|
2240
2353
|
): { indexSeq: number; lifecycleRequestId?: string } | undefined {
|
|
2241
2354
|
const lifecycleRequestId = registration.lifecycleRequestId;
|
|
@@ -2247,6 +2360,7 @@ export class SessionIndex {
|
|
|
2247
2360
|
item.sessionId === registration.sessionId &&
|
|
2248
2361
|
item.endpointGeneration === registration.endpointGeneration &&
|
|
2249
2362
|
item.pid === registration.pid &&
|
|
2363
|
+
item.endpointFileId === registration.endpointFileId &&
|
|
2250
2364
|
resolveEquivalentPath(item.locator.cwd) === resolveEquivalentPath(registration.locator.cwd) &&
|
|
2251
2365
|
path.resolve(item.locator.stateRoot) === path.resolve(registration.locator.stateRoot) &&
|
|
2252
2366
|
(lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId) &&
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type ManagedTmuxLaunchProof,
|
|
14
14
|
verifyManagedGjcTmuxSession,
|
|
15
15
|
} from "../../gjc-runtime/tmux-sessions";
|
|
16
|
-
import { processIncarnation } from "./process-incarnation";
|
|
16
|
+
import { observeProcessIncarnation, processIncarnation } from "./process-incarnation";
|
|
17
17
|
import type { SpawnSubstrateLaunchSpec, SpawnSubstrateProof, SpawnSubstrateProvider } from "./spawn-authority";
|
|
18
18
|
|
|
19
19
|
export type SpawnMultiplexerSelection = "none" | "tmux" | "psmux" | "proof_failed";
|
|
@@ -249,6 +249,9 @@ function startHeadless(spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv):
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
function defaultIsProcessGone(pid: number): boolean {
|
|
252
|
+
const observation = observeProcessIncarnation(pid);
|
|
253
|
+
if (observation.status === "absent") return true;
|
|
254
|
+
if (observation.status === "present") return false;
|
|
252
255
|
try {
|
|
253
256
|
process.kill(pid, 0);
|
|
254
257
|
return false;
|
|
@@ -15,7 +15,12 @@ const BROKER_OPERATIONS = new Set([
|
|
|
15
15
|
"session.close",
|
|
16
16
|
"session.delete",
|
|
17
17
|
"session.reconcile_uncertain",
|
|
18
|
+
"session.lookup",
|
|
18
19
|
"broker.shutdown",
|
|
20
|
+
"broker.status",
|
|
21
|
+
"broker.prepare_restart",
|
|
22
|
+
"broker.commit_restart",
|
|
23
|
+
"broker.cancel_restart",
|
|
19
24
|
"session.control",
|
|
20
25
|
"broker.lookup_lifecycle",
|
|
21
26
|
"model.resolve",
|
|
@@ -67,6 +67,20 @@ export function observeProcess(
|
|
|
67
67
|
* `uncertain` counts as occupied: refusing a launch is recoverable by picking
|
|
68
68
|
* another worktree name, whereas two live sessions sharing a checkout corrupts
|
|
69
69
|
* work already done.
|
|
70
|
+
*
|
|
71
|
+
* A row carrying BOTH `terminalUncertain` and `forcedStaleRelease` is the one
|
|
72
|
+
* exception. That pair is written only by the forced stop of a stale-endpoint
|
|
73
|
+
* session: teardown was requested explicitly, so parking its worktree until an
|
|
74
|
+
* OS probe happens to prove exit would leave the checkout locked indefinitely
|
|
75
|
+
* (#5581) exactly when the probe returns `uncertain`. Such a row is released
|
|
76
|
+
* regardless of the process observation.
|
|
77
|
+
*
|
|
78
|
+
* `terminalUncertain` on its own is not enough. The fail-closed tail of a
|
|
79
|
+
* signal-escalated teardown sets it after SIGKILL without ever proving the child
|
|
80
|
+
* gone, so that process may still be running in the checkout; releasing on the
|
|
81
|
+
* flag alone would let a second session into the same worktree. Those rows stay
|
|
82
|
+
* under the strict "only definitive exit releases" rule like every other
|
|
83
|
+
* ordinary retained row.
|
|
70
84
|
*/
|
|
71
85
|
export function worktreeOccupant(
|
|
72
86
|
sessions: readonly IndexedSession[],
|
|
@@ -79,7 +93,13 @@ export function worktreeOccupant(
|
|
|
79
93
|
const target = resolveEquivalentPath(worktreePath);
|
|
80
94
|
for (const session of sessions) {
|
|
81
95
|
const sessionWorktreeRoot = session.locator.worktreeRoot;
|
|
82
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
session.terminal ||
|
|
98
|
+
(session.terminalUncertain === true && session.forcedStaleRelease === true) ||
|
|
99
|
+
typeof sessionWorktreeRoot !== "string" ||
|
|
100
|
+
sessionWorktreeRoot.length === 0
|
|
101
|
+
)
|
|
102
|
+
continue;
|
|
83
103
|
if (resolveEquivalentPath(sessionWorktreeRoot) !== target) continue;
|
|
84
104
|
// `live` is heartbeat-derived and can be stale. Only positive process-exit
|
|
85
105
|
// evidence releases a matching retained session's worktree.
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
type ChatDaemonKind,
|
|
11
11
|
chatDaemonGeneration,
|
|
12
12
|
clearChatDaemonControlRequest,
|
|
13
|
+
clearChatDoctorControlRequest,
|
|
13
14
|
hasSafeChatDaemonStateShape,
|
|
14
15
|
readChatDaemonControlRequest,
|
|
15
16
|
readChatDaemonState,
|
|
17
|
+
readChatDoctorControlRequest,
|
|
16
18
|
releaseChatDaemonOwnership,
|
|
17
19
|
renewChatDaemonHeartbeat,
|
|
18
20
|
} from "./chat-daemon-control";
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
notificationConfigFromFile,
|
|
25
27
|
resolveNotificationProvider,
|
|
26
28
|
} from "./config";
|
|
29
|
+
import { doctorDaemonIdentityMatches, doctorDaemonOccupancySettled } from "./doctor-daemon-restart";
|
|
27
30
|
|
|
28
31
|
export interface ChatDaemonRuntimeHandle {
|
|
29
32
|
start(): Promise<void>;
|
|
@@ -31,6 +34,26 @@ export interface ChatDaemonRuntimeHandle {
|
|
|
31
34
|
transportHealthy?(): boolean;
|
|
32
35
|
/** Executes operator commands that must run inside the owning daemon. */
|
|
33
36
|
bindExistingRoot?(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
|
|
37
|
+
prepareDoctorRestart?(): void;
|
|
38
|
+
cancelDoctorRestart?(): void;
|
|
39
|
+
doctorOccupancy?(): {
|
|
40
|
+
attached: number;
|
|
41
|
+
inflight: number;
|
|
42
|
+
inbound: number;
|
|
43
|
+
outbound: number;
|
|
44
|
+
cleanup: number;
|
|
45
|
+
};
|
|
46
|
+
doctorRestartReady?(): boolean;
|
|
47
|
+
doctorRestartStatus?(
|
|
48
|
+
identity: {
|
|
49
|
+
owner: "discord" | "slack";
|
|
50
|
+
ownerId: string;
|
|
51
|
+
generation: number;
|
|
52
|
+
incarnation: string;
|
|
53
|
+
},
|
|
54
|
+
requestId: string,
|
|
55
|
+
leaseExpiresAt: number,
|
|
56
|
+
): unknown;
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
export interface RunChatDaemonInternalDeps {
|
|
@@ -237,7 +260,47 @@ export async function runChatDaemonInternal(
|
|
|
237
260
|
);
|
|
238
261
|
};
|
|
239
262
|
const bindExistingRoot = activeRuntime.bindExistingRoot?.bind(activeRuntime);
|
|
263
|
+
let doctorPreparedRequestId: string | undefined;
|
|
240
264
|
while (!stopping) {
|
|
265
|
+
const doctorRequest = await readChatDoctorControlRequest(agentDir, kind);
|
|
266
|
+
if (doctorRequest) {
|
|
267
|
+
const identity = {
|
|
268
|
+
owner: kind,
|
|
269
|
+
ownerId,
|
|
270
|
+
generation: chatDaemonGeneration(kind),
|
|
271
|
+
incarnation: ownedIncarnation,
|
|
272
|
+
} as const;
|
|
273
|
+
// The shared predicate is the single definition of "this request addresses
|
|
274
|
+
// exactly me"; spelling the four fields out here would let this owner and
|
|
275
|
+
// the doctor client drift apart silently.
|
|
276
|
+
if (
|
|
277
|
+
doctorDaemonIdentityMatches(doctorRequest, identity) &&
|
|
278
|
+
(doctorRequest.leaseExpiresAt === undefined || doctorRequest.leaseExpiresAt > Date.now())
|
|
279
|
+
) {
|
|
280
|
+
if (doctorRequest.action === "prepare") {
|
|
281
|
+
activeRuntime.prepareDoctorRestart?.();
|
|
282
|
+
doctorPreparedRequestId = doctorRequest.requestId;
|
|
283
|
+
} else if (doctorRequest.action === "cancel" && doctorPreparedRequestId === doctorRequest.requestId) {
|
|
284
|
+
activeRuntime.cancelDoctorRestart?.();
|
|
285
|
+
await clearChatDoctorControlRequest(agentDir, kind, doctorRequest.requestId);
|
|
286
|
+
doctorPreparedRequestId = undefined;
|
|
287
|
+
} else if (doctorRequest.action === "commit" && doctorPreparedRequestId === doctorRequest.requestId) {
|
|
288
|
+
const occupancy = activeRuntime.doctorOccupancy?.();
|
|
289
|
+
if (
|
|
290
|
+
occupancy !== undefined &&
|
|
291
|
+
doctorDaemonOccupancySettled(occupancy) &&
|
|
292
|
+
activeRuntime.doctorRestartReady?.()
|
|
293
|
+
) {
|
|
294
|
+
// Keep committed intent durable until the restart
|
|
295
|
+
// coordinator proves old death/cleanup and successor
|
|
296
|
+
// publication.
|
|
297
|
+
stopping = true;
|
|
298
|
+
await stopRuntime();
|
|
299
|
+
doctorPreparedRequestId = undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
241
304
|
const request = await readChatDaemonControlRequest(agentDir, kind);
|
|
242
305
|
if (request?.ownerId === ownerId && request.incarnation === incarnation) {
|
|
243
306
|
await clearChatDaemonControlRequest(agentDir, kind, request.requestId);
|