@gajae-code/coding-agent 0.11.11 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Kind-aware invocation reconciliation (prompt | skill) with optional durable store.
3
+ * Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
4
+ */
5
+ import {
6
+ PROMPT_RECONCILIATION_ACTIVE_CAPACITY,
7
+ PROMPT_RECONCILIATION_TERMINAL_CAPACITY,
8
+ PROMPT_RECONCILIATION_TERMINAL_TTL_MS,
9
+ type PromptCorrelation,
10
+ type PromptReconciliationStatus,
11
+ sanitizePromptFailure,
12
+ type TurnPromptReconciliation,
13
+ } from "./prompt-reconciliation";
14
+ import type { DurableReconciliationRecord, ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
15
+
16
+ export type { ReconciliationKind };
17
+
18
+ export interface KindCorrelation extends PromptCorrelation {
19
+ kind: ReconciliationKind;
20
+ }
21
+
22
+ export interface KindAwareReconciliation {
23
+ admit(kind: ReconciliationKind, clientRef?: string): void;
24
+ releaseAdmission(kind: ReconciliationKind, clientRef?: string): void;
25
+ noteAccepted(
26
+ kind: ReconciliationKind,
27
+ correlation: PromptCorrelation,
28
+ clientRef?: string,
29
+ extra?: { skillName?: string },
30
+ ): Promise<void>;
31
+ noteTransition(
32
+ kind: ReconciliationKind,
33
+ correlation: PromptCorrelation | undefined,
34
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
35
+ ): Promise<void>;
36
+ lookup(
37
+ kind: ReconciliationKind,
38
+ selector: { commandId?: string; turnId?: string; clientRef?: string },
39
+ ): TurnPromptReconciliation;
40
+ cleanup(): void;
41
+ activeCount(kind: ReconciliationKind): number;
42
+ /** Hydrate from durable store (call once at session host start). */
43
+ hydrateFromStore(): Promise<void>;
44
+ }
45
+
46
+ export function createKindAwareReconciliation(
47
+ options: { now?: () => number; store?: ReconciliationStore | null } = {},
48
+ ): KindAwareReconciliation {
49
+ const now = options.now ?? Date.now;
50
+ const store = options.store ?? null;
51
+ const records = new Map<string, DurableReconciliationRecord>();
52
+ const clientRefIndex = new Map<string, string>(); // `${kind}\0${clientRef}` -> key
53
+ const reservedClientRefs = new Map<ReconciliationKind, Set<string>>();
54
+ const reservations: Array<{ kind: ReconciliationKind; clientRef?: string }> = [];
55
+
56
+ const keyOf = (kind: ReconciliationKind, correlation: PromptCorrelation) =>
57
+ `${kind}:${correlation.commandId}:${correlation.turnId}`;
58
+ const refKey = (kind: ReconciliationKind, clientRef: string) => `${kind}\0${clientRef}`;
59
+
60
+ const reservedFor = (kind: ReconciliationKind) => {
61
+ let set = reservedClientRefs.get(kind);
62
+ if (!set) {
63
+ set = new Set();
64
+ reservedClientRefs.set(kind, set);
65
+ }
66
+ return set;
67
+ };
68
+
69
+ const remove = (key: string) => {
70
+ const record = records.get(key);
71
+ if (!record) return;
72
+ records.delete(key);
73
+ if (record.clientRef !== undefined) {
74
+ const rk = refKey(record.kind, record.clientRef);
75
+ if (clientRefIndex.get(rk) === key) clientRefIndex.delete(rk);
76
+ }
77
+ };
78
+
79
+ const cleanup = () => {
80
+ const at = now();
81
+ for (const [key, record] of records)
82
+ if (record.terminalAt !== undefined && record.terminalAt + PROMPT_RECONCILIATION_TERMINAL_TTL_MS <= at)
83
+ remove(key);
84
+ for (const kind of ["prompt", "skill"] as const) {
85
+ const terminalEntries = [...records.entries()].filter(
86
+ ([, record]) => record.kind === kind && record.terminalAt !== undefined,
87
+ );
88
+ if (terminalEntries.length <= PROMPT_RECONCILIATION_TERMINAL_CAPACITY) continue;
89
+ terminalEntries.sort((a, b) => (a[1].terminalAt as number) - (b[1].terminalAt as number));
90
+ for (const [key] of terminalEntries.slice(0, terminalEntries.length - PROMPT_RECONCILIATION_TERMINAL_CAPACITY))
91
+ remove(key);
92
+ }
93
+ };
94
+
95
+ const activeCount = (kind: ReconciliationKind) => {
96
+ let count = 0;
97
+ for (const record of records.values()) if (record.kind === kind && record.terminalAt === undefined) count++;
98
+ return count;
99
+ };
100
+
101
+ const reservationCount = (kind: ReconciliationKind) => reservations.filter(r => r.kind === kind).length;
102
+
103
+ const consumeReservation = (kind: ReconciliationKind, clientRef?: string) => {
104
+ const index = reservations.findIndex(r => r.kind === kind && r.clientRef === clientRef);
105
+ if (index === -1) return;
106
+ reservations.splice(index, 1);
107
+ if (clientRef !== undefined && !reservations.some(r => r.kind === kind && r.clientRef === clientRef))
108
+ reservedFor(kind).delete(clientRef);
109
+ };
110
+
111
+ const persist = async () => {
112
+ if (!store) return;
113
+ await store.transact(() => [...records.values()].map(r => ({ ...r })));
114
+ };
115
+
116
+ const admit = (kind: ReconciliationKind, clientRef?: string) => {
117
+ cleanup();
118
+ const reserved = reservedFor(kind);
119
+ if (clientRef !== undefined && (clientRefIndex.has(refKey(kind, clientRef)) || reserved.has(clientRef)))
120
+ throw Object.assign(
121
+ new Error("A submission with this clientRef is already retained; never reuse a clientRef for retry."),
122
+ { code: "client_ref_conflict" },
123
+ );
124
+ if (activeCount(kind) + reservationCount(kind) >= PROMPT_RECONCILIATION_ACTIVE_CAPACITY)
125
+ throw Object.assign(new Error("Too many active submissions; reconcile or await terminal state."), {
126
+ code: "reconciliation_capacity",
127
+ });
128
+ reservations.push({ kind, clientRef });
129
+ if (clientRef !== undefined) reserved.add(clientRef);
130
+ };
131
+
132
+ const releaseAdmission = (kind: ReconciliationKind, clientRef?: string) => {
133
+ consumeReservation(kind, clientRef);
134
+ };
135
+
136
+ const noteAccepted = async (
137
+ kind: ReconciliationKind,
138
+ correlation: PromptCorrelation,
139
+ clientRef?: string,
140
+ extra?: { skillName?: string },
141
+ ) => {
142
+ cleanup();
143
+ consumeReservation(kind, clientRef);
144
+ const at = now();
145
+ const key = keyOf(kind, correlation);
146
+ const record: DurableReconciliationRecord = {
147
+ kind,
148
+ commandId: correlation.commandId,
149
+ turnId: correlation.turnId,
150
+ ...(clientRef !== undefined ? { clientRef } : {}),
151
+ status: "accepted",
152
+ acceptedAt: at,
153
+ ...(extra?.skillName ? { skillName: extra.skillName } : {}),
154
+ };
155
+ records.set(key, record);
156
+ if (clientRef !== undefined) clientRefIndex.set(refKey(kind, clientRef), key);
157
+ await persist();
158
+ };
159
+
160
+ const noteTransition = async (
161
+ kind: ReconciliationKind,
162
+ correlation: PromptCorrelation | undefined,
163
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
164
+ ) => {
165
+ if (!correlation) return;
166
+ const record = records.get(keyOf(kind, correlation));
167
+ if (!record || record.terminalAt !== undefined) return;
168
+ if (frame.type === "agent_start") {
169
+ if (record.status === "accepted") {
170
+ record.status = "in_flight";
171
+ record.startedAt = now();
172
+ await persist();
173
+ }
174
+ return;
175
+ }
176
+ record.terminalAt = now();
177
+ if (frame.type === "agent_failed") {
178
+ record.status = "failed";
179
+ record.error = sanitizePromptFailure(frame.error);
180
+ } else {
181
+ record.status = "terminal_ok";
182
+ }
183
+ cleanup();
184
+ await persist();
185
+ };
186
+
187
+ const lookup = (
188
+ kind: ReconciliationKind,
189
+ selector: { commandId?: string; turnId?: string; clientRef?: string },
190
+ ): TurnPromptReconciliation => {
191
+ cleanup();
192
+ const key =
193
+ selector.clientRef !== undefined
194
+ ? clientRefIndex.get(refKey(kind, selector.clientRef))
195
+ : selector.commandId !== undefined && selector.turnId !== undefined
196
+ ? keyOf(kind, { commandId: selector.commandId, turnId: selector.turnId })
197
+ : undefined;
198
+ const record = key === undefined ? undefined : records.get(key);
199
+ if (!record) return { status: "unknown" };
200
+ const identity = {
201
+ commandId: record.commandId,
202
+ turnId: record.turnId,
203
+ ...(record.clientRef !== undefined ? { clientRef: record.clientRef } : {}),
204
+ acceptedAt: record.acceptedAt,
205
+ };
206
+ if (record.status === "accepted") return { status: "accepted", ...identity };
207
+ if (record.status === "in_flight")
208
+ return { status: "in_flight", ...identity, startedAt: record.startedAt as number };
209
+ const terminal = {
210
+ ...identity,
211
+ ...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
212
+ terminalAt: record.terminalAt as number,
213
+ };
214
+ if (record.status === "terminal_ok") return { status: "terminal_ok", ...terminal };
215
+ return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
216
+ };
217
+
218
+ const hydrateFromStore = async () => {
219
+ if (!store) return;
220
+ const loaded = await store.load();
221
+ records.clear();
222
+ clientRefIndex.clear();
223
+ for (const record of loaded) {
224
+ const key = keyOf(record.kind, record);
225
+ records.set(key, record);
226
+ if (record.clientRef !== undefined) clientRefIndex.set(refKey(record.kind, record.clientRef), key);
227
+ }
228
+ };
229
+
230
+ return {
231
+ admit,
232
+ releaseAdmission,
233
+ noteAccepted,
234
+ noteTransition,
235
+ lookup,
236
+ cleanup,
237
+ activeCount,
238
+ hydrateFromStore,
239
+ };
240
+ }
241
+
242
+ export type { PromptReconciliationStatus };
@@ -12,7 +12,8 @@
12
12
  * - Terminal records are retained for TERMINAL_TTL_MS and bounded to
13
13
  * TERMINAL_CAPACITY, evicted oldest-terminal-first by terminalAt; only then
14
14
  * does a lookup honestly report `unknown`.
15
- * - The durability floor is the live session process: restart means `unknown`.
15
+ * - Process-local durability floor is the live session process: restart means `unknown`.
16
+ * Session-scoped durable retention (kind-aware store) is provided by reconciliation-store.ts (#3032).
16
17
  * - The clientRef index is session-runtime scoped; a ref conflicts only while
17
18
  * retained and must never be reused as a retry mechanism.
18
19
  * - Terminal transitions settle once: the first terminal outcome wins.
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Session-scoped durable store for kind-aware invocation reconciliation (#3032/#3035).
3
+ *
4
+ * Path is always a private sibling of the transcript, never under artifactsDir:
5
+ * <dirname(sessionFile)>/.sdk-reconciliation/<safeSessionId>.json
6
+ *
7
+ * Safe session ids only: /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
8
+ * Atomic write: temp + fsync + rename + 0600. Corrupt → quarantine + empty.
9
+ * Non-terminal records settle to failed/process_restart on bootstrap (recon incomplete).
10
+ */
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
13
+ import type { PromptCorrelation, PromptReconciliationStatus } from "./prompt-reconciliation";
14
+
15
+ export const RECONCILIATION_STORE_VERSION = 1;
16
+ export const RECONCILIATION_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
+ export const RECONCILIATION_DIR_NAME = ".sdk-reconciliation";
18
+
19
+ export type ReconciliationKind = "prompt" | "skill";
20
+
21
+ export interface DurableReconciliationRecord extends PromptCorrelation {
22
+ kind: ReconciliationKind;
23
+ clientRef?: string;
24
+ status: PromptReconciliationStatus;
25
+ error?: { code: string; message: string };
26
+ acceptedAt: number;
27
+ startedAt?: number;
28
+ terminalAt?: number;
29
+ /** Skill-only safe token; never skill args bodies. */
30
+ skillName?: string;
31
+ }
32
+
33
+ export interface ReconciliationStoreDocument {
34
+ version: typeof RECONCILIATION_STORE_VERSION;
35
+ sessionId: string;
36
+ records: DurableReconciliationRecord[];
37
+ }
38
+
39
+ export interface ReconciliationStoreFs {
40
+ mkdir(directory: string, options: { recursive: true; mode: number }): Promise<unknown>;
41
+ readFile(file: string, encoding: "utf8"): Promise<string>;
42
+ writeFile(file: string, data: string, options: { mode: number }): Promise<void>;
43
+ rename(from: string, to: string): Promise<void>;
44
+ unlink(file: string): Promise<void>;
45
+ open(
46
+ file: string,
47
+ flags: string,
48
+ ): Promise<{
49
+ sync(): Promise<void>;
50
+ close(): Promise<void>;
51
+ writeFile(data: string, encoding: "utf8"): Promise<void>;
52
+ }>;
53
+ }
54
+
55
+ const nodeFs: ReconciliationStoreFs = {
56
+ mkdir: fs.mkdir,
57
+ readFile: fs.readFile,
58
+ writeFile: fs.writeFile,
59
+ rename: fs.rename,
60
+ unlink: fs.unlink,
61
+ open: fs.open as ReconciliationStoreFs["open"],
62
+ };
63
+
64
+ export function isSafeReconciliationSessionId(sessionId: string): boolean {
65
+ return RECONCILIATION_SESSION_ID_PATTERN.test(sessionId);
66
+ }
67
+
68
+ /** Derive private store path; throws if sessionId is unsafe (path escape). */
69
+ export function reconciliationStorePath(sessionFile: string, sessionId: string): string {
70
+ if (!isSafeReconciliationSessionId(sessionId))
71
+ throw Object.assign(new Error("Unsafe session id for reconciliation store path."), {
72
+ code: "invalid_input",
73
+ });
74
+ return path.join(path.dirname(sessionFile), RECONCILIATION_DIR_NAME, `${sessionId}.json`);
75
+ }
76
+
77
+ function isRecord(value: unknown): value is Record<string, unknown> {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+
81
+ function parseDocument(raw: string, expectedSessionId: string): ReconciliationStoreDocument {
82
+ const value = JSON.parse(raw) as unknown;
83
+ if (!isRecord(value) || value.version !== RECONCILIATION_STORE_VERSION)
84
+ throw new Error("invalid reconciliation store version");
85
+ if (value.sessionId !== expectedSessionId) throw new Error("session id mismatch");
86
+ if (!Array.isArray(value.records)) throw new Error("invalid records");
87
+ return value as unknown as ReconciliationStoreDocument;
88
+ }
89
+
90
+ /**
91
+ * Settle non-terminal durable records after process death: failed/process_restart.
92
+ * Meaning: reconciliation incomplete — not proof of agent failure.
93
+ */
94
+ export function settleProcessRestart(
95
+ records: DurableReconciliationRecord[],
96
+ now: number,
97
+ ): DurableReconciliationRecord[] {
98
+ return records.map(record => {
99
+ if (record.terminalAt !== undefined) return record;
100
+ return {
101
+ ...record,
102
+ status: "failed",
103
+ terminalAt: now,
104
+ error: { code: "process_restart", message: "Reconciliation incomplete after process restart." },
105
+ };
106
+ });
107
+ }
108
+
109
+ export interface ReconciliationStore {
110
+ readonly path: string | null;
111
+ readonly sessionId: string;
112
+ /** Serialize mutations; reload not required for single-process host (in-memory + write). */
113
+ transact(mutator: (records: DurableReconciliationRecord[]) => DurableReconciliationRecord[]): Promise<void>;
114
+ load(): Promise<DurableReconciliationRecord[]>;
115
+ /** Snapshot currently held in memory after last load/transact. */
116
+ snapshot(): DurableReconciliationRecord[];
117
+ delete(): Promise<void>;
118
+ }
119
+
120
+ export function createReconciliationStore(options: {
121
+ sessionFile: string | null | undefined;
122
+ sessionId: string;
123
+ fs?: ReconciliationStoreFs;
124
+ now?: () => number;
125
+ }): ReconciliationStore {
126
+ const fileFs = options.fs ?? nodeFs;
127
+ const now = options.now ?? Date.now;
128
+ const sessionId = options.sessionId;
129
+ const filePath =
130
+ options.sessionFile && isSafeReconciliationSessionId(sessionId)
131
+ ? reconciliationStorePath(options.sessionFile, sessionId)
132
+ : null;
133
+
134
+ let memory: DurableReconciliationRecord[] = [];
135
+ let chain: Promise<void> = Promise.resolve();
136
+
137
+ const writeAtomic = async (document: ReconciliationStoreDocument): Promise<void> => {
138
+ if (!filePath) return;
139
+ const directory = path.dirname(filePath);
140
+ await fileFs.mkdir(directory, { recursive: true, mode: 0o700 });
141
+ const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
142
+ try {
143
+ await fileFs.writeFile(temporary, `${JSON.stringify(document)}\n`, { mode: 0o600 });
144
+ try {
145
+ const handle = await fileFs.open(temporary, "r+");
146
+ try {
147
+ await handle.sync();
148
+ } finally {
149
+ await handle.close();
150
+ }
151
+ } catch {
152
+ // fsync optional on some fs seams
153
+ }
154
+ await fileFs.rename(temporary, filePath);
155
+ } catch (error) {
156
+ await fileFs.unlink(temporary).catch(() => {});
157
+ throw Object.assign(error instanceof Error ? error : new Error("reconciliation persist failed"), {
158
+ code: "reconciliation_persist_failed",
159
+ });
160
+ }
161
+ };
162
+
163
+ const load = async (): Promise<DurableReconciliationRecord[]> => {
164
+ if (!filePath) {
165
+ memory = [];
166
+ return memory;
167
+ }
168
+ try {
169
+ const raw = await fileFs.readFile(filePath, "utf8");
170
+ const document = parseDocument(raw, sessionId);
171
+ memory = settleProcessRestart(document.records, now());
172
+ // Persist settled state so restart is sticky.
173
+ if (memory.some((r, i) => r !== document.records[i]))
174
+ await writeAtomic({ version: RECONCILIATION_STORE_VERSION, sessionId, records: memory });
175
+ return memory;
176
+ } catch (error) {
177
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
178
+ memory = [];
179
+ return memory;
180
+ }
181
+ // Corrupt → quarantine
182
+ try {
183
+ await fileFs.rename(filePath, `${filePath}.corrupt.${now()}`);
184
+ } catch {
185
+ // ignore
186
+ }
187
+ memory = [];
188
+ return memory;
189
+ }
190
+ };
191
+
192
+ const transact = async (
193
+ mutator: (records: DurableReconciliationRecord[]) => DurableReconciliationRecord[],
194
+ ): Promise<void> => {
195
+ const run = async () => {
196
+ const next = mutator(memory.map(r => ({ ...r })));
197
+ memory = next;
198
+ await writeAtomic({ version: RECONCILIATION_STORE_VERSION, sessionId, records: memory });
199
+ };
200
+ const pending = chain.then(run, run);
201
+ chain = pending.then(
202
+ () => undefined,
203
+ () => undefined,
204
+ );
205
+ await pending;
206
+ };
207
+
208
+ const deleteStore = async (): Promise<void> => {
209
+ memory = [];
210
+ if (!filePath) return;
211
+ await fileFs.unlink(filePath).catch(error => {
212
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
213
+ });
214
+ };
215
+
216
+ return {
217
+ path: filePath,
218
+ sessionId,
219
+ transact,
220
+ load,
221
+ snapshot: () => memory.map(r => ({ ...r })),
222
+ delete: deleteStore,
223
+ };
224
+ }
@@ -158,7 +158,11 @@ function invoke(
158
158
  case "workflow.plan_approve":
159
159
  return surface.approvePlan(text(input, "id"), input.choice, input.expectedSessionId as string | undefined);
160
160
  case "skill.invoke":
161
- return surface.invokeSkill(text(input, "name"), input.args);
161
+ return surface.invokeSkill(
162
+ text(input, "name"),
163
+ input.args,
164
+ typeof input.clientRef === "string" ? input.clientRef : undefined,
165
+ );
162
166
  case "mode.plan.set":
163
167
  return surface.setPlanMode(input.on as boolean);
164
168
  case "mode.goal.operate":
@@ -19,7 +19,7 @@ export interface ControlSurface {
19
19
  idempotencyKey?: string,
20
20
  ): Promise<ControlValue> | ControlValue;
21
21
  approvePlan(id: string, choice: ControlValue, expectedSessionId?: string): Promise<ControlValue> | ControlValue;
22
- invokeSkill(name: string, args: ControlValue): Promise<ControlValue> | ControlValue;
22
+ invokeSkill(name: string, args: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
23
23
  setPlanMode(on: boolean): Promise<ControlValue> | ControlValue;
24
24
  operateGoal(op: string, objective?: string): Promise<ControlValue> | ControlValue;
25
25
  replaceTodo(items: ControlValue): Promise<ControlValue> | ControlValue;
@@ -9,6 +9,13 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
9
9
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
10
10
  /** Best-effort diagnostic observation of accepted control/query frames. */
11
11
  onRequest?: SdkRequestObserver;
12
+ /** Runs before a control response is sent; identity transitions use sendTerminal. */
13
+ beforeControlResponse?: (
14
+ connectionId: string,
15
+ request: SdkFrame,
16
+ response: SdkFrame,
17
+ sendTerminal: () => Promise<void>,
18
+ ) => void | Promise<void>;
12
19
  /** Runs only after a successful control response has been sent to the client. */
13
20
  afterControlResponse?: (connectionId: string, request: SdkFrame, response: SdkFrame) => void | Promise<void>;
14
21
  installProviderDefinitions?: (capability: string, definitions: unknown) => void;
@@ -36,6 +43,14 @@ function errorFrame(connectionId: string, frame: SdkFrame, error: unknown): SdkF
36
43
  ? candidate.code
37
44
  : "internal";
38
45
  const message = typeof candidate?.message === "string" ? candidate.message : "SDK host operation failed.";
46
+ if (frame.type === "control_request") {
47
+ return {
48
+ type: "control_response",
49
+ id: typeof frame.id === "string" ? frame.id : "",
50
+ ok: false,
51
+ error: { code, message },
52
+ };
53
+ }
39
54
  return {
40
55
  type: "reverse_response",
41
56
  id: typeof frame.id === "string" ? frame.id : "",
@@ -214,7 +229,14 @@ export class SessionSdkHost {
214
229
  const result = await this.#options.control?.(connectionId, frame);
215
230
  if (result !== undefined) {
216
231
  const response = { type: "control_response", ...(result as SdkFrame) };
217
- await this.#send(connectionId, response);
232
+ let terminalSent = false;
233
+ const sendTerminal = async (): Promise<void> => {
234
+ if (terminalSent) return;
235
+ terminalSent = true;
236
+ await this.#send(connectionId, response);
237
+ };
238
+ await this.#options.beforeControlResponse?.(connectionId, frame, response, sendTerminal);
239
+ await sendTerminal();
218
240
  await this.#options.afterControlResponse?.(connectionId, frame, response);
219
241
  }
220
242
  break;
@@ -46,6 +46,11 @@ export interface SessionSurface {
46
46
  getJobs(): unknown | Promise<unknown>;
47
47
  /** Q26 keyed lookup of a submitted prompt's authoritative reconciliation status. */
48
48
  getPromptStatus?(selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown | Promise<unknown>;
49
+ getSkillInvokeStatus?(selector: {
50
+ commandId?: string;
51
+ turnId?: string;
52
+ clientRef?: string;
53
+ }): unknown | Promise<unknown>;
49
54
  /** Q27 effective model-profile catalog from the live session registry. */
50
55
  getModelProfiles?(): unknown[] | Promise<unknown[]>;
51
56
  /** Query rows backed by the session's installed binding map. */
@@ -127,6 +132,7 @@ const names = [
127
132
  "runtime.jobs.list",
128
133
  "turn.prompt_status",
129
134
  "models.profiles.list",
135
+ "skill.invoke_status",
130
136
  ];
131
137
 
132
138
  export class QueryHandlers {
@@ -156,6 +162,7 @@ export class QueryHandlers {
156
162
  if (query === "Q02") return await this.#transcriptBody(request);
157
163
  if (query === "Q23") return await this.#resourceBody(request);
158
164
  if (query === "Q26") return await this.#promptStatus(request);
165
+ if (query === "Q28") return await this.#skillInvokeStatus(request);
159
166
  if (query === "Q24") return await this.#artifact(request);
160
167
  if (query === "Q27" && request.input && Object.keys(request.input).length > 0)
161
168
  return this.#error(request, "invalid_request", false, "models.profiles.list does not accept input fields.");
@@ -517,6 +524,54 @@ export class QueryHandlers {
517
524
  return { id: request.id, ok: true, page };
518
525
  }
519
526
 
527
+ async #skillInvokeStatus(request: QueryRequest): Promise<QueryResponse> {
528
+ if (request.cursor)
529
+ return this.#error(request, "invalid_request", false, "skill.invoke_status does not support cursors.");
530
+ const input = request.input ?? {};
531
+ for (const key of Object.keys(input))
532
+ if (key !== "commandId" && key !== "turnId" && key !== "clientRef")
533
+ return this.#error(
534
+ request,
535
+ "invalid_request",
536
+ false,
537
+ `skill.invoke_status does not accept selector field "${key}".`,
538
+ );
539
+ const commandId = typeof input.commandId === "string" && input.commandId ? input.commandId : undefined;
540
+ const turnId = typeof input.turnId === "string" && input.turnId ? input.turnId : undefined;
541
+ const rawClientRef = typeof input.clientRef === "string" ? input.clientRef : undefined;
542
+ const trimmedClientRef = rawClientRef?.trim();
543
+ if (rawClientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
544
+ return this.#error(
545
+ request,
546
+ "invalid_request",
547
+ false,
548
+ "clientRef must be a non-empty string of at most 128 characters.",
549
+ );
550
+ const clientRef = trimmedClientRef || undefined;
551
+ if ((commandId === undefined) !== (turnId === undefined))
552
+ return this.#error(request, "invalid_request", false, "commandId and turnId must be provided together.");
553
+ if (commandId !== undefined && clientRef !== undefined)
554
+ return this.#error(
555
+ request,
556
+ "invalid_request",
557
+ false,
558
+ "Provide exactly one selector: a commandId/turnId pair or a clientRef.",
559
+ );
560
+ if (commandId === undefined && clientRef === undefined)
561
+ return this.#error(
562
+ request,
563
+ "invalid_request",
564
+ false,
565
+ "skill.invoke_status requires a commandId/turnId pair or a clientRef.",
566
+ );
567
+ if (typeof this.surface.getSkillInvokeStatus !== "function")
568
+ return this.#error(request, "unavailable", false, "skill.invoke_status is unavailable for this session.");
569
+ const result = await this.surface.getSkillInvokeStatus(
570
+ clientRef !== undefined ? { clientRef } : { commandId: commandId!, turnId: turnId! },
571
+ );
572
+ return { id: request.id, ok: true, result };
573
+ }
574
+
520
575
  async #promptStatus(request: QueryRequest): Promise<QueryResponse> {
521
576
  if (request.cursor)
522
577
  return this.#error(request, "invalid_request", false, "turn.prompt_status does not support cursors.");