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
@@ -3,9 +3,12 @@ import { estimateMessagesTokens } from '../context-utils.mjs';
3
3
  import { estimateTokens } from '../token-estimate.mjs';
4
4
  import { persistToolResultArtifactSync, previewToolResultArtifact } from '../tool-result-offload.mjs';
5
5
  import {
6
+ EXECUTION_TEXT_SOURCE,
6
7
  isActualUserInstructionMessage,
8
+ isCompactConversationRow,
7
9
  isProtectedContextAckMessage,
8
10
  latestActualUserInstructionIndex,
11
+ pendingTaskNotificationIndexes,
9
12
  } from './messages.mjs';
10
13
  import { staleObservations, supersedeObservation } from './observation-supersession.mjs';
11
14
  import { carryMessageId, withoutMessageIdsReplacer } from '../message-ids.mjs';
@@ -168,9 +171,10 @@ function shrinkGroup(group, archive, level) {
168
171
  });
169
172
  }
170
173
 
171
- // Walk from the anchor: kept groups verbatim, user instructions, and the
172
- // conversational assistant text the mode allows.
173
- function assembleTail(messages, kept, { anchor, firstKept, preserveConversation }) {
174
+ // Walk from the anchor: kept groups verbatim, user instructions, unanswered
175
+ // task notifications, and the conversation the mode allows (rule-only:
176
+ // assistant text and runtime conversation rows).
177
+ function assembleTail(messages, kept, { anchor, firstKept, preserveConversation, pending }) {
174
178
  const tail = [];
175
179
  for (let i = Math.max(0, anchor); i < messages.length; i += 1) {
176
180
  const group = kept.get(i);
@@ -184,8 +188,16 @@ function assembleTail(messages, kept, { anchor, firstKept, preserveConversation
184
188
  // Omitted execution stays in the archive; its conversational text
185
189
  // remains verbatim without replaying an orphaned call or reasoning.
186
190
  if (message.toolCalls?.length) {
187
- // The text-only copy is this message's rewrite: it keeps its id.
188
- if (message.content) tail.push(carryMessageId({ role: 'assistant', content: message.content }, message));
191
+ // The text-only copy is this message's rewrite: it keeps its id, and
192
+ // its marker keeps it from reading as a final reply.
193
+ if (message.content) {
194
+ tail.push(
195
+ carryMessageId(
196
+ { role: 'assistant', content: message.content, meta: { source: EXECUTION_TEXT_SOURCE } },
197
+ message
198
+ )
199
+ );
200
+ }
189
201
  } else {
190
202
  tail.push(message);
191
203
  }
@@ -193,6 +205,8 @@ function assembleTail(messages, kept, { anchor, firstKept, preserveConversation
193
205
  }
194
206
  if (
195
207
  isActualUserInstructionMessage(message) ||
208
+ pending.has(i) ||
209
+ (preserveConversation && isCompactConversationRow(message)) ||
196
210
  (i >= firstKept && message?.role === 'assistant' && !message.toolCalls?.length)
197
211
  ) {
198
212
  tail.push(message);
@@ -297,13 +311,22 @@ export function buildExecutionTail(
297
311
  throw new Error('compact: latest execution group cannot fit the tool-history budget; original context preserved');
298
312
  }
299
313
  const latest = latestActualUserInstructionIndex(messages);
314
+ const pendingIndexes = pendingTaskNotificationIndexes(messages);
315
+ const pending = new Set(pendingIndexes);
300
316
  const firstKept = kept.size ? Math.min(...kept.keys()) : messages.length;
301
317
  const latestKept = latest < 0 ? firstKept : latest;
302
- const anchor = preserveConversation ? 0 : requestStart(messages, Math.min(firstKept, latestKept));
303
- const tail = assembleTail(messages, kept, { anchor, firstKept, preserveConversation });
318
+ const anchor = preserveConversation
319
+ ? 0
320
+ : Math.min(requestStart(messages, Math.min(firstKept, latestKept)), ...pendingIndexes);
321
+ const tail = assembleTail(messages, kept, { anchor, firstKept, preserveConversation, pending });
304
322
  // With no execution to retain, keep the existing latest-request contract.
305
323
  if (!preserveConversation && !groups.length && !previousRecovery.length) {
306
- return { messages: latest < 0 ? [] : [messages[latest]], toolTokens: 0, toolBudget: budget, retainedGroups: 0 };
324
+ return {
325
+ messages: messages.filter((_message, index) => index === latest || pending.has(index)),
326
+ toolTokens: 0,
327
+ toolBudget: budget,
328
+ retainedGroups: 0,
329
+ };
307
330
  }
308
331
  return {
309
332
  messages: [...recovery, ...tail],
@@ -10,6 +10,7 @@ import {
10
10
  isProtectedContextUserMessage,
11
11
  isSummaryMessage,
12
12
  } from './user-message-kind.mjs';
13
+ import { CONTROL_MARKER_RE } from '../../../../shared/tool-execution-contract.mjs';
13
14
 
14
15
  export function compactHeader(oldHistory) {
15
16
  // Stable display ids are bookkeeping, not conversation: the header names
@@ -53,6 +54,49 @@ export function isProtectedContextAckMessage(m) {
53
54
  );
54
55
  }
55
56
 
57
+ // The text-only copy rule-only Compact keeps of an omitted tool-call turn.
58
+ // It is execution, not a final reply, though it no longer carries its calls.
59
+ export const EXECUTION_TEXT_SOURCE = 'compact-execution-text';
60
+
61
+ // Delivered task notifications (a background agent's or task's completion,
62
+ // stored as a role:'user' turn with source 'task-notification') the model has
63
+ // not answered yet: every one after the latest final assistant reply. They
64
+ // are turn input but neither a user instruction nor summarized conversation,
65
+ // so a Compact must carry them verbatim or the report is lost.
66
+ export function pendingTaskNotificationIndexes(messages) {
67
+ const list = Array.isArray(messages) ? messages : [];
68
+ const pending = [];
69
+ for (let index = list.length - 1; index >= 0; index -= 1) {
70
+ const message = list[index];
71
+ if (
72
+ message?.role === 'assistant' &&
73
+ !message.toolCalls?.length &&
74
+ !isProtectedContextAckMessage(message) &&
75
+ message.meta?.source !== EXECUTION_TEXT_SOURCE
76
+ ) {
77
+ break;
78
+ }
79
+ if (message?.role === 'user' && message.meta?.source === 'task-notification') pending.unshift(index);
80
+ }
81
+ return pending;
82
+ }
83
+
84
+ // Runtime rows that are conversation, not instructions: task and channel
85
+ // notifications (answered or not), routine and task results posted into a
86
+ // home thread, and turn interruption markers. Rule-only Compact keeps them
87
+ // with the dialogue; the summary reads them as data.
88
+ const CONVERSATION_RUNTIME_SOURCES = new Set(['task-notification', 'schedule-result']);
89
+
90
+ export function isCompactConversationRow(message) {
91
+ if (message?.role !== 'user') return false;
92
+ if (CONVERSATION_RUNTIME_SOURCES.has(String(message.meta?.source || ''))) return true;
93
+ return (
94
+ typeof message.content === 'string' &&
95
+ CONTROL_MARKER_RE.test(message.content.trim()) &&
96
+ !isActualUserInstructionMessage(message)
97
+ );
98
+ }
99
+
56
100
  function referenceFilesManifestMessage(message) {
57
101
  if (message?.role !== 'user' || typeof message.content !== 'string') return null;
58
102
  const text = message.content.trimStart();
@@ -5,16 +5,19 @@ import { estimateMessagesTokens, reconcileDedupStubs, sanitizeToolPairs } from '
5
5
  import { HANDOFF_TIMEOUT_MAX_MS, SUMMARY_OUTPUT_TOKENS, compactDebugLog } from './constants.mjs';
6
6
  import {
7
7
  contentText,
8
+ redactRawSecretString,
8
9
  redactToolCallSecretsInMessages,
9
10
  safeEstimateMessagesTokens,
10
11
  textByteLength,
11
12
  } from './text-utils.mjs';
12
13
  import {
13
14
  contextAckMessage,
15
+ isCompactConversationRow,
14
16
  isProtectedContextAckMessage,
15
17
  isSummaryMessage,
16
18
  latestActualUserInstructionIndex,
17
19
  latestActualUserInstructionMessage,
20
+ pendingTaskNotificationIndexes,
18
21
  splitProtectedContext,
19
22
  } from './messages.mjs';
20
23
  import { activeTurnContinuationMessage } from './continuation.mjs';
@@ -102,28 +105,76 @@ function chunkConversationMessage(role, content, source = {}) {
102
105
  return out;
103
106
  }
104
107
 
105
- function pureConversationForHandoff(messages) {
108
+ // What the summary reads of runtime conversation rows and tool calls. The
109
+ // rows stay whole in rule-only Compact; the calls stay in the execution tail
110
+ // and archive.
111
+ const SUMMARY_RUNTIME_ROW_CHARS = 4_000;
112
+ const SUMMARY_TOOL_ARGS_CHARS = 200;
113
+ const SUMMARY_TOOL_OUTCOME_CHARS = 300;
114
+
115
+ function boundedSummaryText(text, limit) {
116
+ return text.length > limit ? `${text.slice(0, limit)}… [${text.length - limit} more chars]` : text;
117
+ }
118
+
119
+ function toolCallSummaryLine(call, result) {
120
+ const rawArgs = typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments ?? {});
121
+ const args = boundedSummaryText(rawArgs.replace(/\s+/g, ' '), SUMMARY_TOOL_ARGS_CHARS);
122
+ // Arguments arrive redacted (splitFreshSource); the outcome is redacted
123
+ // here, before any cut can split a secret from its key.
124
+ const outcome = redactRawSecretString(contentText(result?.content)).trim();
125
+ // An ask_user result is the user's own answer: never cut.
126
+ const shown =
127
+ call.name === 'ask_user' ? outcome : boundedSummaryText(outcome.replace(/\s+/g, ' '), SUMMARY_TOOL_OUTCOME_CHARS);
128
+ return `[tool call] ${call.name}(${args}) → ${result ? shown || '(empty result)' : '(no result)'}`;
129
+ }
130
+
131
+ // The conversation Compact measures and summarizes: user and assistant text
132
+ // (the memory-ingest projection) plus runtime conversation rows. `summary`
133
+ // bounds those rows and adds one line per tool call with its outcome.
134
+ function compactionConversationRows(messages, { summary = false } = {}) {
135
+ const list = messages || [];
136
+ const results = new Map(list.filter((m) => m?.role === 'tool').map((m) => [m.toolCallId, m]));
106
137
  const out = [];
107
- for (const message of messages || []) {
138
+ for (const message of list) {
108
139
  if (!message || typeof message !== 'object') continue;
109
- const role = normalizeIngestRole(message.role);
110
- if (!role || shouldExcludeIngestMessage(message)) continue;
111
140
  const content = contentText(message.content).trim();
112
- if (!content) continue;
113
- out.push(...chunkConversationMessage(role, content, message));
141
+ if (isCompactConversationRow(message)) {
142
+ // A notification can carry a background command's output: the summary
143
+ // reads it redacted like a tool outcome.
144
+ const text = summary ? boundedSummaryText(redactRawSecretString(content), SUMMARY_RUNTIME_ROW_CHARS) : content;
145
+ if (text) out.push(...chunkConversationMessage('user', text, message));
146
+ continue;
147
+ }
148
+ const role = normalizeIngestRole(message.role);
149
+ if (role && content && !shouldExcludeIngestMessage(message)) {
150
+ out.push(...chunkConversationMessage(role, content, message));
151
+ }
152
+ if (summary && message.role === 'assistant' && message.toolCalls?.length) {
153
+ const lines = message.toolCalls.map((call) => toolCallSummaryLine(call, results.get(call.id)));
154
+ out.push(...chunkConversationMessage('assistant', lines.join('\n'), message));
155
+ }
114
156
  }
115
157
  return out;
116
158
  }
117
159
 
118
- export function conversationCompactionInput(messages) {
160
+ /** The conversation Compact measures against its summary threshold, or with
161
+ * `summary`, the input the summary is written from. */
162
+ export function conversationCompactionInput(messages, { summary = false } = {}) {
119
163
  const source = splitFreshSource(messages);
120
- const latest = latestActualUserInstructionIndex(source.live);
164
+ // The turn input (the latest actual request and unanswered notifications)
165
+ // is mandatory and must never be replaced by a summary. Skills and runtime
166
+ // injections are excluded by the same conversation projection used by
167
+ // summary generation.
168
+ const turnInput = new Set([
169
+ latestActualUserInstructionIndex(source.live),
170
+ ...pendingTaskNotificationIndexes(source.live),
171
+ ]);
121
172
  return [
122
173
  ...(source.previousSummaryMessage ? [source.previousSummaryMessage] : []),
123
- // The latest actual request is mandatory and must never be replaced by
124
- // a summary. Tool outputs, skills and runtime injections are excluded
125
- // by the same conversation projection used by summary generation.
126
- ...pureConversationForHandoff(source.live.filter((_, index) => index !== latest)),
174
+ ...compactionConversationRows(
175
+ source.live.filter((_, index) => !turnInput.has(index)),
176
+ { summary }
177
+ ),
127
178
  ];
128
179
  }
129
180
 
@@ -229,7 +280,8 @@ export async function generateFreshHandoffSummary(provider, messages, model, bud
229
280
  const startedAt = Date.now();
230
281
  const budget = effectiveBudget(budgetTokens, opts);
231
282
  const source = splitFreshSource(messages);
232
- let head = opts.filterOldHistoryForIngest === true ? pureConversationForHandoff(source.live) : source.live;
283
+ let head =
284
+ opts.filterOldHistoryForIngest === true ? compactionConversationRows(source.live, { summary: true }) : source.live;
233
285
  if (head.length === 0 && !source.previousSummary) {
234
286
  throw new Error('generateFreshHandoffSummary: no compactable session history');
235
287
  }
@@ -341,7 +393,12 @@ function prependLatestUserContext(message, prefix) {
341
393
  if (replacesGoalState) {
342
394
  suffix = suffix.replace(/<system-reminder>\s*<goal_state>[\s\S]*?<\/goal_state>\s*<\/system-reminder>\s*/gi, '');
343
395
  }
344
- return withRuntimeUserContext(bare, { prefix: `${text}\n\n`, suffix });
396
+ // The kept suffix still carries the turn's agent list reminder: it stays told.
397
+ const told = bare !== message && message.meta.agentListing ? { agentListing: message.meta.agentListing } : null;
398
+ return withRuntimeUserContext(told ? { ...bare, meta: { ...bare.meta, ...told } } : bare, {
399
+ prefix: `${text}\n\n`,
400
+ suffix,
401
+ });
345
402
  }
346
403
 
347
404
  // The retained execution tail plus the current-turn messages that must
@@ -360,8 +417,11 @@ function freshContextTail(source, budgetTokens, preserveConversation, opts) {
360
417
  const retainedTail = execution.messages.map((message, index) =>
361
418
  index === latestIndex ? prependLatestUserContext(message, opts.latestUserPrefix) : message
362
419
  );
420
+ // The turn's input: the latest real request or an unanswered notification
421
+ // (a bot home thread may hold no human request at all).
422
+ const turnInput = !!latestUser || pendingTaskNotificationIndexes(source.live).length > 0;
363
423
  const activeTurnContinuation =
364
- latestUser && opts.activeTurn === true ? activeTurnContinuationMessage(source.live) : null;
424
+ turnInput && opts.activeTurn === true ? activeTurnContinuationMessage(source.live) : null;
365
425
  // Older compacted sessions may have only synthetic Goal turns left.
366
426
  // Preserve the current snapshot without inventing a human instruction
367
427
  // to attach it to or resurrecting every historical Goal reminder.
@@ -376,6 +436,7 @@ function freshContextTail(source, budgetTokens, preserveConversation, opts) {
376
436
  return {
377
437
  execution,
378
438
  latestUser,
439
+ turnInput,
379
440
  retainedTail,
380
441
  activeTurnContinuation,
381
442
  volatileTail: [
@@ -408,20 +469,57 @@ function skillLoaderPair(live, bodyIndex, name, retainedCallIds) {
408
469
  return [];
409
470
  }
410
471
 
472
+ const SKILL_RELOAD_SOURCE = 'compact-skill-reload';
473
+
474
+ function skillReloadReminder(name) {
475
+ return {
476
+ role: 'user',
477
+ content: `[mixdog-runtime] Skill "${name}" was active, but its instructions did not fit after Compact. Reload it with the Skill tool before relying on them.`,
478
+ meta: { source: SKILL_RELOAD_SOURCE, synthetic: true, skill: name },
479
+ };
480
+ }
481
+
411
482
  // Preserve complete, most-recent skill bodies from the durable transcript.
412
483
  // Never summarize/truncate operating instructions or displace the handoff
413
- // and current task. An omitted body remains eligible for a normal reload.
484
+ // and current task. An omitted body leaves a one-line reload reminder, which
485
+ // stands through later Compacts until the skill is loaded again. Reminders
486
+ // are charged to the same budget: every skill's reminder is reserved before
487
+ // any body is restored, and restoring a body releases its own reminder.
414
488
  function restoreSkillBodies(live, skillBudget, retainedTail) {
415
489
  const retainedCallIds = new Set(retainedTail.filter((m) => m?.role === 'tool').map((m) => m.toolCallId));
490
+ const bodies = latestSkillBodies(live);
491
+ const loaded = new Set(bodies.map((body) => body.name));
492
+ const reload = live
493
+ .filter((m) => m?.meta?.source === SKILL_RELOAD_SOURCE && !loaded.has(m.meta.skill))
494
+ .map((m) => m.meta.skill);
495
+ const reminderCost = (name) => estimateMessagesTokens([skillReloadReminder(name)]);
496
+ let reserved = [...new Set([...reload, ...loaded])].reduce((sum, name) => sum + reminderCost(name), 0);
497
+ const dropped = [];
416
498
  const restoredSkills = [];
417
499
  let skillTokens = 0;
418
- for (const { name, message } of latestSkillBodies(live).reverse()) {
500
+ for (const { name, message } of bodies.reverse()) {
419
501
  const restored = [...skillLoaderPair(live, live.lastIndexOf(message), name, retainedCallIds), message];
420
502
  const cost = estimateMessagesTokens(restored);
421
- if (skillTokens + cost > skillBudget) continue;
503
+ const own = reminderCost(name);
504
+ if (skillTokens + cost + reserved - own > skillBudget) {
505
+ dropped.unshift(name);
506
+ continue;
507
+ }
422
508
  restoredSkills.unshift(...restored);
423
509
  skillTokens += cost;
510
+ reserved -= own;
511
+ }
512
+ // Newest skill first; only a budget too small for the reminders themselves
513
+ // leaves one out.
514
+ const pending = [...new Set([...reload, ...dropped])];
515
+ const reminded = new Set();
516
+ for (const name of pending.toReversed()) {
517
+ const cost = reminderCost(name);
518
+ if (skillTokens + cost > skillBudget) continue;
519
+ reminded.add(name);
520
+ skillTokens += cost;
424
521
  }
522
+ restoredSkills.push(...pending.filter((name) => reminded.has(name)).map(skillReloadReminder));
425
523
  return { restoredSkills, skillTokens };
426
524
  }
427
525
 
@@ -548,7 +646,7 @@ function freshContextLayout(source, budgetTokens, opts) {
548
646
  ? fitFreshContextSummaryMessage(source.live, handoffText, Number.MAX_SAFE_INTEGER)
549
647
  : source.previousSummaryMessage;
550
648
  const tail = freshContextTail(source, budgetTokens, !replaceConversation, opts);
551
- const stableAck = tail.latestUser && summaryMessage ? stableAckMessage(source.previousAck, tail.volatileTail) : null;
649
+ const stableAck = tail.turnInput && summaryMessage ? stableAckMessage(source.previousAck, tail.volatileTail) : null;
552
650
  const stableHead = [
553
651
  ...source.protectedPrefix,
554
652
  ...(summaryMessage ? [summaryMessage] : []),
@@ -48,7 +48,7 @@ const SENSITIVE_RAW_KEY_RES = [
48
48
  // string. Consumes the WHOLE value after the key — spaces, `Bearer `/`Basic `
49
49
  // scheme words, quoted values with internal spaces, and `;`-separated cookie
50
50
  // pairs — so no secret fragment survives.
51
- function redactRawSecretString(text) {
51
+ export function redactRawSecretString(text) {
52
52
  let value = String(text ?? '');
53
53
  if (!value) return value;
54
54
  for (const keyRe of SENSITIVE_RAW_KEY_RES) {
@@ -87,6 +87,9 @@ export function isContextAckMessage(message, previous = null) {
87
87
  const SYNTHETIC_USER_SOURCES = new Set([
88
88
  // A schedule's output posted into its owner's home thread: untrusted data.
89
89
  'schedule-result',
90
+ // A delivered task or channel notification: turn input, never the human's
91
+ // request, whatever its text looks like.
92
+ 'task-notification',
90
93
  'goal-continuation',
91
94
  'goal-closeout',
92
95
  'compact-context',