@sayknow-cli/coding-agent 0.3.12 → 0.3.15
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/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -1,13 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import type { AssistantMessage } from "@sayknow-cli/ai";
|
|
5
|
-
import {
|
|
6
|
+
import { postmortem } from "@sayknow-cli/utils";
|
|
6
7
|
import { sessionRuntimeDir } from "./session-layout";
|
|
8
|
+
import {
|
|
9
|
+
isValidOwnerIntent,
|
|
10
|
+
lifecyclePaths,
|
|
11
|
+
type ObserveTerminalRequest,
|
|
12
|
+
type OwnerIntent,
|
|
13
|
+
type OwnerVerdict,
|
|
14
|
+
observeOwnerTerminal,
|
|
15
|
+
type TerminalSignal,
|
|
16
|
+
} from "./tmux-owner-isolation";
|
|
7
17
|
|
|
18
|
+
/** Managed tmux owner provenance propagated only to the launched child process. */
|
|
19
|
+
export const SKC_TMUX_OWNER_GENERATION_ENV = "SKC_TMUX_OWNER_GENERATION";
|
|
20
|
+
export const SKC_TMUX_OWNER_STATE_DIR_ENV = "SKC_TMUX_OWNER_STATE_DIR";
|
|
21
|
+
export const SKC_TMUX_OWNER_SERVER_KEY_ENV = "SKC_TMUX_OWNER_SERVER_KEY";
|
|
8
22
|
export const SKC_COORDINATOR_SESSION_STATE_FILE_ENV = "SKC_COORDINATOR_SESSION_STATE_FILE";
|
|
9
23
|
export const SKC_COORDINATOR_SESSION_ID_ENV = "SKC_COORDINATOR_SESSION_ID";
|
|
10
24
|
export const SKC_COORDINATOR_SESSION_BRANCH_ENV = "SKC_COORDINATOR_SESSION_BRANCH";
|
|
25
|
+
export const SKC_COORDINATOR_SESSION_LAUNCH_ID_ENV = "SKC_COORDINATOR_SESSION_LAUNCH_ID";
|
|
26
|
+
export const SKC_COORDINATOR_SESSION_READINESS_FILE_ENV = "SKC_COORDINATOR_SESSION_READINESS_FILE";
|
|
27
|
+
|
|
28
|
+
export type RuntimeInputReadyMarker = Readonly<{
|
|
29
|
+
schema_version: 1;
|
|
30
|
+
session_id: string;
|
|
31
|
+
launch_id: string;
|
|
32
|
+
state: "ready_for_input";
|
|
33
|
+
event: "interactive_input_ready";
|
|
34
|
+
source: "skc_interactive_runtime";
|
|
35
|
+
ready_for_input: true;
|
|
36
|
+
created_at: string;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
11
39
|
const SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV = "SKC_SESSION_PROMPT_ACCEPTED_JSON";
|
|
12
40
|
const SKC_SESSION_WORKTREE_BASELINE_DIRTY_ENV = "SKC_SESSION_WORKTREE_BASELINE_DIRTY";
|
|
13
41
|
|
|
@@ -19,6 +47,7 @@ const HEARTBEAT_MS = 1000;
|
|
|
19
47
|
|
|
20
48
|
type LastPayloadCacheEntry = { mtimeMs: number; size: number; payload: Record<string, unknown> };
|
|
21
49
|
const lastPayloadByStateFile = new Map<string, LastPayloadCacheEntry>();
|
|
50
|
+
const stateFileWriteChains = new Map<string, Promise<void>>();
|
|
22
51
|
|
|
23
52
|
/** Test-only counters for runtime sidecar hot-path assertions. */
|
|
24
53
|
export const __sessionStateSidecarPerfCounters = {
|
|
@@ -28,18 +57,30 @@ export const __sessionStateSidecarPerfCounters = {
|
|
|
28
57
|
},
|
|
29
58
|
};
|
|
30
59
|
|
|
31
|
-
const lastWrittenPayloadByStateFile = new Map<string, Record<string, unknown>>();
|
|
32
|
-
|
|
33
60
|
interface RuntimeStateEvent {
|
|
34
61
|
type: string;
|
|
35
62
|
messages?: unknown[];
|
|
36
63
|
}
|
|
37
64
|
|
|
38
|
-
interface
|
|
65
|
+
export interface OwnerTerminalContext {
|
|
66
|
+
generation: string;
|
|
67
|
+
stateDir: string;
|
|
68
|
+
socketKey: string;
|
|
69
|
+
scope?: string | null;
|
|
70
|
+
ownerPid?: number | null;
|
|
71
|
+
ownerName?: string | null;
|
|
72
|
+
operatorDispatchId?: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RuntimeStateContext {
|
|
39
76
|
sessionId: string;
|
|
40
77
|
cwd: string;
|
|
41
78
|
sessionFile?: string | null;
|
|
42
79
|
branch?: string | null;
|
|
80
|
+
/** Public-safe owner metadata used to persist the canonical terminal verdict. */
|
|
81
|
+
ownerTerminal?: OwnerTerminalContext | null;
|
|
82
|
+
/** Internal fail-closed marker set only when managed owner metadata is malformed or missing. */
|
|
83
|
+
ownerTerminalMetadataInvalid?: boolean;
|
|
43
84
|
}
|
|
44
85
|
|
|
45
86
|
interface RuntimeStateSidecarPayload {
|
|
@@ -60,19 +101,166 @@ export type TerminalRuntimeStateStatus =
|
|
|
60
101
|
reason:
|
|
61
102
|
| "missing_state_file"
|
|
62
103
|
| "invalid_json"
|
|
104
|
+
| "invalid_state_marker"
|
|
63
105
|
| "session_id_mismatch"
|
|
64
106
|
| "cwd_mismatch"
|
|
65
107
|
| "session_file_mismatch"
|
|
66
108
|
| "non_terminal_state";
|
|
67
109
|
};
|
|
68
110
|
|
|
111
|
+
function runtimeReadinessMarkerConflict(): Error {
|
|
112
|
+
const error = new Error("runtime_readiness_marker_conflict");
|
|
113
|
+
Object.assign(error, { code: "runtime_readiness_marker_conflict" });
|
|
114
|
+
return error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isRuntimeInputReadyMarker(value: unknown): value is RuntimeInputReadyMarker {
|
|
118
|
+
if (!value || typeof value !== "object") return false;
|
|
119
|
+
const marker = value as Record<string, unknown>;
|
|
120
|
+
return (
|
|
121
|
+
marker.schema_version === 1 &&
|
|
122
|
+
typeof marker.session_id === "string" &&
|
|
123
|
+
typeof marker.launch_id === "string" &&
|
|
124
|
+
marker.state === "ready_for_input" &&
|
|
125
|
+
marker.event === "interactive_input_ready" &&
|
|
126
|
+
marker.source === "skc_interactive_runtime" &&
|
|
127
|
+
marker.ready_for_input === true &&
|
|
128
|
+
typeof marker.created_at === "string" &&
|
|
129
|
+
marker.created_at.length > 0 &&
|
|
130
|
+
Number.isFinite(Date.parse(marker.created_at))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function immutableRuntimeInputReadyMarker(marker: RuntimeInputReadyMarker): RuntimeInputReadyMarker {
|
|
135
|
+
return Object.freeze({ ...marker });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function readRuntimeInputReadyMarker(readinessFile: string): Promise<RuntimeInputReadyMarker | null> {
|
|
139
|
+
let text: string;
|
|
140
|
+
try {
|
|
141
|
+
text = await Bun.file(readinessFile).text();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const code = (error as { code?: unknown }).code;
|
|
144
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
145
|
+
throw runtimeReadinessMarkerConflict();
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const marker = JSON.parse(text) as unknown;
|
|
149
|
+
if (!isRuntimeInputReadyMarker(marker)) throw runtimeReadinessMarkerConflict();
|
|
150
|
+
return immutableRuntimeInputReadyMarker(marker);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if ((error as { code?: unknown }).code === "runtime_readiness_marker_conflict") throw error;
|
|
153
|
+
throw runtimeReadinessMarkerConflict();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function persistCoordinatorRuntimeInputReady(): Promise<RuntimeInputReadyMarker | null> {
|
|
158
|
+
const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
159
|
+
const sessionId = process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim();
|
|
160
|
+
const launchId = process.env[SKC_COORDINATOR_SESSION_LAUNCH_ID_ENV]?.trim();
|
|
161
|
+
const readinessFile = process.env[SKC_COORDINATOR_SESSION_READINESS_FILE_ENV]?.trim();
|
|
162
|
+
if (!stateFile || !sessionId || !launchId || !readinessFile) return null;
|
|
163
|
+
|
|
164
|
+
const expected = { sessionId, launchId };
|
|
165
|
+
const existing = await readRuntimeInputReadyMarker(readinessFile);
|
|
166
|
+
if (existing) {
|
|
167
|
+
if (existing.session_id !== expected.sessionId || existing.launch_id !== expected.launchId) {
|
|
168
|
+
throw runtimeReadinessMarkerConflict();
|
|
169
|
+
}
|
|
170
|
+
return existing;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const marker = immutableRuntimeInputReadyMarker({
|
|
174
|
+
schema_version: 1,
|
|
175
|
+
session_id: expected.sessionId,
|
|
176
|
+
launch_id: expected.launchId,
|
|
177
|
+
state: "ready_for_input",
|
|
178
|
+
event: "interactive_input_ready",
|
|
179
|
+
source: "skc_interactive_runtime",
|
|
180
|
+
ready_for_input: true,
|
|
181
|
+
created_at: new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
const tempFile = path.join(
|
|
184
|
+
path.dirname(readinessFile),
|
|
185
|
+
`.${path.basename(readinessFile)}.${process.pid}.${randomUUID()}.tmp`,
|
|
186
|
+
);
|
|
187
|
+
try {
|
|
188
|
+
await fs.mkdir(path.dirname(readinessFile), { recursive: true });
|
|
189
|
+
await fs.writeFile(tempFile, `${JSON.stringify(marker)}\n`, { flag: "wx" });
|
|
190
|
+
try {
|
|
191
|
+
await fs.link(tempFile, readinessFile);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if ((error as { code?: unknown }).code !== "EEXIST") throw runtimeReadinessMarkerConflict();
|
|
194
|
+
const raced = await readRuntimeInputReadyMarker(readinessFile);
|
|
195
|
+
if (raced && raced.session_id === expected.sessionId && raced.launch_id === expected.launchId) return raced;
|
|
196
|
+
throw runtimeReadinessMarkerConflict();
|
|
197
|
+
}
|
|
198
|
+
return marker;
|
|
199
|
+
} finally {
|
|
200
|
+
await fs.rm(tempFile, { force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
69
204
|
function sameResolvedPath(left: string, right: string): boolean {
|
|
70
205
|
return path.resolve(left) === path.resolve(right);
|
|
71
206
|
}
|
|
72
207
|
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
208
|
+
function normalizedIdentity(context: Pick<RuntimeStateContext, "sessionId" | "cwd" | "sessionFile">): {
|
|
209
|
+
sessionId: string;
|
|
210
|
+
cwd: string;
|
|
211
|
+
workdir: string;
|
|
212
|
+
sessionFile: string | null;
|
|
213
|
+
} {
|
|
214
|
+
const explicitStateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
215
|
+
const sessionId = explicitStateFile
|
|
216
|
+
? process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId.trim()
|
|
217
|
+
: context.sessionId.trim();
|
|
218
|
+
const cwd = context.cwd.trim();
|
|
219
|
+
if (!sessionId || !cwd) throw new PreviousRuntimeStateReadError();
|
|
220
|
+
return {
|
|
221
|
+
sessionId,
|
|
222
|
+
cwd: path.resolve(cwd),
|
|
223
|
+
workdir: path.resolve(cwd),
|
|
224
|
+
sessionFile: context.sessionFile == null ? null : path.resolve(context.sessionFile),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function serializeStateFileWrite<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
|
|
229
|
+
const prior = stateFileWriteChains.get(stateFile) ?? Promise.resolve();
|
|
230
|
+
const current = prior.catch(() => {}).then(operation);
|
|
231
|
+
const settled = current.then(
|
|
232
|
+
() => undefined,
|
|
233
|
+
() => undefined,
|
|
234
|
+
);
|
|
235
|
+
stateFileWriteChains.set(stateFile, settled);
|
|
236
|
+
try {
|
|
237
|
+
return await current;
|
|
238
|
+
} finally {
|
|
239
|
+
if (stateFileWriteChains.get(stateFile) === settled) stateFileWriteChains.delete(stateFile);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function validRuntimeStateMarker(value: unknown): value is RuntimeStateSidecarPayload {
|
|
244
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
245
|
+
const payload = value as RuntimeStateSidecarPayload;
|
|
246
|
+
return (
|
|
247
|
+
payload.schema_version === 1 &&
|
|
248
|
+
typeof payload.session_id === "string" &&
|
|
249
|
+
payload.session_id.trim().length > 0 &&
|
|
250
|
+
payload.session_id === payload.session_id.trim() &&
|
|
251
|
+
(payload.state === "ready_for_input" ||
|
|
252
|
+
payload.state === "running" ||
|
|
253
|
+
payload.state === "needs_user_input" ||
|
|
254
|
+
payload.state === "completed" ||
|
|
255
|
+
payload.state === "errored") &&
|
|
256
|
+
typeof payload.cwd === "string" &&
|
|
257
|
+
payload.cwd.trim().length > 0 &&
|
|
258
|
+
typeof payload.workdir === "string" &&
|
|
259
|
+
payload.workdir.trim().length > 0 &&
|
|
260
|
+
Object.hasOwn(payload, "session_file") &&
|
|
261
|
+
(payload.session_file === null ||
|
|
262
|
+
(typeof payload.session_file === "string" && payload.session_file.trim().length > 0))
|
|
263
|
+
);
|
|
76
264
|
}
|
|
77
265
|
|
|
78
266
|
export async function readTerminalRuntimeStateMarker(input: {
|
|
@@ -83,28 +271,34 @@ export async function readTerminalRuntimeStateMarker(input: {
|
|
|
83
271
|
}): Promise<TerminalRuntimeStateStatus> {
|
|
84
272
|
const stateFile = input.stateFile?.trim();
|
|
85
273
|
const sessionId = input.sessionId?.trim();
|
|
86
|
-
|
|
87
|
-
|
|
274
|
+
const cwd = input.cwd?.trim();
|
|
275
|
+
if (!stateFile || !sessionId || !cwd || input.sessionId !== sessionId)
|
|
276
|
+
return { terminal: false, reason: "missing_state_file" };
|
|
277
|
+
let value: unknown;
|
|
88
278
|
try {
|
|
89
|
-
|
|
279
|
+
value = JSON.parse(await Bun.file(stateFile).text());
|
|
90
280
|
} catch (error) {
|
|
91
281
|
const code = (error as { code?: unknown }).code;
|
|
92
282
|
return {
|
|
93
283
|
terminal: false,
|
|
94
|
-
reason: code === "ENOENT"
|
|
284
|
+
reason: code === "ENOENT" ? "missing_state_file" : "invalid_json",
|
|
95
285
|
};
|
|
96
286
|
}
|
|
287
|
+
if (!validRuntimeStateMarker(value)) return { terminal: false, reason: "invalid_state_marker" };
|
|
288
|
+
const payload = value;
|
|
97
289
|
if (payload.session_id !== sessionId) return { terminal: false, reason: "session_id_mismatch" };
|
|
98
|
-
if (
|
|
290
|
+
if (!sameResolvedPath(payload.cwd as string, cwd) || !sameResolvedPath(payload.workdir as string, cwd))
|
|
99
291
|
return { terminal: false, reason: "cwd_mismatch" };
|
|
100
|
-
|
|
292
|
+
const sessionFile = input.sessionFile == null ? null : path.resolve(input.sessionFile);
|
|
101
293
|
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
294
|
+
payload.session_file !== sessionFile &&
|
|
295
|
+
!(
|
|
296
|
+
typeof payload.session_file === "string" &&
|
|
297
|
+
typeof sessionFile === "string" &&
|
|
298
|
+
sameResolvedPath(payload.session_file, sessionFile)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
106
301
|
return { terminal: false, reason: "session_file_mismatch" };
|
|
107
|
-
}
|
|
108
302
|
if (payload.state === "completed" || payload.state === "errored") return { terminal: true, state: payload.state };
|
|
109
303
|
return { terminal: false, reason: "non_terminal_state" };
|
|
110
304
|
}
|
|
@@ -161,35 +355,105 @@ export function eventAffectsCoordinatorRuntimeState(event: RuntimeStateEvent): b
|
|
|
161
355
|
return stateForEvent(event) !== null;
|
|
162
356
|
}
|
|
163
357
|
|
|
358
|
+
class PreviousRuntimeStateReadError extends Error {
|
|
359
|
+
constructor() {
|
|
360
|
+
super("Existing runtime state marker is invalid or unreadable; refusing to overwrite.");
|
|
361
|
+
this.name = "PreviousRuntimeStateReadError";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isAbsentStateFileError(error: unknown): boolean {
|
|
366
|
+
return (error as { code?: unknown }).code === "ENOENT";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function parsePreviousPayload(raw: string): Record<string, unknown> {
|
|
370
|
+
const payload: unknown = JSON.parse(raw);
|
|
371
|
+
if (!validPreviousRuntimeStatePayload(payload)) throw new PreviousRuntimeStateReadError();
|
|
372
|
+
return payload;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function validPreviousRuntimeStatePayload(value: unknown): value is Record<string, unknown> {
|
|
376
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
377
|
+
const payload = value as Record<string, unknown>;
|
|
378
|
+
if (
|
|
379
|
+
payload.schema_version !== 1 ||
|
|
380
|
+
typeof payload.session_id !== "string" ||
|
|
381
|
+
payload.session_id.trim().length === 0 ||
|
|
382
|
+
(payload.state !== "booting" &&
|
|
383
|
+
payload.state !== "ready_for_input" &&
|
|
384
|
+
payload.state !== "running" &&
|
|
385
|
+
payload.state !== "needs_user_input" &&
|
|
386
|
+
payload.state !== "completed" &&
|
|
387
|
+
payload.state !== "errored" &&
|
|
388
|
+
payload.state !== "stale" &&
|
|
389
|
+
payload.state !== "unknown")
|
|
390
|
+
)
|
|
391
|
+
return false;
|
|
392
|
+
if (typeof payload.cwd !== "string" || payload.cwd.trim().length === 0) return false;
|
|
393
|
+
if (typeof payload.workdir !== "string" || payload.workdir.trim().length === 0) return false;
|
|
394
|
+
if (
|
|
395
|
+
!Object.hasOwn(payload, "session_file") ||
|
|
396
|
+
(payload.session_file !== null && typeof payload.session_file !== "string")
|
|
397
|
+
)
|
|
398
|
+
return false;
|
|
399
|
+
if (payload.ready_for_input !== undefined && typeof payload.ready_for_input !== "boolean") return false;
|
|
400
|
+
if (payload.live !== undefined && payload.live !== null && typeof payload.live !== "boolean") return false;
|
|
401
|
+
if (payload.reason !== undefined && payload.reason !== null && typeof payload.reason !== "string") return false;
|
|
402
|
+
if (
|
|
403
|
+
payload.updated_at !== undefined &&
|
|
404
|
+
(typeof payload.updated_at !== "string" || !Number.isFinite(Date.parse(payload.updated_at)))
|
|
405
|
+
)
|
|
406
|
+
return false;
|
|
407
|
+
if (payload.ready_for_input !== undefined) {
|
|
408
|
+
const expectedReady = payload.state === "ready_for_input" || payload.state === "completed";
|
|
409
|
+
if (payload.ready_for_input !== expectedReady) return false;
|
|
410
|
+
}
|
|
411
|
+
if (payload.live !== undefined && payload.live !== null && payload.live !== (payload.state === "running"))
|
|
412
|
+
return false;
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
164
416
|
function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
417
|
+
let raw: string;
|
|
165
418
|
try {
|
|
166
|
-
|
|
167
|
-
} catch {
|
|
168
|
-
|
|
419
|
+
raw = fsSync.readFileSync(stateFile, "utf8");
|
|
420
|
+
} catch (error) {
|
|
421
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
422
|
+
if (isAbsentStateFileError(error)) return {};
|
|
423
|
+
throw new PreviousRuntimeStateReadError();
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
return parsePreviousPayload(raw);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
429
|
+
if (error instanceof PreviousRuntimeStateReadError) throw error;
|
|
430
|
+
throw new PreviousRuntimeStateReadError();
|
|
169
431
|
}
|
|
170
432
|
}
|
|
171
433
|
|
|
172
434
|
async function readPreviousPayloadForEvent(stateFile: string): Promise<Record<string, unknown>> {
|
|
173
|
-
|
|
174
|
-
const cachedWritten = lastWrittenPayloadByStateFile.get(stateFile);
|
|
175
|
-
if (cachedWritten) return cachedWritten;
|
|
176
|
-
}
|
|
177
|
-
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
435
|
+
let stat: fsSync.Stats;
|
|
178
436
|
try {
|
|
179
437
|
stat = await fs.stat(stateFile);
|
|
180
|
-
} catch {
|
|
438
|
+
} catch (error) {
|
|
439
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
440
|
+
if (isAbsentStateFileError(error)) return {};
|
|
441
|
+
throw new PreviousRuntimeStateReadError();
|
|
442
|
+
}
|
|
443
|
+
if (!stat.isFile()) {
|
|
181
444
|
lastPayloadByStateFile.delete(stateFile);
|
|
182
|
-
|
|
445
|
+
throw new PreviousRuntimeStateReadError();
|
|
183
446
|
}
|
|
184
447
|
const cached = lastPayloadByStateFile.get(stateFile);
|
|
185
448
|
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.payload;
|
|
186
449
|
try {
|
|
187
|
-
const payload =
|
|
450
|
+
const payload = parsePreviousPayload(await Bun.file(stateFile).text());
|
|
188
451
|
lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
|
|
189
452
|
return payload;
|
|
190
|
-
} catch {
|
|
453
|
+
} catch (error) {
|
|
191
454
|
lastPayloadByStateFile.delete(stateFile);
|
|
192
|
-
|
|
455
|
+
if (error instanceof PreviousRuntimeStateReadError) throw error;
|
|
456
|
+
throw new PreviousRuntimeStateReadError();
|
|
193
457
|
}
|
|
194
458
|
}
|
|
195
459
|
|
|
@@ -213,7 +477,6 @@ function shouldSkipRuntimeStateWrite(
|
|
|
213
477
|
}
|
|
214
478
|
|
|
215
479
|
function rememberWrittenPayload(stateFile: string, payload: Record<string, unknown>): void {
|
|
216
|
-
lastWrittenPayloadByStateFile.set(stateFile, payload);
|
|
217
480
|
try {
|
|
218
481
|
const stat = fsSync.statSync(stateFile);
|
|
219
482
|
lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
|
|
@@ -223,30 +486,42 @@ function rememberWrittenPayload(stateFile: string, payload: Record<string, unkno
|
|
|
223
486
|
}
|
|
224
487
|
function shouldPreserveTerminalPayload(
|
|
225
488
|
previous: RuntimeStateSidecarPayload,
|
|
226
|
-
input: { sessionId: string; cwd: string; sessionFile
|
|
489
|
+
input: { sessionId: string; cwd: string; sessionFile: string | null },
|
|
227
490
|
): boolean {
|
|
491
|
+
if (!validRuntimeStateMarker(previous)) return false;
|
|
228
492
|
if (previous.state !== "completed" && previous.state !== "errored") return false;
|
|
229
493
|
const source = previous.final_response?.source;
|
|
230
494
|
if (source !== "agent_end" && source !== "launch_error") return false;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
input.sessionFile
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
return true;
|
|
495
|
+
return (
|
|
496
|
+
previous.session_id === input.sessionId &&
|
|
497
|
+
sameResolvedPath(previous.cwd as string, input.cwd) &&
|
|
498
|
+
sameResolvedPath(previous.workdir as string, input.cwd) &&
|
|
499
|
+
(previous.session_file === input.sessionFile ||
|
|
500
|
+
(typeof previous.session_file === "string" &&
|
|
501
|
+
typeof input.sessionFile === "string" &&
|
|
502
|
+
sameResolvedPath(previous.session_file, input.sessionFile)))
|
|
503
|
+
);
|
|
242
504
|
}
|
|
243
505
|
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
input: { sessionId: string; cwd: string; sessionFile
|
|
247
|
-
):
|
|
248
|
-
|
|
249
|
-
|
|
506
|
+
function assertPreviousRuntimeStateIdentity(
|
|
507
|
+
previous: Record<string, unknown>,
|
|
508
|
+
input: { sessionId: string; cwd: string; sessionFile: string | null },
|
|
509
|
+
): void {
|
|
510
|
+
if (Object.keys(previous).length === 0) return;
|
|
511
|
+
if (
|
|
512
|
+
previous.session_id !== input.sessionId ||
|
|
513
|
+
typeof previous.cwd !== "string" ||
|
|
514
|
+
typeof previous.workdir !== "string" ||
|
|
515
|
+
!sameResolvedPath(previous.cwd, input.cwd) ||
|
|
516
|
+
!sameResolvedPath(previous.workdir, input.cwd) ||
|
|
517
|
+
(previous.session_file !== input.sessionFile &&
|
|
518
|
+
!(
|
|
519
|
+
typeof previous.session_file === "string" &&
|
|
520
|
+
typeof input.sessionFile === "string" &&
|
|
521
|
+
sameResolvedPath(previous.session_file, input.sessionFile)
|
|
522
|
+
))
|
|
523
|
+
)
|
|
524
|
+
throw new PreviousRuntimeStateReadError();
|
|
250
525
|
}
|
|
251
526
|
|
|
252
527
|
function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
|
|
@@ -269,9 +544,11 @@ function basePayload(input: {
|
|
|
269
544
|
reason: string | null;
|
|
270
545
|
sessionId: string;
|
|
271
546
|
}): Record<string, unknown> {
|
|
547
|
+
const identity = normalizedIdentity(input.context);
|
|
548
|
+
if (identity.sessionId !== input.sessionId) throw new PreviousRuntimeStateReadError();
|
|
272
549
|
return {
|
|
273
550
|
schema_version: 1,
|
|
274
|
-
session_id:
|
|
551
|
+
session_id: identity.sessionId,
|
|
275
552
|
state: input.state,
|
|
276
553
|
ready_for_input: input.state === "completed" || input.state === "ready_for_input",
|
|
277
554
|
updated_at: input.now,
|
|
@@ -281,10 +558,11 @@ function basePayload(input: {
|
|
|
281
558
|
reason: input.reason,
|
|
282
559
|
source: input.source,
|
|
283
560
|
event: input.event,
|
|
284
|
-
cwd:
|
|
285
|
-
workdir:
|
|
561
|
+
cwd: identity.cwd,
|
|
562
|
+
workdir: identity.workdir,
|
|
286
563
|
branch: branchForContext(input.context),
|
|
287
|
-
session_file:
|
|
564
|
+
session_file: identity.sessionFile,
|
|
565
|
+
...(input.context.ownerTerminal ? { owner_generation: input.context.ownerTerminal.generation } : {}),
|
|
288
566
|
};
|
|
289
567
|
}
|
|
290
568
|
function booleanFromUnknown(value: unknown): boolean | null {
|
|
@@ -372,10 +650,10 @@ function postmortemExitDetails(
|
|
|
372
650
|
const observedChanges = observedRecoverableWorktreeChanges(typeof previous.cwd === "string" ? previous.cwd : cwd);
|
|
373
651
|
const worktreeBaselineDirty = worktreeBaselineDirtyFromEnvOrMarker();
|
|
374
652
|
const worktreeChangedSinceBaseline = worktreeBaselineDirty === false && observedChanges;
|
|
653
|
+
const previousStateIsTerminal = previous.state === "completed" || previous.state === "errored";
|
|
375
654
|
if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
|
|
376
655
|
const exitCode = numericProcessExitCode(0) ?? 0;
|
|
377
|
-
const exitedBeforeTerminalState =
|
|
378
|
-
exitCode === 0 && reason === postmortem.Reason.EXIT && previous.state === "running";
|
|
656
|
+
const exitedBeforeTerminalState = exitCode === 0 && reason === postmortem.Reason.EXIT && !previousStateIsTerminal;
|
|
379
657
|
const state: RuntimeState = exitCode === 0 && !exitedBeforeTerminalState ? "completed" : "errored";
|
|
380
658
|
const exitReason = exitedBeforeTerminalState
|
|
381
659
|
? "process_exit_before_terminal_state"
|
|
@@ -442,10 +720,129 @@ function postmortemExitDetails(
|
|
|
442
720
|
};
|
|
443
721
|
}
|
|
444
722
|
|
|
445
|
-
function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
723
|
+
async function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
724
|
+
await writeStateFile(stateFile, payload);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
interface StateFileLockOwner {
|
|
728
|
+
pid: number;
|
|
729
|
+
start_time: string;
|
|
730
|
+
token: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function processStartTime(pid: number): string | null {
|
|
734
|
+
try {
|
|
735
|
+
const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
736
|
+
const close = stat.lastIndexOf(")");
|
|
737
|
+
const fields = stat
|
|
738
|
+
.slice(close + 1)
|
|
739
|
+
.trim()
|
|
740
|
+
.split(/\s+/);
|
|
741
|
+
return fields[19] ?? null;
|
|
742
|
+
} catch {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function validLockOwner(value: unknown): value is StateFileLockOwner {
|
|
748
|
+
if (!value || typeof value !== "object") return false;
|
|
749
|
+
const owner = value as Partial<StateFileLockOwner>;
|
|
750
|
+
return (
|
|
751
|
+
typeof owner.pid === "number" &&
|
|
752
|
+
Number.isSafeInteger(owner.pid) &&
|
|
753
|
+
owner.pid > 0 &&
|
|
754
|
+
typeof owner.start_time === "string" &&
|
|
755
|
+
typeof owner.token === "string" &&
|
|
756
|
+
owner.token.length > 0
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function lockOwnerIsAlive(value: unknown): boolean {
|
|
761
|
+
if (!validLockOwner(value)) return false;
|
|
762
|
+
const owner = value;
|
|
763
|
+
try {
|
|
764
|
+
process.kill(owner.pid, 0);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return false;
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
const currentStartTime = processStartTime(owner.pid);
|
|
770
|
+
return currentStartTime === null || currentStartTime === owner.start_time;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function reclaimStaleStateFileLock(lockFile: string): Promise<void> {
|
|
774
|
+
let raw: string;
|
|
775
|
+
try {
|
|
776
|
+
raw = await fs.readFile(lockFile, "utf8");
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
let owner: unknown;
|
|
782
|
+
try {
|
|
783
|
+
owner = JSON.parse(raw);
|
|
784
|
+
} catch {
|
|
785
|
+
owner = null;
|
|
786
|
+
}
|
|
787
|
+
if (!validLockOwner(owner)) {
|
|
788
|
+
const stat = await fs.stat(lockFile);
|
|
789
|
+
if (Date.now() - stat.mtimeMs < 30_000) return;
|
|
790
|
+
} else if (lockOwnerIsAlive(owner)) return;
|
|
791
|
+
try {
|
|
792
|
+
if ((await fs.readFile(lockFile, "utf8")) === raw) await fs.rm(lockFile);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function withStateFileLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
|
|
799
|
+
const lockFile = `${stateFile}.lock`;
|
|
800
|
+
const owner: StateFileLockOwner = {
|
|
801
|
+
pid: process.pid,
|
|
802
|
+
start_time: processStartTime(process.pid) ?? "unknown",
|
|
803
|
+
token: randomUUID(),
|
|
804
|
+
};
|
|
805
|
+
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
806
|
+
for (let attempt = 0; attempt < 12_000; attempt++) {
|
|
807
|
+
let handle: fs.FileHandle | undefined;
|
|
808
|
+
try {
|
|
809
|
+
handle = await fs.open(lockFile, "wx");
|
|
810
|
+
try {
|
|
811
|
+
await handle.writeFile(JSON.stringify(owner));
|
|
812
|
+
} catch (error) {
|
|
813
|
+
await handle.close().catch(() => undefined);
|
|
814
|
+
handle = undefined;
|
|
815
|
+
await fs.rm(lockFile, { force: true }).catch(() => undefined);
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
const outcome = await operation().then(
|
|
819
|
+
value => ({ ok: true as const, value }),
|
|
820
|
+
error => ({ ok: false as const, error }),
|
|
821
|
+
);
|
|
822
|
+
await handle.close();
|
|
823
|
+
try {
|
|
824
|
+
if ((await fs.readFile(lockFile, "utf8")) === JSON.stringify(owner)) await fs.rm(lockFile);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
827
|
+
}
|
|
828
|
+
if (!outcome.ok) throw outcome.error;
|
|
829
|
+
return outcome.value;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (handle) throw error;
|
|
832
|
+
if ((error as { code?: unknown }).code !== "EEXIST") throw error;
|
|
833
|
+
await reclaimStaleStateFileLock(lockFile);
|
|
834
|
+
await Bun.sleep(5);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
throw new PreviousRuntimeStateReadError();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function coordinatorTransactionLockFile(stateFile: string): string {
|
|
841
|
+
return path.resolve(path.dirname(stateFile), "..", "locks", "mutation.lock");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function withCoordinatorTransactionLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
|
|
845
|
+
return await withStateFileLock(coordinatorTransactionLockFile(stateFile), operation);
|
|
449
846
|
}
|
|
450
847
|
|
|
451
848
|
async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
@@ -454,110 +851,334 @@ async function writeStateFile(stateFile: string, payload: Record<string, unknown
|
|
|
454
851
|
rememberWrittenPayload(stateFile, payload);
|
|
455
852
|
}
|
|
456
853
|
|
|
854
|
+
function contextWithManagedOwnerGeneration(context: RuntimeStateContext): RuntimeStateContext {
|
|
855
|
+
if (context.ownerTerminal) return context;
|
|
856
|
+
const ownerTerminal = ownerTerminalContextFromEnvironment();
|
|
857
|
+
if (ownerTerminal === "invalid") throw new PreviousRuntimeStateReadError();
|
|
858
|
+
return ownerTerminal ? { ...context, ownerTerminal } : context;
|
|
859
|
+
}
|
|
860
|
+
|
|
457
861
|
export async function persistCoordinatorRuntimeStateFromEvent(
|
|
458
862
|
event: RuntimeStateEvent,
|
|
459
863
|
context: RuntimeStateContext,
|
|
460
864
|
): Promise<void> {
|
|
461
865
|
__sessionStateSidecarPerfCounters.persistFromEventCalls += 1;
|
|
462
866
|
const stateFile = runtimeStateFileForContext(context);
|
|
463
|
-
if (!stateFile) return;
|
|
464
867
|
const state = stateForEvent(event);
|
|
465
|
-
if (!state) return;
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
868
|
+
if (!stateFile || !state) return;
|
|
869
|
+
context = contextWithManagedOwnerGeneration(context);
|
|
870
|
+
const identity = normalizedIdentity(context);
|
|
871
|
+
await serializeStateFileWrite(
|
|
872
|
+
stateFile,
|
|
873
|
+
async () =>
|
|
874
|
+
await withCoordinatorTransactionLock(
|
|
875
|
+
stateFile,
|
|
876
|
+
async () =>
|
|
877
|
+
await withStateFileLock(stateFile, async () => {
|
|
878
|
+
const nowMs = Date.now();
|
|
879
|
+
const now = new Date(nowMs).toISOString();
|
|
880
|
+
const previous = await readPreviousPayloadForEvent(stateFile);
|
|
881
|
+
assertPreviousRuntimeStateIdentity(previous, identity);
|
|
882
|
+
const payload = {
|
|
883
|
+
...basePayload({
|
|
884
|
+
context,
|
|
885
|
+
previous,
|
|
886
|
+
state,
|
|
887
|
+
now,
|
|
888
|
+
source: "agent_session_event",
|
|
889
|
+
event: event.type,
|
|
890
|
+
reason: null,
|
|
891
|
+
sessionId: identity.sessionId,
|
|
892
|
+
}),
|
|
893
|
+
...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
|
|
894
|
+
...(finalResponseForEvent(event) ? { final_response: finalResponseForEvent(event) } : {}),
|
|
895
|
+
...(state === "errored"
|
|
896
|
+
? { error: { code: "agent_error", message: "SKC agent reported an error", recoverable: true } }
|
|
897
|
+
: {}),
|
|
898
|
+
};
|
|
899
|
+
if (shouldSkipRuntimeStateWrite(previous, payload, nowMs)) return;
|
|
900
|
+
await writeStateFile(stateFile, payload);
|
|
901
|
+
}),
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function ownerTerminalSignal(reason: postmortem.Reason): TerminalSignal {
|
|
907
|
+
if (reason === postmortem.Reason.SIGTERM) return "SIGTERM";
|
|
908
|
+
if (reason === postmortem.Reason.SIGINT) return "SIGINT";
|
|
909
|
+
if (reason === postmortem.Reason.SIGHUP) return "SIGHUP";
|
|
910
|
+
if (reason === postmortem.Reason.EXIT) return "EXIT";
|
|
911
|
+
if (reason === postmortem.Reason.MANUAL) return "MANUAL";
|
|
912
|
+
return "UNKNOWN";
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function ownerTerminalPayload(verdict: OwnerVerdict, _owner: OwnerTerminalContext): Record<string, unknown> {
|
|
916
|
+
return {
|
|
917
|
+
generation: verdict.generation,
|
|
918
|
+
socket_key: verdict.server_key,
|
|
919
|
+
signal: verdict.signal,
|
|
920
|
+
result: verdict.result,
|
|
921
|
+
classification: verdict.classification,
|
|
922
|
+
observer: verdict.observer,
|
|
923
|
+
observed_at: verdict.observed_at,
|
|
924
|
+
...(verdict.intent_id ? { intent_id: verdict.intent_id } : {}),
|
|
925
|
+
dedupe_key: verdict.dedupe_key,
|
|
495
926
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function ownerTerminalContextFromEnvironment(): OwnerTerminalContext | "invalid" | null {
|
|
930
|
+
const generation = process.env[SKC_TMUX_OWNER_GENERATION_ENV];
|
|
931
|
+
const stateDir = process.env[SKC_TMUX_OWNER_STATE_DIR_ENV];
|
|
932
|
+
const socketKey = process.env[SKC_TMUX_OWNER_SERVER_KEY_ENV];
|
|
933
|
+
const supplied = [generation, stateDir, socketKey].some(value => value !== undefined);
|
|
934
|
+
const managedLaunch = process.platform === "linux" && process.env.SKC_TMUX_LAUNCHED === "1";
|
|
935
|
+
if (!supplied) return managedLaunch ? "invalid" : null;
|
|
936
|
+
const normalizedGeneration = generation?.trim();
|
|
937
|
+
const normalizedStateDir = stateDir?.trim();
|
|
938
|
+
const normalizedSocketKey = socketKey?.trim();
|
|
939
|
+
if (
|
|
940
|
+
!normalizedGeneration ||
|
|
941
|
+
!normalizedStateDir ||
|
|
942
|
+
!normalizedSocketKey ||
|
|
943
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalizedGeneration) ||
|
|
944
|
+
!path.isAbsolute(normalizedStateDir) ||
|
|
945
|
+
/[\u0000-\u001f\u007f]/.test(normalizedSocketKey)
|
|
946
|
+
) {
|
|
947
|
+
return "invalid";
|
|
502
948
|
}
|
|
949
|
+
return { generation: normalizedGeneration, stateDir: normalizedStateDir, socketKey: normalizedSocketKey };
|
|
503
950
|
}
|
|
504
951
|
|
|
505
|
-
|
|
952
|
+
async function persistInvalidOwnerTerminalMetadata(
|
|
506
953
|
reason: postmortem.Reason,
|
|
507
954
|
context: RuntimeStateContext,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
? process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
|
|
513
|
-
: context.sessionId;
|
|
514
|
-
const preserveInput = { sessionId, cwd: context.cwd, sessionFile: context.sessionFile };
|
|
515
|
-
const cachedTerminal = cachedTerminalPayload(stateFile, preserveInput);
|
|
516
|
-
const previous: Record<string, unknown> = cachedTerminal
|
|
517
|
-
? (cachedTerminal as unknown as Record<string, unknown>)
|
|
518
|
-
: readPreviousPayload(stateFile);
|
|
519
|
-
if (cachedTerminal || shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload, preserveInput)) return;
|
|
520
|
-
const previousForDetails: RuntimeStateSidecarPayload =
|
|
521
|
-
(previous as RuntimeStateSidecarPayload).state === "completed" ||
|
|
522
|
-
(previous as RuntimeStateSidecarPayload).state === "errored"
|
|
523
|
-
? { ...(previous as RuntimeStateSidecarPayload), state: "running" }
|
|
524
|
-
: (previous as RuntimeStateSidecarPayload);
|
|
955
|
+
stateFile: string,
|
|
956
|
+
sessionId: string,
|
|
957
|
+
previous: Record<string, unknown>,
|
|
958
|
+
): Promise<void> {
|
|
525
959
|
const now = new Date().toISOString();
|
|
526
|
-
|
|
527
|
-
const payload = {
|
|
960
|
+
await writeStateFileSync(stateFile, {
|
|
528
961
|
...basePayload({
|
|
529
962
|
context,
|
|
530
963
|
previous,
|
|
531
|
-
state:
|
|
964
|
+
state: "errored",
|
|
532
965
|
now,
|
|
533
966
|
source: "process_postmortem",
|
|
534
|
-
event: "
|
|
535
|
-
reason:
|
|
967
|
+
event: "owner_terminal",
|
|
968
|
+
reason: "owner_metadata_invalid",
|
|
536
969
|
sessionId,
|
|
537
970
|
}),
|
|
538
971
|
ended_at: now,
|
|
539
972
|
detected_at: now,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
973
|
+
signal: ownerTerminalSignal(reason),
|
|
974
|
+
error: {
|
|
975
|
+
code: "owner_metadata_invalid",
|
|
976
|
+
message: "SKC managed tmux owner metadata was unavailable or invalid",
|
|
977
|
+
recoverable: true,
|
|
978
|
+
},
|
|
979
|
+
recovery: {
|
|
980
|
+
action: "recover_or_resume_session",
|
|
981
|
+
reason: "managed tmux owner provenance could not be validated",
|
|
982
|
+
},
|
|
545
983
|
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function operatorDispatchIdForOwner(
|
|
988
|
+
owner: OwnerTerminalContext,
|
|
989
|
+
request: Omit<ObserveTerminalRequest, "operator_dispatch_id">,
|
|
990
|
+
): Promise<string | undefined> {
|
|
551
991
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
992
|
+
const intent = JSON.parse(
|
|
993
|
+
await Bun.file(lifecyclePaths(owner.stateDir, request.session_id, owner.generation).intentFile).text(),
|
|
994
|
+
) as unknown;
|
|
995
|
+
if (!isValidOwnerIntent(intent)) return undefined;
|
|
996
|
+
const dispatchId = owner.operatorDispatchId ?? intent.dispatch_id;
|
|
997
|
+
return isValidOwnerIntent(intent as OwnerIntent, { ...request, operator_dispatch_id: dispatchId })
|
|
998
|
+
? dispatchId
|
|
999
|
+
: undefined;
|
|
1000
|
+
} catch {
|
|
1001
|
+
return undefined;
|
|
555
1002
|
}
|
|
556
1003
|
}
|
|
557
1004
|
|
|
1005
|
+
async function persistCoordinatorRuntimeStateFromOwnerTerminalPostmortem(
|
|
1006
|
+
reason: postmortem.Reason,
|
|
1007
|
+
context: RuntimeStateContext,
|
|
1008
|
+
stateFile: string,
|
|
1009
|
+
sessionId: string,
|
|
1010
|
+
previous: Record<string, unknown>,
|
|
1011
|
+
): Promise<void> {
|
|
1012
|
+
const owner = context.ownerTerminal;
|
|
1013
|
+
if (!owner) return;
|
|
1014
|
+
try {
|
|
1015
|
+
const now = new Date().toISOString();
|
|
1016
|
+
const observation: Omit<ObserveTerminalRequest, "operator_dispatch_id"> = {
|
|
1017
|
+
schema_version: 1,
|
|
1018
|
+
op: "observe_terminal",
|
|
1019
|
+
session_id: sessionId,
|
|
1020
|
+
owner_generation: owner.generation,
|
|
1021
|
+
state_dir: owner.stateDir,
|
|
1022
|
+
socket_key: owner.socketKey,
|
|
1023
|
+
observer: "sidecar",
|
|
1024
|
+
observed_at: now,
|
|
1025
|
+
signal: ownerTerminalSignal(reason),
|
|
1026
|
+
exit_code: numericProcessExitCode(null),
|
|
1027
|
+
exit_kind: String(reason),
|
|
1028
|
+
reason: "process_postmortem",
|
|
1029
|
+
};
|
|
1030
|
+
const operatorDispatchId = await operatorDispatchIdForOwner(owner, observation);
|
|
1031
|
+
const verdict = await observeOwnerTerminal({
|
|
1032
|
+
...observation,
|
|
1033
|
+
...(operatorDispatchId ? { operator_dispatch_id: operatorDispatchId } : {}),
|
|
1034
|
+
});
|
|
1035
|
+
const expected = verdict.classification === "expected_operator_shutdown";
|
|
1036
|
+
const state: RuntimeState = expected ? "completed" : "errored";
|
|
1037
|
+
const payload = {
|
|
1038
|
+
...basePayload({
|
|
1039
|
+
context,
|
|
1040
|
+
previous,
|
|
1041
|
+
state,
|
|
1042
|
+
now,
|
|
1043
|
+
source: "process_postmortem",
|
|
1044
|
+
event: "owner_terminal",
|
|
1045
|
+
reason: verdict.classification,
|
|
1046
|
+
sessionId,
|
|
1047
|
+
}),
|
|
1048
|
+
ended_at: now,
|
|
1049
|
+
detected_at: now,
|
|
1050
|
+
owner_terminal: ownerTerminalPayload(verdict, owner),
|
|
1051
|
+
...(expected
|
|
1052
|
+
? {}
|
|
1053
|
+
: {
|
|
1054
|
+
error: {
|
|
1055
|
+
code: verdict.classification,
|
|
1056
|
+
message: "SKC owner terminal verdict requires session recovery",
|
|
1057
|
+
recoverable: true,
|
|
1058
|
+
},
|
|
1059
|
+
recovery: {
|
|
1060
|
+
action: "recover_or_resume_session",
|
|
1061
|
+
reason: "owner terminal verdict was not an expected operator shutdown",
|
|
1062
|
+
},
|
|
1063
|
+
}),
|
|
1064
|
+
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
1065
|
+
};
|
|
1066
|
+
await writeStateFileSync(stateFile, payload);
|
|
1067
|
+
} catch {
|
|
1068
|
+
const now = new Date().toISOString();
|
|
1069
|
+
await writeStateFileSync(stateFile, {
|
|
1070
|
+
...basePayload({
|
|
1071
|
+
context,
|
|
1072
|
+
previous,
|
|
1073
|
+
state: "errored",
|
|
1074
|
+
now,
|
|
1075
|
+
source: "process_postmortem",
|
|
1076
|
+
event: "owner_terminal",
|
|
1077
|
+
reason: "owner_verdict_unavailable",
|
|
1078
|
+
sessionId,
|
|
1079
|
+
}),
|
|
1080
|
+
ended_at: now,
|
|
1081
|
+
detected_at: now,
|
|
1082
|
+
error: {
|
|
1083
|
+
code: "owner_verdict_unavailable",
|
|
1084
|
+
message: "SKC owner terminal verdict was unavailable",
|
|
1085
|
+
recoverable: true,
|
|
1086
|
+
},
|
|
1087
|
+
recovery: {
|
|
1088
|
+
action: "recover_or_resume_session",
|
|
1089
|
+
reason: "owner terminal could not be authoritatively classified",
|
|
1090
|
+
},
|
|
1091
|
+
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
export async function persistCoordinatorRuntimeStateFromPostmortem(
|
|
1097
|
+
reason: postmortem.Reason,
|
|
1098
|
+
context: RuntimeStateContext,
|
|
1099
|
+
): Promise<void> {
|
|
1100
|
+
const stateFile = runtimeStateFileForContext(context);
|
|
1101
|
+
if (!stateFile) return;
|
|
1102
|
+
const identity = normalizedIdentity(context);
|
|
1103
|
+
await serializeStateFileWrite(
|
|
1104
|
+
stateFile,
|
|
1105
|
+
async () =>
|
|
1106
|
+
await withCoordinatorTransactionLock(
|
|
1107
|
+
stateFile,
|
|
1108
|
+
async () =>
|
|
1109
|
+
await withStateFileLock(stateFile, async () => {
|
|
1110
|
+
const previous = readPreviousPayload(stateFile);
|
|
1111
|
+
assertPreviousRuntimeStateIdentity(previous, identity);
|
|
1112
|
+
if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload, identity)) return;
|
|
1113
|
+
// The immutable owner verdict remains in its lifecycle artifact; never replace a
|
|
1114
|
+
// complete agent terminal payload merely to mirror that verdict here.
|
|
1115
|
+
if (process.platform === "linux" && context.ownerTerminalMetadataInvalid) {
|
|
1116
|
+
await persistInvalidOwnerTerminalMetadata(
|
|
1117
|
+
reason,
|
|
1118
|
+
context,
|
|
1119
|
+
stateFile,
|
|
1120
|
+
identity.sessionId,
|
|
1121
|
+
previous,
|
|
1122
|
+
);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (process.platform === "linux" && context.ownerTerminal) {
|
|
1126
|
+
await persistCoordinatorRuntimeStateFromOwnerTerminalPostmortem(
|
|
1127
|
+
reason,
|
|
1128
|
+
context,
|
|
1129
|
+
stateFile,
|
|
1130
|
+
identity.sessionId,
|
|
1131
|
+
previous,
|
|
1132
|
+
);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const previousForDetails: RuntimeStateSidecarPayload =
|
|
1136
|
+
(previous as RuntimeStateSidecarPayload).state === "completed" ||
|
|
1137
|
+
(previous as RuntimeStateSidecarPayload).state === "errored"
|
|
1138
|
+
? { ...(previous as RuntimeStateSidecarPayload), state: "running" }
|
|
1139
|
+
: (previous as RuntimeStateSidecarPayload);
|
|
1140
|
+
const now = new Date().toISOString();
|
|
1141
|
+
const details = postmortemExitDetails(reason, previousForDetails, identity.cwd);
|
|
1142
|
+
const payload = {
|
|
1143
|
+
...basePayload({
|
|
1144
|
+
context,
|
|
1145
|
+
previous,
|
|
1146
|
+
state: details.state,
|
|
1147
|
+
now,
|
|
1148
|
+
source: "process_postmortem",
|
|
1149
|
+
event: "process_exit",
|
|
1150
|
+
reason: details.reason,
|
|
1151
|
+
sessionId: identity.sessionId,
|
|
1152
|
+
}),
|
|
1153
|
+
ended_at: now,
|
|
1154
|
+
detected_at: now,
|
|
1155
|
+
exit_kind: details.exitKind,
|
|
1156
|
+
exit_code: details.exitCode,
|
|
1157
|
+
signal: details.signal,
|
|
1158
|
+
...(details.error ? { error: details.error } : {}),
|
|
1159
|
+
...(details.recovery ? { recovery: details.recovery } : {}),
|
|
1160
|
+
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
1161
|
+
prompt_accepted: details.promptAccepted,
|
|
1162
|
+
observed_recoverable_worktree_changes: details.observedRecoverableWorktreeChanges,
|
|
1163
|
+
worktree_baseline_dirty: details.worktreeBaselineDirty,
|
|
1164
|
+
worktree_changed_since_baseline: details.worktreeChangedSinceBaseline,
|
|
1165
|
+
};
|
|
1166
|
+
await writeStateFileSync(stateFile, payload);
|
|
1167
|
+
}),
|
|
1168
|
+
),
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
558
1172
|
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
559
1173
|
if (!runtimeStateFileForContext(context)) return () => {};
|
|
560
|
-
|
|
561
|
-
|
|
1174
|
+
const ownerTerminal = ownerTerminalContextFromEnvironment();
|
|
1175
|
+
const finalizerContext: RuntimeStateContext =
|
|
1176
|
+
ownerTerminal === "invalid"
|
|
1177
|
+
? { ...context, ownerTerminalMetadataInvalid: true }
|
|
1178
|
+
: ownerTerminal
|
|
1179
|
+
? { ...context, ownerTerminal }
|
|
1180
|
+
: context;
|
|
1181
|
+
return postmortem.register("coordinator-runtime-state", async reason => {
|
|
1182
|
+
await persistCoordinatorRuntimeStateFromPostmortem(reason, finalizerContext);
|
|
562
1183
|
});
|
|
563
1184
|
}
|