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,7 +58,15 @@ function strictPositiveInt(value, label) {
58
58
  return value;
59
59
  }
60
60
 
61
- const OVERRIDE_KEYS = new Set(['manual', 'hidden', 'name', 'contextWindow', 'maxOutputTokens', 'reasoning']);
61
+ const OVERRIDE_KEYS = new Set([
62
+ 'manual',
63
+ 'hidden',
64
+ 'name',
65
+ 'contextWindow',
66
+ 'maxOutputTokens',
67
+ 'supportsVision',
68
+ 'reasoning',
69
+ ]);
62
70
  const REASONING_KEYS = new Set(['levels', 'wireFormat', 'wireValues', 'replay']);
63
71
  const DISCOVERY_KEYS = new Set(['newModelsHidden']);
64
72
  // v2 config fields, plus the transport fields callers pass alongside them and
@@ -220,6 +228,9 @@ function normalizeOverride(id, raw, protocol, legacy) {
220
228
  if (contextWindow) out.contextWindow = contextWindow;
221
229
  const maxOutputTokens = positiveInt(raw.maxOutputTokens, `${label}.maxOutputTokens`);
222
230
  if (maxOutputTokens) out.maxOutputTokens = maxOutputTokens;
231
+ // Tri-state image input: absent = auto (catalog / default), true / false = the user's word.
232
+ const supportsVision = optionalBoolean(raw.supportsVision, `${label}.supportsVision`);
233
+ if (supportsVision !== undefined) out.supportsVision = supportsVision;
223
234
  if (raw.reasoning !== undefined) {
224
235
  const reasoning = normalizeReasoning(raw.reasoning, `${label}.reasoning`, protocol);
225
236
  if (Object.keys(reasoning).length) out.reasoning = reasoning;
@@ -420,6 +431,8 @@ function effectiveRow(base, override, { providerId, discovered, newModelsHidden
420
431
  outputTokens: ov.maxOutputTokens ?? (Number(base.outputTokens) > 0 ? Number(base.outputTokens) : null),
421
432
  hidden,
422
433
  manual: ov.manual === true,
434
+ // An explicit override (false included) beats the catalog; unset keeps the base.
435
+ ...(typeof ov.supportsVision === 'boolean' ? { supportsVision: ov.supportsVision } : {}),
423
436
  supportsReasoning: levels ? levels.some((level) => level !== 'none') : base.supportsReasoning === true,
424
437
  ...reasoningLevelFields(levels),
425
438
  customReasoning: { wireFormat: reasoning.wireFormat ?? 'standard', wireValues, replay },
@@ -3,7 +3,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
3
3
  // Grok CLI client version for the cli-chat-proxy version gate (HTTP 426).
4
4
  // Effective = env override, else max(floor, learned-from-426, live npm latest
5
5
  // of the official @xai-official/grok CLI).
6
- export const GROK_CLI_VERSION_FLOOR = '1.0.62';
6
+ export const GROK_CLI_VERSION_FLOOR = '1.0.67';
7
7
  export const GROK_CLI_NPM_PACKAGE = '@xai-official/grok';
8
8
 
9
9
  const live = createNpmVersionSource(GROK_CLI_NPM_PACKAGE, { persistKey: 'grok-cli' });
@@ -518,6 +518,16 @@ export function providerNativePdf(name) {
518
518
  export function providerReadsImages(name) {
519
519
  return providers.get(String(name || '').toLowerCase())?.takesImages !== false;
520
520
  }
521
+ /**
522
+ * The user's image-input override for one model of a custom provider: true or
523
+ * false as set, undefined when unset (auto) or the provider is not a loaded
524
+ * custom one. A pure lookup; it never instantiates a provider.
525
+ */
526
+ export function providerModelVisionOverride(name, model) {
527
+ const overrides = providers.get(String(name || '').toLowerCase())?.customConfig?.modelOverrides;
528
+ const value = overrides && Object.hasOwn(overrides, model) ? overrides[model]?.supportsVision : undefined;
529
+ return typeof value === 'boolean' ? value : undefined;
530
+ }
521
531
  /**
522
532
  * Whether the named provider's adapter declares `consumesReadWindows`: it
523
533
  * needs the structured clip report of read results. A pure lookup; a proxy
@@ -9,7 +9,7 @@ import { OpenAICompatProvider, OPENAI_COMPAT_PRESETS } from './openai-compat.mjs
9
9
  import { originBoundFetch } from './lib/origin-bound-fetch.mjs';
10
10
  import { withInclusiveUsage } from './lib/inclusive-usage.mjs';
11
11
  import { getModelMetadataSync } from './model-catalog.mjs';
12
- import { resolveWireRoute, sendOnRoute } from './wire-route.mjs';
12
+ import { resolveWireRoute, sendOnRoute, sendWireRoute } from './wire-route.mjs';
13
13
 
14
14
  // A Files API file-ID image anywhere in the history (tool results nest content).
15
15
  function hasFileIdImage(value, depth = 0) {
@@ -66,6 +66,10 @@ export class GatewayMessagesEngine extends AnthropicProvider {
66
66
  _sendOnce(messages, model, tools, opts) {
67
67
  return super._sendOnce(messages, model, tools, gatewayMessagesOpts(this.config, model, messages, opts));
68
68
  }
69
+ // Same ceiling a send resolves, so a retry never asks above the gateway's limit.
70
+ escalatedMaxTokens(model, opts) {
71
+ return super.escalatedMaxTokens(model, gatewayMessagesOpts(this.config, model, [], opts));
72
+ }
69
73
  _createClient(apiKey) {
70
74
  const client = super._createClient(apiKey);
71
75
  if (this.config.authScheme === 'bearer') {
@@ -138,6 +142,14 @@ export class WireGatewayProvider {
138
142
  });
139
143
  }
140
144
 
145
+ // A higher one-shot max_tokens exists only on the Messages wire; the
146
+ // route is the same one the send itself resolves. Other wires: null.
147
+ async escalatedMaxTokens(model, opts, tools) {
148
+ const useModel = model || this.openai.defaultModel;
149
+ const route = await sendWireRoute(this, useModel, tools, opts);
150
+ return route?.wire === 'messages' ? this.anthropic.escalatedMaxTokens(useModel, opts) : null;
151
+ }
152
+
141
153
  reloadApiKey() {
142
154
  this.openai.reloadApiKey();
143
155
  this.anthropic.reloadApiKey();
@@ -65,6 +65,10 @@ export function createGoalRuntime({
65
65
  // context still owes, from the rules and state already delivered into it.
66
66
  // In memory on purpose: a restart is a fresh context that needs them again.
67
67
  continuationTiers: new Map(),
68
+ // Session -> Goal id whose turn ended in a stop (failed, cancelled, usage
69
+ // limit) the record could not save. The record still reads active, so
70
+ // without this a full disk would start turn after turn into the same error.
71
+ unsavedStops: new Map(),
68
72
  closed: false,
69
73
  };
70
74
  Object.assign(ctx, createGoalStore(ctx));
@@ -116,6 +120,7 @@ export function createGoalRuntime({
116
120
  ctx.observedGoals.clear();
117
121
  ctx.idleReviewTurns.clear();
118
122
  ctx.continuationTiers.clear();
123
+ ctx.unsavedStops.clear();
119
124
  listeners.clear();
120
125
  // Accepted writes keep their ordering until they settle. Callers may
121
126
  // await this barrier before releasing the session's backing resources.
@@ -114,6 +114,7 @@ export function createGoalTurnLifecycle(ctx) {
114
114
  turnStartedAt,
115
115
  idleReviewTurns,
116
116
  continuationTiers,
117
+ unsavedStops,
117
118
  withMutation,
118
119
  readRecord,
119
120
  commit,
@@ -163,7 +164,14 @@ export function createGoalTurnLifecycle(ctx) {
163
164
  applyTurnOutcome(goal, detail, at);
164
165
  }
165
166
  goal.updatedAt = at;
166
- const published = await commit(id, goal);
167
+ let published;
168
+ try {
169
+ published = await commit(id, goal);
170
+ } catch (error) {
171
+ if (goal.status !== 'active') unsavedStops.set(id, goal.id);
172
+ throw error;
173
+ }
174
+ unsavedStops.delete(id);
167
175
  if (automaticTurnWithoutActivity(detail, published)) idleReviewTurns.set(id, idleReviewKey(published));
168
176
  return published;
169
177
  });
@@ -189,6 +197,7 @@ export function createGoalTurnLifecycle(ctx) {
189
197
  if (goal?.status !== 'active') return { run: false, reason: goal?.status || 'missing', goal };
190
198
  if (runningAgentWork(agentStatus)) return { run: false, reason: 'agent-running', goal };
191
199
  const id = assertSessionId(sessionId);
200
+ if (unsavedStops.get(id) === goal.id) return { run: false, reason: 'unsaved-stop', goal };
192
201
  const answered = idleReviewTurns.get(id) === idleReviewKey(goal);
193
202
  if (settledDurationWait(goal)) {
194
203
  // The model already answered this exact list with no new work, so the
@@ -622,6 +622,18 @@ function reconcileStateMatches(a, b) {
622
622
  return true;
623
623
  }
624
624
 
625
+ // Tools whose schema the session itself keeps exposed: the active surface and
626
+ // the callable/discovered state. Session fields, so a compaction keeps them;
627
+ // discoveries known only from message history (native tool references) are
628
+ // not here, since compaction can remove that history.
629
+ function persistentToolSurfaceNames(session) {
630
+ return new Set([
631
+ ...(session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean),
632
+ ...parseToolSelection(session.deferredCallableTools),
633
+ ...parseToolSelection(session.deferredDiscoveredTools),
634
+ ]);
635
+ }
636
+
625
637
  function reconcileDeferredMcpToolCatalogUncached(session, liveMcpTools) {
626
638
  const live = filterDisallowedTools(Array.isArray(liveMcpTools) ? liveMcpTools : [], session.disallowedTools);
627
639
  const hadSnapshot = Array.isArray(session.deferredMcpToolNames);
@@ -660,11 +672,7 @@ function reconcileDeferredMcpToolCatalogUncached(session, liveMcpTools) {
660
672
  const knownBefore = isToolBridgeEnabled(session)
661
673
  ? new Map(deferredCatalogUnion(session).map((tool) => [clean(tool?.name), tool]))
662
674
  : null;
663
- const active = new Set([
664
- ...(session.tools || []).map((tool) => clean(tool?.name)).filter(Boolean),
665
- ...parseToolSelection(session.deferredCallableTools),
666
- ...parseToolSelection(session.deferredDiscoveredTools),
667
- ]);
675
+ const active = persistentToolSurfaceNames(session);
668
676
 
669
677
  const liveMcpByName = new Map();
670
678
  for (const tool of live) {
@@ -723,6 +731,33 @@ function reconcileDeferredMcpToolCatalogUncached(session, liveMcpTools) {
723
731
  return [...added.map((entry) => entry.name), ...removed];
724
732
  }
725
733
 
734
+ /**
735
+ * Compaction strips the delta reminder from older user turns and the boot
736
+ * manifest in the system prompt is not rebuilt, so a late MCP tool announced
737
+ * before a compaction would stay invisible. Re-arm it, exactly as compaction
738
+ * re-marks the Goal reminder (markPendingGoalReminder): every currently
739
+ * connected tool the boot manifest never listed and the session has not loaded
740
+ * or discovered rides the next real prompt as one more delta, acknowledged
741
+ * only once the provider accepts that turn.
742
+ * Returns the re-armed names, or null when there are none.
743
+ */
744
+ export function rearmLateDeferredTools(session) {
745
+ if (!session || !Array.isArray(session.tools) || fixedSurfaceSession(session)) return null;
746
+ if (!Array.isArray(session.deferredMcpToolNames) || !Array.isArray(session.deferredAnnouncedTools)) return null;
747
+ // Listed in the boot manifest, or already loaded/discovered: still visible.
748
+ const listed = new Set([...session.deferredAnnouncedTools, ...persistentToolSurfaceNames(session)]);
749
+ const connected = new Set(session.deferredMcpToolNames);
750
+ const added = [];
751
+ for (const tool of Array.isArray(session.deferredLateToolCatalog) ? session.deferredLateToolCatalog : []) {
752
+ const name = clean(tool?.name);
753
+ if (!name || !connected.has(name) || listed.has(name) || !rosterPermits(session, tool)) continue;
754
+ added.push({ name, description: lateAnnouncementDescription(tool?.description) });
755
+ }
756
+ if (!added.length) return null;
757
+ mergePendingDeferredToolDelta(session, { added });
758
+ return added.map((entry) => entry.name);
759
+ }
760
+
726
761
  export function scopedMcpToolsFor(session, config = null) {
727
762
  return filterMcpToolsForSession(getMcpTools(session?.mcpScopeId), session?.cwd || null, config);
728
763
  }
@@ -73,9 +73,9 @@ export const SESSION_TOOL = {
73
73
  openWorldHint: false,
74
74
  agentHidden: true,
75
75
  },
76
- trigger: 'Session lookup/control, bot status, agent task sessions, title, compact, fork, Project and worktrees.',
76
+ trigger: 'Session lookup/control, bot status, agent task sessions and their initial location, title, compact, fork, Project.',
77
77
  description:
78
- 'Inspect sessions or control another by exact ID, never a guessed one. send acknowledges queue intake, not completion: promise a report of it only when its result has reportsBack: true. create returns status: started — started, not done yet; it reports its final answer, after the agents it started finish, unless its result says reportsBack: false. reportsBack: false carries reportsBackReason, the reason to tell the user. Cancel/delete and worktree actions require the user’s request. Other actions affect only the current session. Pass files on with attachments; a reported turn returns the files it produced as attachments.',
78
+ 'Inspect sessions or control another by exact ID, never a guessed one. send acknowledges queue intake, not completion: promise a report of it only when its result has reportsBack: true. create returns status: started — started, not done yet; it reports its final answer, after the agents it started finish, unless its result says reportsBack: false. reportsBack: false carries reportsBackReason, the reason to tell the user. Cancel/delete require the user’s request. Other actions affect only the current session. A new session’s initial location (Project folder branch or its own worktree) is set here, with create’s worktree; worktree operations on existing sessions (enter, return, merge, remove, list, prune) use the git tool’s action:"worktree". Pass files on with attachments; a reported turn returns the files it produced as attachments.',
79
79
  inputSchema: {
80
80
  type: 'object',
81
81
  properties: {
@@ -97,13 +97,9 @@ export const SESSION_TOOL = {
97
97
  'list-projects',
98
98
  'favorite',
99
99
  'archive',
100
- 'merge-worktree',
101
- 'remove-worktree',
102
- 'list-worktrees',
103
- 'prune-worktrees',
104
100
  ],
105
101
  description:
106
- 'status defaults to current; list pages sessions; bots shows each bot’s activity, work, approvals, routines and attention flags, or with session_id (a bot id or its chat) its routines and recent sessions; read returns latest items; send queues prompt (reportsBack: the turn result reports back here); cancel stops the turn; delete removes the session. create (bot conversations) starts a visible agent task session from brief (started, not done yet); unknown Project: list-projects first, still ambiguous: ask the user. compact runs after this turn; fork copies it without switching. merge-worktree merges into the Project’s checked-out branch; remove-worktree returns to the Project (repeat on WORKTREE_REMOVAL_INCOMPLETE); list-worktrees includes retaining sessions; prune-worktrees removes only clean, merged, unretained worktrees.',
102
+ 'status defaults to current; list pages sessions, without sub-agent or archived ones unless include_agents/include_archived; bots shows each bot’s activity, work, approvals, routines and attention flags, or with session_id (a bot id or its chat) its routines and recent sessions; read returns latest items; send queues prompt (reportsBack: the turn result reports back here); cancel stops the turn; delete removes the session. create (bot conversations) starts a visible agent task session from brief (started, not done yet); unknown Project: list-projects first, still ambiguous: ask the user. compact runs after this turn; fork copies it without switching.',
107
103
  },
108
104
  title: { type: 'string', description: 'rename: new current-session title; create: task title.' },
109
105
  session_id: {
@@ -112,6 +108,11 @@ export const SESSION_TOOL = {
112
108
  'ID from list; required for read/send/cancel/delete, optional for status/bots. send/cancel/delete cannot target the caller.',
113
109
  },
114
110
  prompt: { type: 'string', description: 'send: message to enqueue; loads a cold session.' },
111
+ to: {
112
+ type: 'string',
113
+ description:
114
+ 'send to a group room (sess_room_*): the member bot it is for, by agent id or room name (or start prompt with @name); only that member answers. A member session (sess_rmem_*) is never a send target.',
115
+ },
115
116
  limit: {
116
117
  type: 'integer',
117
118
  minimum: 1,
@@ -120,6 +121,14 @@ export const SESSION_TOOL = {
120
121
  'list/read: maximum rows, default 25; bots: sessions, default 10. Output: 8KiB per page, 12KiB for initial read tail; follow nextOffset/nextTextOffset for more.',
121
122
  },
122
123
  cursor: { type: 'string', description: 'list: returned nextCursor.' },
124
+ include_agents: {
125
+ type: 'boolean',
126
+ description: 'list: also sub-agent sessions, default false; your own agents: agent list/status.',
127
+ },
128
+ include_archived: {
129
+ type: 'boolean',
130
+ description: 'list: also archived sessions (archived: true), default false.',
131
+ },
123
132
  offset: {
124
133
  type: 'integer',
125
134
  minimum: 0,
@@ -141,13 +150,25 @@ export const SESSION_TOOL = {
141
150
  'Existing directory; implies set-project. Resolve names with list-projects first. Shell cd does not switch Project.',
142
151
  },
143
152
  compact: { type: 'boolean', description: 'fork: compact the copy.' },
144
- force: {
145
- type: 'boolean',
146
- description: 'remove-worktree: allow uncommitted/unmerged work only when explicitly requested.',
147
- },
148
153
  value: { type: 'boolean', description: 'favorite/archive: on or off.' },
149
154
  brief: { type: 'string', description: 'create: self-contained instruction, the first message.' },
150
155
  project: { type: 'string', description: 'create: registered Project name or existing directory.' },
156
+ worktree: {
157
+ type: 'object',
158
+ description:
159
+ 'create: where the new session starts, never carrying uncommitted changes. {mode:"branch"}: the Project folder on its current branch. {mode:"worktree", name?}: its own new managed worktree (name: letters, digits, ".", "_", "-"; default from the title); a non-git folder or unregistered Project is an error. Omitted: the user’s New task default. The brief is sent only once the session is placed.',
160
+ properties: {
161
+ mode: { type: 'string', enum: ['branch', 'worktree'] },
162
+ name: { type: 'string', maxLength: 64, description: 'mode worktree: worktree name.' },
163
+ },
164
+ required: ['mode'],
165
+ additionalProperties: false,
166
+ },
167
+ confirm_large: {
168
+ type: 'boolean',
169
+ description:
170
+ 'create: only after the user confirmed the large-repository warning (WORKTREE_LARGE_REPO_CONFIRM, with its size) for the worktree.',
171
+ },
151
172
  model: {
152
173
  type: 'string',
153
174
  description: 'create: provider/model or preset; default is the Lead model (else the last-used one).',
@@ -64,10 +64,13 @@ export function toSessionWorkflowMeta(workflow) {
64
64
  }
65
65
 
66
66
  // Older sessions stored delegation policy here. Current summaries retain the
67
- // effective capability so an empty agent catalog still removes the tool.
68
- export function workflowDisallowsAgentTool(workflow) {
67
+ // effective capability: an empty agent catalog removes the tool from an
68
+ // agent-mode Lead only. A coding Lead keeps `agent` whichever agents exist;
69
+ // the agents it may delegate to reach it as a listing reminder
70
+ // (manager/agent-listing-notice.mjs) and a spawn of one switched off is refused.
71
+ export function workflowDisallowsAgentTool(workflow, { agentMode = false } = {}) {
69
72
  if (!workflow || typeof workflow !== 'object') return false;
70
- if (workflow.delegatesAgents === false) return true;
73
+ if (agentMode && workflow.delegatesAgents === false) return true;
71
74
  return Boolean(workflow.agentsConfigured === true && Array.isArray(workflow.agents) && workflow.agents.length === 0);
72
75
  }
73
76
 
@@ -61,7 +61,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
61
61
  // owns a bounded recovery ladder in ./loop/no-tool-turn.mjs, which either
62
62
  // appended a recovery turn ('continue') or produced the terminal response.
63
63
  if (!state.response.toolCalls?.length) {
64
- const outcome = state.noToolTurn.resolve(state.response, state.iterations);
64
+ const outcome = await state.noToolTurn.resolve(state.response, state.iterations);
65
65
  state.response = outcome.response;
66
66
  if (outcome.action === 'continue') continue;
67
67
  break;
@@ -9,6 +9,11 @@ export interface RoomRow {
9
9
  to?: string[] | string;
10
10
  toName?: string[] | string;
11
11
  failed?: boolean;
12
+ /** A user-kind row another session sent: its session, the sending bot
13
+ * (none for the user's Lead) and the name it is shown under. */
14
+ fromSession?: string;
15
+ fromAgent?: string;
16
+ fromName?: string;
12
17
  limit?: 'rounds' | 'messages';
13
18
  rounds?: number;
14
19
  messages?: number;
@@ -93,6 +98,7 @@ export function parseRoomAsk(
93
98
  text: unknown,
94
99
  members?: readonly RoomMemberLabel[]
95
100
  ): { to: string[]; text: string; error?: 'member' | 'message' };
101
+ export function roomMemberNamed(value: unknown, members?: readonly RoomMemberLabel[]): string;
96
102
  export function roomMentionOf(text: unknown, members?: readonly RoomMemberLabel[], selfId?: string): string;
97
103
  export function roomRowTargets(row?: Partial<RoomRow>): RoomMemberLabel[];
98
104
  export function isPassReply(text: unknown): boolean;
@@ -139,6 +145,8 @@ export function runRoomExchange(options: {
139
145
  /** The user's newest message has no answer yet (default true): until a
140
146
  * member speaks, a step must pick. */
141
147
  waiting?: boolean;
148
+ /** The called members (`to`) answer and nobody else follows. */
149
+ only?: boolean;
142
150
  }): Promise<RoomExchangeResult>;
143
151
  export function roomDataLabel(value: unknown): string;
144
152
  export function roomDataLines(text: unknown): string[];
@@ -165,6 +173,10 @@ export function roomTurnPrompt(options: {
165
173
  scene?: RoomScene;
166
174
  /** The user's newest message still has no answer (default: it is the newest row). */
167
175
  waiting?: boolean;
176
+ /** The sender of the message this member was asked from another session. */
177
+ askedBy?: string;
178
+ /** The member's own reported work it relays to the room this turn. */
179
+ relay?: string;
168
180
  }): string | RoomPromptPart[];
169
181
  export function normalizeRoomRow(row?: Partial<RoomRow> & Record<string, unknown>): RoomRow;
170
182
  export function roomRowMessage(row: Partial<RoomRow>): {
@@ -179,3 +191,4 @@ export function roomRowMessage(row: Partial<RoomRow>): {
179
191
  export function isRoomRowMessage(message: unknown): boolean;
180
192
  export function roomRowItem(message: unknown, id?: unknown): Record<string, unknown>;
181
193
  export function roomRowsOfItems(items: unknown): RoomRow[];
194
+ export function roomRowSender(room: unknown): { fromAgent?: string; sender?: 'lead' };
@@ -12,6 +12,7 @@
12
12
  // content to the members the row reaches, once each.
13
13
  import { escapeHarnessTags } from '../../../shared/shell-output-summary.mjs';
14
14
  import { normalizeMessageAttachments, transcriptAttachmentFields } from '../../../shared/transcript-metadata.mjs';
15
+ import { AGENT_MESSAGE_NOTE } from '../../../shared/agent-message-envelope.mjs';
15
16
  import { agentHomeSessionId } from './agent-home.mjs';
16
17
 
17
18
  const ROOM_PREFIX = 'sess_room_';
@@ -41,6 +42,7 @@ const TOOL_SUMMARY_LIMIT = 400;
41
42
  const ATTACHMENT_REF = /^[a-f0-9]{64}$/;
42
43
  const PART_TYPES = new Set(['image', 'file', 'text']);
43
44
  const ATTACHMENT_LIMIT = 64;
45
+ const SENDER_SESSION = /^[A-Za-z0-9_.:-]{1,200}$/;
44
46
 
45
47
  export function isRoomId(value) {
46
48
  return typeof value === 'string' && ROOM_ID.test(value);
@@ -138,6 +140,13 @@ function memberAt(text, members, boundary) {
138
140
  return tie ? null : best;
139
141
  }
140
142
 
143
+ /** The member `value` names exactly (its id or display name, any case); ''
144
+ * for none, or a name two members share. */
145
+ export function roomMemberNamed(value, members = []) {
146
+ const text = String(value ?? '').trim();
147
+ return (text && memberAt(text, members, /(?!)/)?.id) || '';
148
+ }
149
+
141
150
  /** A user message to the room: `/ask <agent>[, <agent>…] <message>` addresses
142
151
  * those members, in that order, by id or by name. `{ to, text }`, `to` the
143
152
  * ordered member ids ([] for the whole room); `error` is 'member' (no such
@@ -283,7 +292,10 @@ export async function runRoomExchange({
283
292
  stopped = () => false,
284
293
  superseded = () => false,
285
294
  waiting = true,
295
+ only = false,
286
296
  }) {
297
+ // A room whose last member was removed has nobody to hear it: finished, no turns.
298
+ if (!members.length) return { outcome: 'done', rounds: 0, steps: 0, messages: 0 };
287
299
  const targets = [...new Set((Array.isArray(to) ? to : [to]).filter((id) => members.includes(id)))];
288
300
  const maxSteps = maxRounds * members.length;
289
301
  const turns = [];
@@ -337,6 +349,9 @@ export async function runRoomExchange({
337
349
  };
338
350
  const answered = await callEach(targets);
339
351
  if (answered) return answered;
352
+ // `only`: the called members answer and nobody else follows (a message
353
+ // from another session, a member relaying its own work).
354
+ if (only && targets.length) return end('done');
340
355
  for (;;) {
341
356
  let halt = halted();
342
357
  if (halt) return halt;
@@ -456,8 +471,14 @@ export function roomTurnPrompt({
456
471
  instruction = '',
457
472
  scene = 'work',
458
473
  waiting = rows.at(-1)?.kind === 'user' && !rows.at(-1)?.seen,
474
+ askedBy = '',
475
+ relay = '',
459
476
  }) {
460
477
  const userLast = rows.at(-1)?.kind === 'user';
478
+ // A relay of the member's own reported work, or a message that waits for
479
+ // an answer: no permission to pass.
480
+ const urged = waiting || Boolean(relay);
481
+ const asker = askedBy ? label(askedBy) : '';
461
482
  const others = members.filter((member) => member.id !== self.id).map((member) => label(member.name));
462
483
  // `addressed`: the members the user asked (or true for this member alone).
463
484
  const asked = Array.isArray(addressed) ? addressed.length > 0 : addressed === true;
@@ -465,11 +486,15 @@ export function roomTurnPrompt({
465
486
  const note = String(instruction ?? '').trim() ? label(instruction).slice(0, ROOM_INSTRUCTION_LIMIT) : '';
466
487
  const lines = [
467
488
  `[mixdog-runtime] Group room "${label(roomName)}". You are ${label(self.name)}; the other members are ${others.join(', ')}. The user and the members share this one room and members speak one at a time.`,
468
- askedWith
469
- ? 'The user asked you and other named members directly: only those members answer this time, one at a time.'
470
- : asked
471
- ? 'The user asked you directly: only you answer this time.'
472
- : 'It is your turn.',
489
+ relay
490
+ ? 'Results of work you started yourself (sessions you created or sent to) arrived; they follow the room lines below. Relay them to the room now, briefly and in your own words: only you speak this time.'
491
+ : askedWith
492
+ ? 'The user asked you and other named members directly: only those members answer this time, one at a time.'
493
+ : asked
494
+ ? asker
495
+ ? `${asker} asked you directly from another session: only you answer this time.`
496
+ : 'The user asked you directly: only you answer this time.'
497
+ : 'It is your turn.',
473
498
  ...(asked
474
499
  ? [
475
500
  'When the user wants another member involved, whatever name they use, write that member as @Name in your reply so the room can hand them a turn.',
@@ -491,9 +516,11 @@ export function roomTurnPrompt({
491
516
  // so a message to the room never meets silence.
492
517
  ...(waiting
493
518
  ? [
494
- userLast
495
- ? 'The user just spoke and nobody has answered yet: reply to the user now, in their language. Do not pass.'
496
- : "Nobody has answered the user's newest message yet (the member messages after it were written before it arrived): reply to that message now, in the user's language. Do not pass.",
519
+ asker
520
+ ? `${asker}'s message has no answer yet: reply to it now. Do not pass.`
521
+ : userLast
522
+ ? 'The user just spoke and nobody has answered yet: reply to the user now, in their language. Do not pass.'
523
+ : "Nobody has answered the user's newest message yet (the member messages after it were written before it arrived): reply to that message now, in the user's language. Do not pass.",
497
524
  ]
498
525
  : []),
499
526
  ...(scene === 'casual'
@@ -506,13 +533,13 @@ export function roomTurnPrompt({
506
533
  ]),
507
534
  'Do not offer help or ask what the user needs unless they asked for help, and do not summarise the room or speak for the others.',
508
535
  'Speak from your own role and the work you remember; never invent facts the user could rely on.',
509
- waiting
536
+ urged
510
537
  ? 'Never say you have nothing to add or nothing to report; say something in character instead.'
511
538
  : `Never say you have nothing to add or nothing to report; when you truly have nothing to say, reply exactly "${ROOM_PASS_REPLY}".`,
512
539
  'Do not end every message with a question.',
513
540
  ]
514
541
  : [
515
- ...(waiting
542
+ ...(urged
516
543
  ? []
517
544
  : [
518
545
  `Reply with your message to the room as plain text. Speak only to answer an open question, add relevant evidence, name a concrete disagreement or propose a useful next step; otherwise reply exactly "${ROOM_PASS_REPLY}".`,
@@ -527,7 +554,7 @@ export function roomTurnPrompt({
527
554
  ];
528
555
  if (!rows.some((row) => !row.seen)) {
529
556
  lines.push('Nothing new was said since your last turn.');
530
- if (!waiting) {
557
+ if (!urged) {
531
558
  lines.push(
532
559
  `This is not a direct call: speak only for what the lines above ask of you; otherwise reply exactly "${ROOM_PASS_REPLY}".`
533
560
  );
@@ -578,12 +605,24 @@ export function roomTurnPrompt({
578
605
  : rest.length
579
606
  ? `to you and ${rest.join(', ')} only`
580
607
  : 'to you only';
581
- lines.push(`The user${row.seen ? ' (earlier, already shown to you)' : ''}, ${target}:`, String(row.text ?? ''));
608
+ // A message another session sent into the room names its real sender: a
609
+ // bot's words carry the trust note of every bot-to-bot message.
610
+ const who = row.fromName
611
+ ? `${label(row.fromName)} (${row.fromAgent ? 'another bot' : "the user's Lead"}, from another session, not the user)`
612
+ : 'The user';
613
+ lines.push(
614
+ `${who}${row.seen ? ' (earlier, already shown to you)' : ''}, ${target}:`,
615
+ ...(row.fromAgent ? [AGENT_MESSAGE_NOTE] : []),
616
+ String(row.text ?? '')
617
+ );
582
618
  // The user's files follow their words, for the members the message reaches.
583
619
  const files = !row.seen && (rest.length < targets.length || !targets.length) ? storedParts(row.parts) : [];
584
620
  if (files.length) content.push({ type: 'text', text: lines.splice(0).join('\n') }, ...files);
585
621
  }
586
622
  if (open) lines.push('</room-transcript>');
623
+ // The member's reported work (agentTaskWakePrompt: runtime text, task
624
+ // output already escaped inside its own boundary).
625
+ if (relay) lines.push(String(relay));
587
626
  if (!content.length) return lines.join('\n');
588
627
  return lines.length ? [...content, { type: 'text', text: lines.join('\n') }] : content;
589
628
  }
@@ -653,12 +692,19 @@ export function normalizeRoomRow(row = {}) {
653
692
  const parts = carriesFiles ? storedParts(row.parts) : [];
654
693
  const attachments = carriesFiles ? normalizeMessageAttachments(row.attachments) : [];
655
694
  const images = row.kind === 'user' ? storedImages(row.images) : [];
695
+ // A user-kind row another session sent (session send to the room): its
696
+ // sender session, the sending bot (none for the user's Lead) and its name.
697
+ const fromSession =
698
+ row.kind === 'user' && typeof row.fromSession === 'string' && SENDER_SESSION.test(row.fromSession)
699
+ ? row.fromSession
700
+ : '';
656
701
  return {
657
702
  id,
658
703
  kind: row.kind,
659
704
  ...(Number.isFinite(row.at) ? { at: row.at } : {}),
660
705
  ...named('speaker'),
661
706
  ...named('name'),
707
+ ...(fromSession ? { fromSession, ...named('fromAgent'), fromName: cleanName(row.fromName) || 'Lead' } : {}),
662
708
  ...(targets.length
663
709
  ? { to: targets.map((target) => target.id), toName: targets.map((target) => target.name || target.id) }
664
710
  : {}),
@@ -730,9 +776,19 @@ export function roomRowItem(message, id) {
730
776
  // A user row's (or member speech's) files, in the fields a chat prompt's row draws them from.
731
777
  ...(room.kind === 'user' || room.kind === 'speech' ? transcriptAttachmentFields(message.meta.transcript) : {}),
732
778
  ...(room.kind === 'user' && Array.isArray(images) && images.length ? { images } : {}),
779
+ ...roomRowSender(room),
733
780
  };
734
781
  }
735
782
 
783
+ /** The attribution a user-kind room row from another session shows on its
784
+ * bubble, in the fields a chat prompt row has: the sending bot
785
+ * (`fromAgent`), else the user's Lead (`sender: 'lead'`); none for the
786
+ * user's own message. */
787
+ export function roomRowSender(room) {
788
+ if (room?.kind !== 'user' || !room.fromSession) return {};
789
+ return room.fromAgent ? { fromAgent: room.fromAgent } : { sender: 'lead' };
790
+ }
791
+
736
792
  /** The room rows of a transcript (`{ room }` items) as row descriptors, in order. */
737
793
  export function roomRowsOfItems(items) {
738
794
  const rows = [];
@@ -1,4 +1,8 @@
1
- import { isProtectedContextAckMessage, latestActualUserInstructionIndex } from './messages.mjs';
1
+ import {
2
+ isProtectedContextAckMessage,
3
+ latestActualUserInstructionIndex,
4
+ pendingTaskNotificationIndexes,
5
+ } from './messages.mjs';
2
6
 
3
7
  export const ACTIVE_TURN_CONTINUATION_SOURCE = 'compact-active-turn-continuation';
4
8
  export const ACTIVE_TURN_CONTINUATION_ANCHOR = '<active-turn-continuation>';
@@ -22,7 +26,7 @@ function makeActiveTurnContinuationMessage() {
22
26
  content: [
23
27
  '<system-reminder>',
24
28
  ACTIVE_TURN_CONTINUATION_ANCHOR,
25
- 'The latest real user request was already in progress when Compact ran.',
29
+ 'The latest real user request or task notification was already in progress when Compact ran.',
26
30
  'The handoff and retained execution records above describe progress so far. A plan is not evidence that a tool succeeded; use the recorded outcomes.',
27
31
  'Continue from that state without repeating progress messages or completed tool calls.',
28
32
  'Use tools only for incomplete or failed work, or to recover missing evidence from an execution archive.',
@@ -38,10 +42,15 @@ function makeActiveTurnContinuationMessage() {
38
42
 
39
43
  export function activeTurnContinuationMessage(messages) {
40
44
  const list = Array.isArray(messages) ? messages : [];
41
- const latestUserIndex = latestActualUserInstructionIndex(list);
42
- if (latestUserIndex < 0) return null;
43
- const afterLatestUser = list.slice(latestUserIndex + 1);
44
- if (!afterLatestUser.some(isExistingContinuation) && !afterLatestUser.some(isExecutionActivity)) {
45
+ // The turn's latest input: a real user request or an unanswered task
46
+ // notification. Only execution after it is progress of the current turn.
47
+ const latestInputIndex = Math.max(
48
+ latestActualUserInstructionIndex(list),
49
+ pendingTaskNotificationIndexes(list).at(-1) ?? -1
50
+ );
51
+ if (latestInputIndex < 0) return null;
52
+ const afterLatestInput = list.slice(latestInputIndex + 1);
53
+ if (!afterLatestInput.some(isExistingContinuation) && !afterLatestInput.some(isExecutionActivity)) {
45
54
  return null;
46
55
  }
47
56
  return makeActiveTurnContinuationMessage();