@jungjaehoon/mama-os 0.51.2 → 0.52.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 (228) hide show
  1. package/dist/agent/agent-loop.d.ts +58 -0
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +503 -142
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
  6. package/dist/agent/claude-cli-wrapper.js +3 -2
  7. package/dist/agent/claude-cli-wrapper.js.map +1 -1
  8. package/dist/agent/cline-cli-adapter.d.ts +2 -0
  9. package/dist/agent/cline-cli-adapter.d.ts.map +1 -1
  10. package/dist/agent/cline-cli-adapter.js +2 -0
  11. package/dist/agent/cline-cli-adapter.js.map +1 -1
  12. package/dist/agent/code-act/constants.d.ts +1 -1
  13. package/dist/agent/code-act/constants.d.ts.map +1 -1
  14. package/dist/agent/code-act/constants.js +5 -2
  15. package/dist/agent/code-act/constants.js.map +1 -1
  16. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  17. package/dist/agent/code-act/host-bridge.js +102 -4
  18. package/dist/agent/code-act/host-bridge.js.map +1 -1
  19. package/dist/agent/code-act/tool-catalog.d.ts +2 -0
  20. package/dist/agent/code-act/tool-catalog.d.ts.map +1 -1
  21. package/dist/agent/code-act/tool-catalog.js +20 -7
  22. package/dist/agent/code-act/tool-catalog.js.map +1 -1
  23. package/dist/agent/codex-app-server-process.d.ts +112 -0
  24. package/dist/agent/codex-app-server-process.d.ts.map +1 -1
  25. package/dist/agent/codex-app-server-process.js +518 -1
  26. package/dist/agent/codex-app-server-process.js.map +1 -1
  27. package/dist/agent/gateway-tool-executor.d.ts +17 -0
  28. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  29. package/dist/agent/gateway-tool-executor.js +236 -53
  30. package/dist/agent/gateway-tool-executor.js.map +1 -1
  31. package/dist/agent/gateway-tools.md +9 -3
  32. package/dist/agent/model-runner.d.ts +16 -0
  33. package/dist/agent/model-runner.d.ts.map +1 -1
  34. package/dist/agent/model-runner.js.map +1 -1
  35. package/dist/agent/persistent-cli-adapter.d.ts +5 -0
  36. package/dist/agent/persistent-cli-adapter.d.ts.map +1 -1
  37. package/dist/agent/persistent-cli-adapter.js +7 -0
  38. package/dist/agent/persistent-cli-adapter.js.map +1 -1
  39. package/dist/agent/persistent-cli-process.d.ts.map +1 -1
  40. package/dist/agent/persistent-cli-process.js +27 -2
  41. package/dist/agent/persistent-cli-process.js.map +1 -1
  42. package/dist/agent/skill-loader.d.ts +7 -2
  43. package/dist/agent/skill-loader.d.ts.map +1 -1
  44. package/dist/agent/skill-loader.js +29 -8
  45. package/dist/agent/skill-loader.js.map +1 -1
  46. package/dist/agent/subagent-run-reconcile.d.ts +23 -0
  47. package/dist/agent/subagent-run-reconcile.d.ts.map +1 -0
  48. package/dist/agent/subagent-run-reconcile.js +56 -0
  49. package/dist/agent/subagent-run-reconcile.js.map +1 -0
  50. package/dist/agent/tool-registry.d.ts.map +1 -1
  51. package/dist/agent/tool-registry.js +126 -3
  52. package/dist/agent/tool-registry.js.map +1 -1
  53. package/dist/agent/types.d.ts +52 -2
  54. package/dist/agent/types.d.ts.map +1 -1
  55. package/dist/agent/types.js.map +1 -1
  56. package/dist/api/graph-api.d.ts.map +1 -1
  57. package/dist/api/graph-api.js +0 -22
  58. package/dist/api/graph-api.js.map +1 -1
  59. package/dist/cli/commands/start.d.ts +8 -7
  60. package/dist/cli/commands/start.d.ts.map +1 -1
  61. package/dist/cli/commands/start.js +154 -74
  62. package/dist/cli/commands/start.js.map +1 -1
  63. package/dist/cli/config/types.d.ts.map +1 -1
  64. package/dist/cli/config/types.js +2 -1
  65. package/dist/cli/config/types.js.map +1 -1
  66. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  67. package/dist/cli/runtime/agent-loop-init.js +1 -0
  68. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  69. package/dist/cli/runtime/api-routes-init.d.ts +1 -0
  70. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  71. package/dist/cli/runtime/api-routes-init.js +82 -58
  72. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  73. package/dist/db/migrations/operator-procedures.d.ts +4 -0
  74. package/dist/db/migrations/operator-procedures.d.ts.map +1 -0
  75. package/dist/db/migrations/operator-procedures.js +63 -0
  76. package/dist/db/migrations/operator-procedures.js.map +1 -0
  77. package/dist/gateways/message-router.d.ts +6 -3
  78. package/dist/gateways/message-router.d.ts.map +1 -1
  79. package/dist/gateways/message-router.js +43 -131
  80. package/dist/gateways/message-router.js.map +1 -1
  81. package/dist/gateways/telegram-format.d.ts.map +1 -1
  82. package/dist/gateways/telegram-format.js +2 -0
  83. package/dist/gateways/telegram-format.js.map +1 -1
  84. package/dist/gateways/telegram-response-presenter.d.ts.map +1 -1
  85. package/dist/gateways/telegram-response-presenter.js +4 -0
  86. package/dist/gateways/telegram-response-presenter.js.map +1 -1
  87. package/dist/mcp/code-act-mcp-config.d.ts +49 -0
  88. package/dist/mcp/code-act-mcp-config.d.ts.map +1 -0
  89. package/dist/mcp/code-act-mcp-config.js +124 -0
  90. package/dist/mcp/code-act-mcp-config.js.map +1 -0
  91. package/dist/multi-agent/runtime-process.d.ts +10 -0
  92. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  93. package/dist/multi-agent/runtime-process.js +8 -0
  94. package/dist/multi-agent/runtime-process.js.map +1 -1
  95. package/dist/operator/board-delta-gate.d.ts +34 -10
  96. package/dist/operator/board-delta-gate.d.ts.map +1 -1
  97. package/dist/operator/board-delta-gate.js +68 -24
  98. package/dist/operator/board-delta-gate.js.map +1 -1
  99. package/dist/operator/board-refresh-gate.d.ts +11 -6
  100. package/dist/operator/board-refresh-gate.d.ts.map +1 -1
  101. package/dist/operator/board-refresh-gate.js +12 -11
  102. package/dist/operator/board-refresh-gate.js.map +1 -1
  103. package/dist/operator/board-slot-instructions.d.ts +30 -0
  104. package/dist/operator/board-slot-instructions.d.ts.map +1 -1
  105. package/dist/operator/board-slot-instructions.js +110 -6
  106. package/dist/operator/board-slot-instructions.js.map +1 -1
  107. package/dist/operator/console-brief.d.ts +26 -13
  108. package/dist/operator/console-brief.d.ts.map +1 -1
  109. package/dist/operator/console-brief.js +159 -83
  110. package/dist/operator/console-brief.js.map +1 -1
  111. package/dist/operator/experience-evidence.d.ts +18 -0
  112. package/dist/operator/experience-evidence.d.ts.map +1 -0
  113. package/dist/operator/experience-evidence.js +82 -0
  114. package/dist/operator/experience-evidence.js.map +1 -0
  115. package/dist/operator/experience-hints.d.ts +44 -0
  116. package/dist/operator/experience-hints.d.ts.map +1 -0
  117. package/dist/operator/experience-hints.js +98 -0
  118. package/dist/operator/experience-hints.js.map +1 -0
  119. package/dist/operator/mama-memory-port.js +1 -3
  120. package/dist/operator/mama-memory-port.js.map +1 -1
  121. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  122. package/dist/operator/operator-trigger-loop.js +3 -2
  123. package/dist/operator/operator-trigger-loop.js.map +1 -1
  124. package/dist/operator/owner-action-effects.d.ts +13 -1
  125. package/dist/operator/owner-action-effects.d.ts.map +1 -1
  126. package/dist/operator/owner-action-effects.js +17 -4
  127. package/dist/operator/owner-action-effects.js.map +1 -1
  128. package/dist/operator/owner-event-inbox.d.ts +7 -1
  129. package/dist/operator/owner-event-inbox.d.ts.map +1 -1
  130. package/dist/operator/owner-event-inbox.js +8 -0
  131. package/dist/operator/owner-event-inbox.js.map +1 -1
  132. package/dist/operator/owner-event-loop.d.ts +28 -2
  133. package/dist/operator/owner-event-loop.d.ts.map +1 -1
  134. package/dist/operator/owner-event-loop.js +60 -2
  135. package/dist/operator/owner-event-loop.js.map +1 -1
  136. package/dist/operator/owner-event-policy.d.ts +20 -0
  137. package/dist/operator/owner-event-policy.d.ts.map +1 -1
  138. package/dist/operator/owner-event-policy.js +34 -5
  139. package/dist/operator/owner-event-policy.js.map +1 -1
  140. package/dist/operator/owner-event-prompt.d.ts +13 -4
  141. package/dist/operator/owner-event-prompt.d.ts.map +1 -1
  142. package/dist/operator/owner-event-prompt.js +31 -38
  143. package/dist/operator/owner-event-prompt.js.map +1 -1
  144. package/dist/operator/owner-runtime-journal.d.ts.map +1 -1
  145. package/dist/operator/owner-runtime-journal.js +7 -3
  146. package/dist/operator/owner-runtime-journal.js.map +1 -1
  147. package/dist/operator/owner-runtime.d.ts +6 -0
  148. package/dist/operator/owner-runtime.d.ts.map +1 -1
  149. package/dist/operator/owner-runtime.js +48 -5
  150. package/dist/operator/owner-runtime.js.map +1 -1
  151. package/dist/operator/procedure-activation.d.ts +7 -0
  152. package/dist/operator/procedure-activation.d.ts.map +1 -0
  153. package/dist/operator/procedure-activation.js +132 -0
  154. package/dist/operator/procedure-activation.js.map +1 -0
  155. package/dist/operator/procedure-projection.d.ts +25 -0
  156. package/dist/operator/procedure-projection.d.ts.map +1 -0
  157. package/dist/operator/procedure-projection.js +80 -0
  158. package/dist/operator/procedure-projection.js.map +1 -0
  159. package/dist/operator/procedure-runtime.d.ts +45 -0
  160. package/dist/operator/procedure-runtime.d.ts.map +1 -0
  161. package/dist/operator/procedure-runtime.js +496 -0
  162. package/dist/operator/procedure-runtime.js.map +1 -0
  163. package/dist/operator/procedure-store.d.ts +97 -0
  164. package/dist/operator/procedure-store.d.ts.map +1 -0
  165. package/dist/operator/procedure-store.js +368 -0
  166. package/dist/operator/procedure-store.js.map +1 -0
  167. package/dist/operator/subagent-stimulus.d.ts +34 -0
  168. package/dist/operator/subagent-stimulus.d.ts.map +1 -0
  169. package/dist/operator/subagent-stimulus.js +145 -0
  170. package/dist/operator/subagent-stimulus.js.map +1 -0
  171. package/dist/operator/task-ledger.d.ts +40 -3
  172. package/dist/operator/task-ledger.d.ts.map +1 -1
  173. package/dist/operator/task-ledger.js +63 -10
  174. package/dist/operator/task-ledger.js.map +1 -1
  175. package/dist/operator/thread-brief-memory.d.ts +29 -0
  176. package/dist/operator/thread-brief-memory.d.ts.map +1 -0
  177. package/dist/operator/thread-brief-memory.js +61 -0
  178. package/dist/operator/thread-brief-memory.js.map +1 -0
  179. package/dist/operator/trigger-author.d.ts +2 -1
  180. package/dist/operator/trigger-author.d.ts.map +1 -1
  181. package/dist/operator/trigger-author.js +23 -0
  182. package/dist/operator/trigger-author.js.map +1 -1
  183. package/dist/operator/trigger-matcher.js +1 -0
  184. package/dist/operator/trigger-matcher.js.map +1 -1
  185. package/dist/operator/trigger-registry.d.ts +5 -4
  186. package/dist/operator/trigger-registry.d.ts.map +1 -1
  187. package/dist/operator/trigger-registry.js +78 -24
  188. package/dist/operator/trigger-registry.js.map +1 -1
  189. package/dist/operator/trigger-review.d.ts +1 -1
  190. package/dist/operator/trigger-review.d.ts.map +1 -1
  191. package/dist/operator/trigger-review.js +24 -4
  192. package/dist/operator/trigger-review.js.map +1 -1
  193. package/dist/operator/trigger-types.d.ts +10 -1
  194. package/dist/operator/trigger-types.d.ts.map +1 -1
  195. package/dist/operator/worker-run.d.ts +13 -1
  196. package/dist/operator/worker-run.d.ts.map +1 -1
  197. package/dist/operator/worker-run.js +5 -2
  198. package/dist/operator/worker-run.js.map +1 -1
  199. package/dist/operator/workorder-consumer.d.ts +106 -14
  200. package/dist/operator/workorder-consumer.d.ts.map +1 -1
  201. package/dist/operator/workorder-consumer.js +295 -93
  202. package/dist/operator/workorder-consumer.js.map +1 -1
  203. package/dist/operator/workorder-hooks.d.ts +42 -9
  204. package/dist/operator/workorder-hooks.d.ts.map +1 -1
  205. package/dist/operator/workorder-hooks.js +52 -13
  206. package/dist/operator/workorder-hooks.js.map +1 -1
  207. package/dist/operator/workorder-publishers.d.ts +9 -1
  208. package/dist/operator/workorder-publishers.d.ts.map +1 -1
  209. package/dist/operator/workorder-publishers.js +21 -2
  210. package/dist/operator/workorder-publishers.js.map +1 -1
  211. package/package.json +2 -2
  212. package/dist/operator/learning-context.d.ts +0 -49
  213. package/dist/operator/learning-context.d.ts.map +0 -1
  214. package/dist/operator/learning-context.js +0 -129
  215. package/dist/operator/learning-context.js.map +0 -1
  216. package/dist/operator/learning-markers.d.ts +0 -19
  217. package/dist/operator/learning-markers.d.ts.map +0 -1
  218. package/dist/operator/learning-markers.js +0 -147
  219. package/dist/operator/learning-markers.js.map +0 -1
  220. package/dist/operator/learning-read.d.ts +0 -16
  221. package/dist/operator/learning-read.d.ts.map +0 -1
  222. package/dist/operator/learning-read.js +0 -18
  223. package/dist/operator/learning-read.js.map +0 -1
  224. package/dist/operator/turn-observer.d.ts +0 -34
  225. package/dist/operator/turn-observer.d.ts.map +0 -1
  226. package/dist/operator/turn-observer.js +0 -79
  227. package/dist/operator/turn-observer.js.map +0 -1
  228. package/templates/skills/heartbeat-report.md +0 -75
@@ -69,6 +69,7 @@ const index_js_2 = require("../concurrency/index.js");
69
69
  const session_pool_js_1 = require("./session-pool.js");
70
70
  const principal_js_1 = require("../gateways/principal.js");
71
71
  const owner_runtime_js_1 = require("../operator/owner-runtime.js");
72
+ const owner_event_policy_js_1 = require("../operator/owner-event-policy.js");
72
73
  const os_1 = require("os");
73
74
  const path_1 = require("path");
74
75
  const crypto_1 = require("crypto");
@@ -278,31 +279,15 @@ function loadBackendAgentsMd(backend, verbose = false) {
278
279
  return '';
279
280
  }
280
281
  function loadComposedSystemPrompt(verbose = false, context) {
281
- const mamaHome = (0, path_1.join)((0, os_1.homedir)(), '.mama');
282
282
  const layers = [];
283
- // Load persona files: SOUL.md, IDENTITY.md, USER.md
284
- const personaFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
285
- for (const file of personaFiles) {
286
- const path = (0, path_1.join)(mamaHome, file);
287
- if ((0, fs_1.existsSync)(path)) {
288
- if (verbose)
289
- console.log(`[AgentLoop] Loaded persona: ${file}`);
290
- const content = (0, fs_1.readFileSync)(path, 'utf-8');
291
- layers.push(content);
292
- }
293
- else {
294
- if (verbose)
295
- console.log(`[AgentLoop] Persona file not found (skipping): ${file}`);
296
- }
297
- }
298
283
  // Load skill catalog (on-demand mode — full content injected per-message by PromptEnhancer)
299
- const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(verbose), context);
284
+ const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(verbose, { includePaths: context?.roleName === 'owner_console' }), context);
300
285
  if (skillCatalog.length > 0) {
301
286
  const skillDirective = [
302
287
  '# Installed Skills',
303
288
  '',
304
- 'To invoke a skill, include its keywords in your message.',
305
- 'The full skill instructions will be injected automatically when matched.',
289
+ 'Choose relevant skills from their descriptions and the current task; keywords are optional hints.',
290
+ 'Read the selected instructions (source path when listed) before applying them. Learned procedures use procedure_list/read.',
306
291
  '',
307
292
  ...skillCatalog,
308
293
  ].join('\n');
@@ -344,6 +329,8 @@ function getRunGatewayToolsPrompt(context, disallowed) {
344
329
  privateConnectorPolicy: DEFAULT_PRIVATE_CONNECTOR_POLICY,
345
330
  }).prompt;
346
331
  }
332
+ /** Korean/English phrasings that assert a correction, save, update or send has happened. */
333
+ const COMPLETION_CLAIM_PATTERN = /(?:반영|저장|기록|등록|갱신|적용|수정|전송|발송)\s*(?:완료|했습니다|됐습니다|되었습니다|하였습니다)|\b(?:saved|updated|applied|recorded|sent)\b/i;
347
334
  const CANONICAL_CODE_ACT_HEADING = '## Code-Act: Gateway Tool Execution via Sandbox';
348
335
  const CODE_ACT_MCP_COMPAT_NAME = 'mcp__code-act__code_act';
349
336
  const GENERATED_CODE_ACT_START = '<!-- MAMA_GENERATED_CODE_ACT_START -->';
@@ -420,11 +407,13 @@ function combineCodeActSessionPolicyFingerprint(callerFingerprint, policy) {
420
407
  codeActPolicy: policy.fingerprintPayload,
421
408
  });
422
409
  }
423
- function ownerRuntimeSessionPolicyFingerprint(role, model) {
410
+ function ownerRuntimeSessionPolicyFingerprint(role, model, supportsNativeSubagents) {
424
411
  return JSON.stringify({
425
412
  version: 1,
426
413
  subject: owner_runtime_js_1.OWNER_RUNTIME_SESSION_KEY,
427
- subagentPolicy: owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS,
414
+ // The fingerprint names the policy the session actually carries: a runner without
415
+ // native subagents is never told to delegate, so it must not be pinned to that text.
416
+ subagentPolicy: supportsNativeSubagents ? owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS : null,
428
417
  telegramFormatPolicy: telegram_format_js_1.TELEGRAM_FORMAT_GUIDE,
429
418
  model: model ?? null,
430
419
  allowedTools: [...(role?.allowedTools ?? [])].sort(),
@@ -513,6 +502,34 @@ function withExecutionSurface(executionContext, executionSurface) {
513
502
  executionSurface,
514
503
  };
515
504
  }
505
+ /** Normal coding tasks often need 10+ consecutive Bash calls; 5 was too low. */
506
+ const MAX_CONSECUTIVE_SAME_HOST_TOOL = 15;
507
+ /**
508
+ * The parent run's shape, kept only so a child announced mid-run (or after it) can be
509
+ * given an authority of its OWN: its own model run under the parent's, its own envelope,
510
+ * its own bridge with its own counters.
511
+ */
512
+ /**
513
+ * The options a native child of this run inherits. Everything is carried EXCEPT the role,
514
+ * which is projected onto the unattended surface: the owner's 1:1 chat turn holds the
515
+ * gateway shell and file writer, and a child answers to no one inside the turn.
516
+ */
517
+ function withUnattendedSubagentRole(options) {
518
+ const agentContext = options?.agentContext;
519
+ if (!agentContext) {
520
+ return { ...options };
521
+ }
522
+ const role = (0, owner_event_policy_js_1.projectUnattendedRole)(agentContext.role);
523
+ return {
524
+ ...options,
525
+ agentContext: {
526
+ ...agentContext,
527
+ role,
528
+ capabilities: [...role.allowedTools],
529
+ limitations: (role.blockedTools ?? []).map((tool) => `Cannot use ${tool}`),
530
+ },
531
+ };
532
+ }
516
533
  class AgentLoop {
517
534
  /**
518
535
  * Construction-wide child-runtime tool capability.
@@ -526,6 +543,22 @@ class AgentLoop {
526
543
  probesDurableSession;
527
544
  ownerRecoveryJournalEnabled;
528
545
  agent;
546
+ /**
547
+ * Whether the ACTIVE runner can spawn a native subagent the host can observe.
548
+ *
549
+ * Read by the owner prompt path and forwarded to the scheduled work-order contract, so
550
+ * neither has to string-match a backend name. A mocked runner that predates this field
551
+ * is treated as incapable rather than silently promised delegation.
552
+ */
553
+ get supportsNativeSubagents() {
554
+ return this.agent.supportsNativeSubagents === true;
555
+ }
556
+ /**
557
+ * What a native child started by a run on this session needs to be given its own
558
+ * authority. Keyed by session key, overwritten by each run on that session, and dropped
559
+ * once the run finished and no child of it is still registered.
560
+ */
561
+ subagentRunContexts = new Map();
529
562
  persistentCLI = null;
530
563
  mcpExecutor;
531
564
  maxTurns;
@@ -622,27 +655,12 @@ class AgentLoop {
622
655
  // Priority 1 (never cut): CLAUDE.md base instructions
623
656
  // Priority 2 (cut if extreme): personas (SOUL, IDENTITY, USER) + gateway tools
624
657
  // Priority 3 (cut first): context prompt + skills + onboarding
625
- const mamaHome = (0, path_1.join)((0, os_1.homedir)(), '.mama');
626
658
  const claudeMd = loadSystemPrompt();
627
- const personaFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
628
- const personaParts = [];
629
- for (const file of personaFiles) {
630
- const p = (0, path_1.join)(mamaHome, file);
631
- if ((0, fs_1.existsSync)(p))
632
- personaParts.push((0, fs_1.readFileSync)(p, 'utf-8'));
633
- }
634
- const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(), options.agentContext ?? null);
659
+ const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(false, {
660
+ includePaths: options.agentContext?.roleName === 'owner_console',
661
+ }), options.agentContext ?? null);
635
662
  promptLayers = [
636
663
  { name: 'claudeMd', content: claudeMd, priority: 1 },
637
- ...(personaParts.length > 0
638
- ? [
639
- {
640
- name: 'personas',
641
- content: personaParts.join('\n\n---\n\n'),
642
- priority: 2,
643
- },
644
- ]
645
- : []),
646
664
  ...(skillCatalog.length > 0
647
665
  ? [
648
666
  {
@@ -650,7 +668,7 @@ class AgentLoop {
650
668
  content: [
651
669
  '# Installed Skills',
652
670
  '',
653
- 'To invoke a skill, include its keywords in your message.',
671
+ 'Choose skills by their purpose and the current task, not only literal keywords.',
654
672
  '',
655
673
  ...skillCatalog,
656
674
  ].join('\n'),
@@ -750,6 +768,9 @@ class AgentLoop {
750
768
  mcpConfigPath: this.useCodeAct
751
769
  ? undefined
752
770
  : (options.mcpConfigPath ?? (useMCPMode ? mcpConfigPath : undefined)),
771
+ // A Codex-native child outlives the parent turn. The host - not the child's
772
+ // parent bridge - issues its authority: own model run, own envelope, own counters.
773
+ createSubagentBridge: (info) => this.createSubagentBridge(info),
753
774
  });
754
775
  logger.debug('Codex app-server backend enabled');
755
776
  }
@@ -792,6 +813,8 @@ class AgentLoop {
792
813
  // ToolSearch/Agent, native gathering that bypasses report verification).
793
814
  // Callers opt in per loop; agent-loop-init locks down the main persona.
794
815
  tools: options.builtinTools,
816
+ // agent.effort: the boot gate already proved it is a level this backend accepts.
817
+ effort: options.codexEffort,
795
818
  // Pass configured timeout (default in PersistentCLI: 120s — too short for complex tasks)
796
819
  requestTimeout: options.timeoutMs,
797
820
  });
@@ -952,6 +975,15 @@ class AgentLoop {
952
975
  setAgentEventBus(eventBus) {
953
976
  this.mcpExecutor.setAgentEventBus(eventBus);
954
977
  }
978
+ /**
979
+ * The model runner backing this loop, for host adapters that subscribe to runner
980
+ * lifecycle events (e.g. native subagent completion waking the standing owner).
981
+ * Returned as `unknown` on purpose: the runner surface differs per backend, so the
982
+ * caller must feature-detect rather than assume an emitter.
983
+ */
984
+ getModelRunner() {
985
+ return this.agent;
986
+ }
955
987
  /**
956
988
  * Run the agent loop with a user prompt
957
989
  *
@@ -1010,15 +1042,29 @@ class AgentLoop {
1010
1042
  // Queue wait consumes no execution authority. Never renew a signed envelope implicitly:
1011
1043
  // only a host-supplied issuer can prepare the current grant after both lane waits.
1012
1044
  if (options?.prepareEnvelope) {
1045
+ const runEnvelopeIssuer = options.prepareEnvelope;
1013
1046
  options = {
1014
1047
  ...options,
1015
- envelope: await options.prepareEnvelope(),
1048
+ envelope: await runEnvelopeIssuer(),
1016
1049
  prepareEnvelope: undefined,
1050
+ // A native child outlives this envelope and needs one of its OWN. The run's issuer
1051
+ // is the fallback when the host supplied no dedicated subagent issuer, so it is kept
1052
+ // reachable here instead of being lost with this rewrite - without it a child would
1053
+ // silently get no authority at all.
1054
+ prepareSubagentEnvelope: options.prepareSubagentEnvelope ?? (async () => await runEnvelopeIssuer()),
1017
1055
  };
1018
1056
  if (this.stopped) {
1019
1057
  throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
1020
1058
  }
1021
1059
  }
1060
+ if (options?.prepareContent) {
1061
+ const admitted = await options.prepareContent();
1062
+ content = admitted.content;
1063
+ options = { ...options, procedureRefs: admitted.procedureRefs, prepareContent: undefined };
1064
+ if (this.stopped) {
1065
+ throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
1066
+ }
1067
+ }
1022
1068
  if (options?.agentContext) {
1023
1069
  options = {
1024
1070
  ...options,
@@ -1057,6 +1103,13 @@ class AgentLoop {
1057
1103
  },
1058
1104
  };
1059
1105
  let toolExecutionContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext(options), backgroundTasks);
1106
+ if (toolExecutionContext) {
1107
+ toolExecutionContext = {
1108
+ ...toolExecutionContext,
1109
+ procedureStimulus: ownerJournalPrompt,
1110
+ procedureRefs: options?.procedureRefs,
1111
+ };
1112
+ }
1060
1113
  // Track this run's tier for code-act execution and prompt sizing.
1061
1114
  if (options?.agentContext) {
1062
1115
  const rawTier = options.agentContext.tier ?? 1;
@@ -1069,11 +1122,14 @@ class AgentLoop {
1069
1122
  // Infinite loop prevention
1070
1123
  let consecutiveToolCalls = 0;
1071
1124
  let lastToolName = '';
1072
- const MAX_CONSECUTIVE_SAME_TOOL = 15; // Increased from 5 - normal coding tasks often need 10+ consecutive Bash calls
1073
- const EMERGENCY_MAX_TURNS = Math.max(this.maxTurns + 10, 50); // Always above maxTurns
1125
+ const MAX_CONSECUTIVE_SAME_TOOL = MAX_CONSECUTIVE_SAME_HOST_TOOL;
1126
+ const EMERGENCY_MAX_TURNS = this.emergencyMaxCalls(); // Always above maxTurns
1074
1127
  // Track channel key for session release
1075
1128
  const channelKey = options?.sessionKey ??
1076
1129
  (0, session_pool_js_1.buildChannelKey)(options?.source ?? 'default', (0, principal_js_1.laneChannelId)(options?.channelId ?? this.sessionKey, 'owner'));
1130
+ // The context object THIS run created, compared by identity before the finally marks
1131
+ // it finished: a later run on the same session key replaces the map entry.
1132
+ let ownedSubagentRunContext;
1077
1133
  // Use session pool for conversation continuity
1078
1134
  // IMPORTANT: If caller passes cliSessionId, use it directly to avoid double-locking
1079
1135
  // MessageRouter already calls getSession() and passes the result via options
@@ -1125,7 +1181,7 @@ class AgentLoop {
1125
1181
  : undefined;
1126
1182
  const outerCodeActAllowed = this.useCodeAct && roleAllowsOuterCodeAct(options?.agentContext?.role, this.disallowedTools);
1127
1183
  const ownerPolicyFingerprint = ownerRuntime
1128
- ? ownerRuntimeSessionPolicyFingerprint(sessionPolicyRole, options?.model ?? this.model)
1184
+ ? ownerRuntimeSessionPolicyFingerprint(sessionPolicyRole, options?.model ?? this.model, this.supportsNativeSubagents)
1129
1185
  : undefined;
1130
1186
  const effectiveSessionPolicyFingerprint = isDurableRuntime && codeActPolicy
1131
1187
  ? combineCodeActSessionPolicyFingerprint(ownerPolicyFingerprint ?? options?.sessionPolicyFingerprint, codeActPolicy)
@@ -1175,112 +1231,92 @@ class AgentLoop {
1175
1231
  ...options,
1176
1232
  modelRunId: ownedModelRunId,
1177
1233
  }), backgroundTasks);
1234
+ if (toolExecutionContext) {
1235
+ toolExecutionContext = {
1236
+ ...toolExecutionContext,
1237
+ procedureStimulus: ownerJournalPrompt,
1238
+ procedureRefs: options?.procedureRefs,
1239
+ };
1240
+ }
1178
1241
  }
1179
1242
  nativeEffects = new native_effect_observer_js_1.NativeEffectReplayBoundary(this.createNativeEffectObserver?.(toolExecutionContext));
1180
- let nativeToolCallCount = 0;
1181
- let nativeConsecutiveToolCalls = 0;
1182
- let nativeLastToolSignature = '';
1243
+ // This run's bridge is ITS OWN: the loop guards, the emergency budget, the history
1244
+ // writes and the turn observers all belong to this run. A Codex-native child gets a
1245
+ // separate bridge from `createSubagentBridge` - sharing this closure let a child trip
1246
+ // the parent's loop guard and append its traces to the parent's committed model run.
1247
+ const hostToolDefinitions = this.hostToolDefinitionsFor(options, outerCodeActAllowed);
1183
1248
  const hostToolBridge = isDurableRuntime && this.isGatewayMode
1184
- ? {
1185
- tools: this.useCodeAct
1186
- ? outerCodeActAllowed
1187
- ? tool_registry_js_1.ToolRegistry.getHostToolDefinitions({ allowedTools: [index_js_1.CODE_ACT_MARKER] })
1188
- : []
1189
- : tool_registry_js_1.ToolRegistry.getHostToolDefinitions({
1190
- allowedTools: options?.agentContext?.role.allowedTools,
1191
- blockedTools: options?.agentContext?.role.blockedTools,
1192
- disallowedTools: this.disallowedTools,
1193
- viewer: options?.agentContext?.platform === 'viewer',
1194
- }),
1195
- execute: async (call) => {
1196
- const callSignal = call.signal ?? new AbortController().signal;
1197
- callSignal.throwIfAborted();
1198
- if (nativeToolCallCount >= EMERGENCY_MAX_TURNS) {
1199
- return {
1200
- content: `Native tool call budget exceeded emergency maximum turns (${EMERGENCY_MAX_TURNS})`,
1201
- isError: true,
1202
- abort: true,
1203
- };
1204
- }
1205
- const toolSignature = call.name === index_js_1.CODE_ACT_MARKER && typeof call.input.code === 'string'
1206
- ? `${call.name}:${call.input.code.trim()}`
1207
- : call.name;
1208
- const nextConsecutiveCount = toolSignature === nativeLastToolSignature ? nativeConsecutiveToolCalls + 1 : 1;
1209
- if (nextConsecutiveCount >= MAX_CONSECUTIVE_SAME_TOOL) {
1210
- return {
1211
- content: `Infinite loop detected: Tool "${call.name}" called ${nextConsecutiveCount} times consecutively`,
1212
- isError: true,
1213
- abort: true,
1214
- };
1215
- }
1216
- nativeToolCallCount += 1;
1217
- nativeConsecutiveToolCalls = nextConsecutiveCount;
1218
- nativeLastToolSignature = toolSignature;
1219
- const toolUse = {
1220
- type: 'tool_use',
1221
- id: call.callId,
1222
- name: call.name,
1223
- input: call.input,
1224
- };
1225
- // Codex does not return completed host exchanges, so record them
1226
- // here. Cline reports the paired custom-tool exchange from its Hub
1227
- // event stream and AgentLoop appends it exactly once below.
1228
- if (isCodex) {
1229
- history.push({ role: 'assistant', content: [toolUse] });
1230
- runScope.onTurn?.({
1231
- turn,
1232
- role: 'assistant',
1233
- content: [toolUse],
1234
- stopReason: 'tool_use',
1235
- });
1236
- }
1237
- const callExecutionContext = toolExecutionContext
1238
- ? {
1239
- ...toolExecutionContext,
1240
- gatewayCallId: call.callId,
1241
- signal: callSignal,
1242
- }
1243
- : null;
1244
- const [toolResult] = await this.executeTools([toolUse], options?.stopAfterSuccessfulTools ?? [], callExecutionContext, runScope);
1245
- if (!toolResult) {
1246
- callSignal.throwIfAborted();
1247
- return {
1248
- content: `Native tool "${call.name}" returned no result`,
1249
- isError: true,
1250
- abort: true,
1251
- };
1252
- }
1253
- if (!toolResult.terminalCode) {
1254
- callSignal.throwIfAborted();
1255
- }
1256
- if (isCodex) {
1257
- const persistedToolResult = historyToolResult(toolResult);
1258
- history.push({ role: 'user', content: [persistedToolResult] });
1259
- runScope.onTurn?.({
1260
- turn,
1261
- role: 'user',
1262
- content: [persistedToolResult],
1263
- });
1249
+ ? this.buildHostToolBridge({
1250
+ tools: hostToolDefinitions,
1251
+ runScope,
1252
+ executionContext: () => toolExecutionContext,
1253
+ stopAfterSuccessfulTools: options?.stopAfterSuccessfulTools ?? [],
1254
+ emergencyMaxCalls: EMERGENCY_MAX_TURNS,
1255
+ maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_TOOL,
1256
+ // Codex does not return completed host exchanges, so record them here. Cline
1257
+ // reports the paired custom-tool exchange from its Hub event stream and
1258
+ // AgentLoop appends it exactly once below.
1259
+ ...(isCodex
1260
+ ? {
1261
+ recordExchange: {
1262
+ assistant: (toolUse) => {
1263
+ history.push({ role: 'assistant', content: [toolUse] });
1264
+ runScope.onTurn?.({
1265
+ turn,
1266
+ role: 'assistant',
1267
+ content: [toolUse],
1268
+ stopReason: 'tool_use',
1269
+ });
1270
+ },
1271
+ result: (toolResult) => {
1272
+ const persistedToolResult = historyToolResult(toolResult);
1273
+ history.push({ role: 'user', content: [persistedToolResult] });
1274
+ runScope.onTurn?.({
1275
+ turn,
1276
+ role: 'user',
1277
+ content: [persistedToolResult],
1278
+ });
1279
+ },
1280
+ },
1264
1281
  }
1265
- return {
1266
- content: toolResult.content,
1267
- isError: toolResult.is_error === true,
1268
- abort: toolResult.abort === true,
1269
- terminalCode: toolResult.terminalCode,
1270
- stop: toolResult.is_error !== true &&
1271
- (options?.stopAfterSuccessfulTools ?? []).includes(call.name),
1272
- };
1273
- },
1274
- }
1282
+ : {}),
1283
+ })
1275
1284
  : undefined;
1285
+ if (hostToolBridge && isCodex) {
1286
+ // A child can be announced during this run and outlive it, so what its authority
1287
+ // needs is recorded now, per session key, and dropped when nothing needs it.
1288
+ // A previous run's context object stays referenced by ITS still-live children, so
1289
+ // this run starts its own count rather than inheriting one it can never settle.
1290
+ // The child NEVER inherits this turn's role verbatim: an owner 1:1 chat turn holds
1291
+ // the gateway shell and file writer, and a child is unattended by definition.
1292
+ const childOptions = withUnattendedSubagentRole(options);
1293
+ ownedSubagentRunContext = {
1294
+ options: childOptions,
1295
+ parentModelRunId: ownedModelRunId ?? options?.modelRunId ?? null,
1296
+ cliSessionId: resolvedCliSessionId,
1297
+ tools: this.hostToolDefinitionsFor(childOptions),
1298
+ tier: runScope.tier,
1299
+ activeChildren: 0,
1300
+ runFinished: false,
1301
+ };
1302
+ this.subagentRunContexts.set(channelKey, ownedSubagentRunContext);
1303
+ }
1276
1304
  const prepareSystemPrompt = (requestedSystemPrompt, isResumingSession, includeOwnerRecovery = false) => {
1277
1305
  let baseSystemPrompt = requestedSystemPrompt ?? this.defaultSystemPrompt;
1278
- if (ownerRuntime && !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS)) {
1306
+ if (ownerRuntime &&
1307
+ this.supportsNativeSubagents &&
1308
+ !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS)) {
1309
+ // On a runner with no native subagent (persistent Claude persona, Cline) the
1310
+ // delegation policy asks for something the runtime cannot do. It is omitted, and
1311
+ // nothing replaces it: the turn's own result contract already says what must exist.
1279
1312
  baseSystemPrompt = `${baseSystemPrompt}\n\n${owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS}`;
1280
1313
  }
1281
1314
  if (ownerRuntime && !baseSystemPrompt.includes(telegram_format_js_1.TELEGRAM_FORMAT_GUIDE)) {
1282
1315
  baseSystemPrompt = `${baseSystemPrompt}\n\n${telegram_format_js_1.TELEGRAM_FORMAT_GUIDE}`;
1283
1316
  }
1317
+ if (ownerRuntime && !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_RUNTIME_RULES)) {
1318
+ baseSystemPrompt = `${baseSystemPrompt}\n\n${owner_runtime_js_1.OWNER_RUNTIME_RULES}`;
1319
+ }
1284
1320
  let gatewayToolsPrompt = '';
1285
1321
  if (this.isGatewayMode && this.useCodeAct) {
1286
1322
  baseSystemPrompt = stripTrailingCanonicalCodeActSection(stripGenericGatewayToolsCatalog(baseSystemPrompt));
@@ -1386,9 +1422,19 @@ class AgentLoop {
1386
1422
  // That rebuild costs an embedding search, so hand the backend a lazy builder it
1387
1423
  // invokes only inside the resume branch; a live thread never pays for it.
1388
1424
  const freshSystemPromptBuilder = options?.freshSessionSystemPrompt;
1389
- const resumeInstructions = isDurableRuntime && freshSystemPromptBuilder
1390
- ? async () => prepareSystemPrompt(await freshSystemPromptBuilder(), false, false)
1391
- : undefined;
1425
+ // The owner-event and scheduled lanes carry no per-call systemPrompt at all: their
1426
+ // perCallSystemPrompt IS the complete composed owner policy (the ownerRuntime branch
1427
+ // above). Without a resume builder those lanes fell back to the turn-text
1428
+ // <system-reminder> replay - the same full prompt, but billed as USER text on the
1429
+ // first turn after every daemon restart. Re-anchor it through baseInstructions
1430
+ // instead; the reminder is then never needed.
1431
+ const resumeInstructions = !isDurableRuntime
1432
+ ? undefined
1433
+ : freshSystemPromptBuilder
1434
+ ? async () => prepareSystemPrompt(await freshSystemPromptBuilder(), false, false)
1435
+ : ownerRuntime
1436
+ ? async () => perCallSystemPrompt
1437
+ : undefined;
1392
1438
  // Reset StopContinuation state for this channel to prevent leaking
1393
1439
  // retry counts from previous invocations
1394
1440
  if (this.stopContinuationHandler) {
@@ -1434,6 +1480,12 @@ class AgentLoop {
1434
1480
  nativeEffects.settled(name, toolUseId, isError);
1435
1481
  ext?.onToolComplete?.(name, toolUseId, isError);
1436
1482
  },
1483
+ // A spawn is an admission, not an external effect: forwarded verbatim with NO
1484
+ // nativeEffects call, so it never writes a `native_tool` ledger row and never
1485
+ // makes the occurrence unsafe to replay.
1486
+ onSubagentStart: (info) => {
1487
+ ext?.onSubagentStart?.(info);
1488
+ },
1437
1489
  onFinal: (finalResponse) => {
1438
1490
  ext?.onFinal?.(finalResponse);
1439
1491
  },
@@ -1453,7 +1505,8 @@ class AgentLoop {
1453
1505
  let requestSystemPrompt = perCallSystemPrompt;
1454
1506
  let provisionalDurableSessionId;
1455
1507
  // All three backends preserve context and receive only the new user message.
1456
- const promptText = this.formatLastMessageOnly(history);
1508
+ const basePromptText = this.formatLastMessageOnly(history);
1509
+ let promptText = basePromptText;
1457
1510
  const promptStart = Date.now();
1458
1511
  const throwFinalCliError = (error) => {
1459
1512
  const normalizedError = error instanceof Error ? error : new Error(String(error));
@@ -1562,11 +1615,22 @@ class AgentLoop {
1562
1615
  }
1563
1616
  shouldResume = false;
1564
1617
  }
1618
+ promptText = this.withProcedureHints(basePromptText, turn, {
1619
+ threadId: resolvedCliSessionId ?? channelKey,
1620
+ // A NEW pool session has no prior thread to carry procedures forward from, even
1621
+ // when the durable runtime is asked to resume: fresh is about the thread.
1622
+ fresh: !shouldResume || sessionIsNew,
1623
+ context: toolExecutionContext,
1624
+ });
1565
1625
  piResult = await this.agent.prompt(promptText, callbacks, {
1566
1626
  model: options?.model,
1567
1627
  resumeSession: shouldResume,
1568
1628
  systemPrompt: requestSystemPrompt,
1569
1629
  resumeInstructions,
1630
+ promptTelemetry: {
1631
+ kind: options?.promptKind ?? (options?.source === 'operator' ? 'scheduled' : 'chat'),
1632
+ brief: options?.promptBrief ?? 'omitted',
1633
+ },
1570
1634
  sessionKey: channelKey,
1571
1635
  sessionPolicyFingerprint: effectiveSessionPolicyFingerprint,
1572
1636
  sessionId: resolvedCliSessionId ?? undefined,
@@ -1704,6 +1768,11 @@ class AgentLoop {
1704
1768
  else if (ownerRuntime) {
1705
1769
  resetSystemPrompt = prepareSystemPrompt(options?.systemPrompt, false, true);
1706
1770
  }
1771
+ promptText = this.withProcedureHints(basePromptText, turn, {
1772
+ threadId: newSessionId,
1773
+ fresh: true,
1774
+ context: toolExecutionContext,
1775
+ });
1707
1776
  piResult = await this.agent.prompt(promptText, callbacks, {
1708
1777
  model: options?.model,
1709
1778
  resumeSession: false, // Force new session
@@ -2037,6 +2106,11 @@ class AgentLoop {
2037
2106
  result.modelRunProvenance = 'commit_failed';
2038
2107
  logger.error(`Model run ${ownedModelRunId} may remain uncommitted; provenance reported as commit_failed`);
2039
2108
  }
2109
+ if (ownerRuntime && finalResponse.trim()) {
2110
+ // Observation only: a completion claim with no durable write in this run is the
2111
+ // failure the owner sees as "corrected" work that never persisted (2026-09-09).
2112
+ void this.observeCompletionClaim(ownedModelRunId, finalResponse);
2113
+ }
2040
2114
  if (ownerRuntime && this.ownerRuntimeJournal && finalResponse.trim()) {
2041
2115
  try {
2042
2116
  this.ownerRuntimeJournal.append({
@@ -2076,6 +2150,16 @@ class AgentLoop {
2076
2150
  throw nativeEffects.failure(error);
2077
2151
  }
2078
2152
  finally {
2153
+ this.mcpExecutor.releaseProcedureRun?.(toolExecutionContext);
2154
+ // A child that outlives this run still needs its authority recipe, so the context is
2155
+ // dropped only once the run finished AND no child of it is still registered.
2156
+ if (ownedSubagentRunContext) {
2157
+ ownedSubagentRunContext.runFinished = true;
2158
+ if (ownedSubagentRunContext.activeChildren === 0 &&
2159
+ this.subagentRunContexts.get(channelKey) === ownedSubagentRunContext) {
2160
+ this.subagentRunContexts.delete(channelKey);
2161
+ }
2162
+ }
2079
2163
  // Always release session lock, even on error
2080
2164
  // BUT only if we own the session (not passed by caller)
2081
2165
  if (ownedSession) {
@@ -2126,6 +2210,250 @@ class AgentLoop {
2126
2210
  },
2127
2211
  };
2128
2212
  }
2213
+ /** Always above maxTurns: the last-resort stop for a runaway native tool loop. */
2214
+ emergencyMaxCalls() {
2215
+ return Math.max(this.maxTurns + 10, 50);
2216
+ }
2217
+ /**
2218
+ * The host tool surface the given options' role grants.
2219
+ *
2220
+ * The Code-Act gate is derived from THOSE options by default, never from the caller's
2221
+ * own role: a child runs under a projected unattended role, so a parent that may hold
2222
+ * outer Code-Act must not hand that marker to a child whose role forbids it.
2223
+ */
2224
+ hostToolDefinitionsFor(options, outerCodeActAllowed = this.useCodeAct &&
2225
+ roleAllowsOuterCodeAct(options?.agentContext?.role, this.disallowedTools)) {
2226
+ return this.useCodeAct
2227
+ ? outerCodeActAllowed
2228
+ ? tool_registry_js_1.ToolRegistry.getHostToolDefinitions({ allowedTools: [index_js_1.CODE_ACT_MARKER] })
2229
+ : []
2230
+ : tool_registry_js_1.ToolRegistry.getHostToolDefinitions({
2231
+ allowedTools: options?.agentContext?.role.allowedTools,
2232
+ blockedTools: options?.agentContext?.role.blockedTools,
2233
+ disallowedTools: this.disallowedTools,
2234
+ viewer: options?.agentContext?.platform === 'viewer',
2235
+ });
2236
+ }
2237
+ /**
2238
+ * One dynamic-tool bridge. Every guard it applies - the emergency call budget and the
2239
+ * same-signature loop guard - is private to the bridge, so a parent and its children
2240
+ * never share a counter. `recordExchange` is the ONLY way a bridge writes to a
2241
+ * conversation: a child passes none and therefore cannot touch the parent's history.
2242
+ */
2243
+ buildHostToolBridge(params) {
2244
+ let toolCallCount = 0;
2245
+ let consecutiveToolCalls = 0;
2246
+ let lastToolSignature = '';
2247
+ return {
2248
+ tools: params.tools,
2249
+ execute: async (call) => {
2250
+ const callSignal = call.signal ?? new AbortController().signal;
2251
+ callSignal.throwIfAborted();
2252
+ if (toolCallCount >= params.emergencyMaxCalls) {
2253
+ return {
2254
+ content: `Native tool call budget exceeded emergency maximum turns (${params.emergencyMaxCalls})`,
2255
+ isError: true,
2256
+ abort: true,
2257
+ };
2258
+ }
2259
+ const toolSignature = call.name === index_js_1.CODE_ACT_MARKER && typeof call.input.code === 'string'
2260
+ ? `${call.name}:${call.input.code.trim()}`
2261
+ : call.name;
2262
+ const nextConsecutiveCount = toolSignature === lastToolSignature ? consecutiveToolCalls + 1 : 1;
2263
+ if (nextConsecutiveCount >= params.maxConsecutiveSameTool) {
2264
+ return {
2265
+ content: `Infinite loop detected: Tool "${call.name}" called ${nextConsecutiveCount} times consecutively`,
2266
+ isError: true,
2267
+ abort: true,
2268
+ };
2269
+ }
2270
+ toolCallCount += 1;
2271
+ consecutiveToolCalls = nextConsecutiveCount;
2272
+ lastToolSignature = toolSignature;
2273
+ const toolUse = {
2274
+ type: 'tool_use',
2275
+ id: call.callId,
2276
+ name: call.name,
2277
+ input: call.input,
2278
+ };
2279
+ params.recordExchange?.assistant(toolUse);
2280
+ const executionContext = params.executionContext();
2281
+ const callExecutionContext = executionContext
2282
+ ? {
2283
+ ...executionContext,
2284
+ gatewayCallId: call.callId,
2285
+ signal: callSignal,
2286
+ }
2287
+ : null;
2288
+ const [toolResult] = await this.executeTools([toolUse], [...params.stopAfterSuccessfulTools], callExecutionContext, params.runScope);
2289
+ if (!toolResult) {
2290
+ callSignal.throwIfAborted();
2291
+ return {
2292
+ content: `Native tool "${call.name}" returned no result`,
2293
+ isError: true,
2294
+ abort: true,
2295
+ };
2296
+ }
2297
+ if (!toolResult.terminalCode) {
2298
+ callSignal.throwIfAborted();
2299
+ }
2300
+ params.recordExchange?.result(toolResult);
2301
+ return {
2302
+ content: toolResult.content,
2303
+ isError: toolResult.is_error === true,
2304
+ abort: toolResult.abort === true,
2305
+ terminalCode: toolResult.terminalCode,
2306
+ stop: toolResult.is_error !== true && params.stopAfterSuccessfulTools.includes(call.name),
2307
+ };
2308
+ },
2309
+ };
2310
+ }
2311
+ /**
2312
+ * Give one Codex-native child its OWN authority.
2313
+ *
2314
+ * The parent's bridge closes over an envelope issued for the PARENT's wall, which the
2315
+ * child cannot renew - inheriting it meant a long delegated run losing every tool
2316
+ * mid-flight and still reporting "done". So the child gets: its own model run under the
2317
+ * parent's, its own envelope from a host issuer, its own execution context, and its own
2318
+ * bridge counters. When no issuer is reachable it gets nothing and says so - the process
2319
+ * refuses its calls with `subagent authority unavailable` rather than quietly borrowing.
2320
+ */
2321
+ async createSubagentBridge(info) {
2322
+ const context = this.subagentRunContexts.get(info.sessionKey);
2323
+ if (!context) {
2324
+ console.warn(`[AgentLoop] subagent authority unavailable: no run context for session ${info.sessionKey} ` +
2325
+ `(thread=${info.agentThreadId})`);
2326
+ return null;
2327
+ }
2328
+ if (context.runFinished) {
2329
+ // Only a child announced DURING the run inherits that run's issuer. A finished run
2330
+ // stays reachable while a sibling is live; it must not mint authority hours later.
2331
+ console.warn(`[AgentLoop] subagent authority refused: run for session ${info.sessionKey} already ended ` +
2332
+ `(thread=${info.agentThreadId})`);
2333
+ return null;
2334
+ }
2335
+ // Claimed synchronously: the parent's finally must not drop the context while this
2336
+ // factory is still awaiting an envelope or a model run.
2337
+ context.activeChildren += 1;
2338
+ const abandon = (reason) => {
2339
+ console.warn(reason);
2340
+ this.releaseSubagentRunContext(info.sessionKey, context);
2341
+ return null;
2342
+ };
2343
+ const issuer = context.options.prepareSubagentEnvelope ?? context.options.prepareEnvelope;
2344
+ if (!issuer) {
2345
+ return abandon(`[AgentLoop] subagent authority unavailable: no host envelope issuer for ${info.sessionKey} ` +
2346
+ `(thread=${info.agentThreadId})`);
2347
+ }
2348
+ let envelope;
2349
+ try {
2350
+ envelope = await issuer();
2351
+ }
2352
+ catch (error) {
2353
+ return abandon(`[AgentLoop] subagent authority issuance failed thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
2354
+ }
2355
+ if (!envelope) {
2356
+ return abandon(`[AgentLoop] subagent authority unavailable: issuer returned no envelope ` +
2357
+ `(thread=${info.agentThreadId})`);
2358
+ }
2359
+ const childOptions = {
2360
+ ...context.options,
2361
+ envelope,
2362
+ prepareEnvelope: undefined,
2363
+ parentModelRunId: context.parentModelRunId,
2364
+ sourceMessageRef: `subagent:${info.agentThreadId}`,
2365
+ };
2366
+ let modelRunId;
2367
+ try {
2368
+ const modelRun = await this.mcpExecutor.beginRuntimeModelRun(this.buildModelRunInput(childOptions, context.cliSessionId));
2369
+ modelRunId = modelRun.model_run_id;
2370
+ }
2371
+ catch (error) {
2372
+ return abandon(`[AgentLoop] subagent model run could not begin thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
2373
+ }
2374
+ const childTasks = [];
2375
+ const backgroundTasks = {
2376
+ register(task) {
2377
+ const observedTask = Promise.resolve(task);
2378
+ observedTask.catch(() => {
2379
+ // Re-thrown later by the child's release drain.
2380
+ });
2381
+ childTasks.push(observedTask);
2382
+ },
2383
+ };
2384
+ let childContext;
2385
+ let bridge;
2386
+ // The child's model run is already OPEN. Anything that throws while assembling its
2387
+ // context or bridge must fail that run and release the parent's context, or the run
2388
+ // stays `running` forever and the context stays pinned by a child that never existed.
2389
+ try {
2390
+ childContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext({ ...childOptions, modelRunId }), backgroundTasks);
2391
+ if (childContext) {
2392
+ childContext = { ...childContext, subagentThreadId: info.agentThreadId };
2393
+ }
2394
+ // The child's own run scope: no stream callbacks, no onTurn, no onToolUse. Its work
2395
+ // is not this run's turns, and the parent's model run is already committed by then.
2396
+ const runScope = { tier: context.tier, ...(0, temporal_code_act_breaker_js_1.createTemporalCodeActBreakerState)() };
2397
+ bridge = this.buildHostToolBridge({
2398
+ tools: context.tools,
2399
+ runScope,
2400
+ executionContext: () => childContext,
2401
+ stopAfterSuccessfulTools: [],
2402
+ emergencyMaxCalls: this.emergencyMaxCalls(),
2403
+ maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_HOST_TOOL,
2404
+ });
2405
+ }
2406
+ catch (error) {
2407
+ const summary = error instanceof Error ? error.message : String(error);
2408
+ try {
2409
+ await this.mcpExecutor.failRuntimeModelRun(modelRunId, `subagent authority could not be assembled: ${summary}`);
2410
+ }
2411
+ catch (failError) {
2412
+ logger.warn(`Failed to mark subagent model run ${modelRunId} failed: ${failError instanceof Error ? failError.message : String(failError)}`);
2413
+ }
2414
+ return abandon(`[AgentLoop] subagent authority could not be assembled thread=${info.agentThreadId}: ${summary}`);
2415
+ }
2416
+ let released = false;
2417
+ return {
2418
+ bridge,
2419
+ release: async (outcome) => {
2420
+ if (released) {
2421
+ return;
2422
+ }
2423
+ released = true;
2424
+ try {
2425
+ await this.drainBackgroundTasks(childTasks);
2426
+ }
2427
+ catch (error) {
2428
+ logger.warn(`AgentLoop subagent background drain failed: ${error instanceof Error ? error.message : String(error)}`);
2429
+ }
2430
+ try {
2431
+ if (outcome.status === 'completed') {
2432
+ await this.mcpExecutor.commitRuntimeModelRun(modelRunId, `agent_loop subagent ${info.agentPath || info.agentThreadId} completed`);
2433
+ }
2434
+ else {
2435
+ // `unknown` is not success: the run is marked failed with the reason so the
2436
+ // ledger never carries a completion nobody confirmed.
2437
+ await this.mcpExecutor.failRuntimeModelRun(modelRunId, outcome.error ?? `subagent ${outcome.status}`);
2438
+ }
2439
+ }
2440
+ catch (error) {
2441
+ logger.warn(`AgentLoop subagent model run ${modelRunId} could not be closed: ${error instanceof Error ? error.message : String(error)}`);
2442
+ }
2443
+ this.mcpExecutor.releaseProcedureRun?.(childContext);
2444
+ this.releaseSubagentRunContext(info.sessionKey, context);
2445
+ },
2446
+ };
2447
+ }
2448
+ /** Drop a run's subagent context once the run finished and no child still holds it. */
2449
+ releaseSubagentRunContext(sessionKey, context) {
2450
+ context.activeChildren = Math.max(0, context.activeChildren - 1);
2451
+ if (context.runFinished &&
2452
+ context.activeChildren === 0 &&
2453
+ this.subagentRunContexts.get(sessionKey) === context) {
2454
+ this.subagentRunContexts.delete(sessionKey);
2455
+ }
2456
+ }
2129
2457
  /**
2130
2458
  * Execute tools from response content blocks
2131
2459
  */
@@ -2379,6 +2707,39 @@ class AgentLoop {
2379
2707
  * Format only the last user message for persistent CLI
2380
2708
  * Persistent CLI maintains context automatically, so we only send the new message
2381
2709
  */
2710
+ /**
2711
+ * Turn 1 only, once the backend has said whether the thread is live: a fresh or re-opened
2712
+ * thread is told the relevant procedures once, a live thread only what it has not been
2713
+ * told (procedure-runtime.ts). Later turns of a run carry tool results, not hints.
2714
+ */
2715
+ withProcedureHints(basePromptText, turn, input) {
2716
+ if (turn !== 1)
2717
+ return basePromptText;
2718
+ const hints = this.mcpExecutor.prepareProcedureContext?.(input.context, {
2719
+ threadId: input.threadId,
2720
+ fresh: input.fresh,
2721
+ });
2722
+ // Logged even when empty: with no procedures stored, this is the only sign the
2723
+ // assembler ran and the thread decision it saw.
2724
+ console.log(`[experience] thread=${input.threadId} fresh=${input.fresh} hints=${hints?.hints.length ?? 0}${hints?.hints.length ? `(${hints.hints.join(',')})` : ''} chars=${hints?.text.length ?? 0}`);
2725
+ if (!hints?.text)
2726
+ return basePromptText;
2727
+ return `${hints.text}\n\n${basePromptText}`;
2728
+ }
2729
+ async observeCompletionClaim(modelRunId, finalResponse) {
2730
+ const claim = finalResponse.match(COMPLETION_CLAIM_PATTERN)?.[0];
2731
+ if (!claim || !modelRunId)
2732
+ return;
2733
+ try {
2734
+ const wrote = await this.mcpExecutor.runHadDurableWrite?.(modelRunId);
2735
+ if (wrote === false) {
2736
+ console.warn(`[evidence] completion claim without a durable write: run=${modelRunId} claim="${claim}"`);
2737
+ }
2738
+ }
2739
+ catch (error) {
2740
+ console.warn(`[evidence] claim check unavailable: ${error instanceof Error ? error.message : String(error)}`);
2741
+ }
2742
+ }
2382
2743
  formatLastMessageOnly(history) {
2383
2744
  const imageReaderTool = this.backend === 'cline' ? 'read_files' : this.backend === 'codex' ? 'view_image' : 'Read';
2384
2745
  // Find the last user message in the history