mixdog 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/package.json +8 -8
  2. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  3. package/src/agents/reviewer/AGENT.md +5 -7
  4. package/src/agents/reviewer/agent.json +1 -1
  5. package/src/defaults/shipped-history.json +4 -0
  6. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  7. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  8. package/src/rules/lead/LEAD.md +5 -8
  9. package/src/rules/shared/45-destructive.md +5 -1
  10. package/src/rules/shared/60-verification.md +6 -4
  11. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  13. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  17. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  19. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
  21. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  22. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  24. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
  26. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
  29. package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
  30. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  31. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
  32. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
  34. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
  37. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
  38. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
  39. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
  41. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  42. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  45. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
  47. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
  50. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  51. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  52. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  53. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
  54. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  57. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
  58. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  59. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  60. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  61. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
  63. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  64. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
  66. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
  68. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  69. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  70. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  71. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
  72. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
  73. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
  74. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  75. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  76. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  77. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
  79. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  81. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
  84. package/src/runtime/attachments/legacy-office.mjs +110 -0
  85. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  86. package/src/runtime/attachments/store.mjs +60 -3
  87. package/src/runtime/effort-judge/judge-client.mjs +61 -14
  88. package/src/runtime/effort-judge/model-install.mjs +34 -2
  89. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  90. package/src/runtime/office/portable/portable-soffice.mjs +132 -30
  91. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  92. package/src/runtime/shared/agent-avatar.mjs +3 -2
  93. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  94. package/src/runtime/shared/agent-room-host.mjs +58 -0
  95. package/src/runtime/shared/bot-hops.mjs +16 -11
  96. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  97. package/src/runtime/shared/redact-directories.mjs +18 -0
  98. package/src/runtime/shared/remove-directory.mjs +28 -0
  99. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  100. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  101. package/src/runtime/shared/tool-execution-contract.mjs +4 -0
  102. package/src/runtime/shared/tool-work-units.mjs +12 -9
  103. package/src/runtime/shared/turn-snapshot.mjs +233 -122
  104. package/src/runtime/shared/user-data-guard.mjs +51 -19
  105. package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
  106. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  107. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  108. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  109. package/src/runtime/shared/worktrees/registry.mjs +3 -1
  110. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  111. package/src/runtime/tidy/process.mjs +41 -8
  112. package/src/runtime/tidy/resolve.mjs +10 -7
  113. package/src/session-runtime/agent-home-delivery.mjs +73 -91
  114. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  115. package/src/session-runtime/agent-room-delivery.mjs +2 -63
  116. package/src/session-runtime/agent-task-sessions.mjs +194 -64
  117. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  118. package/src/session-runtime/boot/apis.mjs +3 -0
  119. package/src/session-runtime/boot/facade.mjs +5 -6
  120. package/src/session-runtime/boot/tools.mjs +23 -1
  121. package/src/session-runtime/held-requests.mjs +20 -25
  122. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
  123. package/src/session-runtime/model-recency.mjs +1 -0
  124. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  125. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  126. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  127. package/src/session-runtime/runtime-review-api.mjs +4 -1
  128. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  129. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  130. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
  131. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  132. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
  133. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  134. package/src/session-runtime/services/provider-admin.mjs +12 -2
  135. package/src/session-runtime/services/worktree-access.mjs +27 -0
  136. package/src/session-runtime/session-hooks.mjs +9 -17
  137. package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
  138. package/src/session-runtime/setup-tool/executor.mjs +4 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
  140. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
  141. package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
  142. package/src/session-runtime/tool-policy-refresh.mjs +1 -0
  143. package/src/session-runtime/tool-surface.mjs +9 -5
  144. package/src/session-runtime/turn/session-ops.mjs +8 -0
  145. package/src/session-runtime/turn/turn-run.mjs +5 -1
  146. package/src/session-runtime/usage-stats-api.mjs +1 -1
  147. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
  148. package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
  149. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  150. package/src/session-runtime/workspace/transitions.mjs +279 -67
  151. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  152. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  153. package/src/standalone/daemon-session-control.mjs +140 -23
  154. package/src/standalone/daemon-telemetry.mjs +9 -0
  155. package/src/standalone/daemon.mjs +5 -0
  156. package/src/standalone/local-session-runtime.mjs +11 -2
  157. package/src/standalone/session-protocol.mjs +1 -0
  158. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  159. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  160. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  161. package/src/standalone/session-service.mjs +10 -0
  162. package/src/standalone/session-tool-output.mjs +1 -0
  163. package/src/standalone/staged-import.mjs +47 -0
  164. package/src/tui/App.jsx +2 -0
  165. package/src/tui/app/message-selector.mjs +15 -2
  166. package/src/tui/app/model-options.mjs +16 -2
  167. package/src/tui/app/model-picker.mjs +2 -1
  168. package/src/tui/app/route-pickers.mjs +3 -1
  169. package/src/tui/dist/index.mjs +403 -210
  170. package/src/tui/paste-attachments.mjs +0 -1
  171. package/src/tui/paste-text-policy.mjs +4 -5
  172. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  173. package/src/tui/session/notice-surface.mjs +3 -0
  174. package/src/tui/session/session-api/intake/abort.mjs +33 -17
  175. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  176. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  177. package/src/tui/session/tool-approval.mjs +11 -48
  178. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  179. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  180. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  181. package/src/session-runtime/room-member-gate.mjs +0 -61
  182. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -58,6 +58,7 @@ export function createLoopState({ provider, messages, model, tools, cwd, sendOpt
58
58
  lastUsage: undefined,
59
59
  firstTurnUsage: undefined,
60
60
  response: undefined,
61
+ lastStreamedChars: 0,
61
62
  lastSendTools: tools,
62
63
  contextOverflowRetryUsed: false,
63
64
  // Set when a provider context-overflow refusal triggers the in-turn
@@ -133,6 +134,10 @@ export function createLoopState({ provider, messages, model, tools, cwd, sendOpt
133
134
  sessionId,
134
135
  sessionAgent: state.sessionAgent,
135
136
  drainSteering: state.drainSteering,
137
+ // Only a provider that can offer a higher max_tokens implements this.
138
+ escalateMaxTokens: () => state.provider?.escalatedMaxTokens?.(state.model, state.opts, state.lastSendTools) ?? null,
139
+ // Characters the latest send put on screen through the live text stream.
140
+ streamedChars: () => state.lastStreamedChars,
136
141
  });
137
142
  return state;
138
143
  }
@@ -6,6 +6,7 @@
6
6
  // continuations with no intervening tool batch, the current text is accepted
7
7
  // as the final answer.
8
8
  import { writeLoopDiagnostic } from './diagnostic.mjs';
9
+ import { resetOutputLimitRecovery } from './output-limit.mjs';
9
10
 
10
11
  const PROVIDER_CONTINUATION_NO_TOOL_LIMIT = Math.max(
11
12
  1,
@@ -38,6 +39,7 @@ export function resumeContinuation(
38
39
  state.providerContinuationCount += 1;
39
40
  state.continuationsSinceToolBatch += 1;
40
41
  state.emptyNudgeStreak = 0;
42
+ resetOutputLimitRecovery(state);
41
43
  trace(iteration, 'provider_continuation', {
42
44
  signal,
43
45
  stop_reason: stopReason,
@@ -25,7 +25,7 @@ export function nudgeEmptyTurn(response, { stopReason, isIncompleteStop }, { sta
25
25
  messages.push({
26
26
  role: 'user',
27
27
  content: isIncompleteStop
28
- ? `[mixdog-runtime] Empty truncated continuation (stopReason=${stopReason}). Return the remaining final handoff; use tools only for required evidence still missing.`
28
+ ? `[mixdog-runtime] Empty incomplete turn (stopReason=${stopReason}). Return the remaining final handoff; use tools only for required evidence still missing.`
29
29
  : `[mixdog-runtime] Empty response (${state.emptyNudgeStreak}/${EMPTY_NUDGE_MAX}). Return final text, or use tools only for required evidence still missing.`,
30
30
  });
31
31
  return { action: 'continue', response };
@@ -1,13 +1,22 @@
1
- // Max-output ladder.
1
+ // Max-output recovery — the single rule for every turn the provider cut at its
2
+ // output ceiling, with or without visible text.
2
3
  //
3
- // A provider max-output stop is not a completed assistant turn, even when it
4
- // contains useful text. Preserve each partial in the provider transcript and
5
- // grant at most MAX_OUTPUT_RECOVERY_LIMIT direct continuations before
6
- // surfacing a hard truncation.
4
+ // A max-output stop is not a completed assistant turn. A turn that spent its
5
+ // whole budget (typically on thinking) would repeat identically if re-sent
6
+ // unchanged, so recovery changes the request each time:
7
+ // 1. Escalation, once per progress window: re-send the same transcript with
8
+ // a higher max_tokens when the provider has one to offer (no message
9
+ // added; the cut partial is discarded and regenerated).
10
+ // 2. Up to MAX_OUTPUT_RECOVERY_LIMIT resume prompts at the normal
11
+ // max_tokens, preserving any partial text in the transcript.
12
+ // 3. Then the turn ends with a visible notice that the output limit was hit.
13
+ // Real progress (a tool-call turn, a continued text turn) resets both steps.
14
+ import { isOutputLimitStopReason } from '../termination.mjs';
15
+
7
16
  const MAX_OUTPUT_RECOVERY_LIMIT = 3;
8
- const MAX_OUTPUT_EXHAUSTED_NOTICE = `[mixdog-runtime] Output remained truncated after ${MAX_OUTPUT_RECOVERY_LIMIT} continuation attempts.`;
17
+ const MAX_OUTPUT_EXHAUSTED_NOTICE = `[mixdog-runtime] The model hit its output limit and the response stayed truncated after ${MAX_OUTPUT_RECOVERY_LIMIT} recovery attempts.`;
9
18
  const MAX_OUTPUT_RESUME_PROMPT =
10
- 'Output token limit hit. Resume directly — no apology, no recap. Pick up exactly where the previous text stopped.';
19
+ '[mixdog-runtime] Output token limit hit. Resume directly — no apology, no recap. Pick up exactly where you left off, and break the remaining work into smaller pieces.';
11
20
  // A response whose stream was cut mid-text (and could not be withdrawn from
12
21
  // the screen) resumes through the same ladder with its own reason.
13
22
  const STREAM_CUT_RESUME_PROMPT =
@@ -19,25 +28,65 @@ function resumePrompt(response, attempt) {
19
28
  : { role: 'user', content: MAX_OUTPUT_RESUME_PROMPT, meta: { source: 'max-output-recovery', attempt } };
20
29
  }
21
30
 
22
- export function resolveOutputLimit(response, { state, segments, messages }) {
23
- segments.record(response.content);
31
+ // Real progress ends the recovery window: the next cut starts a fresh ladder.
32
+ export function resetOutputLimitRecovery(state) {
33
+ state.maxOutputRecoveryCount = 0;
34
+ state.outputEscalationUsed = false;
35
+ }
36
+
37
+ // The escalation discards the cut response, so whatever it already put on the
38
+ // live stream must be withdrawn through the owner's retraction channel — the
39
+ // same one transport replays use. Text the owner cannot withdraw would be
40
+ // shown twice, so that case does not escalate.
41
+ async function withdrawStreamedText(opts, chars) {
42
+ if (!(chars > 0)) return true;
43
+ if (typeof opts?.onTextReset !== 'function') return false;
44
+ try {
45
+ return (await opts.onTextReset({ chars, reasoning: false, reason: 'output-limit-escalation' })) === true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export async function resolveOutputLimit(
52
+ response,
53
+ { state, segments, messages, opts, escalateMaxTokens, streamedChars }
54
+ ) {
55
+ const hasContent = typeof response.content === 'string' && response.content.trim().length > 0;
56
+ if (!state.outputEscalationUsed && isOutputLimitStopReason(response.stopReason ?? response.stop_reason)) {
57
+ state.outputEscalationUsed = true;
58
+ // A provider may resolve the higher limit asynchronously (route lookup);
59
+ // failing to find one only skips the escalation.
60
+ const limit = await Promise.resolve()
61
+ .then(escalateMaxTokens)
62
+ .catch(() => null);
63
+ if (limit > 0 && (await withdrawStreamedText(opts, streamedChars()))) {
64
+ // Taken off the shared options by the very next send (sendProviderRequest).
65
+ opts.maxOutputTokensOverride = limit;
66
+ return { action: 'continue', response };
67
+ }
68
+ }
69
+ if (hasContent) segments.record(response.content);
24
70
  if (state.maxOutputRecoveryCount < MAX_OUTPUT_RECOVERY_LIMIT) {
25
71
  // The partial assistant turn must be visible to the model so it can
26
72
  // resume at the exact cutoff instead of reconstructing or repeating
27
73
  // it. askSession persists this natural recovery chain;
28
74
  // historyContent below prevents the aggregate returned to callers
29
- // from being persisted a second time.
30
- segments.commitIntermediate(response);
75
+ // from being persisted a second time. An empty cut has no output to keep.
76
+ if (hasContent) segments.commitIntermediate(response);
31
77
  state.maxOutputRecoveryCount += 1;
32
78
  messages.push(resumePrompt(response, state.maxOutputRecoveryCount));
33
79
  return { action: 'continue', response };
34
80
  }
35
- const terminalSegment = `${response.content}\n\n${MAX_OUTPUT_EXHAUSTED_NOTICE}`;
81
+ const terminalSegment = hasContent
82
+ ? `${response.content}\n\n${MAX_OUTPUT_EXHAUSTED_NOTICE}`
83
+ : MAX_OUTPUT_EXHAUSTED_NOTICE;
84
+ const earlier = (hasContent ? segments.parts.slice(0, -1) : segments.parts).join('');
36
85
  return {
37
86
  action: 'break',
38
87
  response: {
39
88
  ...response,
40
- content: `${segments.parts.slice(0, -1).join('')}${terminalSegment}`,
89
+ content: `${earlier}${earlier && !hasContent ? '\n\n' : ''}${terminalSegment}`,
41
90
  historyContent: terminalSegment,
42
91
  maxOutputRecoveryAttempts: state.maxOutputRecoveryCount,
43
92
  },
@@ -14,16 +14,27 @@ import { HIDDEN_AGENT_NAMES } from './hidden-agents.mjs';
14
14
  import { INCOMPLETE_STOP_REASONS, isOutputLimitStopReason, providerContinuationSignal } from './termination.mjs';
15
15
  import { appendAgentTrace } from '../../agent-trace.mjs';
16
16
  import { createTurnSegments } from './no-tool-turn/segments.mjs';
17
- import { resolveOutputLimit } from './no-tool-turn/output-limit.mjs';
17
+ import { resetOutputLimitRecovery, resolveOutputLimit } from './no-tool-turn/output-limit.mjs';
18
18
  import { resolveRefusal } from './no-tool-turn/refusal.mjs';
19
19
  import { resumeContinuation } from './no-tool-turn/continuation.mjs';
20
20
  import { nudgeEmptyTurn } from './no-tool-turn/empty-nudge.mjs';
21
21
 
22
- export function createNoToolTurnResolver({ messages, opts, sessionId, sessionAgent, drainSteering }) {
22
+ // `escalateMaxTokens()` asks the live provider for a higher one-shot max_tokens
23
+ // (number) or reports none (null/undefined) — the loop stays provider-agnostic.
24
+ export function createNoToolTurnResolver({
25
+ messages,
26
+ opts,
27
+ sessionId,
28
+ sessionAgent,
29
+ drainSteering,
30
+ escalateMaxTokens = () => null,
31
+ streamedChars = () => 0,
32
+ }) {
23
33
  const segments = createTurnSegments({ messages, opts });
24
34
  // Per-turn recovery counters shared by the ladders.
25
35
  const state = {
26
36
  maxOutputRecoveryCount: 0,
37
+ outputEscalationUsed: false,
27
38
  refusalRetryUsed: false,
28
39
  emptyNudgeStreak: 0,
29
40
  // Structured provider continuation signals honored this turn. Diagnostic
@@ -42,7 +53,7 @@ export function createNoToolTurnResolver({ messages, opts, sessionId, sessionAge
42
53
  /* best-effort telemetry */
43
54
  }
44
55
  };
45
- const ladder = { state, segments, messages, sessionId, trace };
56
+ const ladder = { state, segments, messages, opts, sessionId, trace, escalateMaxTokens, streamedChars };
46
57
 
47
58
  // A no-tool message ends the turn. Unresolved tool failures remain visible
48
59
  // in history and can be reported directly without a synthetic continuation
@@ -66,11 +77,13 @@ export function createNoToolTurnResolver({ messages, opts, sessionId, sessionAge
66
77
  // { action: 'continue' | 'break', response }. 'continue' means the
67
78
  // recovery appended to the transcript and the loop must re-send;
68
79
  // 'break' returns the (possibly aggregated) terminal response.
69
- resolve(response, iteration) {
80
+ async resolve(response, iteration) {
70
81
  const hasContent = typeof response.content === 'string' && response.content.trim().length > 0;
71
82
  const stopReason = response.stopReason ?? response.stop_reason ?? null;
72
83
  const isOutputLimitStop = isOutputLimitStopReason(stopReason);
73
- if (hasContent && (isOutputLimitStop || response.streamCut === true)) return resolveOutputLimit(response, ladder);
84
+ if (isOutputLimitStop || (hasContent && response.streamCut === true)) {
85
+ return resolveOutputLimit(response, ladder);
86
+ }
74
87
  if (stopReason === 'refusal') return resolveRefusal(response, hasContent, ladder);
75
88
  // Output-limit stops keep the bounded max-output ladder above and
76
89
  // refusals (already returned) keep the bounded refusal retry; both
@@ -108,13 +121,16 @@ export function createNoToolTurnResolver({ messages, opts, sessionId, sessionAge
108
121
  })
109
122
  ) {
110
123
  state.emptyNudgeStreak = 0;
124
+ resetOutputLimitRecovery(state);
111
125
  return { action: 'continue', response };
112
126
  }
113
127
  return finishTurn(response);
114
128
  },
115
- // A turn that produced tool calls is never an empty contract violation.
129
+ // A turn that produced tool calls is never an empty contract violation,
130
+ // and it is real progress: a later output-limit cut starts a fresh ladder.
116
131
  noteToolCallTurn() {
117
132
  state.emptyNudgeStreak = 0;
133
+ resetOutputLimitRecovery(state);
118
134
  },
119
135
  noteToolBatchCompleted() {
120
136
  state.continuationsSinceToolBatch = 0;
@@ -186,10 +186,16 @@ export async function sendProviderRequest(state, round) {
186
186
  // included: the span the usage ledger recorded as `duration_ms` (stamped on
187
187
  // the response); wall time only for a response no ledger row timed.
188
188
  const requestStartedAt = Date.now();
189
+ // The output-limit escalation is one-shot: take it off the shared options
190
+ // before sending, so this attempt alone carries it whatever the outcome
191
+ // (retry, route fallback, thrown error, abort).
192
+ const maxOutputTokensOverride = opts.maxOutputTokensOverride;
193
+ delete opts.maxOutputTokensOverride;
189
194
  let result;
190
195
  try {
191
196
  result = await runWithProviderRequestToolsScope(round.requestToolScope, () =>
192
197
  sendWithRecovery({
198
+ maxOutputTokensOverride,
193
199
  provider,
194
200
  messages: providerMessages,
195
201
  recoveryMessages: messageSource,
@@ -384,6 +390,7 @@ export function settleSendResult(state, round, sent) {
384
390
  if (state.imageStrip) settleImageStrip(state, round, sent);
385
391
  opts.onToolCall = undefined;
386
392
  delete opts.cacheBreakIntent;
393
+ state.lastStreamedChars = sent.result.streamedChars || 0;
387
394
  state.contextOverflowRetryUsed = false;
388
395
  // A completed send ends the outage this budget was covering; the next
389
396
  // iteration is a fresh request and must get the full replay budget again.
@@ -7,12 +7,7 @@ import { runAbortable, throwIfAborted } from '../../../../../shared/abort-race.m
7
7
  import { evaluateExecPolicy } from '../../../tools/bash-policy-scan.mjs';
8
8
  import { beginSessionApprovalWait } from '../../manager/runtime-liveness.mjs';
9
9
  import { agentMcpToolPermission, writeAgentMcpToolPermission } from '../../../mcp/agent-connectors.mjs';
10
- import {
11
- actingContactBotId,
12
- humanGateDecision,
13
- recordHumanDecision,
14
- rememberSurfaceOf,
15
- } from '../../human-gate/gate.mjs';
10
+ import { actingBotId, actingContactBotId, humanGateDecision, recordHumanDecision } from '../../human-gate/gate.mjs';
16
11
  import { applyEditedArgs } from '../../human-gate/requests.mjs';
17
12
 
18
13
  const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
@@ -60,18 +55,22 @@ export const ALLOW_NOT_SAVED_NOTICE = 'Allowed this once. The setting could not
60
55
  export const CONNECTOR_APPROVAL_REASON = 'This connector tool is set to ask before it runs for this agent.';
61
56
 
62
57
  /**
63
- * A connector tool the running agent set to `needs_approval` asks through the
64
- * ordinary approval hook, whatever the PreToolUse hooks decided. Run after
65
- * them, on the final call: a renamed call is judged under its final name.
66
- * A call the outbound gate owns (its card approved it, or a remembered
67
- * outbound grant answered it) is not asked a second time.
58
+ * A connector tool a bot's own session (its home chat or room seat; a
59
+ * schedule/webhook run, which the approval hook then answers by run policy)
60
+ * runs, and the bot set to `needs_approval`, asks through the ordinary
61
+ * approval hook, whatever the PreToolUse hooks decided. Any other session (a
62
+ * coding Lead, a task session, a workflow agent, a sub-agent) runs it unasked.
63
+ * Run after the hooks, on the final call: a renamed call is judged under its
64
+ * final name. A call the outbound gate owns (its card approved it, or a
65
+ * remembered outbound grant answered it) is not asked a second time.
68
66
  * @returns {Promise<{ denial: string } | null>} the denial, or null to run
69
67
  */
70
68
  export function applyConnectorApproval(call, { agentId, callerSessionId, executeOpts, toolApprovalHook }) {
71
69
  if (call.outboundApproved) return null;
70
+ if (!actingBotId(callerSessionId)) return null;
72
71
  if (agentMcpToolPermission(agentId, call.name) !== 'needs_approval') return null;
73
72
  // "Always allow" is offered only to the contact bot that is acting, for its
74
- // own policy entry for this one tool: never another bot, Mixy or the Lead.
73
+ // own policy entry for this one tool: never another bot or Mixy.
75
74
  const botId = actingContactBotId(callerSessionId);
76
75
  const rememberable = Boolean(botId) && botId === agentId;
77
76
  return askApproval(call, {
@@ -79,14 +78,7 @@ export function applyConnectorApproval(call, { agentId, callerSessionId, execute
79
78
  executeOpts,
80
79
  toolApprovalHook,
81
80
  decision: { reason: CONNECTOR_APPROVAL_REASON },
82
- request: rememberable
83
- ? {
84
- kind: 'approval',
85
- agentId: botId,
86
- remember: ['once', 'agent'],
87
- rememberAt: rememberSurfaceOf(callerSessionId),
88
- }
89
- : null,
81
+ request: rememberable ? { kind: 'approval', agentId: botId, remember: ['once', 'agent'] } : null,
90
82
  onApproved: async (approval) => {
91
83
  if (!rememberable || approval?.remember !== 'agent') return;
92
84
  try {
@@ -7,6 +7,7 @@ import { enqueuePendingMessage, markCompletionEntry } from '../../manager/pendin
7
7
  import { markSessionToolOutputTail } from '../../manager/runtime-liveness.mjs';
8
8
  import { createScopedCacheOutcome } from '../../cache/scoped-cache-outcome.mjs';
9
9
  import { modelVisibleToolCompletionMessage } from '../../../../../shared/tool-execution-contract.mjs';
10
+ import { turnHopOf } from '../../../../../shared/bot-hops.mjs';
10
11
  import { _isScopedCacheableTool } from '../tool-classify.mjs';
11
12
  import { refreshDeferredMcpToolCatalog } from '../../../runtime-core/tool-catalog.mjs';
12
13
  import { coerceToolArgsForSession } from '../arg-schema-coerce.mjs';
@@ -84,8 +85,12 @@ export function prepareToolCall(name, args, cwd, callerSessionId, sessionRef, ex
84
85
  const notificationSessionId = resolveToolCompletionSessionId({ callerSessionId });
85
86
  const notifyFn =
86
87
  typeof executeOpts.notifyFn === 'function' ? executeOpts.notifyFn : completionNotifier(notificationSessionId);
88
+ // Background work launched in a bot-descended turn reports back at that hop:
89
+ // it rides the job's notify context, so every completion's meta carries it.
90
+ const hop = turnHopOf(sessionRef);
87
91
  const completionToolOpts = {
88
92
  ...toolOpts,
93
+ ...(hop > 0 ? { hop } : {}),
89
94
  sessionId: callerSessionId,
90
95
  agent: sessionRef?.agent || null,
91
96
  callerSessionId: notificationSessionId || callerSessionId,
@@ -17,7 +17,7 @@ import { isOnDeferredToolSurface } from '../deferred-call-through.mjs';
17
17
  import { _executeCodeGraphToolLazy } from '../../manager/runtime-loaders.mjs';
18
18
  import { requiresReadBody } from '../tool-classify.mjs';
19
19
  import { executeCursorNativeBuiltin } from '../../../tools/builtin/external-tool-adapters.mjs';
20
- import { providerNativePdf, providerReadsImages } from '../../../providers/registry.mjs';
20
+ import { providerModelVisionOverride, providerNativePdf, providerReadsImages } from '../../../providers/registry.mjs';
21
21
  import { getModelMetadataSync } from '../../../../../shared/llm/model-catalog.mjs';
22
22
 
23
23
  /**
@@ -25,16 +25,22 @@ import { getModelMetadataSync } from '../../../../../shared/llm/model-catalog.mj
25
25
  * only whether a short PDF may arrive whole as a document block. A scanned
26
26
  * page arrives as a page image whenever the model sees images — the provider
27
27
  * sends image input (only the text-only local runtime does not) and the
28
- * catalog does not say the model lacks it — and as OCR text otherwise.
28
+ * catalog does not say the model lacks it — and as OCR text otherwise. A
29
+ * custom model's own image-input setting (on or off) beats the catalog.
29
30
  */
30
31
  export function pdfReadModes(
31
32
  sessionRef,
32
- { nativePdf = providerNativePdf, readsImages = providerReadsImages, catalogRow = getModelMetadataSync } = {}
33
+ {
34
+ nativePdf = providerNativePdf,
35
+ readsImages = providerReadsImages,
36
+ catalogRow = getModelMetadataSync,
37
+ visionOverride = providerModelVisionOverride,
38
+ } = {}
33
39
  ) {
34
40
  const provider = sessionRef?.provider;
35
41
  const model = sessionRef?.model;
36
- const seesImages =
37
- readsImages(provider) && (model ? catalogRow(model, provider)?.supportsVision : undefined) !== false;
42
+ const vision = model ? (visionOverride(provider, model) ?? catalogRow(model, provider)?.supportsVision) : undefined;
43
+ const seesImages = readsImages(provider) && vision !== false;
38
44
  return { pdfPageMode: seesImages ? 'image' : 'ocr', pdfNativeDocument: nativePdf(provider) };
39
45
  }
40
46
 
@@ -0,0 +1,102 @@
1
+ // manager/agent-listing-notice.mjs
2
+ // The agents a coding Lead may delegate to, kept current without touching the
3
+ // request prefix (the workflow switch's pattern, workflow-switch-notice.mjs).
4
+ // What the conversation was told is read from the conversation itself, every
5
+ // turn: the "# Available Agents" list the system prompt carries (the one the
6
+ // session was built with: a rebuilt prompt keeps it, prompt-utils.mjs
7
+ // presentedAgentCatalogSlice), then each agent list reminder still in
8
+ // `session.messages`, in order (the user turn that carried one records its
9
+ // change as `meta.agentListing`). A turn whose message stays (answered or
10
+ // failed) counts; one rolled back or compacted away does not, so its change is
11
+ // told again. When the agents enabled at this turn's start (resolved by the
12
+ // session runtime, passed in with the turn) differ from that, the turn carries
13
+ // one reminder naming the agents added and removed.
14
+ import { agentListEntries, presentedAgentCatalogSlice, suffixUserTurnReminders } from './prompt-utils.mjs';
15
+ import { stripRuntimeUserContext, withRuntimeUserContext } from '../runtime-user-context.mjs';
16
+
17
+ /** The agent list the system prompt carries: one complete generated slice of
18
+ * the BP3 block, [] when it carries none (or none valid). */
19
+ function promptAgentList(session) {
20
+ return (session && agentListEntries(presentedAgentCatalogSlice(session) || '')) || [];
21
+ }
22
+
23
+ const ref = (entry) => ({ id: String(entry.id), name: String(entry.name) });
24
+
25
+ /** The agents the conversation was told about: the prompt's list, then every
26
+ * agent list reminder the conversation still holds, in order. */
27
+ export function announcedAgents(session) {
28
+ const told = new Map(promptAgentList(session).map((entry) => [entry.id, ref(entry)]));
29
+ for (const message of Array.isArray(session?.messages) ? session.messages : []) {
30
+ const change = message?.role === 'user' ? message.meta?.agentListing : null;
31
+ if (!change) continue;
32
+ for (const entry of Array.isArray(change.removed) ? change.removed : []) told.delete(entry?.id);
33
+ for (const entry of Array.isArray(change.added) ? change.added : []) {
34
+ if (entry?.id) told.set(entry.id, ref(entry));
35
+ }
36
+ }
37
+ return [...told.values()];
38
+ }
39
+
40
+ function agentListingContent(added, removed, current) {
41
+ return [
42
+ '<system-reminder>',
43
+ '# Available Agents Changed',
44
+ 'This updates the Available Agents list in the system prompt and every earlier Available Agents Changed reminder.',
45
+ ...(added.length
46
+ ? ['New agents are now available to delegate to with `agent`:', ...added.map((entry) => entry.line)]
47
+ : []),
48
+ ...(removed.length
49
+ ? [
50
+ 'These agents are no longer available; an `agent` call naming one is refused:',
51
+ ...removed.map((entry) => `- ${entry.name} (${entry.id})`),
52
+ ]
53
+ : []),
54
+ ...(current.length ? [] : ['No agents are enabled now: do the work yourself until one is enabled.']),
55
+ '</system-reminder>',
56
+ ].join('\n');
57
+ }
58
+
59
+ /**
60
+ * The agent list reminder this user turn carries, or null when the
61
+ * conversation already knows `current`, the agents enabled at this turn's
62
+ * start (`agentListEntries` entries, from the session runtime's ask options;
63
+ * absent for agent-mode threads and delegated workers: no reminder).
64
+ * @returns {{ content: string, meta: { added: object[], removed: object[] } } | null}
65
+ */
66
+ export function buildAgentListingNotice(session, current) {
67
+ if (!Array.isArray(current)) return null;
68
+ const announced = announcedAgents(session);
69
+ const now = new Set(current.map((entry) => entry.id));
70
+ const told = new Set(announced.map((entry) => entry.id));
71
+ const added = current.filter((entry) => !told.has(entry.id));
72
+ const removed = announced.filter((entry) => !now.has(entry.id));
73
+ if (!added.length && !removed.length) return null;
74
+ return {
75
+ content: agentListingContent(added, removed, current),
76
+ meta: { added: added.map(ref), removed: removed.map(ref) },
77
+ };
78
+ }
79
+
80
+ /**
81
+ * A retained failed turn resubmitted as it was (its own message, its input
82
+ * untouched) learns what changed since: the reminder joins its runtime
83
+ * context, and `meta.agentListing` becomes the net change against what the
84
+ * conversation was told before that message, so later turns read it back as a
85
+ * normal turn's. `index` is the message's place in `session.messages`.
86
+ */
87
+ export function withAgentListingNotice(session, index, message, current) {
88
+ const notice = buildAgentListingNotice(session, current);
89
+ if (!notice) return message;
90
+ const before = { ...session, messages: session.messages.filter((_, i) => i !== index) };
91
+ const net = buildAgentListingNotice(before, current);
92
+ const { agentListing: _told, ...meta } = message.meta || {};
93
+ const stripped = stripRuntimeUserContext({ ...message, meta });
94
+ const owned = stripped.meta?.runtimeUserContext ? null : message.meta?.runtimeUserContext;
95
+ return withRuntimeUserContext(
96
+ { ...stripped, meta: { ...stripped.meta, ...(net ? { agentListing: net.meta } : {}) } },
97
+ {
98
+ prefix: owned?.prefix || '',
99
+ suffix: `${owned?.suffix || ''}${suffixUserTurnReminders('', notice.content)}`,
100
+ }
101
+ );
102
+ }
@@ -34,6 +34,7 @@ const promptSourceFields = (source, execution) => {
34
34
  const tailTurnFor = (group) => ({
35
35
  content: group.content,
36
36
  entries: group.entries,
37
+ ...(group.hop ? { hop: group.hop } : {}),
37
38
  ...(group.mode === PENDING_MODE_TASK_NOTIFICATION
38
39
  ? promptSourceFields(PENDING_MODE_TASK_NOTIFICATION, group.execution)
39
40
  : {}),
@@ -46,6 +47,7 @@ const queuedTurnInput = (tailTurn) => ({
46
47
  context: null,
47
48
  explicitPrefetch: null,
48
49
  pendingEntries: tailTurn.entries,
50
+ ...(tailTurn.hop ? { hop: tailTurn.hop } : {}),
49
51
  promptSource: promptSourceFields(tailTurn.source, tailTurn.execution),
50
52
  });
51
53
 
@@ -84,7 +84,19 @@ async function runAskTurn({ sessionId, opened, input, askOpts, onToolCall, cwdOv
84
84
  cwdOverride || opened.turn.session.cwd || process.cwd(),
85
85
  opened.runtime.controller?.signal
86
86
  );
87
- const prepared = await prepareAskTurn({ sessionId, opened, input, cwdOverride, transcriptMeta: transcript.user });
87
+ const prepared = await prepareAskTurn({
88
+ sessionId,
89
+ opened,
90
+ input,
91
+ cwdOverride,
92
+ // A task notification resumes the bot-chain hop of the turn that launched its work.
93
+ transcriptMeta: input.hop ? { ...transcript.user, hop: input.hop } : transcript.user,
94
+ // The agents enabled as this user turn is prepared (the session
95
+ // runtime's resolver; coding Leads): the agent list reminder compares
96
+ // them with what the model was told. Asked again for every user turn
97
+ // the ask runs, queued and background follow-ups included.
98
+ currentAgents: askOpts?.agentsNow?.(),
99
+ });
88
100
  const result = await runAskAgentLoop({
89
101
  sessionId,
90
102
  opened,
@@ -15,6 +15,7 @@ import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
15
15
  import { _getAgentLoop } from './runtime-loaders.mjs';
16
16
  import { runAbortable, throwIfAborted } from '../../../../shared/abort-race.mjs';
17
17
  import { applyTurnContextMeta, buildUserTurn, startProviderPrewarm, traceTurnContext } from './ask-turn-prepare.mjs';
18
+ import { withAgentListingNotice } from './agent-listing-notice.mjs';
18
19
  import { retainedFailedTurnInput, rewindUnansweredPrompt } from './failed-turn-rewind.mjs';
19
20
  import { buildAgentLoopOptions, trackedToolCallHook } from './ask-turn-loop-options.mjs';
20
21
  import { _api_call_with_interrupt } from './ask-call-interrupt.mjs';
@@ -23,11 +24,11 @@ import { stampTurnHop } from '../../../../shared/bot-hops.mjs';
23
24
 
24
25
  /** Resolves the provider, builds the user turn and pre-commits it; returns
25
26
  * what the loop and the commit need from this preparation. */
26
- export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, transcriptMeta }) {
27
+ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, transcriptMeta, currentAgents }) {
27
28
  const { turn, runtime, turnSignal, askGeneration, checkpoint, turnToken, startedAt, codexTurnId } = opened;
28
29
  const session = turn.session;
29
30
  // Where the turn sits in a bot-to-bot chain (session-runtime/bot-hops.mjs).
30
- stampTurnHop(session, transcriptMeta, input.promptSource);
31
+ stampTurnHop(session, transcriptMeta);
31
32
  const retained = input.retryTurnId
32
33
  ? retainedFailedTurnInput(session.messages, input.retryTurnId, input.retryAttemptId)
33
34
  : null;
@@ -71,21 +72,28 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
71
72
  ? (() => {
72
73
  const historyMessages = filterModelVisibleSessionMessages(session.messages.slice(0, retained.index));
73
74
  const { completion, ...previousTranscript } = retained.message.meta.transcript;
74
- const userTurnMessage = {
75
- ...retained.message,
76
- meta: {
77
- ...retained.message.meta,
78
- transcript: {
79
- ...previousTranscript,
80
- ...transcriptMeta,
81
- messageId: previousTranscript.messageId,
82
- previousCompletions: [
83
- ...(previousTranscript.previousCompletions || []),
84
- { ...completion, at: completion.at ?? previousTranscript.at },
85
- ],
75
+ // The retained message keeps its input; agents enabled or switched
76
+ // off since it was built ride its runtime context.
77
+ const userTurnMessage = withAgentListingNotice(
78
+ session,
79
+ retained.index,
80
+ {
81
+ ...retained.message,
82
+ meta: {
83
+ ...retained.message.meta,
84
+ transcript: {
85
+ ...previousTranscript,
86
+ ...transcriptMeta,
87
+ messageId: previousTranscript.messageId,
88
+ previousCompletions: [
89
+ ...(previousTranscript.previousCompletions || []),
90
+ { ...completion, at: completion.at ?? previousTranscript.at },
91
+ ],
92
+ },
86
93
  },
87
94
  },
88
- };
95
+ currentAgents
96
+ );
89
97
  return { historyMessages, userTurnMessage, outgoing: [...historyMessages, userTurnMessage] };
90
98
  })()
91
99
  : buildUserTurn({
@@ -97,6 +105,7 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
97
105
  transcriptMeta,
98
106
  turnPromptSource: input.promptSource,
99
107
  effortConfiguration: null,
108
+ currentAgents,
100
109
  }),
101
110
  wireRoutePending,
102
111
  ]);
@@ -21,6 +21,7 @@ import {
21
21
  } from './prompt-utils.mjs';
22
22
  import { snapshotPendingWorkflowNotice } from './workflow-switch-notice.mjs';
23
23
  import { snapshotPendingOrchestrationModeNotice } from './orchestration-mode-switch-notice.mjs';
24
+ import { buildAgentListingNotice } from './agent-listing-notice.mjs';
24
25
  import { codexWireSendOpts } from './session-id.mjs';
25
26
  import { _tryBridgeExplicitPrefetch } from './prefetch-bridge.mjs';
26
27
  import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
@@ -150,6 +151,7 @@ export async function buildUserTurn({
150
151
  transcriptMeta,
151
152
  turnPromptSource,
152
153
  effortConfiguration,
154
+ currentAgents,
153
155
  }) {
154
156
  const explicitPrefetchResult = await _tryBridgeExplicitPrefetch(session, explicitPrefetch, turnSignal);
155
157
  let contextBlock = '';
@@ -188,9 +190,12 @@ export async function buildUserTurn({
188
190
  // A mid-conversation orchestration mode switch, after the workflow notice
189
191
  // so the mode it names wins over the one a workflow reminder carries.
190
192
  const orchestrationModeNotice = snapshotPendingOrchestrationModeNotice(session);
193
+ // Agents enabled or switched off since the conversation was last told.
194
+ const agentListingNotice = buildAgentListingNotice(session, currentAgents);
191
195
  const reminderBlock = [
192
196
  workflowNotice?.content || '',
193
197
  orchestrationModeNotice?.content || '',
198
+ agentListingNotice?.content || '',
194
199
  workspaceNotice?.content || '',
195
200
  setupNotice?.content || '',
196
201
  currentTimeBlock ? `<system-reminder>\n# Current Time\n${currentTimeBlock}\n</system-reminder>` : '',
@@ -205,6 +210,8 @@ export async function buildUserTurn({
205
210
  ...(transcriptMeta ? { transcript: transcriptMeta } : {}),
206
211
  ...(turnPromptSource || {}),
207
212
  ...(effortConfiguration ? { effortConfiguration } : {}),
213
+ // What its reminder told, read back by later turns (agent-listing-notice.mjs).
214
+ ...(agentListingNotice ? { agentListing: agentListingNotice.meta } : {}),
208
215
  };
209
216
  const userTurnMessage = withRuntimeUserContext(
210
217
  {
@@ -221,6 +228,7 @@ export async function buildUserTurn({
221
228
  goalReminder,
222
229
  workflowNotice,
223
230
  orchestrationModeNotice,
231
+ agentListingNotice,
224
232
  workspaceNotice,
225
233
  setupNotice,
226
234
  userTurnMessage,
@@ -59,6 +59,7 @@ export function createAskSteeringDrain({ sessionId, turn, askOpts }) {
59
59
  ids: group.ids,
60
60
  mode: group.mode,
61
61
  ...(group.execution ? { execution: group.execution } : {}),
62
+ ...(group.hop ? { transcriptMeta: { hop: group.hop } } : {}),
62
63
  });
63
64
  }
64
65
  } else {