@astrosheep/keiyaku 2.8.4 → 2.9.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 (229) hide show
  1. package/README.md +35 -15
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
  4. package/build/agents/harness/control-types.js +1 -0
  5. package/build/agents/harness/execution-handle.js +23 -3
  6. package/build/agents/harness/index.js +6 -607
  7. package/build/agents/harness/outcome.js +141 -0
  8. package/build/agents/harness/projection.js +310 -0
  9. package/build/agents/harness/router.js +201 -0
  10. package/build/agents/harness/runtime.js +2 -1
  11. package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
  12. package/build/agents/launch-snapshot/resolve.js +72 -0
  13. package/build/agents/opencode-sdk.js +24 -3
  14. package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
  15. package/build/agents/providers/claude-agent-sdk/events.js +218 -0
  16. package/build/agents/providers/claude-agent-sdk/session.js +158 -0
  17. package/build/agents/providers/codex-app-server/adapter.js +347 -0
  18. package/build/agents/providers/codex-app-server/events.js +352 -0
  19. package/build/agents/providers/codex-app-server/session.js +266 -0
  20. package/build/agents/providers/codex-sdk.js +23 -5
  21. package/build/agents/providers/opencode-sdk/adapter.js +344 -0
  22. package/build/agents/providers/opencode-sdk/events.js +354 -0
  23. package/build/agents/providers/opencode-sdk/session.js +180 -0
  24. package/build/agents/providers/pi/adapter.js +269 -0
  25. package/build/agents/providers/pi/checkpoint.js +41 -0
  26. package/build/agents/providers/pi/events.js +282 -0
  27. package/build/agents/{selector_v2.js → selector.js} +8 -6
  28. package/build/cli/commands/akuma/akuma/handler.js +4 -0
  29. package/build/cli/commands/akuma/akuma/meta.js +16 -0
  30. package/build/cli/commands/akuma/catalog.js +8 -0
  31. package/build/cli/commands/akuma/list/handler.js +15 -0
  32. package/build/cli/commands/akuma/list/meta-ls.js +9 -0
  33. package/build/cli/commands/akuma/view/handler.js +34 -0
  34. package/build/cli/commands/akuma/view/meta.js +8 -0
  35. package/build/cli/commands/akuma.js +1 -1
  36. package/build/cli/commands/contract/amend/handler.js +10 -0
  37. package/build/cli/commands/contract/amend/meta.js +12 -0
  38. package/build/cli/commands/contract/arc/handler.js +9 -0
  39. package/build/cli/commands/contract/arc/meta.js +26 -0
  40. package/build/cli/commands/contract/bind/handler.js +23 -0
  41. package/build/cli/commands/contract/bind/meta.js +30 -0
  42. package/build/cli/commands/contract/catalog.js +16 -0
  43. package/build/cli/commands/contract/forfeit/handler.js +13 -0
  44. package/build/cli/commands/contract/forfeit/meta.js +12 -0
  45. package/build/cli/commands/contract/log/handler.js +12 -0
  46. package/build/cli/commands/contract/log/meta.js +12 -0
  47. package/build/cli/commands/contract/petition/handler.js +26 -0
  48. package/build/cli/commands/contract/petition/meta.js +23 -0
  49. package/build/cli/commands/contract/renew/handler.js +10 -0
  50. package/build/cli/commands/contract/renew/meta.js +17 -0
  51. package/build/cli/commands/projection/call/handler.js +39 -0
  52. package/build/cli/commands/projection/call/meta.js +9 -0
  53. package/build/cli/commands/projection/catalog.js +14 -0
  54. package/build/cli/commands/projection/kill/handler.js +35 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +53 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +23 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +34 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +21 -0
  63. package/build/cli/commands/projection/wait/meta.js +11 -0
  64. package/build/cli/commands/registry.js +100 -0
  65. package/build/cli/commands/shared.js +71 -0
  66. package/build/cli/commands/skills/catalog.js +6 -0
  67. package/build/cli/commands/skills/install/handler.js +10 -0
  68. package/build/cli/commands/skills/install/meta.js +9 -0
  69. package/build/cli/commands/skills/skills/handler.js +4 -0
  70. package/build/cli/commands/skills/skills/meta.js +10 -0
  71. package/build/cli/commands/system/catalog.js +8 -0
  72. package/build/cli/commands/system/completion/handler.js +10 -0
  73. package/build/cli/commands/system/completion/meta.js +9 -0
  74. package/build/cli/commands/system/dump-env/handler.js +3 -0
  75. package/build/cli/commands/system/dump-env/meta.js +10 -0
  76. package/build/cli/commands/system/guide/handler.js +4 -0
  77. package/build/cli/commands/system/guide/meta.js +9 -0
  78. package/build/cli/commands/task/add/handler.js +21 -0
  79. package/build/cli/commands/task/add/meta.js +8 -0
  80. package/build/cli/commands/task/catalog.js +22 -0
  81. package/build/cli/commands/task/doctor/handler.js +4 -0
  82. package/build/cli/commands/task/doctor/meta.js +8 -0
  83. package/build/cli/commands/task/done/handler.js +17 -0
  84. package/build/cli/commands/task/done/meta.js +8 -0
  85. package/build/cli/commands/task/drop/handler.js +6 -0
  86. package/build/cli/commands/task/drop/meta.js +8 -0
  87. package/build/cli/commands/task/ls/handler.js +13 -0
  88. package/build/cli/commands/task/ls/meta.js +8 -0
  89. package/build/cli/commands/task/shared.js +131 -0
  90. package/build/cli/commands/task/start/handler.js +6 -0
  91. package/build/cli/commands/task/start/meta.js +8 -0
  92. package/build/cli/commands/task/stop/handler.js +17 -0
  93. package/build/cli/commands/task/stop/meta.js +8 -0
  94. package/build/cli/commands/task/task/handler.js +5 -0
  95. package/build/cli/commands/task/task/meta.js +11 -0
  96. package/build/cli/commands/task/update/handler.js +18 -0
  97. package/build/cli/commands/task/update/meta.js +8 -0
  98. package/build/cli/commands/task/view/handler.js +14 -0
  99. package/build/cli/commands/task/view/meta.js +8 -0
  100. package/build/cli/completion.js +26 -27
  101. package/build/cli/flags.js +79 -2
  102. package/build/cli/help.js +27 -49
  103. package/build/cli/index.js +121 -374
  104. package/build/cli/output-stream-error-policy.js +17 -0
  105. package/build/cli/parse.js +159 -72
  106. package/build/cli/projection-address.js +5 -0
  107. package/build/cli/render/arc.js +1 -1
  108. package/build/cli/render/errors.js +4 -3
  109. package/build/cli/render/format.js +14 -0
  110. package/build/cli/render/kanshi.js +106 -0
  111. package/build/cli/render/petition.js +24 -1
  112. package/build/cli/render/projection-activity.js +5 -1
  113. package/build/cli/render/shared.js +2 -2
  114. package/build/cli/render/status.js +79 -13
  115. package/build/cli/render/task.js +113 -0
  116. package/build/cli/render/wait.js +23 -3
  117. package/build/cli/types.js +1 -1
  118. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
  119. package/build/config/env.js +9 -4
  120. package/build/config/settings/disease.js +26 -0
  121. package/build/config/settings/knobs.js +8 -0
  122. package/build/config/settings/loader.js +244 -0
  123. package/build/config/settings/schema.js +276 -0
  124. package/build/core/addressing.js +1 -1
  125. package/build/core/amend.js +3 -3
  126. package/build/core/arc.js +5 -4
  127. package/build/core/bind-workspace.js +123 -0
  128. package/build/core/bind.js +75 -155
  129. package/build/core/call/call.js +224 -0
  130. package/build/core/call/context.js +238 -0
  131. package/build/core/call/execution.js +217 -0
  132. package/build/core/call/prompt.js +42 -0
  133. package/build/core/call/reference.js +24 -0
  134. package/build/core/call-persist.js +1 -1
  135. package/build/core/claim-delivery.js +247 -0
  136. package/build/core/claim.js +39 -277
  137. package/build/core/context.js +2 -1
  138. package/build/core/{render.js → contract-view.js} +12 -140
  139. package/build/core/draft.js +107 -9
  140. package/build/core/entry.js +9 -1
  141. package/build/core/execution-coordinate.js +4 -20
  142. package/build/core/execution-pact.js +16 -31
  143. package/build/core/forfeit.js +35 -22
  144. package/build/core/hints.js +2 -2
  145. package/build/core/log.js +2 -2
  146. package/build/core/petition-claim-gates.js +8 -6
  147. package/build/core/petition-preview.js +97 -0
  148. package/build/core/petition-run.js +3 -2
  149. package/build/core/petition.js +39 -26
  150. package/build/core/projection/generation/database.js +306 -0
  151. package/build/core/projection/generation/identity.js +84 -0
  152. package/build/core/projection/generation/model.js +201 -0
  153. package/build/core/projection/generation/store.js +73 -0
  154. package/build/core/projection/generation/transitions.js +258 -0
  155. package/build/core/projection/heart.js +0 -307
  156. package/build/core/projection/tell/database.js +109 -0
  157. package/build/core/projection/tell/model.js +97 -0
  158. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  159. package/build/core/projection-activity.js +23 -2
  160. package/build/core/projection-core.js +2 -1
  161. package/build/core/projection-execution-observer.js +259 -0
  162. package/build/core/projection-generation-continuation.js +1 -1
  163. package/build/core/projection-generation-execution.js +90 -20
  164. package/build/core/projection-generation-launcher.js +1 -1
  165. package/build/core/projection-generation-runner.js +198 -4
  166. package/build/core/projection-identity.js +1 -1
  167. package/build/core/projection-kill.js +50 -17
  168. package/build/core/projection-life-observer.js +1 -1
  169. package/build/core/projection-life-protocol.js +10 -0
  170. package/build/core/projection-mint.js +1 -1
  171. package/build/core/projection-status.js +45 -32
  172. package/build/core/projection-wait.js +12 -2
  173. package/build/core/projection-wake.js +5 -2
  174. package/build/core/{queue_v2.js → queue.js} +2 -1
  175. package/build/core/registry.js +1 -1
  176. package/build/core/renew.js +7 -7
  177. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  178. package/build/core/response-artifact-catalog.js +7 -0
  179. package/build/core/seal.js +44 -16
  180. package/build/core/status/board.js +286 -0
  181. package/build/core/status/drift.js +99 -0
  182. package/build/core/status/lifecycle.js +84 -0
  183. package/build/core/status/reconciliation.js +131 -0
  184. package/build/core/stored-agent-event.js +28 -4
  185. package/build/core/target-ref.js +10 -0
  186. package/build/core/task/board.js +175 -0
  187. package/build/core/task/commands.js +211 -0
  188. package/build/core/task/document.js +189 -0
  189. package/build/core/task/model.js +1 -0
  190. package/build/core/task/settlement-git.js +207 -0
  191. package/build/core/task/settlement-policy.js +82 -0
  192. package/build/core/task/source-board.js +84 -0
  193. package/build/core/task/source-model.js +1 -0
  194. package/build/core/task/validation.js +44 -0
  195. package/build/core/task-contract.js +224 -0
  196. package/build/core/task-doctor.js +14 -0
  197. package/build/core/task-git-actor.js +38 -0
  198. package/build/core/task-git-runtime.js +34 -0
  199. package/build/core/task-git-store.js +375 -0
  200. package/build/core/task-repository.js +1 -0
  201. package/build/core/task-store-repository.js +86 -0
  202. package/build/core/task-store.js +1 -0
  203. package/build/core/task-worktree.js +51 -0
  204. package/build/core/task.js +4 -0
  205. package/build/core/transcripts.js +138 -56
  206. package/build/core/validation.js +17 -0
  207. package/build/core/verdict.js +2 -2
  208. package/build/core/worktree-path.js +24 -6
  209. package/build/flow-error.js +3 -2
  210. package/build/generated/version.js +1 -1
  211. package/build/git/commits.js +3 -0
  212. package/build/git/diff/parsers.js +0 -94
  213. package/build/index.js +2 -0
  214. package/build/keiyaku-constants.js +2 -0
  215. package/build/keiyaku.js +4 -3
  216. package/package.json +3 -2
  217. package/skills/keiyaku/SKILL.md +1 -1
  218. package/skills/keiyaku-akuma/SKILL.md +11 -6
  219. package/build/agents/harness/pump.js +0 -158
  220. package/build/agents/providers/codex-app-server.js +0 -864
  221. package/build/agents/providers/opencode-sdk.js +0 -363
  222. package/build/agents/providers/pi.js +0 -557
  223. package/build/config/settings.js +0 -533
  224. package/build/core/call.js +0 -686
  225. package/build/core/projection/generation.js +0 -412
  226. package/build/core/projection/identity.js +0 -81
  227. package/build/core/projection/mint.js +0 -174
  228. package/build/core/projection-generation-store.js +0 -707
  229. package/build/core/status.js +0 -539
@@ -0,0 +1,352 @@
1
+ import { boundActivityExcerpt, diffstatFromUnifiedPatch } from "../../harness/activity-values.js";
2
+ import { AGENT_PLAN_ITEM_TEXT_MAX_CHARS, AGENT_PLAN_MAX_ITEMS, AGENT_THOUGHT_TEXT_MAX_CHARS, } from "../../harness/events.js";
3
+ import { createAbortError, emitProviderEvidence, coerceString } from "../../harness/runtime.js";
4
+ const CODEX_APP_SERVER_PROVIDER = "codex-app-server";
5
+ const CODEX_APP_SERVER_NOTIFICATION_ACKNOWLEDGED = new Set([
6
+ "item/agentMessage/delta",
7
+ "item/commandExecution/outputDelta",
8
+ "item/fileChange/outputDelta",
9
+ "item/reasoning/summaryTextDelta",
10
+ "item/reasoning/summaryPartAdded",
11
+ "item/reasoning/textDelta",
12
+ ]);
13
+ const CODEX_APP_SERVER_NOTIFICATION_MAPPED = new Set([
14
+ "item/completed",
15
+ "turn/started",
16
+ "turn/completed",
17
+ "error",
18
+ ]);
19
+ export function codexAppServerNotificationDisposition(method) {
20
+ if (CODEX_APP_SERVER_NOTIFICATION_ACKNOWLEDGED.has(method))
21
+ return "acknowledged-unpersisted";
22
+ if (CODEX_APP_SERVER_NOTIFICATION_MAPPED.has(method))
23
+ return "mapped";
24
+ return "unknown";
25
+ }
26
+ export function appServerRecord(value) {
27
+ return value !== null && typeof value === "object" && !Array.isArray(value)
28
+ ? value
29
+ : undefined;
30
+ }
31
+ const CODEX_APP_SERVER_ACTIVITY_ITEM_TYPES = [
32
+ "commandExecution",
33
+ "fileChange",
34
+ "webSearch",
35
+ "mcpToolCall",
36
+ ];
37
+ function isCodexAppServerActivityItemType(value) {
38
+ return CODEX_APP_SERVER_ACTIVITY_ITEM_TYPES.includes(value);
39
+ }
40
+ export function appServerActivityCall(itemType, item) {
41
+ switch (itemType) {
42
+ case "commandExecution":
43
+ return typeof item.command === "string"
44
+ ? {
45
+ kind: "run",
46
+ command: item.command,
47
+ ...(typeof item.description === "string" && item.description ? { description: item.description } : {}),
48
+ }
49
+ : { kind: "other", display: itemType };
50
+ case "fileChange": {
51
+ const changes = Array.isArray(item.changes)
52
+ ? item.changes.flatMap((entry) => {
53
+ const change = appServerRecord(entry);
54
+ if (!change || typeof change.path !== "string")
55
+ return [];
56
+ if (change.kind !== "add" && change.kind !== "update" && change.kind !== "delete")
57
+ return [];
58
+ const op = change.kind;
59
+ return [{ op, path: change.path }];
60
+ })
61
+ : [];
62
+ if (changes.length > 0)
63
+ return { kind: "fileChange", changes };
64
+ return { kind: "other", display: typeof item.path === "string" ? `fileChange ${item.path}` : itemType };
65
+ }
66
+ case "webSearch":
67
+ return typeof item.query === "string" ? { kind: "search", query: item.query } : { kind: "other", display: itemType };
68
+ case "mcpToolCall":
69
+ return typeof item.server === "string" && typeof item.tool === "string"
70
+ ? { kind: "other", display: `${item.server}.${item.tool}` }
71
+ : { kind: "other", display: itemType };
72
+ }
73
+ }
74
+ export function appServerActivityResult(item) {
75
+ const exitCode = typeof item.exitCode === "number" && Number.isFinite(item.exitCode) ? item.exitCode : undefined;
76
+ const message = typeof item.aggregatedOutput === "string" && item.aggregatedOutput
77
+ ? boundActivityExcerpt(item.aggregatedOutput)
78
+ : typeof item.output === "string" && item.output
79
+ ? boundActivityExcerpt(item.output)
80
+ : typeof item.error === "string" && item.error
81
+ ? boundActivityExcerpt(item.error)
82
+ : undefined;
83
+ const details = item.details !== null && typeof item.details === "object" && !Array.isArray(item.details)
84
+ ? item.details
85
+ : undefined;
86
+ const patch = typeof details?.patch === "string" ? details.patch : undefined;
87
+ const diffstat = patch ? diffstatFromUnifiedPatch(patch) : undefined;
88
+ return {
89
+ status: item.status === "failed" || (exitCode !== undefined && exitCode !== 0) ? "error" : "ok",
90
+ ...(message ? { message } : {}),
91
+ ...(exitCode === undefined ? {} : { exitCode }),
92
+ ...(diffstat ? { diffstat } : {}),
93
+ };
94
+ }
95
+ export function extractReasoningSummary(item) {
96
+ const summary = item.summary;
97
+ const text = typeof summary === "string"
98
+ ? summary
99
+ : Array.isArray(summary)
100
+ ? summary.map((part) => {
101
+ if (typeof part === "string")
102
+ return part;
103
+ if (!part || typeof part !== "object")
104
+ return "";
105
+ const record = part;
106
+ if (record.type !== "summary_text" && record.type !== "text")
107
+ return "";
108
+ return typeof record.text === "string" ? record.text : "";
109
+ }).join("\n")
110
+ : "";
111
+ return text.trim().length > 0 ? text : undefined;
112
+ }
113
+ export function extractPlanItems(item) {
114
+ if (!Array.isArray(item.items))
115
+ return [];
116
+ return item.items.slice(0, AGENT_PLAN_MAX_ITEMS).flatMap((value) => {
117
+ if (!value || typeof value !== "object")
118
+ return [];
119
+ const record = value;
120
+ if (typeof record.text !== "string" || record.text.trim().length === 0)
121
+ return [];
122
+ return [{
123
+ text: record.text.slice(0, AGENT_PLAN_ITEM_TEXT_MAX_CHARS),
124
+ done: record.completed === true || record.done === true,
125
+ ...(record.active === true ? { active: true } : {}),
126
+ }];
127
+ });
128
+ }
129
+ export function createCodexAppServerTurnState() {
130
+ return {
131
+ responseSeenTurnIds: new Set(),
132
+ completedBeforeResponseTurnIds: new Set(),
133
+ activeTurnAcceptingSteer: false,
134
+ lastAgentMessage: "",
135
+ };
136
+ }
137
+ export function recordActiveTurnId(state, candidate, source) {
138
+ const turnId = coerceString(candidate);
139
+ if (!turnId)
140
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} ${source} omitted active turn id`);
141
+ if (!state.activeTurnId) {
142
+ state.activeTurnId = turnId;
143
+ state.activeTurnSource = source;
144
+ return turnId;
145
+ }
146
+ if (state.activeTurnId !== turnId) {
147
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} active turn id mismatch: ${state.activeTurnSource ?? "first"}=${state.activeTurnId}, ${source}=${turnId}`);
148
+ }
149
+ return state.activeTurnId;
150
+ }
151
+ export function recordTurnStartResponse(state, candidate) {
152
+ const turnId = coerceString(candidate);
153
+ if (!turnId)
154
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} response omitted active turn id`);
155
+ if (state.completedBeforeResponseTurnIds.delete(turnId))
156
+ return turnId;
157
+ state.responseSeenTurnIds.add(turnId);
158
+ return recordActiveTurnId(state, turnId, "response");
159
+ }
160
+ export function createCodexAppServerTurnMonitor(options) {
161
+ const { server, builder, state, builderDiagnostic, onEvidence, onSyncCheckpoint, onDeliveryOpportunity, enqueue, drainPendingTells, rejectPendingTells, setFailActiveTurn, pendingTellCount, isDrainingTells, setFinishAfterTellDrain, } = options;
162
+ const turnCompleted = new Promise((resolve, reject) => {
163
+ let unsubNotifications = () => { };
164
+ let unsubServerRequest = () => { };
165
+ let settled = false;
166
+ const failTurn = (error) => {
167
+ if (settled)
168
+ return;
169
+ settled = true;
170
+ rejectPendingTells(error);
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ builder.emit({ type: "diagnostic", text: message });
173
+ builder.emit({ type: "fail", message });
174
+ unsubNotifications();
175
+ unsubServerRequest();
176
+ reject(error);
177
+ };
178
+ const failServerRequest = (request) => {
179
+ // Policy A forbids interactive server→client requests. Fail immediately with named diagnostic.
180
+ const message = `${CODEX_APP_SERVER_PROVIDER} unexpected server request '${request.method}' (id=${String(request.id)}); non-interactive policy forbids server→client requests`;
181
+ failTurn(new Error(message));
182
+ };
183
+ const requestDeliveryOpportunity = () => {
184
+ try {
185
+ onDeliveryOpportunity?.({ enqueue });
186
+ }
187
+ catch {
188
+ // Delivery observation cannot alter provider control flow.
189
+ }
190
+ };
191
+ unsubServerRequest = server.onServerRequest((request) => {
192
+ try {
193
+ builder.ingress(CODEX_APP_SERVER_PROVIDER, request.method, request, "mapped", () => {
194
+ failServerRequest(request);
195
+ });
196
+ }
197
+ finally {
198
+ requestDeliveryOpportunity();
199
+ }
200
+ });
201
+ unsubNotifications = server.onNotification((notification) => {
202
+ try {
203
+ builder.ingress(CODEX_APP_SERVER_PROVIDER, notification.method, notification, codexAppServerNotificationDisposition(notification.method), () => {
204
+ if (notification.method === "item/agentMessage/delta") {
205
+ const delta = coerceString(notification.params?.delta);
206
+ if (delta) {
207
+ state.lastAgentMessage = `${state.lastAgentMessage}${delta}`;
208
+ }
209
+ }
210
+ if (notification.method === "item/completed") {
211
+ const item = notification.params?.item;
212
+ if (!item) {
213
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/completed lacked required item`);
214
+ }
215
+ const id = coerceString(item.id);
216
+ const itemType = coerceString(item.type);
217
+ if (!id || !itemType) {
218
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/completed lacked required item id or type`);
219
+ }
220
+ if (itemType === "agentMessage") {
221
+ const text = coerceString(item.text);
222
+ if (text) {
223
+ state.lastAgentMessage = text;
224
+ builder.emit({ type: "message", id, text });
225
+ }
226
+ }
227
+ else if (itemType === "reasoning") {
228
+ const text = extractReasoningSummary(item);
229
+ if (text) {
230
+ const truncated = text.length > AGENT_THOUGHT_TEXT_MAX_CHARS;
231
+ builder.emit({
232
+ type: "thought",
233
+ id,
234
+ text: text.slice(0, AGENT_THOUGHT_TEXT_MAX_CHARS),
235
+ final: true,
236
+ ...(truncated ? { truncated: true } : {}),
237
+ });
238
+ }
239
+ }
240
+ else if (itemType === "todo_list" || itemType === "todoList") {
241
+ builder.emit({
242
+ type: "plan",
243
+ id,
244
+ items: extractPlanItems(item),
245
+ });
246
+ }
247
+ else if (isCodexAppServerActivityItemType(itemType)) {
248
+ builder.emit({
249
+ type: "activity",
250
+ phase: "completed",
251
+ id,
252
+ tool: itemType,
253
+ call: appServerActivityCall(itemType, item),
254
+ result: appServerActivityResult(item),
255
+ });
256
+ }
257
+ }
258
+ if (notification.method === "turn/started") {
259
+ const turn = notification.params?.turn;
260
+ const turnId = recordActiveTurnId(state, turn?.id, "notification");
261
+ builder.emit({ type: "turn", phase: "started" });
262
+ state.activeTurnAcceptingSteer = true;
263
+ const checkpoint = { fence: { kind: "turn", turnId }, consumptionEvidence: false };
264
+ if (onEvidence) {
265
+ emitProviderEvidence(onEvidence, {
266
+ type: "checkpoint",
267
+ provider: CODEX_APP_SERVER_PROVIDER,
268
+ fence: checkpoint.fence,
269
+ source: "notification",
270
+ }, builderDiagnostic);
271
+ }
272
+ // turn/start may respond before the server has installed its active
273
+ // turn. The native notification is the first steer-safe boundary.
274
+ onSyncCheckpoint?.({ enqueue }, checkpoint);
275
+ void drainPendingTells();
276
+ }
277
+ if (notification.method === "turn/completed") {
278
+ const turn = notification.params?.turn ?? {};
279
+ const status = coerceString(turn.status) ?? "unknown";
280
+ // Either response or notification may mint the turn fence first;
281
+ // every later native source must agree with it.
282
+ const turnId = recordActiveTurnId(state, turn.id, "notification");
283
+ if (!state.responseSeenTurnIds.delete(turnId)) {
284
+ state.completedBeforeResponseTurnIds.add(turnId);
285
+ }
286
+ // Public completed only for exact native status completed.
287
+ if (status === "completed") {
288
+ builder.emit({ type: "turn", phase: "completed" });
289
+ // This must stay in the native notification call stack: the harness
290
+ // checkpoint and its observer evidence describe this exact crossing.
291
+ state.activeTurnAcceptingSteer = false;
292
+ state.activeTurnId = undefined;
293
+ state.activeTurnSource = undefined;
294
+ const checkpoint = { fence: { kind: "turn", turnId }, consumptionEvidence: true };
295
+ if (onEvidence) {
296
+ emitProviderEvidence(onEvidence, {
297
+ type: "checkpoint",
298
+ provider: CODEX_APP_SERVER_PROVIDER,
299
+ fence: checkpoint.fence,
300
+ source: "notification",
301
+ }, builderDiagnostic);
302
+ }
303
+ onSyncCheckpoint?.({ enqueue }, checkpoint);
304
+ void drainPendingTells();
305
+ if (pendingTellCount() > 0 || isDrainingTells()) {
306
+ setFinishAfterTellDrain(() => {
307
+ if (settled || state.activeTurnId !== undefined || pendingTellCount() > 0)
308
+ return;
309
+ settled = true;
310
+ unsubNotifications();
311
+ unsubServerRequest();
312
+ resolve({ status, turn });
313
+ });
314
+ return;
315
+ }
316
+ }
317
+ if (settled)
318
+ return;
319
+ settled = true;
320
+ unsubNotifications();
321
+ unsubServerRequest();
322
+ resolve({ status, turn });
323
+ }
324
+ if (notification.method === "error") {
325
+ const message = coerceString(notification.params?.message) ?? `${CODEX_APP_SERVER_PROVIDER} error`;
326
+ builder.emit({ type: "diagnostic", text: message });
327
+ }
328
+ });
329
+ }
330
+ catch (error) {
331
+ // Notification listeners run outside the driver's await chain;
332
+ // convert validation failures into the controlled completion path.
333
+ failTurn(error);
334
+ }
335
+ finally {
336
+ if (notification.method !== "turn/completed") {
337
+ requestDeliveryOpportunity();
338
+ }
339
+ }
340
+ });
341
+ setFailActiveTurn(failTurn);
342
+ builder.signal.addEventListener("abort", () => {
343
+ unsubNotifications();
344
+ unsubServerRequest();
345
+ failTurn(createAbortError(`${CODEX_APP_SERVER_PROVIDER} exec cancelled by client`));
346
+ }, { once: true });
347
+ });
348
+ // Abort can settle this before initialization reaches the await below.
349
+ // Attach a handler immediately while preserving the original awaited error.
350
+ turnCompleted.catch(() => undefined);
351
+ return { turnCompleted };
352
+ }
@@ -0,0 +1,266 @@
1
+ import { spawn } from "node:child_process";
2
+ import { emitProviderEvidence, sha256Utf8 } from "../../harness/runtime.js";
3
+ import { coerceString } from "../../harness/runtime.js";
4
+ const CODEX_APP_SERVER_PROVIDER = "codex-app-server";
5
+ export class CodexAppServerProcess {
6
+ onEvidence;
7
+ onEvidenceDiagnostic;
8
+ child;
9
+ pending = new Map();
10
+ notificationListeners = new Set();
11
+ serverRequestListeners = new Set();
12
+ exitPromise;
13
+ nextId = 1;
14
+ stdoutBuffer = "";
15
+ closed = false;
16
+ exitSettled = false;
17
+ gracefulClose;
18
+ forceClose;
19
+ codexHome;
20
+ constructor(executable, env, codexHome, onEvidence, onEvidenceDiagnostic) {
21
+ this.onEvidence = onEvidence;
22
+ this.onEvidenceDiagnostic = onEvidenceDiagnostic;
23
+ this.codexHome = codexHome;
24
+ const spawnEnv = {
25
+ ...env,
26
+ ...(codexHome ? { CODEX_HOME: codexHome } : {}),
27
+ };
28
+ this.child = spawn(executable, ["app-server", "--listen", "stdio://"], {
29
+ env: spawnEnv,
30
+ stdio: ["pipe", "pipe", "pipe"],
31
+ });
32
+ if (this.onEvidence && this.child.pid)
33
+ emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "spawn", pid: this.child.pid }, this.onEvidenceDiagnostic);
34
+ this.child.stdout.setEncoding("utf8");
35
+ this.child.stderr.setEncoding("utf8");
36
+ this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
37
+ // Preserve the pre-evidence diagnostic stream exactly. Process evidence
38
+ // never retains stderr bytes, avoiding credential boundary ambiguity.
39
+ this.child.stderr.on("data", (chunk) => this.onEvidenceDiagnostic?.(chunk));
40
+ this.child.on("error", (error) => this.failAll(error));
41
+ this.exitPromise = new Promise((resolve) => {
42
+ this.child.once("close", (exitCode) => {
43
+ this.exitSettled = true;
44
+ this.closed = true;
45
+ // With no observer, retain the historical error bytes. Evidence adds
46
+ // structured exit facts without changing provider behavior.
47
+ this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
48
+ if (this.onEvidence)
49
+ emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "exit", exitCode, stderrTail: "" }, this.onEvidenceDiagnostic);
50
+ resolve();
51
+ });
52
+ });
53
+ }
54
+ onNotification(listener) {
55
+ this.notificationListeners.add(listener);
56
+ return () => this.notificationListeners.delete(listener);
57
+ }
58
+ onServerRequest(listener) {
59
+ this.serverRequestListeners.add(listener);
60
+ return () => this.serverRequestListeners.delete(listener);
61
+ }
62
+ onStdout(chunk) {
63
+ this.stdoutBuffer += chunk;
64
+ let newline = this.stdoutBuffer.indexOf("\n");
65
+ while (newline >= 0) {
66
+ const line = this.stdoutBuffer.slice(0, newline).trim();
67
+ this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
68
+ if (line.length > 0) {
69
+ this.handleLine(line);
70
+ }
71
+ newline = this.stdoutBuffer.indexOf("\n");
72
+ }
73
+ }
74
+ handleLine(line) {
75
+ let message;
76
+ try {
77
+ message = JSON.parse(line);
78
+ }
79
+ catch {
80
+ return;
81
+ }
82
+ if (!message || typeof message !== "object")
83
+ return;
84
+ const record = message;
85
+ if ("id" in record && ("result" in record || "error" in record)) {
86
+ const id = record.id;
87
+ const pending = this.pending.get(id);
88
+ if (!pending)
89
+ return;
90
+ this.pending.delete(id);
91
+ if ("error" in record && record.error) {
92
+ const error = record.error;
93
+ pending.reject(new Error(error.message ?? `${CODEX_APP_SERVER_PROVIDER} RPC error`));
94
+ return;
95
+ }
96
+ pending.resolve(record.result);
97
+ return;
98
+ }
99
+ // Server→client request: method + id. Never ignore, auto-approve, or auto-deny (faye #316).
100
+ if (typeof record.method === "string" && "id" in record) {
101
+ const request = {
102
+ id: record.id,
103
+ method: record.method,
104
+ params: record.params ?? undefined,
105
+ };
106
+ for (const listener of this.serverRequestListeners) {
107
+ listener(request);
108
+ }
109
+ return;
110
+ }
111
+ if (typeof record.method === "string" && !("id" in record)) {
112
+ const notification = {
113
+ method: record.method,
114
+ params: record.params ?? undefined,
115
+ };
116
+ for (const listener of this.notificationListeners) {
117
+ listener(notification);
118
+ }
119
+ }
120
+ }
121
+ failAll(error) {
122
+ for (const pending of this.pending.values()) {
123
+ pending.reject(error);
124
+ }
125
+ this.pending.clear();
126
+ }
127
+ request(method, params) {
128
+ if (this.closed) {
129
+ return Promise.reject(new Error(`${CODEX_APP_SERVER_PROVIDER} process is closed`));
130
+ }
131
+ const id = this.nextId++;
132
+ const payload = params === undefined ? { id, method } : { id, method, params };
133
+ const serialized = `${JSON.stringify(payload)}\n`;
134
+ return new Promise((resolve, reject) => {
135
+ this.pending.set(id, { resolve, reject });
136
+ this.child.stdin.write(serialized, (error) => {
137
+ if (error) {
138
+ this.pending.delete(id);
139
+ reject(error);
140
+ }
141
+ });
142
+ // The write call above is the process boundary. Emitting afterward
143
+ // avoids evidence for a synchronous write failure while keeping the
144
+ // outbound fact before any asynchronous RPC acknowledgement.
145
+ this.emitOutbound(method, params);
146
+ });
147
+ }
148
+ notify(method, params) {
149
+ const payload = params === undefined ? { method } : { method, params };
150
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
151
+ }
152
+ emitOutbound(method, params) {
153
+ if (!this.onEvidence)
154
+ return;
155
+ const kind = method === "thread/start" ? "thread-start" : method === "thread/resume" ? "thread-resume" : method === "turn/start" ? "turn-start" : method === "turn/steer" ? "steer" : undefined;
156
+ if (!kind)
157
+ return;
158
+ const text = method === "turn/steer"
159
+ ? coerceString((params?.input?.[0]?.text))
160
+ : JSON.stringify(params ?? {});
161
+ if (text === undefined)
162
+ return;
163
+ emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind, ...sha256Utf8(text) }, this.onEvidenceDiagnostic);
164
+ const developerInstructions = coerceString(params?.developerInstructions);
165
+ if ((method === "thread/start" || method === "thread/resume") && developerInstructions !== undefined) {
166
+ emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind: "system-prompt", ...sha256Utf8(developerInstructions) }, this.onEvidenceDiagnostic);
167
+ }
168
+ }
169
+ async close(mode = "graceful") {
170
+ if (this.exitSettled) {
171
+ this.closed = true;
172
+ return;
173
+ }
174
+ if (mode === "force") {
175
+ this.forceClose ??= this.closeForce();
176
+ await this.forceClose;
177
+ return;
178
+ }
179
+ if (this.forceClose) {
180
+ await this.forceClose;
181
+ return;
182
+ }
183
+ this.gracefulClose ??= this.closeGracefully();
184
+ await this.gracefulClose;
185
+ }
186
+ closePipes(propagate) {
187
+ const failures = [];
188
+ for (const close of [
189
+ () => this.child.stdin.end(),
190
+ () => this.child.stdout.destroy(),
191
+ () => this.child.stderr.destroy(),
192
+ ]) {
193
+ try {
194
+ close();
195
+ }
196
+ catch (error) {
197
+ if (propagate)
198
+ failures.push(error);
199
+ }
200
+ }
201
+ return failures;
202
+ }
203
+ async closeForce() {
204
+ this.closed = true;
205
+ this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
206
+ const failures = this.closePipes(true);
207
+ if (!this.exitSettled) {
208
+ try {
209
+ const killed = this.child.kill("SIGKILL");
210
+ if (!killed && !this.exitSettled) {
211
+ failures.push(new Error(`${CODEX_APP_SERVER_PROVIDER} failed to force kill process`));
212
+ }
213
+ }
214
+ catch (error) {
215
+ failures.push(error);
216
+ }
217
+ }
218
+ const hardBound = new Promise((_, reject) => {
219
+ const timer = setTimeout(() => reject(new Error(`${CODEX_APP_SERVER_PROVIDER} forced process exit was not observed`)), 2000);
220
+ timer.unref?.();
221
+ });
222
+ try {
223
+ await Promise.race([this.exitPromise, hardBound]);
224
+ }
225
+ catch (error) {
226
+ failures.push(error);
227
+ }
228
+ if (failures.length === 1)
229
+ throw failures[0];
230
+ if (failures.length > 1) {
231
+ throw new AggregateError(failures, `${CODEX_APP_SERVER_PROVIDER} forced close failed`);
232
+ }
233
+ }
234
+ async closeGracefully() {
235
+ this.closed = true;
236
+ this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
237
+ this.closePipes(false);
238
+ if (!this.exitSettled && this.child.exitCode === null && this.child.signalCode === null) {
239
+ try {
240
+ this.child.kill("SIGTERM");
241
+ }
242
+ catch {
243
+ // Graceful cleanup is best-effort.
244
+ }
245
+ }
246
+ const forceKill = new Promise((resolve) => {
247
+ const timer = setTimeout(() => {
248
+ if (!this.exitSettled) {
249
+ try {
250
+ this.child.kill("SIGKILL");
251
+ }
252
+ catch {
253
+ // Graceful escalation is best-effort.
254
+ }
255
+ }
256
+ resolve();
257
+ }, 1000);
258
+ timer.unref?.();
259
+ });
260
+ const hardBound = new Promise((resolve) => {
261
+ const timer = setTimeout(resolve, 2000);
262
+ timer.unref?.();
263
+ });
264
+ await Promise.race([this.exitPromise, Promise.all([forceKill, this.exitPromise]).then(() => undefined), hardBound]);
265
+ }
266
+ }