@gajae-code/coding-agent 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -2,14 +2,13 @@
2
2
  * Kind-aware invocation reconciliation (prompt | skill) with optional durable store.
3
3
  * Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
4
4
  */
5
+ import type { PromptReconciliationStatus, SdkPromptTerminalOutcome, TurnPromptReconciliation } from "../prompt-status";
5
6
  import {
6
7
  PROMPT_RECONCILIATION_ACTIVE_CAPACITY,
7
8
  PROMPT_RECONCILIATION_TERMINAL_CAPACITY,
8
9
  PROMPT_RECONCILIATION_TERMINAL_TTL_MS,
9
10
  type PromptCorrelation,
10
- type PromptReconciliationStatus,
11
11
  sanitizePromptFailure,
12
- type TurnPromptReconciliation,
13
12
  } from "./prompt-reconciliation";
14
13
  import type { DurableReconciliationRecord, ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
15
14
 
@@ -33,6 +32,16 @@ export interface KindAwareReconciliation {
33
32
  correlation: PromptCorrelation | undefined,
34
33
  frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
35
34
  ): Promise<void>;
35
+ claimPendingOutcome(
36
+ correlation: PromptCorrelation,
37
+ outcome: SdkPromptTerminalOutcome,
38
+ ): Promise<SdkPromptTerminalOutcome>;
39
+ finalizePromptOutcome(
40
+ correlation: PromptCorrelation,
41
+ outcome?: SdkPromptTerminalOutcome,
42
+ recordError?: { code: string; message: string },
43
+ ): Promise<void>;
44
+ peekPendingOutcome(correlation: PromptCorrelation): SdkPromptTerminalOutcome | undefined;
36
45
  lookup(
37
46
  kind: ReconciliationKind,
38
47
  selector: { commandId?: string; turnId?: string; clientRef?: string },
@@ -48,48 +57,72 @@ export function createKindAwareReconciliation(
48
57
  ): KindAwareReconciliation {
49
58
  const now = options.now ?? Date.now;
50
59
  const store = options.store ?? null;
51
- const records = new Map<string, DurableReconciliationRecord>();
52
- const clientRefIndex = new Map<string, string>(); // `${kind}\0${clientRef}` -> key
60
+ let records = new Map<string, DurableReconciliationRecord>();
61
+ let clientRefIndex = new Map<string, string>();
53
62
  const reservedClientRefs = new Map<ReconciliationKind, Set<string>>();
54
63
  const reservations: Array<{ kind: ReconciliationKind; clientRef?: string }> = [];
64
+ let mutationChain: Promise<void> = Promise.resolve();
55
65
 
56
66
  const keyOf = (kind: ReconciliationKind, correlation: PromptCorrelation) =>
57
67
  `${kind}:${correlation.commandId}:${correlation.turnId}`;
58
68
  const refKey = (kind: ReconciliationKind, clientRef: string) => `${kind}\0${clientRef}`;
59
69
 
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
- }
70
+ const indexRecords = (source: Map<string, DurableReconciliationRecord>) => {
71
+ const index = new Map<string, string>();
72
+ for (const [key, record] of source)
73
+ if (record.clientRef !== undefined) index.set(refKey(record.kind, record.clientRef), key);
74
+ return index;
77
75
  };
78
76
 
79
- const cleanup = () => {
77
+ const cleanupRecords = (source: Map<string, DurableReconciliationRecord>) => {
80
78
  const at = now();
81
- for (const [key, record] of records)
79
+ for (const [key, record] of source)
82
80
  if (record.terminalAt !== undefined && record.terminalAt + PROMPT_RECONCILIATION_TERMINAL_TTL_MS <= at)
83
- remove(key);
81
+ source.delete(key);
84
82
  for (const kind of ["prompt", "skill"] as const) {
85
- const terminalEntries = [...records.entries()].filter(
83
+ const terminalEntries = [...source.entries()].filter(
86
84
  ([, record]) => record.kind === kind && record.terminalAt !== undefined,
87
85
  );
88
86
  if (terminalEntries.length <= PROMPT_RECONCILIATION_TERMINAL_CAPACITY) continue;
89
87
  terminalEntries.sort((a, b) => (a[1].terminalAt as number) - (b[1].terminalAt as number));
90
88
  for (const [key] of terminalEntries.slice(0, terminalEntries.length - PROMPT_RECONCILIATION_TERMINAL_CAPACITY))
91
- remove(key);
89
+ source.delete(key);
90
+ }
91
+ };
92
+
93
+ const queueMutation = async <T>(
94
+ mutate: (candidate: Map<string, DurableReconciliationRecord>) => { value: T; changed: boolean },
95
+ ): Promise<T> => {
96
+ const run = async () => {
97
+ const candidate = new Map([...records].map(([key, record]) => [key, { ...record }]));
98
+ const result = mutate(candidate);
99
+ if (!result.changed) return result.value;
100
+ const candidateIndex = indexRecords(candidate);
101
+ if (store) await store.transact(() => [...candidate.values()].map(record => ({ ...record })));
102
+ records = candidate;
103
+ clientRefIndex = candidateIndex;
104
+ return result.value;
105
+ };
106
+ const pending = mutationChain.then(run, run);
107
+ mutationChain = pending.then(
108
+ () => undefined,
109
+ () => undefined,
110
+ );
111
+ return await pending;
112
+ };
113
+
114
+ const reservedFor = (kind: ReconciliationKind) => {
115
+ let set = reservedClientRefs.get(kind);
116
+ if (!set) {
117
+ set = new Set();
118
+ reservedClientRefs.set(kind, set);
92
119
  }
120
+ return set;
121
+ };
122
+
123
+ const cleanup = () => {
124
+ cleanupRecords(records);
125
+ clientRefIndex = indexRecords(records);
93
126
  };
94
127
 
95
128
  const activeCount = (kind: ReconciliationKind) => {
@@ -98,21 +131,19 @@ export function createKindAwareReconciliation(
98
131
  return count;
99
132
  };
100
133
 
101
- const reservationCount = (kind: ReconciliationKind) => reservations.filter(r => r.kind === kind).length;
134
+ const reservationCount = (kind: ReconciliationKind) => reservations.filter(record => record.kind === kind).length;
102
135
 
103
136
  const consumeReservation = (kind: ReconciliationKind, clientRef?: string) => {
104
- const index = reservations.findIndex(r => r.kind === kind && r.clientRef === clientRef);
137
+ const index = reservations.findIndex(record => record.kind === kind && record.clientRef === clientRef);
105
138
  if (index === -1) return;
106
139
  reservations.splice(index, 1);
107
- if (clientRef !== undefined && !reservations.some(r => r.kind === kind && r.clientRef === clientRef))
140
+ if (
141
+ clientRef !== undefined &&
142
+ !reservations.some(record => record.kind === kind && record.clientRef === clientRef)
143
+ )
108
144
  reservedFor(kind).delete(clientRef);
109
145
  };
110
146
 
111
- const persist = async () => {
112
- if (!store) return;
113
- await store.transact(() => [...records.values()].map(r => ({ ...r })));
114
- };
115
-
116
147
  const admit = (kind: ReconciliationKind, clientRef?: string) => {
117
148
  cleanup();
118
149
  const reserved = reservedFor(kind);
@@ -139,22 +170,20 @@ export function createKindAwareReconciliation(
139
170
  clientRef?: string,
140
171
  extra?: { skillName?: string },
141
172
  ) => {
142
- cleanup();
173
+ await queueMutation(candidate => {
174
+ cleanupRecords(candidate);
175
+ candidate.set(keyOf(kind, correlation), {
176
+ kind,
177
+ commandId: correlation.commandId,
178
+ turnId: correlation.turnId,
179
+ ...(clientRef !== undefined ? { clientRef } : {}),
180
+ status: "accepted",
181
+ acceptedAt: now(),
182
+ ...(extra?.skillName ? { skillName: extra.skillName } : {}),
183
+ });
184
+ return { value: undefined, changed: true };
185
+ });
143
186
  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
187
  };
159
188
 
160
189
  const noteTransition = async (
@@ -163,27 +192,63 @@ export function createKindAwareReconciliation(
163
192
  frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
164
193
  ) => {
165
194
  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") {
195
+ await queueMutation(candidate => {
196
+ const record = candidate.get(keyOf(kind, correlation));
197
+ if (!record || record.terminalAt !== undefined) return { value: undefined, changed: false };
198
+ if (frame.type === "agent_start") {
199
+ if (record.status !== "accepted") return { value: undefined, changed: false };
170
200
  record.status = "in_flight";
171
201
  record.startedAt = now();
172
- await persist();
202
+ return { value: undefined, changed: true };
173
203
  }
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();
204
+ record.terminalAt = now();
205
+ if (frame.type === "agent_failed") {
206
+ record.status = "failed";
207
+ record.error = sanitizePromptFailure(frame.error);
208
+ } else record.status = "terminal_ok";
209
+ cleanupRecords(candidate);
210
+ return { value: undefined, changed: true };
211
+ });
212
+ };
213
+
214
+ const claimPendingOutcome = async (
215
+ correlation: PromptCorrelation,
216
+ outcome: SdkPromptTerminalOutcome,
217
+ ): Promise<SdkPromptTerminalOutcome> =>
218
+ await queueMutation(candidate => {
219
+ const record = candidate.get(keyOf("prompt", correlation));
220
+ if (!record || record.terminalAt !== undefined || record.kind !== "prompt")
221
+ return { value: outcome, changed: false };
222
+ if (record.pendingOutcome !== undefined) return { value: record.pendingOutcome, changed: false };
223
+ record.pendingOutcome = outcome;
224
+ return { value: outcome, changed: true };
225
+ });
226
+
227
+ const finalizePromptOutcome = async (
228
+ correlation: PromptCorrelation,
229
+ outcome?: SdkPromptTerminalOutcome,
230
+ recordError?: { code: string; message: string },
231
+ ) => {
232
+ await queueMutation(candidate => {
233
+ const record = candidate.get(keyOf("prompt", correlation));
234
+ if (!record || record.terminalAt !== undefined || record.kind !== "prompt")
235
+ return { value: undefined, changed: false };
236
+ const finalOutcome = outcome ?? record.pendingOutcome;
237
+ record.terminalAt = now();
238
+ if (finalOutcome?.kind === "failed") {
239
+ record.status = "failed";
240
+ record.error = recordError ?? { code: finalOutcome.code, message: finalOutcome.message };
241
+ } else record.status = "terminal_ok";
242
+ record.outcome = finalOutcome;
243
+ record.pendingOutcome = undefined;
244
+ cleanupRecords(candidate);
245
+ return { value: undefined, changed: true };
246
+ });
185
247
  };
186
248
 
249
+ const peekPendingOutcome = (correlation: PromptCorrelation) =>
250
+ records.get(keyOf("prompt", correlation))?.pendingOutcome;
251
+
187
252
  const lookup = (
188
253
  kind: ReconciliationKind,
189
254
  selector: { commandId?: string; turnId?: string; clientRef?: string },
@@ -210,6 +275,7 @@ export function createKindAwareReconciliation(
210
275
  ...identity,
211
276
  ...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
212
277
  terminalAt: record.terminalAt as number,
278
+ ...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
213
279
  };
214
280
  if (record.status === "terminal_ok") return { status: "terminal_ok", ...terminal };
215
281
  return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
@@ -217,14 +283,20 @@ export function createKindAwareReconciliation(
217
283
 
218
284
  const hydrateFromStore = async () => {
219
285
  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
- }
286
+ const run = async () => {
287
+ const loaded = await store.load();
288
+ const candidate = new Map(loaded.map(record => [keyOf(record.kind, record), { ...record }] as const));
289
+ const candidateIndex = indexRecords(candidate);
290
+ await store.transact(() => [...candidate.values()].map(record => ({ ...record })));
291
+ records = candidate;
292
+ clientRefIndex = candidateIndex;
293
+ };
294
+ const pending = mutationChain.then(run, run);
295
+ mutationChain = pending.then(
296
+ () => undefined,
297
+ () => undefined,
298
+ );
299
+ await pending;
228
300
  };
229
301
 
230
302
  return {
@@ -232,6 +304,9 @@ export function createKindAwareReconciliation(
232
304
  releaseAdmission,
233
305
  noteAccepted,
234
306
  noteTransition,
307
+ claimPendingOutcome,
308
+ finalizePromptOutcome,
309
+ peekPendingOutcome,
235
310
  lookup,
236
311
  cleanup,
237
312
  activeCount,
@@ -270,7 +270,10 @@ function isSafePath(value: string): boolean {
270
270
  // relative are both allowed (full-trust chat), but not injection shapes.
271
271
  if (value.length === 0 || value.length > 4096) return false;
272
272
  if (/[\n\r\0]/.test(value)) return false;
273
- return !/[;&|`$(){}<>*?!\\"']/.test(value);
273
+ // Backslash is a shell escape on POSIX but the native path separator on
274
+ // Windows; allow it there so absolute Windows paths validate correctly.
275
+ const metachars = process.platform === "win32" ? /[;&|`$(){}<>*?!"']/ : /[;&|`$(){}<>*?!\\"']/;
276
+ return !metachars.test(value);
274
277
  }
275
278
 
276
279
  function isSafeBranch(value: string): boolean {