@jungjaehoon/mama-os 0.51.2 → 0.52.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 (204) hide show
  1. package/dist/agent/agent-loop.d.ts +50 -0
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +480 -138
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/code-act/constants.d.ts +1 -1
  6. package/dist/agent/code-act/constants.d.ts.map +1 -1
  7. package/dist/agent/code-act/constants.js +5 -2
  8. package/dist/agent/code-act/constants.js.map +1 -1
  9. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  10. package/dist/agent/code-act/host-bridge.js +102 -4
  11. package/dist/agent/code-act/host-bridge.js.map +1 -1
  12. package/dist/agent/code-act/tool-catalog.d.ts +2 -0
  13. package/dist/agent/code-act/tool-catalog.d.ts.map +1 -1
  14. package/dist/agent/code-act/tool-catalog.js +20 -7
  15. package/dist/agent/code-act/tool-catalog.js.map +1 -1
  16. package/dist/agent/codex-app-server-process.d.ts +112 -0
  17. package/dist/agent/codex-app-server-process.d.ts.map +1 -1
  18. package/dist/agent/codex-app-server-process.js +518 -1
  19. package/dist/agent/codex-app-server-process.js.map +1 -1
  20. package/dist/agent/gateway-tool-executor.d.ts +17 -0
  21. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  22. package/dist/agent/gateway-tool-executor.js +236 -53
  23. package/dist/agent/gateway-tool-executor.js.map +1 -1
  24. package/dist/agent/gateway-tools.md +9 -3
  25. package/dist/agent/model-runner.d.ts +5 -0
  26. package/dist/agent/model-runner.d.ts.map +1 -1
  27. package/dist/agent/model-runner.js.map +1 -1
  28. package/dist/agent/skill-loader.d.ts +7 -2
  29. package/dist/agent/skill-loader.d.ts.map +1 -1
  30. package/dist/agent/skill-loader.js +29 -8
  31. package/dist/agent/skill-loader.js.map +1 -1
  32. package/dist/agent/subagent-run-reconcile.d.ts +23 -0
  33. package/dist/agent/subagent-run-reconcile.d.ts.map +1 -0
  34. package/dist/agent/subagent-run-reconcile.js +56 -0
  35. package/dist/agent/subagent-run-reconcile.js.map +1 -0
  36. package/dist/agent/tool-registry.d.ts.map +1 -1
  37. package/dist/agent/tool-registry.js +126 -3
  38. package/dist/agent/tool-registry.js.map +1 -1
  39. package/dist/agent/types.d.ts +52 -2
  40. package/dist/agent/types.d.ts.map +1 -1
  41. package/dist/agent/types.js.map +1 -1
  42. package/dist/api/graph-api.d.ts.map +1 -1
  43. package/dist/api/graph-api.js +0 -22
  44. package/dist/api/graph-api.js.map +1 -1
  45. package/dist/cli/commands/start.d.ts +8 -7
  46. package/dist/cli/commands/start.d.ts.map +1 -1
  47. package/dist/cli/commands/start.js +148 -74
  48. package/dist/cli/commands/start.js.map +1 -1
  49. package/dist/cli/config/types.d.ts.map +1 -1
  50. package/dist/cli/config/types.js +2 -1
  51. package/dist/cli/config/types.js.map +1 -1
  52. package/dist/cli/runtime/api-routes-init.d.ts +1 -0
  53. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  54. package/dist/cli/runtime/api-routes-init.js +76 -32
  55. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  56. package/dist/db/migrations/operator-procedures.d.ts +4 -0
  57. package/dist/db/migrations/operator-procedures.d.ts.map +1 -0
  58. package/dist/db/migrations/operator-procedures.js +63 -0
  59. package/dist/db/migrations/operator-procedures.js.map +1 -0
  60. package/dist/gateways/message-router.d.ts +4 -3
  61. package/dist/gateways/message-router.d.ts.map +1 -1
  62. package/dist/gateways/message-router.js +43 -131
  63. package/dist/gateways/message-router.js.map +1 -1
  64. package/dist/gateways/telegram-format.d.ts.map +1 -1
  65. package/dist/gateways/telegram-format.js +2 -0
  66. package/dist/gateways/telegram-format.js.map +1 -1
  67. package/dist/multi-agent/runtime-process.d.ts +8 -0
  68. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  69. package/dist/multi-agent/runtime-process.js +6 -0
  70. package/dist/multi-agent/runtime-process.js.map +1 -1
  71. package/dist/operator/board-delta-gate.d.ts +34 -10
  72. package/dist/operator/board-delta-gate.d.ts.map +1 -1
  73. package/dist/operator/board-delta-gate.js +68 -24
  74. package/dist/operator/board-delta-gate.js.map +1 -1
  75. package/dist/operator/board-refresh-gate.d.ts +11 -6
  76. package/dist/operator/board-refresh-gate.d.ts.map +1 -1
  77. package/dist/operator/board-refresh-gate.js +12 -11
  78. package/dist/operator/board-refresh-gate.js.map +1 -1
  79. package/dist/operator/board-slot-instructions.d.ts +30 -0
  80. package/dist/operator/board-slot-instructions.d.ts.map +1 -1
  81. package/dist/operator/board-slot-instructions.js +110 -6
  82. package/dist/operator/board-slot-instructions.js.map +1 -1
  83. package/dist/operator/console-brief.d.ts +26 -13
  84. package/dist/operator/console-brief.d.ts.map +1 -1
  85. package/dist/operator/console-brief.js +159 -83
  86. package/dist/operator/console-brief.js.map +1 -1
  87. package/dist/operator/experience-evidence.d.ts +18 -0
  88. package/dist/operator/experience-evidence.d.ts.map +1 -0
  89. package/dist/operator/experience-evidence.js +82 -0
  90. package/dist/operator/experience-evidence.js.map +1 -0
  91. package/dist/operator/experience-hints.d.ts +44 -0
  92. package/dist/operator/experience-hints.d.ts.map +1 -0
  93. package/dist/operator/experience-hints.js +98 -0
  94. package/dist/operator/experience-hints.js.map +1 -0
  95. package/dist/operator/mama-memory-port.js +1 -3
  96. package/dist/operator/mama-memory-port.js.map +1 -1
  97. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  98. package/dist/operator/operator-trigger-loop.js +3 -2
  99. package/dist/operator/operator-trigger-loop.js.map +1 -1
  100. package/dist/operator/owner-action-effects.d.ts +13 -1
  101. package/dist/operator/owner-action-effects.d.ts.map +1 -1
  102. package/dist/operator/owner-action-effects.js +17 -4
  103. package/dist/operator/owner-action-effects.js.map +1 -1
  104. package/dist/operator/owner-event-inbox.d.ts +7 -1
  105. package/dist/operator/owner-event-inbox.d.ts.map +1 -1
  106. package/dist/operator/owner-event-inbox.js +8 -0
  107. package/dist/operator/owner-event-inbox.js.map +1 -1
  108. package/dist/operator/owner-event-loop.d.ts +28 -2
  109. package/dist/operator/owner-event-loop.d.ts.map +1 -1
  110. package/dist/operator/owner-event-loop.js +60 -2
  111. package/dist/operator/owner-event-loop.js.map +1 -1
  112. package/dist/operator/owner-event-policy.d.ts +20 -0
  113. package/dist/operator/owner-event-policy.d.ts.map +1 -1
  114. package/dist/operator/owner-event-policy.js +34 -5
  115. package/dist/operator/owner-event-policy.js.map +1 -1
  116. package/dist/operator/owner-event-prompt.d.ts +13 -4
  117. package/dist/operator/owner-event-prompt.d.ts.map +1 -1
  118. package/dist/operator/owner-event-prompt.js +31 -38
  119. package/dist/operator/owner-event-prompt.js.map +1 -1
  120. package/dist/operator/owner-runtime-journal.d.ts.map +1 -1
  121. package/dist/operator/owner-runtime-journal.js +7 -3
  122. package/dist/operator/owner-runtime-journal.js.map +1 -1
  123. package/dist/operator/owner-runtime.d.ts +6 -0
  124. package/dist/operator/owner-runtime.d.ts.map +1 -1
  125. package/dist/operator/owner-runtime.js +48 -5
  126. package/dist/operator/owner-runtime.js.map +1 -1
  127. package/dist/operator/procedure-activation.d.ts +7 -0
  128. package/dist/operator/procedure-activation.d.ts.map +1 -0
  129. package/dist/operator/procedure-activation.js +132 -0
  130. package/dist/operator/procedure-activation.js.map +1 -0
  131. package/dist/operator/procedure-projection.d.ts +25 -0
  132. package/dist/operator/procedure-projection.d.ts.map +1 -0
  133. package/dist/operator/procedure-projection.js +80 -0
  134. package/dist/operator/procedure-projection.js.map +1 -0
  135. package/dist/operator/procedure-runtime.d.ts +45 -0
  136. package/dist/operator/procedure-runtime.d.ts.map +1 -0
  137. package/dist/operator/procedure-runtime.js +496 -0
  138. package/dist/operator/procedure-runtime.js.map +1 -0
  139. package/dist/operator/procedure-store.d.ts +97 -0
  140. package/dist/operator/procedure-store.d.ts.map +1 -0
  141. package/dist/operator/procedure-store.js +368 -0
  142. package/dist/operator/procedure-store.js.map +1 -0
  143. package/dist/operator/subagent-stimulus.d.ts +34 -0
  144. package/dist/operator/subagent-stimulus.d.ts.map +1 -0
  145. package/dist/operator/subagent-stimulus.js +145 -0
  146. package/dist/operator/subagent-stimulus.js.map +1 -0
  147. package/dist/operator/task-ledger.d.ts +40 -3
  148. package/dist/operator/task-ledger.d.ts.map +1 -1
  149. package/dist/operator/task-ledger.js +63 -10
  150. package/dist/operator/task-ledger.js.map +1 -1
  151. package/dist/operator/thread-brief-memory.d.ts +29 -0
  152. package/dist/operator/thread-brief-memory.d.ts.map +1 -0
  153. package/dist/operator/thread-brief-memory.js +61 -0
  154. package/dist/operator/thread-brief-memory.js.map +1 -0
  155. package/dist/operator/trigger-author.d.ts +2 -1
  156. package/dist/operator/trigger-author.d.ts.map +1 -1
  157. package/dist/operator/trigger-author.js +23 -0
  158. package/dist/operator/trigger-author.js.map +1 -1
  159. package/dist/operator/trigger-matcher.js +1 -0
  160. package/dist/operator/trigger-matcher.js.map +1 -1
  161. package/dist/operator/trigger-registry.d.ts +5 -4
  162. package/dist/operator/trigger-registry.d.ts.map +1 -1
  163. package/dist/operator/trigger-registry.js +78 -24
  164. package/dist/operator/trigger-registry.js.map +1 -1
  165. package/dist/operator/trigger-review.d.ts +1 -1
  166. package/dist/operator/trigger-review.d.ts.map +1 -1
  167. package/dist/operator/trigger-review.js +24 -4
  168. package/dist/operator/trigger-review.js.map +1 -1
  169. package/dist/operator/trigger-types.d.ts +10 -1
  170. package/dist/operator/trigger-types.d.ts.map +1 -1
  171. package/dist/operator/worker-run.d.ts +7 -1
  172. package/dist/operator/worker-run.d.ts.map +1 -1
  173. package/dist/operator/worker-run.js +5 -2
  174. package/dist/operator/worker-run.js.map +1 -1
  175. package/dist/operator/workorder-consumer.d.ts +91 -14
  176. package/dist/operator/workorder-consumer.d.ts.map +1 -1
  177. package/dist/operator/workorder-consumer.js +272 -93
  178. package/dist/operator/workorder-consumer.js.map +1 -1
  179. package/dist/operator/workorder-hooks.d.ts +42 -9
  180. package/dist/operator/workorder-hooks.d.ts.map +1 -1
  181. package/dist/operator/workorder-hooks.js +52 -13
  182. package/dist/operator/workorder-hooks.js.map +1 -1
  183. package/dist/operator/workorder-publishers.d.ts +9 -1
  184. package/dist/operator/workorder-publishers.d.ts.map +1 -1
  185. package/dist/operator/workorder-publishers.js +21 -2
  186. package/dist/operator/workorder-publishers.js.map +1 -1
  187. package/package.json +2 -2
  188. package/dist/operator/learning-context.d.ts +0 -49
  189. package/dist/operator/learning-context.d.ts.map +0 -1
  190. package/dist/operator/learning-context.js +0 -129
  191. package/dist/operator/learning-context.js.map +0 -1
  192. package/dist/operator/learning-markers.d.ts +0 -19
  193. package/dist/operator/learning-markers.d.ts.map +0 -1
  194. package/dist/operator/learning-markers.js +0 -147
  195. package/dist/operator/learning-markers.js.map +0 -1
  196. package/dist/operator/learning-read.d.ts +0 -16
  197. package/dist/operator/learning-read.d.ts.map +0 -1
  198. package/dist/operator/learning-read.js +0 -18
  199. package/dist/operator/learning-read.js.map +0 -1
  200. package/dist/operator/turn-observer.d.ts +0 -34
  201. package/dist/operator/turn-observer.d.ts.map +0 -1
  202. package/dist/operator/turn-observer.js +0 -79
  203. package/dist/operator/turn-observer.js.map +0 -1
  204. package/templates/skills/heartbeat-report.md +0 -75
@@ -21,6 +21,17 @@ const OVERLOADED_ERROR_CODE = -32001;
21
21
  const OVERLOAD_RETRY_LIMIT = 4;
22
22
  const OVERLOAD_RETRY_BASE_MS = 25;
23
23
  const TURN_START_RECONCILE_GRACE_MS = 250;
24
+ const SUBAGENT_FINAL_TEXT_LIMIT = 4_000;
25
+ /** Wait for the child's own `turn/completed` after the parent announced it finished. */
26
+ const SUBAGENT_COMPLETION_GRACE_MS = 5_000;
27
+ /** A registered child that never completes is reported failed rather than leaked. */
28
+ const SUBAGENT_TTL_MS = 45 * 60_000;
29
+ /** Bound on remembered finished child threads; identity, not history. */
30
+ const MAX_FINISHED_SUBAGENTS = 200;
31
+ const SUBAGENT_AUTHORITY_UNAVAILABLE = 'subagent authority unavailable';
32
+ const SUBAGENT_AUTHORITY_EXPIRED = 'subagent authority expired';
33
+ /** EnvelopeViolation code prefix the enforcer returns once a grant is past its wall. */
34
+ const ENVELOPE_EXPIRED_MARKER = '[expired]';
24
35
  class CodexAppServerRpcError extends Error {
25
36
  code;
26
37
  constructor(code, message) {
@@ -252,6 +263,16 @@ class CodexAppServerProcess {
252
263
  lateTurnStarts = new Map();
253
264
  turnStartReconciliations = new Map();
254
265
  turns = new Map();
266
+ subagents = new Map();
267
+ /**
268
+ * Child threads that already reported a terminal status. The SAME `started`
269
+ * announcement arrives as both an item/started and an item/completed view, and the
270
+ * second view can land after the child's own turn/completed - `subagents.has` no longer
271
+ * dedupes it then, so it would mint a second authority and a second completion for a
272
+ * dead child. Bounded FIFO: identity of recent children, not a growing ledger.
273
+ */
274
+ finishedSubagents = new Set();
275
+ threadContexts = new Map();
255
276
  sessions = new Map();
256
277
  sessionQueues = new Map();
257
278
  connectionQueue = Promise.resolve();
@@ -295,6 +316,7 @@ class CodexAppServerProcess {
295
316
  }
296
317
  const launch = (0, codex_home_js_1.buildCodexAppServerLaunchConfig)(this.options.mcpConfigPath, process.env);
297
318
  if (overrides.resumeSession === false) {
319
+ this.discardSessionThreadState(session.sessionKey, 'session was restarted');
298
320
  this.registry.remove(session.sessionKey);
299
321
  this.sessions.delete(session.sessionKey);
300
322
  }
@@ -323,9 +345,27 @@ class CodexAppServerProcess {
323
345
  this.sessions.set(session.sessionKey, state);
324
346
  }
325
347
  }
326
- const turnText = state.bootstrapPending && session.systemPrompt
348
+ const replayReminder = Boolean(state.bootstrapPending && session.systemPrompt);
349
+ const turnText = replayReminder
327
350
  ? `<system-reminder>\nFresh MAMA runtime context after resuming this durable thread:\n${session.systemPrompt.replace(/<\/system-reminder>/gi, '')}\n</system-reminder>\n\n${text}`
328
351
  : text;
352
+ // One line per turn, where the text that actually reaches the model is known.
353
+ // This is what makes "fixed things once, turns carry only deltas" measurable
354
+ // from daemon.log instead of asserted.
355
+ console.log(`[prompt] thread=${state.threadId} kind=${session.promptTelemetry?.kind ?? 'chat'} ` +
356
+ `chars=${turnText.length} brief=${session.promptTelemetry?.brief ?? 'omitted'} ` +
357
+ `reminder=${replayReminder ? 'sent' : 'omitted'}`);
358
+ // A Codex-native child can be announced after this turn resolved, so remember
359
+ // which session the thread belongs to outside the PendingTurn lifetime. The
360
+ // parent's BRIDGE is deliberately not kept: a child gets its own authority.
361
+ // A session that rotated its thread leaves the previous thread's context behind,
362
+ // and a stale entry both leaks and lets a dead thread announce children.
363
+ this.pruneThreadContexts(session.sessionKey, state.threadId);
364
+ this.threadContexts.set(state.threadId, {
365
+ sessionKey: session.sessionKey,
366
+ onToolUse: callbacks?.onToolUse,
367
+ onToolComplete: callbacks?.onToolComplete,
368
+ });
329
369
  const result = await this.startTurn(state.threadId, turnText, callbacks, session.requestTimeout, session.hostToolBridge, overrides.runTokenBudget);
330
370
  state.bootstrapPending = false;
331
371
  return result;
@@ -346,6 +386,7 @@ class CodexAppServerProcess {
346
386
  }
347
387
  async reset(sessionKey = this.options.sessionKey) {
348
388
  await this.enqueueSession(sessionKey, async () => {
389
+ this.discardSessionThreadState(sessionKey, 'session was reset');
349
390
  this.registry.remove(sessionKey);
350
391
  this.sessions.delete(sessionKey);
351
392
  });
@@ -362,6 +403,8 @@ class CodexAppServerProcess {
362
403
  async stop() {
363
404
  this.stopped = true;
364
405
  await this.shutdown(new Error('Codex app-server process stopped'));
406
+ this.terminateSubagents(new Error('Codex app-server process stopped'));
407
+ this.threadContexts.clear();
365
408
  }
366
409
  async executeSandboxedCommand(command, cwd, roots, signal) {
367
410
  signal?.throwIfAborted();
@@ -419,6 +462,7 @@ class CodexAppServerProcess {
419
462
  policyFingerprint: overrides.policyFingerprint ?? this.options.policyFingerprint,
420
463
  hostToolBridge,
421
464
  resumeInstructions: overrides.resumeInstructions,
465
+ promptTelemetry: overrides.promptTelemetry,
422
466
  };
423
467
  }
424
468
  enqueueSession(sessionKey, operation) {
@@ -734,6 +778,7 @@ class CodexAppServerProcess {
734
778
  onDelta: callbacks?.onDelta,
735
779
  onToolUse: callbacks?.onToolUse,
736
780
  onToolComplete: callbacks?.onToolComplete,
781
+ onSubagentStart: callbacks?.onSubagentStart,
737
782
  nativeItems: new Map(),
738
783
  resolve: resolveTurn,
739
784
  reject: rejectTurn,
@@ -925,6 +970,9 @@ class CodexAppServerProcess {
925
970
  if (!data || typeof data.threadId !== 'string') {
926
971
  return;
927
972
  }
973
+ if (this.handleSubagentNotification(method, data.threadId, data)) {
974
+ return;
975
+ }
928
976
  const turn = this.turns.get(data.threadId);
929
977
  if (!turn) {
930
978
  return;
@@ -1102,6 +1150,459 @@ class CodexAppServerProcess {
1102
1150
  hasToolUse: false,
1103
1151
  });
1104
1152
  }
1153
+ // ─── Codex-native subagents ───────────────────────────────────────────────
1154
+ //
1155
+ // Codex announces a child on the PARENT thread as a `subAgentActivity` item and then
1156
+ // drives the child on its OWN thread id, which outlives the parent turn. Nothing here
1157
+ // blocks a run. A child NEVER inherits the parent turn's bridge: the parent's envelope
1158
+ // expires with the parent's wall and the child cannot renew it, so a child that
1159
+ // inherited it would lose every tool mid-run and still report "done". The host issues
1160
+ // the child its own authority instead (`createSubagentBridge`), or the child has none
1161
+ // and its calls are refused loudly.
1162
+ /** Returns true when this notification belongs to the subagent surface and was consumed. */
1163
+ handleSubagentNotification(method, threadId, data) {
1164
+ if (method === 'item/started' || method === 'item/completed') {
1165
+ const item = object(data.item);
1166
+ if (item?.type === 'subAgentActivity') {
1167
+ // Only a thread this process actually drives may announce children.
1168
+ if (!this.subagents.has(threadId) && !this.threadContexts.has(threadId)) {
1169
+ return false;
1170
+ }
1171
+ if (item.kind === 'started') {
1172
+ this.registerSubagent(threadId, item);
1173
+ }
1174
+ else if (item.kind === 'completed') {
1175
+ this.observeParentSideCompletion(threadId, item);
1176
+ }
1177
+ return true;
1178
+ }
1179
+ const child = this.subagents.get(threadId);
1180
+ if (!child) {
1181
+ return false;
1182
+ }
1183
+ this.refreshParentTurnIdleTimeout(child);
1184
+ if (method === 'item/completed' &&
1185
+ item?.type === 'agentMessage' &&
1186
+ item.phase === 'final_answer' &&
1187
+ typeof item.text === 'string') {
1188
+ child.finalText = item.text.slice(0, SUBAGENT_FINAL_TEXT_LIMIT);
1189
+ }
1190
+ return true;
1191
+ }
1192
+ const child = this.subagents.get(threadId);
1193
+ if (!child) {
1194
+ return false;
1195
+ }
1196
+ this.refreshParentTurnIdleTimeout(child);
1197
+ if (method !== 'turn/completed') {
1198
+ return true;
1199
+ }
1200
+ const completed = object(data.turn);
1201
+ const status = completed?.status;
1202
+ if (status === 'inProgress') {
1203
+ return true;
1204
+ }
1205
+ if (status !== 'completed' && status !== 'failed' && status !== 'interrupted') {
1206
+ return true;
1207
+ }
1208
+ if (!child.finalText) {
1209
+ child.finalText = this.subagentFinalText(completed?.items);
1210
+ }
1211
+ // The child's OWN turn/completed is the only completing event: the parent-side
1212
+ // announcement carries neither status nor result.
1213
+ this.finishSubagent(threadId, status, status === 'failed'
1214
+ ? this.redact(errorMessage(completed?.error, 'Codex app-server subagent turn failed'))
1215
+ : undefined);
1216
+ return true;
1217
+ }
1218
+ subagentFinalText(items) {
1219
+ if (!Array.isArray(items)) {
1220
+ return '';
1221
+ }
1222
+ let text = '';
1223
+ for (const entry of items) {
1224
+ const item = object(entry);
1225
+ if (item?.type === 'agentMessage' && typeof item.text === 'string') {
1226
+ text = item.text;
1227
+ }
1228
+ }
1229
+ return text.slice(0, SUBAGENT_FINAL_TEXT_LIMIT);
1230
+ }
1231
+ /** A working child keeps the parent's idle timeout alive (the parent may be in wait_agent). */
1232
+ refreshParentTurnIdleTimeout(child) {
1233
+ const parentTurn = this.turns.get(child.parentThreadId);
1234
+ if (parentTurn) {
1235
+ this.refreshTurnIdleTimeout(parentTurn);
1236
+ }
1237
+ }
1238
+ /** Drop every thread context this session left behind on a previous thread id. */
1239
+ pruneThreadContexts(sessionKey, currentThreadId) {
1240
+ for (const [threadId, context] of this.threadContexts) {
1241
+ if (context.sessionKey === sessionKey && threadId !== currentThreadId) {
1242
+ this.threadContexts.delete(threadId);
1243
+ }
1244
+ }
1245
+ }
1246
+ knownThreadIds() {
1247
+ const ids = new Set(this.threadContexts.keys());
1248
+ for (const state of this.sessions.values()) {
1249
+ if (state.threadId) {
1250
+ ids.add(state.threadId);
1251
+ }
1252
+ }
1253
+ for (const threadId of this.turns.keys()) {
1254
+ ids.add(threadId);
1255
+ }
1256
+ return ids;
1257
+ }
1258
+ registerSubagent(parentThreadId, item) {
1259
+ const agentThreadId = typeof item.agentThreadId === 'string' ? item.agentThreadId : '';
1260
+ if (!agentThreadId || this.subagents.has(agentThreadId)) {
1261
+ // The same `started` announcement arrives twice (item/started + item/completed views).
1262
+ return;
1263
+ }
1264
+ if (this.finishedSubagents.has(agentThreadId)) {
1265
+ // The second view of that announcement lost the race with the child's own
1266
+ // turn/completed. Re-registering would resurrect a finished child.
1267
+ console.warn(`[CodexAppServer] subagent announcement ignored: thread=${agentThreadId} already finished`);
1268
+ return;
1269
+ }
1270
+ // A child may never claim a thread this process already drives: that would let one
1271
+ // session's announcement capture another session's live parent thread.
1272
+ if (this.knownThreadIds().has(agentThreadId)) {
1273
+ console.warn(`[CodexAppServer] subagent announcement refused: thread=${agentThreadId} is a live thread`);
1274
+ return;
1275
+ }
1276
+ // A grandchild's session comes from the child that announced it.
1277
+ const parent = this.subagents.get(parentThreadId) ?? this.threadContexts.get(parentThreadId);
1278
+ if (!parent) {
1279
+ return;
1280
+ }
1281
+ const agentPath = typeof item.agentPath === 'string' ? item.agentPath : '';
1282
+ // Observed admission: tell the PARENT'S live turn that this run handed work to a
1283
+ // child. Nothing else on the item stream carries that fact - on codex-cli 0.153.4
1284
+ // a spawn surfaces ONLY as `subAgentActivity`, which this handler consumes before
1285
+ // the native-item path. Never routed through onToolUse (effect ledger, see
1286
+ // PromptCallbacks.onSubagentStart).
1287
+ const parentTurn = this.turns.get(parentThreadId);
1288
+ if (parentTurn?.onSubagentStart) {
1289
+ const itemId = typeof item.id === 'string' ? item.id : '';
1290
+ try {
1291
+ parentTurn.onSubagentStart({ agentThreadId, agentPath, itemId });
1292
+ }
1293
+ catch (error) {
1294
+ console.warn(`[CodexAppServer] onSubagentStart callback failed: ${this.toError(error).message}`);
1295
+ }
1296
+ }
1297
+ const factory = this.options.createSubagentBridge;
1298
+ const child = {
1299
+ parentThreadId,
1300
+ sessionKey: parent.sessionKey,
1301
+ agentPath,
1302
+ onToolUse: parent.onToolUse,
1303
+ onToolComplete: parent.onToolComplete,
1304
+ finalText: '',
1305
+ startedAt: Date.now(),
1306
+ toolCallQueue: Promise.resolve(),
1307
+ toolCallResults: new Map(),
1308
+ stoppingCallIds: new Set(),
1309
+ abortController: new AbortController(),
1310
+ authorityWarned: false,
1311
+ authority: factory
1312
+ ? factory({
1313
+ sessionKey: parent.sessionKey,
1314
+ parentThreadId,
1315
+ agentThreadId,
1316
+ agentPath,
1317
+ })
1318
+ .then((authority) => authority ?? null)
1319
+ .catch((error) => {
1320
+ console.warn(`[CodexAppServer] subagent authority factory failed thread=${agentThreadId}: ${this.toError(error).message}`);
1321
+ return null;
1322
+ })
1323
+ : Promise.resolve(null),
1324
+ };
1325
+ child.ttlTimer = setTimeout(() => {
1326
+ child.ttlTimer = undefined;
1327
+ this.finishSubagent(agentThreadId, 'failed', 'subagent produced no completion');
1328
+ }, this.options.subagentTtlMs ?? SUBAGENT_TTL_MS);
1329
+ child.ttlTimer.unref();
1330
+ this.subagents.set(agentThreadId, child);
1331
+ console.log(`[CodexAppServer] subagent started path=${agentPath} thread=${agentThreadId}`);
1332
+ this.emitSubagentEvent({
1333
+ kind: 'started',
1334
+ sessionKey: parent.sessionKey,
1335
+ parentThreadId,
1336
+ agentThreadId,
1337
+ agentPath,
1338
+ });
1339
+ }
1340
+ /**
1341
+ * The parent says a child finished. That is an announcement, not a result: it can win
1342
+ * the race against the child's own `turn/completed`, which is what carries status and
1343
+ * final text. Wait a bounded grace for the real event, then report `unknown`.
1344
+ */
1345
+ observeParentSideCompletion(announcingThreadId, item) {
1346
+ const agentThreadId = typeof item.agentThreadId === 'string' ? item.agentThreadId : '';
1347
+ const child = agentThreadId ? this.subagents.get(agentThreadId) : undefined;
1348
+ if (!child) {
1349
+ return;
1350
+ }
1351
+ if (child.parentThreadId !== announcingThreadId) {
1352
+ console.warn(`[CodexAppServer] subagent completion refused: thread=${announcingThreadId} does not own ${agentThreadId}`);
1353
+ return;
1354
+ }
1355
+ if (child.graceTimer) {
1356
+ return;
1357
+ }
1358
+ child.graceTimer = setTimeout(() => {
1359
+ child.graceTimer = undefined;
1360
+ console.warn(`[CodexAppServer] subagent completion unconfirmed path=${child.agentPath} thread=${agentThreadId}`);
1361
+ this.finishSubagent(agentThreadId, 'unknown');
1362
+ }, this.options.subagentGraceMs ?? SUBAGENT_COMPLETION_GRACE_MS);
1363
+ child.graceTimer.unref();
1364
+ }
1365
+ /**
1366
+ * Emits exactly one completion per child thread and releases its authority with the
1367
+ * same terminal status. Grandchildren keep their own entries.
1368
+ */
1369
+ finishSubagent(agentThreadId, status, error) {
1370
+ const child = this.subagents.get(agentThreadId);
1371
+ if (!child) {
1372
+ return;
1373
+ }
1374
+ this.subagents.delete(agentThreadId);
1375
+ this.rememberFinishedSubagent(agentThreadId);
1376
+ this.clearSubagentTimers(child);
1377
+ console.log(`[CodexAppServer] subagent completed status=${status} path=${child.agentPath} thread=${agentThreadId}`);
1378
+ const finalText = child.finalText ? this.redact(child.finalText) : '';
1379
+ this.emitSubagentEvent({
1380
+ kind: 'completed',
1381
+ sessionKey: child.sessionKey,
1382
+ parentThreadId: child.parentThreadId,
1383
+ agentThreadId,
1384
+ agentPath: child.agentPath,
1385
+ status,
1386
+ ...(finalText ? { finalText } : {}),
1387
+ ...(error ? { error } : {}),
1388
+ });
1389
+ void child.authority
1390
+ .then(async (authority) => {
1391
+ if (!authority) {
1392
+ return;
1393
+ }
1394
+ await authority.release({ status, ...(error ? { error } : {}) });
1395
+ })
1396
+ .catch((releaseError) => {
1397
+ console.warn(`[CodexAppServer] subagent authority release failed thread=${agentThreadId}: ${this.toError(releaseError).message}`);
1398
+ });
1399
+ }
1400
+ /** FIFO-bounded identity of finished children; the oldest id is forgotten first. */
1401
+ rememberFinishedSubagent(agentThreadId) {
1402
+ this.finishedSubagents.add(agentThreadId);
1403
+ while (this.finishedSubagents.size > MAX_FINISHED_SUBAGENTS) {
1404
+ const oldest = this.finishedSubagents.values().next().value;
1405
+ if (oldest === undefined) {
1406
+ return;
1407
+ }
1408
+ this.finishedSubagents.delete(oldest);
1409
+ }
1410
+ }
1411
+ clearSubagentTimers(child) {
1412
+ if (child.ttlTimer) {
1413
+ clearTimeout(child.ttlTimer);
1414
+ child.ttlTimer = undefined;
1415
+ }
1416
+ if (child.graceTimer) {
1417
+ clearTimeout(child.graceTimer);
1418
+ child.graceTimer = undefined;
1419
+ }
1420
+ }
1421
+ emitSubagentEvent(event) {
1422
+ const emit = this.options.onSubagentEvent;
1423
+ if (!emit) {
1424
+ return;
1425
+ }
1426
+ try {
1427
+ emit(event);
1428
+ }
1429
+ catch (error) {
1430
+ console.warn(`[CodexAppServer] subagent event listener failed: ${this.toError(error).message}`);
1431
+ }
1432
+ }
1433
+ /** Every live child is reported interrupted and its authority released. */
1434
+ terminateSubagents(reason) {
1435
+ const safe = this.toError(reason);
1436
+ for (const agentThreadId of [...this.subagents.keys()]) {
1437
+ const child = this.subagents.get(agentThreadId);
1438
+ if (!child) {
1439
+ continue;
1440
+ }
1441
+ child.abortController.abort(safe);
1442
+ this.finishSubagent(agentThreadId, 'interrupted', safe.message);
1443
+ }
1444
+ }
1445
+ /** A reset/restart drops the thread's context and interrupts its children. */
1446
+ discardSessionThreadState(sessionKey, reason) {
1447
+ const threadId = this.sessions.get(sessionKey)?.threadId;
1448
+ if (threadId) {
1449
+ this.threadContexts.delete(threadId);
1450
+ }
1451
+ for (const agentThreadId of [...this.subagents.keys()]) {
1452
+ const child = this.subagents.get(agentThreadId);
1453
+ if (!child || child.sessionKey !== sessionKey) {
1454
+ continue;
1455
+ }
1456
+ child.abortController.abort(new Error(`Codex app-server ${reason}`));
1457
+ this.finishSubagent(agentThreadId, 'interrupted', reason);
1458
+ }
1459
+ }
1460
+ handleSubagentToolRequest(request, agentThreadId, child, data) {
1461
+ this.refreshParentTurnIdleTimeout(child);
1462
+ // The child's authority is host-issued and asynchronous; its calls queue until it
1463
+ // resolves rather than falling back to anything the parent held.
1464
+ void child.authority.then((authority) => {
1465
+ try {
1466
+ const turnId = this.requiredToolString(data, 'turnId');
1467
+ const callId = this.requiredToolString(data, 'callId');
1468
+ const tool = this.requiredToolString(data, 'tool');
1469
+ if (data.namespace !== null && typeof data.namespace !== 'string') {
1470
+ throw new Error('Codex app-server tool call namespace must be null or a string');
1471
+ }
1472
+ const input = object(data.arguments);
1473
+ if (!input) {
1474
+ throw new Error('Codex app-server tool call arguments must be an object');
1475
+ }
1476
+ if (!authority) {
1477
+ if (!child.authorityWarned) {
1478
+ child.authorityWarned = true;
1479
+ console.warn(`[CodexAppServer] ${SUBAGENT_AUTHORITY_UNAVAILABLE} path=${child.agentPath} thread=${agentThreadId}`);
1480
+ }
1481
+ this.replyToolError(request, `${SUBAGENT_AUTHORITY_UNAVAILABLE}: ${tool} was refused`);
1482
+ return;
1483
+ }
1484
+ const bridge = authority.bridge;
1485
+ if (!bridge.tools.some((definition) => definition.name === tool)) {
1486
+ throw new Error(`Codex app-server tool call ${tool} was not advertised`);
1487
+ }
1488
+ this.dispatchSubagentToolCall(request, agentThreadId, child, bridge, {
1489
+ turnId,
1490
+ callId,
1491
+ tool,
1492
+ namespace: data.namespace ?? null,
1493
+ input,
1494
+ });
1495
+ }
1496
+ catch (error) {
1497
+ // A child's protocol error is reported to the child only: the parent turn it was
1498
+ // spawned from may already have resolved, and there is nothing here to fail.
1499
+ this.replyToolError(request, this.toError(error).message);
1500
+ }
1501
+ });
1502
+ }
1503
+ dispatchSubagentToolCall(request, agentThreadId, child, bridge, call) {
1504
+ const { turnId, callId, tool, namespace, input } = call;
1505
+ const identity = JSON.stringify(stableJson({ threadId: agentThreadId, turnId, tool, namespace, arguments: input }));
1506
+ const existing = child.toolCallResults.get(callId);
1507
+ if (existing && existing.identity !== identity) {
1508
+ throw new Error(`Codex app-server callId ${callId} had a conflicting request`);
1509
+ }
1510
+ let execution = existing?.execution;
1511
+ if (!execution) {
1512
+ execution = child.toolCallQueue
1513
+ .then(async () => {
1514
+ if (!this.isSubagentToolActive(request, agentThreadId, child)) {
1515
+ return {
1516
+ result: this.toolResult(false, 'Codex app-server tool call is no longer active'),
1517
+ stop: false,
1518
+ abortError: undefined,
1519
+ };
1520
+ }
1521
+ try {
1522
+ child.onToolUse?.(tool, {
1523
+ nativeToolUseId: callId,
1524
+ subagentThreadId: agentThreadId,
1525
+ agentPath: child.agentPath,
1526
+ });
1527
+ }
1528
+ catch (error) {
1529
+ console.warn(`[CodexAppServer] subagent onToolUse failed: ${this.toError(error).message}`);
1530
+ }
1531
+ let result;
1532
+ try {
1533
+ result = await bridge.execute({
1534
+ callId,
1535
+ name: tool,
1536
+ input,
1537
+ signal: child.abortController.signal,
1538
+ });
1539
+ }
1540
+ catch (error) {
1541
+ result = { content: this.toError(error).message, isError: true };
1542
+ }
1543
+ const resultData = object(result);
1544
+ if (!resultData ||
1545
+ typeof resultData.content !== 'string' ||
1546
+ typeof resultData.isError !== 'boolean') {
1547
+ this.reportSubagentToolComplete(child, tool, callId, true);
1548
+ return {
1549
+ result: this.toolResult(false, 'Host tool returned a malformed result'),
1550
+ stop: false,
1551
+ abortError: undefined,
1552
+ };
1553
+ }
1554
+ this.reportSubagentToolComplete(child, tool, callId, resultData.isError);
1555
+ if (resultData.isError && resultData.content.includes(ENVELOPE_EXPIRED_MARKER)) {
1556
+ // Expired authority is named, never silently downgraded to a plain failure:
1557
+ // a child whose grant ran out must not read as work that merely did not apply.
1558
+ return {
1559
+ result: this.toolResult(false, `${SUBAGENT_AUTHORITY_EXPIRED}: ${resultData.content}`),
1560
+ stop: false,
1561
+ abortError: undefined,
1562
+ };
1563
+ }
1564
+ return {
1565
+ result: this.toolResult(!resultData.isError, resultData.content),
1566
+ stop: resultData.stop === true || resultData.abort === true,
1567
+ abortError: undefined,
1568
+ };
1569
+ })
1570
+ .catch((error) => ({
1571
+ result: this.toolResult(false, this.toError(error).message),
1572
+ stop: false,
1573
+ abortError: undefined,
1574
+ }));
1575
+ child.toolCallResults.set(callId, { identity, execution });
1576
+ child.toolCallQueue = execution.then(() => undefined, () => undefined);
1577
+ }
1578
+ void execution.then(({ result, stop }) => {
1579
+ if (!this.isSubagentToolActive(request, agentThreadId, child)) {
1580
+ if (this.child === request.child) {
1581
+ this.replyToolError(request, 'Codex app-server tool call is no longer active');
1582
+ }
1583
+ return;
1584
+ }
1585
+ this.reply(request.child, { jsonrpc: '2.0', id: request.id, result });
1586
+ if (stop && !child.stoppingCallIds.has(callId)) {
1587
+ child.stoppingCallIds.add(callId);
1588
+ child.abortController.abort(new Error('Codex app-server subagent tool stopped the run'));
1589
+ void this.request('turn/interrupt', { threadId: agentThreadId, turnId }).catch((error) => console.warn(`[CodexAppServer] subagent interrupt failed: ${this.toError(error).message}`));
1590
+ }
1591
+ });
1592
+ }
1593
+ reportSubagentToolComplete(child, tool, callId, isError) {
1594
+ try {
1595
+ child.onToolComplete?.(tool, callId, isError);
1596
+ }
1597
+ catch (error) {
1598
+ console.warn(`[CodexAppServer] subagent onToolComplete failed: ${this.toError(error).message}`);
1599
+ }
1600
+ }
1601
+ isSubagentToolActive(request, agentThreadId, child) {
1602
+ return (this.child === request.child &&
1603
+ this.subagents.get(agentThreadId) === child &&
1604
+ !child.abortController.signal.aborted);
1605
+ }
1105
1606
  handleServerRequest(child, id, method, params) {
1106
1607
  if (method === 'item/tool/call') {
1107
1608
  this.handleDynamicToolRequest({ child, id, params });
@@ -1135,6 +1636,14 @@ class CodexAppServerProcess {
1135
1636
  const data = object(request.params);
1136
1637
  const threadId = typeof data?.threadId === 'string' ? data.threadId : undefined;
1137
1638
  const turn = threadId ? this.turns.get(threadId) : undefined;
1639
+ if (data && threadId && !turn && !expectedTurn) {
1640
+ // A Codex-native child calls on ITS OWN thread id, which never holds a PendingTurn.
1641
+ const child = this.subagents.get(threadId);
1642
+ if (child) {
1643
+ this.handleSubagentToolRequest(request, threadId, child, data);
1644
+ return;
1645
+ }
1646
+ }
1138
1647
  if (!data || !turn || !turn.hostToolBridge || (expectedTurn && turn !== expectedTurn)) {
1139
1648
  this.replyDisabledTool(request);
1140
1649
  return;
@@ -1467,6 +1976,10 @@ class CodexAppServerProcess {
1467
1976
  }
1468
1977
  failAll(error) {
1469
1978
  const safe = this.toError(error);
1979
+ // Pending child tool calls abort exactly like a parent turn's.
1980
+ for (const child of this.subagents.values()) {
1981
+ child.abortController.abort(safe);
1982
+ }
1470
1983
  for (const threadId of [...this.turns.keys()]) {
1471
1984
  this.failTurn(threadId, safe);
1472
1985
  }
@@ -1557,6 +2070,10 @@ class CodexAppServerProcess {
1557
2070
  this.stderr = undefined;
1558
2071
  this.child = undefined;
1559
2072
  this.sessions.clear();
2073
+ // The connection is gone, so every child is gone with it: say so once per child and
2074
+ // release its authority instead of dropping the registry silently.
2075
+ this.terminateSubagents(new Error('Codex app-server connection closed'));
2076
+ this.threadContexts.clear();
1560
2077
  this.lateTurnStarts.clear();
1561
2078
  for (const [threadId, reconciliation] of this.turnStartReconciliations) {
1562
2079
  this.completeTurnStartReconciliation(threadId, reconciliation);