@sayknow-cli/coding-agent 0.3.7 → 0.3.8
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/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +92 -8
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +14 -5
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +127 -24
- package/src/modes/interactive-mode.ts +51 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +29 -3
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/notifications/threaded-render.ts +17 -1
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -17
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +52 -28
- package/src/session/agent-session.ts +1011 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1663 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +218 -71
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -31,6 +31,13 @@ export interface DeepInterviewEstablishedFact {
|
|
|
31
31
|
dimension?: string;
|
|
32
32
|
evidence?: string;
|
|
33
33
|
disputed: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Resolution pointer for a disputed fact: the id of the fact that replaced it
|
|
36
|
+
* after the user confirmed a pivot. A disputed fact without `superseded_by`
|
|
37
|
+
* keeps the deterministic ambiguity floor elevated; setting it releases the
|
|
38
|
+
* pressure while preserving the contradicted fact for audit.
|
|
39
|
+
*/
|
|
40
|
+
superseded_by?: string;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface DeepInterviewTriggerMetadata {
|
|
@@ -66,7 +73,12 @@ export interface DeepInterviewRoundRecord {
|
|
|
66
73
|
answered_at: string;
|
|
67
74
|
scored_at?: string;
|
|
68
75
|
scores?: Record<string, number>;
|
|
76
|
+
/** Effective ambiguity after the deterministic floor clamp (`max(reported, floor)`). */
|
|
69
77
|
ambiguity?: number;
|
|
78
|
+
/** Original LLM-reported ambiguity, preserved for audit when the floor clamped it. */
|
|
79
|
+
reported_ambiguity?: number;
|
|
80
|
+
/** Deterministic floor in effect when this round was scored, when it clamped. */
|
|
81
|
+
ambiguity_floor?: number;
|
|
70
82
|
triggers?: DeepInterviewTriggerMetadata[];
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -125,6 +137,7 @@ const TRANSCRIPT_STATE_FIELDS = [
|
|
|
125
137
|
"rounds",
|
|
126
138
|
"established_facts",
|
|
127
139
|
"current_ambiguity",
|
|
140
|
+
"ambiguity_floor",
|
|
128
141
|
"topology",
|
|
129
142
|
"ontology_snapshots",
|
|
130
143
|
"auto_researched_rounds",
|
|
@@ -150,13 +163,39 @@ const HOISTED_STATE_FIELDS = [
|
|
|
150
163
|
"threshold_source",
|
|
151
164
|
] as const;
|
|
152
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Envelope-reserved keys that are never legitimate interview `state` fields.
|
|
168
|
+
*
|
|
169
|
+
* A malformed write that wraps a whole envelope under `state`
|
|
170
|
+
* (`skc state deep-interview write --input '{"state": <envelope>}'`) leaks these
|
|
171
|
+
* into the nested state. Because normalization otherwise preserves unknown
|
|
172
|
+
* nested fields, they would accrete a recursive `state.state` chain (plus stale
|
|
173
|
+
* `receipt`/`skill`/`version`/... duplicates) that no later merge or write ever
|
|
174
|
+
* removes, permanently corrupting the shape. They are stripped from `state` on
|
|
175
|
+
* normalize so the canonical envelope self-heals on the next write.
|
|
176
|
+
*/
|
|
177
|
+
const ENVELOPE_RESERVED_STATE_KEYS = [
|
|
178
|
+
"state",
|
|
179
|
+
"receipt",
|
|
180
|
+
"skill",
|
|
181
|
+
"version",
|
|
182
|
+
"updated_at",
|
|
183
|
+
"active",
|
|
184
|
+
"current_phase",
|
|
185
|
+
"state_revision",
|
|
186
|
+
"session_id",
|
|
187
|
+
] as const;
|
|
188
|
+
|
|
153
189
|
/**
|
|
154
190
|
* Canonicalize a deep-interview envelope: interview data nested under `state`,
|
|
155
191
|
* legacy flattened fields hoisted in losslessly, transcript duplicates removed
|
|
156
192
|
* from the top level, and `rounds`/`established_facts` guaranteed to be arrays.
|
|
157
193
|
*
|
|
158
|
-
* Idempotent: a canonical envelope is returned unchanged in shape.
|
|
159
|
-
* unknown envelope
|
|
194
|
+
* Idempotent: a canonical envelope is returned unchanged in shape. Preserves all
|
|
195
|
+
* unknown envelope and nested fields except the envelope-reserved keys that leak
|
|
196
|
+
* into `state` (see `ENVELOPE_RESERVED_STATE_KEYS`), which are stripped so a
|
|
197
|
+
* malformed envelope-in-state write cannot permanently nest state. Never mutates
|
|
198
|
+
* the input.
|
|
160
199
|
*/
|
|
161
200
|
export function normalizeDeepInterviewEnvelope(value: unknown): DeepInterviewStateEnvelope {
|
|
162
201
|
const envelope: DeepInterviewStateEnvelope = isPlainObject(value) ? { ...value } : {};
|
|
@@ -169,6 +208,9 @@ export function normalizeDeepInterviewEnvelope(value: unknown): DeepInterviewSta
|
|
|
169
208
|
for (const field of HOISTED_STATE_FIELDS) {
|
|
170
209
|
if (inner[field] === undefined && envelope[field] !== undefined) inner[field] = envelope[field];
|
|
171
210
|
}
|
|
211
|
+
for (const field of ENVELOPE_RESERVED_STATE_KEYS) {
|
|
212
|
+
if (field in inner) delete inner[field];
|
|
213
|
+
}
|
|
172
214
|
|
|
173
215
|
if (!Array.isArray(inner.rounds)) inner.rounds = [];
|
|
174
216
|
if (!Array.isArray(inner.established_facts)) inner.established_facts = [];
|
|
@@ -3,15 +3,32 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AssistantMessage } from "@sayknow-cli/ai";
|
|
5
5
|
import { logger, postmortem } from "@sayknow-cli/utils";
|
|
6
|
+
import { sessionRuntimeDir } from "./session-layout";
|
|
6
7
|
|
|
7
8
|
export const SKC_COORDINATOR_SESSION_STATE_FILE_ENV = "SKC_COORDINATOR_SESSION_STATE_FILE";
|
|
8
9
|
export const SKC_COORDINATOR_SESSION_ID_ENV = "SKC_COORDINATOR_SESSION_ID";
|
|
9
10
|
export const SKC_COORDINATOR_SESSION_BRANCH_ENV = "SKC_COORDINATOR_SESSION_BRANCH";
|
|
11
|
+
const SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV = "SKC_SESSION_PROMPT_ACCEPTED_JSON";
|
|
12
|
+
const SKC_SESSION_WORKTREE_BASELINE_DIRTY_ENV = "SKC_SESSION_WORKTREE_BASELINE_DIRTY";
|
|
10
13
|
|
|
11
14
|
export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
|
|
12
15
|
|
|
13
16
|
type FinalResponseSource = "agent_end" | "launch_error";
|
|
14
17
|
const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
|
|
18
|
+
const HEARTBEAT_MS = 1000;
|
|
19
|
+
|
|
20
|
+
type LastPayloadCacheEntry = { mtimeMs: number; size: number; payload: Record<string, unknown> };
|
|
21
|
+
const lastPayloadByStateFile = new Map<string, LastPayloadCacheEntry>();
|
|
22
|
+
|
|
23
|
+
/** Test-only counters for runtime sidecar hot-path assertions. */
|
|
24
|
+
export const __sessionStateSidecarPerfCounters = {
|
|
25
|
+
persistFromEventCalls: 0,
|
|
26
|
+
reset(): void {
|
|
27
|
+
this.persistFromEventCalls = 0;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const lastWrittenPayloadByStateFile = new Map<string, Record<string, unknown>>();
|
|
15
32
|
|
|
16
33
|
interface RuntimeStateEvent {
|
|
17
34
|
type: string;
|
|
@@ -31,6 +48,7 @@ interface RuntimeStateSidecarPayload {
|
|
|
31
48
|
state?: unknown;
|
|
32
49
|
ready_for_input?: unknown;
|
|
33
50
|
cwd?: unknown;
|
|
51
|
+
workdir?: unknown;
|
|
34
52
|
session_file?: unknown;
|
|
35
53
|
final_response?: { source?: unknown };
|
|
36
54
|
}
|
|
@@ -52,6 +70,11 @@ function sameResolvedPath(left: string, right: string): boolean {
|
|
|
52
70
|
return path.resolve(left) === path.resolve(right);
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
function isCoordinatorOwnedStateFile(stateFile: string): boolean {
|
|
74
|
+
const coordinatorStateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
75
|
+
return !!coordinatorStateFile && sameResolvedPath(coordinatorStateFile, stateFile);
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
export async function readTerminalRuntimeStateMarker(input: {
|
|
56
79
|
stateFile?: string | null;
|
|
57
80
|
sessionId?: string | null;
|
|
@@ -124,7 +147,7 @@ function finalResponseForEvent(event: RuntimeStateEvent): {
|
|
|
124
147
|
};
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
150
|
+
export function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
128
151
|
if (event.type === "agent_start" || event.type === "turn_start") return "running";
|
|
129
152
|
if (event.type === "agent_end") {
|
|
130
153
|
const assistant = lastAssistant(event.messages);
|
|
@@ -134,6 +157,10 @@ function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
|
134
157
|
return null;
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
export function eventAffectsCoordinatorRuntimeState(event: RuntimeStateEvent): boolean {
|
|
161
|
+
return stateForEvent(event) !== null;
|
|
162
|
+
}
|
|
163
|
+
|
|
137
164
|
function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
138
165
|
try {
|
|
139
166
|
return JSON.parse(fsSync.readFileSync(stateFile, "utf8")) as Record<string, unknown>;
|
|
@@ -142,12 +169,92 @@ function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
|
|
145
|
-
function
|
|
172
|
+
async function readPreviousPayloadForEvent(stateFile: string): Promise<Record<string, unknown>> {
|
|
173
|
+
if (!isCoordinatorOwnedStateFile(stateFile)) {
|
|
174
|
+
const cachedWritten = lastWrittenPayloadByStateFile.get(stateFile);
|
|
175
|
+
if (cachedWritten) return cachedWritten;
|
|
176
|
+
}
|
|
177
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
178
|
+
try {
|
|
179
|
+
stat = await fs.stat(stateFile);
|
|
180
|
+
} catch {
|
|
181
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
const cached = lastPayloadByStateFile.get(stateFile);
|
|
185
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.payload;
|
|
186
|
+
try {
|
|
187
|
+
const payload = JSON.parse(await Bun.file(stateFile).text()) as Record<string, unknown>;
|
|
188
|
+
lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
|
|
189
|
+
return payload;
|
|
190
|
+
} catch {
|
|
191
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function withoutUpdatedAt(payload: Record<string, unknown>): Record<string, unknown> {
|
|
197
|
+
const { updated_at: _updatedAt, ...rest } = payload;
|
|
198
|
+
return rest;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function shouldSkipRuntimeStateWrite(
|
|
202
|
+
previous: Record<string, unknown>,
|
|
203
|
+
payload: Record<string, unknown>,
|
|
204
|
+
nowMs: number,
|
|
205
|
+
): boolean {
|
|
206
|
+
if (payload.state === "completed" || payload.state === "errored") return false;
|
|
207
|
+
if (previous.state !== payload.state) return false;
|
|
208
|
+
if (previous.state !== "running" || payload.state !== "running") return false;
|
|
209
|
+
if (JSON.stringify(withoutUpdatedAt(previous)) !== JSON.stringify(withoutUpdatedAt(payload))) return false;
|
|
210
|
+
const previousUpdatedAt = typeof previous.updated_at === "string" ? Date.parse(previous.updated_at) : NaN;
|
|
211
|
+
if (!Number.isFinite(previousUpdatedAt)) return false;
|
|
212
|
+
return nowMs - previousUpdatedAt < HEARTBEAT_MS;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function rememberWrittenPayload(stateFile: string, payload: Record<string, unknown>): void {
|
|
216
|
+
lastWrittenPayloadByStateFile.set(stateFile, payload);
|
|
217
|
+
try {
|
|
218
|
+
const stat = fsSync.statSync(stateFile);
|
|
219
|
+
lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
|
|
220
|
+
} catch {
|
|
221
|
+
lastPayloadByStateFile.delete(stateFile);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function shouldPreserveTerminalPayload(
|
|
225
|
+
previous: RuntimeStateSidecarPayload,
|
|
226
|
+
input: { sessionId: string; cwd: string; sessionFile?: string | null },
|
|
227
|
+
): boolean {
|
|
146
228
|
if (previous.state !== "completed" && previous.state !== "errored") return false;
|
|
147
229
|
const source = previous.final_response?.source;
|
|
148
|
-
|
|
230
|
+
if (source !== "agent_end" && source !== "launch_error") return false;
|
|
231
|
+
if (typeof previous.session_id === "string" && previous.session_id !== input.sessionId) return false;
|
|
232
|
+
if (typeof previous.cwd === "string" && !sameResolvedPath(previous.cwd, input.cwd)) return false;
|
|
233
|
+
if (typeof previous.workdir === "string" && !sameResolvedPath(previous.workdir, input.cwd)) return false;
|
|
234
|
+
if (
|
|
235
|
+
input.sessionFile &&
|
|
236
|
+
typeof previous.session_file === "string" &&
|
|
237
|
+
!sameResolvedPath(previous.session_file, input.sessionFile)
|
|
238
|
+
) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
149
242
|
}
|
|
150
243
|
|
|
244
|
+
function cachedTerminalPayload(
|
|
245
|
+
stateFile: string,
|
|
246
|
+
input: { sessionId: string; cwd: string; sessionFile?: string | null },
|
|
247
|
+
): RuntimeStateSidecarPayload | null {
|
|
248
|
+
const cached = lastWrittenPayloadByStateFile.get(stateFile) as RuntimeStateSidecarPayload | undefined;
|
|
249
|
+
return cached && shouldPreserveTerminalPayload(cached, input) ? cached : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
|
|
253
|
+
const explicit = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
254
|
+
if (explicit) return explicit;
|
|
255
|
+
if (!context.sessionId.trim()) return null;
|
|
256
|
+
return path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json");
|
|
257
|
+
}
|
|
151
258
|
function branchForContext(context: RuntimeStateContext): string | null {
|
|
152
259
|
return context.branch ?? (process.env[SKC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
|
|
153
260
|
}
|
|
@@ -160,10 +267,11 @@ function basePayload(input: {
|
|
|
160
267
|
source: string;
|
|
161
268
|
event: string;
|
|
162
269
|
reason: string | null;
|
|
270
|
+
sessionId: string;
|
|
163
271
|
}): Record<string, unknown> {
|
|
164
272
|
return {
|
|
165
273
|
schema_version: 1,
|
|
166
|
-
session_id:
|
|
274
|
+
session_id: input.sessionId,
|
|
167
275
|
state: input.state,
|
|
168
276
|
ready_for_input: input.state === "completed" || input.state === "ready_for_input",
|
|
169
277
|
updated_at: input.now,
|
|
@@ -179,6 +287,56 @@ function basePayload(input: {
|
|
|
179
287
|
session_file: input.context.sessionFile ?? null,
|
|
180
288
|
};
|
|
181
289
|
}
|
|
290
|
+
function booleanFromUnknown(value: unknown): boolean | null {
|
|
291
|
+
if (typeof value === "boolean") return value;
|
|
292
|
+
if (typeof value === "string") {
|
|
293
|
+
const normalized = value.trim().toLowerCase();
|
|
294
|
+
if (normalized === "true") return true;
|
|
295
|
+
if (normalized === "false") return false;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function promptAcceptedFromEnv(): boolean {
|
|
301
|
+
const promptAcceptedJson = process.env[SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV]?.trim();
|
|
302
|
+
if (!promptAcceptedJson) return false;
|
|
303
|
+
try {
|
|
304
|
+
return fsSync.statSync(promptAcceptedJson).size > 0;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readJsonFileSync(file: string): Record<string, unknown> | null {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(fsSync.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function worktreeBaselineDirtyFromEnvOrMarker(): boolean | null {
|
|
319
|
+
const promptAcceptedJson = process.env[SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV]?.trim();
|
|
320
|
+
if (promptAcceptedJson) {
|
|
321
|
+
const promptAccepted = readJsonFileSync(promptAcceptedJson);
|
|
322
|
+
const promptBaseline = booleanFromUnknown(promptAccepted?.worktreeBaselineDirty);
|
|
323
|
+
if (promptBaseline !== null) return promptBaseline;
|
|
324
|
+
}
|
|
325
|
+
const envValue = booleanFromUnknown(process.env[SKC_SESSION_WORKTREE_BASELINE_DIRTY_ENV]);
|
|
326
|
+
if (envValue !== null) return envValue;
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function observedRecoverableWorktreeChanges(cwd: string): boolean {
|
|
331
|
+
if (!cwd.trim()) return false;
|
|
332
|
+
try {
|
|
333
|
+
const proc = Bun.spawnSync(["git", "status", "--porcelain"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
334
|
+
return proc.exitCode === 0 && proc.stdout.byteLength > 0;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
182
340
|
function publicSafeErrorMessage(message: string): string {
|
|
183
341
|
const normalized = message.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
|
|
184
342
|
if (normalized.length <= MAX_PUBLIC_ERROR_MESSAGE_LENGTH) return normalized;
|
|
@@ -193,32 +351,75 @@ function numericProcessExitCode(defaultCode: number | null): number | null {
|
|
|
193
351
|
return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
|
|
194
352
|
}
|
|
195
353
|
|
|
196
|
-
function postmortemExitDetails(
|
|
354
|
+
function postmortemExitDetails(
|
|
355
|
+
reason: postmortem.Reason,
|
|
356
|
+
previous: RuntimeStateSidecarPayload,
|
|
357
|
+
cwd: string,
|
|
358
|
+
): {
|
|
197
359
|
state: RuntimeState;
|
|
198
360
|
reason: string;
|
|
199
361
|
exitKind: string;
|
|
200
362
|
exitCode: number | null;
|
|
201
363
|
signal: string | null;
|
|
202
364
|
error?: { code: string; message: string; recoverable: true };
|
|
365
|
+
recovery?: { action: string; reason: string };
|
|
366
|
+
promptAccepted: boolean;
|
|
367
|
+
observedRecoverableWorktreeChanges: boolean;
|
|
368
|
+
worktreeBaselineDirty: boolean | null;
|
|
369
|
+
worktreeChangedSinceBaseline: boolean;
|
|
203
370
|
} {
|
|
371
|
+
const promptAccepted = promptAcceptedFromEnv();
|
|
372
|
+
const observedChanges = observedRecoverableWorktreeChanges(typeof previous.cwd === "string" ? previous.cwd : cwd);
|
|
373
|
+
const worktreeBaselineDirty = worktreeBaselineDirtyFromEnvOrMarker();
|
|
374
|
+
const worktreeChangedSinceBaseline = worktreeBaselineDirty === false && observedChanges;
|
|
204
375
|
if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
|
|
205
376
|
const exitCode = numericProcessExitCode(0) ?? 0;
|
|
206
|
-
const
|
|
377
|
+
const exitedBeforeTerminalState =
|
|
378
|
+
exitCode === 0 && reason === postmortem.Reason.EXIT && previous.state === "running";
|
|
379
|
+
const state: RuntimeState = exitCode === 0 && !exitedBeforeTerminalState ? "completed" : "errored";
|
|
380
|
+
const exitReason = exitedBeforeTerminalState
|
|
381
|
+
? "process_exit_before_terminal_state"
|
|
382
|
+
: reason === postmortem.Reason.EXIT
|
|
383
|
+
? "process_exit"
|
|
384
|
+
: "manual_cleanup";
|
|
385
|
+
let classifiedReason = exitReason;
|
|
386
|
+
if (exitedBeforeTerminalState) {
|
|
387
|
+
if (!promptAccepted) classifiedReason = "process_exit_before_prompt_acceptance";
|
|
388
|
+
else if (worktreeChangedSinceBaseline)
|
|
389
|
+
classifiedReason = "accepted_prompt_observed_recoverable_worktree_changes";
|
|
390
|
+
else if (observedChanges)
|
|
391
|
+
classifiedReason = "accepted_prompt_dirty_worktree_observed_without_new_change_proof";
|
|
392
|
+
else classifiedReason = "accepted_prompt_no_useful_output";
|
|
393
|
+
}
|
|
207
394
|
return {
|
|
208
395
|
state,
|
|
209
|
-
reason:
|
|
396
|
+
reason: classifiedReason,
|
|
210
397
|
exitKind: reason,
|
|
211
398
|
exitCode,
|
|
212
399
|
signal: null,
|
|
213
400
|
...(state === "errored"
|
|
214
401
|
? {
|
|
215
402
|
error: {
|
|
216
|
-
code:
|
|
217
|
-
message: publicSafeErrorMessage(
|
|
403
|
+
code: classifiedReason,
|
|
404
|
+
message: publicSafeErrorMessage(
|
|
405
|
+
exitedBeforeTerminalState
|
|
406
|
+
? "SKC process exited before emitting terminal agent state"
|
|
407
|
+
: `SKC process exited with code ${exitCode}`,
|
|
408
|
+
),
|
|
218
409
|
recoverable: true,
|
|
219
410
|
},
|
|
411
|
+
recovery: {
|
|
412
|
+
action: "recover_or_resume_session",
|
|
413
|
+
reason: exitedBeforeTerminalState
|
|
414
|
+
? "previous runtime state was non-terminal; preserve the worktree and inspect the session before retrying"
|
|
415
|
+
: "process exited with a non-zero status",
|
|
416
|
+
},
|
|
220
417
|
}
|
|
221
418
|
: {}),
|
|
419
|
+
promptAccepted,
|
|
420
|
+
observedRecoverableWorktreeChanges: observedChanges,
|
|
421
|
+
worktreeBaselineDirty,
|
|
422
|
+
worktreeChangedSinceBaseline,
|
|
222
423
|
};
|
|
223
424
|
}
|
|
224
425
|
const signalByReason: Partial<Record<postmortem.Reason, string>> = {
|
|
@@ -233,32 +434,53 @@ function postmortemExitDetails(reason: postmortem.Reason): {
|
|
|
233
434
|
exitCode: numericProcessExitCode(null),
|
|
234
435
|
signal: signalByReason[reason] ?? null,
|
|
235
436
|
error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
|
|
437
|
+
recovery: { action: "recover_or_resume_session", reason: "process cleanup ran before terminal agent state" },
|
|
438
|
+
promptAccepted,
|
|
439
|
+
observedRecoverableWorktreeChanges: observedChanges,
|
|
440
|
+
worktreeBaselineDirty,
|
|
441
|
+
worktreeChangedSinceBaseline,
|
|
236
442
|
};
|
|
237
443
|
}
|
|
238
444
|
|
|
239
445
|
function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
|
|
240
446
|
fsSync.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
241
|
-
fsSync.writeFileSync(stateFile, `${JSON.stringify(payload
|
|
447
|
+
fsSync.writeFileSync(stateFile, `${JSON.stringify(payload)}\n`);
|
|
448
|
+
rememberWrittenPayload(stateFile, payload);
|
|
242
449
|
}
|
|
243
450
|
|
|
244
451
|
async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
245
452
|
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
246
|
-
await Bun.write(stateFile, `${JSON.stringify(payload
|
|
453
|
+
await Bun.write(stateFile, `${JSON.stringify(payload)}\n`);
|
|
454
|
+
rememberWrittenPayload(stateFile, payload);
|
|
247
455
|
}
|
|
248
456
|
|
|
249
457
|
export async function persistCoordinatorRuntimeStateFromEvent(
|
|
250
458
|
event: RuntimeStateEvent,
|
|
251
459
|
context: RuntimeStateContext,
|
|
252
460
|
): Promise<void> {
|
|
253
|
-
|
|
461
|
+
__sessionStateSidecarPerfCounters.persistFromEventCalls += 1;
|
|
462
|
+
const stateFile = runtimeStateFileForContext(context);
|
|
254
463
|
if (!stateFile) return;
|
|
255
464
|
const state = stateForEvent(event);
|
|
256
465
|
if (!state) return;
|
|
257
|
-
const
|
|
258
|
-
const
|
|
466
|
+
const nowMs = Date.now();
|
|
467
|
+
const now = new Date(nowMs).toISOString();
|
|
468
|
+
const previous = await readPreviousPayloadForEvent(stateFile);
|
|
259
469
|
const finalResponse = finalResponseForEvent(event);
|
|
470
|
+
const sessionId = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
|
|
471
|
+
? process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
|
|
472
|
+
: context.sessionId;
|
|
260
473
|
const payload = {
|
|
261
|
-
...basePayload({
|
|
474
|
+
...basePayload({
|
|
475
|
+
context,
|
|
476
|
+
previous,
|
|
477
|
+
state,
|
|
478
|
+
now,
|
|
479
|
+
source: "agent_session_event",
|
|
480
|
+
event: event.type,
|
|
481
|
+
reason: null,
|
|
482
|
+
sessionId,
|
|
483
|
+
}),
|
|
262
484
|
...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
|
|
263
485
|
...(finalResponse ? { final_response: finalResponse } : {}),
|
|
264
486
|
...(state === "errored"
|
|
@@ -271,7 +493,9 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
271
493
|
}
|
|
272
494
|
: {}),
|
|
273
495
|
};
|
|
496
|
+
if (state === "completed" || state === "errored") rememberWrittenPayload(stateFile, payload);
|
|
274
497
|
try {
|
|
498
|
+
if (shouldSkipRuntimeStateWrite(previous, payload, nowMs)) return;
|
|
275
499
|
await writeStateFile(stateFile, payload);
|
|
276
500
|
} catch (error) {
|
|
277
501
|
logger.warn("Failed to persist coordinator runtime state", { error: String(error), stateFile });
|
|
@@ -282,12 +506,24 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
282
506
|
reason: postmortem.Reason,
|
|
283
507
|
context: RuntimeStateContext,
|
|
284
508
|
): void {
|
|
285
|
-
const stateFile =
|
|
509
|
+
const stateFile = runtimeStateFileForContext(context);
|
|
286
510
|
if (!stateFile) return;
|
|
287
|
-
const
|
|
288
|
-
|
|
511
|
+
const sessionId = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
|
|
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);
|
|
289
525
|
const now = new Date().toISOString();
|
|
290
|
-
const details = postmortemExitDetails(reason);
|
|
526
|
+
const details = postmortemExitDetails(reason, previousForDetails, context.cwd);
|
|
291
527
|
const payload = {
|
|
292
528
|
...basePayload({
|
|
293
529
|
context,
|
|
@@ -297,6 +533,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
297
533
|
source: "process_postmortem",
|
|
298
534
|
event: "process_exit",
|
|
299
535
|
reason: details.reason,
|
|
536
|
+
sessionId,
|
|
300
537
|
}),
|
|
301
538
|
ended_at: now,
|
|
302
539
|
detected_at: now,
|
|
@@ -304,6 +541,12 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
304
541
|
exit_code: details.exitCode,
|
|
305
542
|
signal: details.signal,
|
|
306
543
|
...(details.error ? { error: details.error } : {}),
|
|
544
|
+
...(details.recovery ? { recovery: details.recovery } : {}),
|
|
545
|
+
previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
|
|
546
|
+
prompt_accepted: details.promptAccepted,
|
|
547
|
+
observed_recoverable_worktree_changes: details.observedRecoverableWorktreeChanges,
|
|
548
|
+
worktree_baseline_dirty: details.worktreeBaselineDirty,
|
|
549
|
+
worktree_changed_since_baseline: details.worktreeChangedSinceBaseline,
|
|
307
550
|
};
|
|
308
551
|
try {
|
|
309
552
|
writeStateFileSync(stateFile, payload);
|
|
@@ -313,7 +556,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
313
556
|
}
|
|
314
557
|
|
|
315
558
|
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
316
|
-
if (!
|
|
559
|
+
if (!runtimeStateFileForContext(context)) return () => {};
|
|
317
560
|
return postmortem.register("coordinator-runtime-state", reason => {
|
|
318
561
|
persistCoordinatorRuntimeStateFromPostmortem(reason, context);
|
|
319
562
|
});
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type WorkflowStateReceipt,
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
|
+
import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
|
|
29
30
|
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
30
31
|
import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
|
|
31
32
|
import {
|
|
@@ -1158,7 +1159,12 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1158
1159
|
|
|
1159
1160
|
const merged =
|
|
1160
1161
|
mode === "deep-interview"
|
|
1161
|
-
?
|
|
1162
|
+
? // Enforce the deterministic ambiguity floor on every reconcile so a
|
|
1163
|
+
// self-reported score can never undercut persisted contradiction evidence.
|
|
1164
|
+
(applyAmbiguityFloorToEnvelope(mergeDeepInterviewEnvelope(existingPayload, payload)).envelope as Record<
|
|
1165
|
+
string,
|
|
1166
|
+
unknown
|
|
1167
|
+
>)
|
|
1162
1168
|
: mergeWithNullDelete(existingPayload, payload);
|
|
1163
1169
|
merged.skill = mode;
|
|
1164
1170
|
merged.current_phase = trimmedPhase;
|
|
@@ -1355,7 +1361,11 @@ async function handleWrite(
|
|
|
1355
1361
|
if (mode === "deep-interview") {
|
|
1356
1362
|
// Deep-interview keeps interview data nested under `state` and merges rounds
|
|
1357
1363
|
// losslessly by durable key; never flatten or delete `state` (that drops recorder history).
|
|
1358
|
-
|
|
1364
|
+
// The deterministic ambiguity floor is applied after the merge so a reported
|
|
1365
|
+
// score written through the CLI can never undercut persisted contradiction evidence.
|
|
1366
|
+
merged = applyAmbiguityFloorToEnvelope(
|
|
1367
|
+
mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") }),
|
|
1368
|
+
).envelope;
|
|
1359
1369
|
} else if (hasFlag(args, "--replace")) {
|
|
1360
1370
|
merged = { ...payload };
|
|
1361
1371
|
} else {
|
|
@@ -304,6 +304,18 @@ function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope)
|
|
|
304
304
|
return layoutActiveStateDir(cwd, requireSessionId(sessionScope, "activeStateDir"));
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
type ActiveStateCacheInvalidator = (cwd?: string, sessionId?: string) => void;
|
|
308
|
+
var activeStateCacheInvalidator: ActiveStateCacheInvalidator | undefined;
|
|
309
|
+
|
|
310
|
+
export function setActiveStateCacheInvalidator(invalidator: ActiveStateCacheInvalidator): void {
|
|
311
|
+
activeStateCacheInvalidator = invalidator;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function invalidateActiveStateCacheForScope(cwd: string, sessionScope?: string | ActiveSessionScope): void {
|
|
315
|
+
const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
|
|
316
|
+
activeStateCacheInvalidator?.(cwd, sessionId);
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
|
|
308
320
|
return layoutActiveSnapshotPath(cwd, requireSessionId(sessionScope, "activeSnapshotPath"));
|
|
309
321
|
}
|
|
@@ -979,6 +991,7 @@ export async function writeActiveEntry(
|
|
|
979
991
|
persistedSourceRevision(entry) || persistedSourceRevision(await readJsonIfPresent(filePath)) + 1,
|
|
980
992
|
},
|
|
981
993
|
);
|
|
994
|
+
invalidateActiveStateCacheForScope(cwd, sessionScope);
|
|
982
995
|
return filePath;
|
|
983
996
|
}
|
|
984
997
|
|
|
@@ -1003,6 +1016,7 @@ export async function removeActiveEntry(
|
|
|
1003
1016
|
}
|
|
1004
1017
|
const deleted = await atomicRemove(filePath);
|
|
1005
1018
|
if (deleted) await maybeAudit(filePath, options);
|
|
1019
|
+
if (deleted) invalidateActiveStateCacheForScope(cwd, sessionScope);
|
|
1006
1020
|
return { path: filePath, deleted };
|
|
1007
1021
|
},
|
|
1008
1022
|
options?.lock,
|
|
@@ -1049,6 +1063,7 @@ export async function rebuildActiveSnapshot(
|
|
|
1049
1063
|
...entries.map(entry => persistedSourceRevision(entry)),
|
|
1050
1064
|
),
|
|
1051
1065
|
});
|
|
1066
|
+
invalidateActiveStateCacheForScope(cwd, sessionScope);
|
|
1052
1067
|
return snapshotPath;
|
|
1053
1068
|
}
|
|
1054
1069
|
|