mixdog 0.9.41 → 0.9.43

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 (48) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-tag-reuse-smoke.mjs +124 -10
  3. package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
  4. package/scripts/arg-guard-test.mjs +16 -0
  5. package/scripts/compact-pressure-test.mjs +256 -1
  6. package/scripts/internal-comms-bench.mjs +0 -1
  7. package/scripts/internal-comms-smoke.mjs +14 -32
  8. package/scripts/internal-tools-normalization-test.mjs +52 -0
  9. package/scripts/max-output-recovery-test.mjs +49 -0
  10. package/scripts/mcp-client-normalization-test.mjs +45 -0
  11. package/scripts/provider-toolcall-test.mjs +23 -0
  12. package/scripts/result-classification-test.mjs +75 -0
  13. package/scripts/smoke.mjs +1 -1
  14. package/scripts/submit-commandbusy-race-test.mjs +99 -7
  15. package/scripts/tui-transcript-perf-test.mjs +56 -1
  16. package/src/agents/reviewer/AGENT.md +4 -0
  17. package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
  18. package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
  19. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
  21. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
  22. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  23. package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
  24. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
  25. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
  26. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
  27. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
  28. package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
  29. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
  30. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
  31. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  33. package/src/runtime/memory/tool-defs.mjs +4 -3
  34. package/src/runtime/shared/tool-surface.mjs +1 -2
  35. package/src/session-runtime/context-status.mjs +8 -2
  36. package/src/standalone/agent-tool/render.mjs +2 -0
  37. package/src/standalone/agent-tool.mjs +202 -22
  38. package/src/tui/components/StatusLine.jsx +4 -26
  39. package/src/tui/dist/index.mjs +46 -31
  40. package/src/tui/engine/session-api.mjs +3 -0
  41. package/src/tui/engine/session-flow.mjs +19 -8
  42. package/src/tui/engine/turn.mjs +24 -2
  43. package/src/tui/engine.mjs +0 -1
  44. package/src/ui/statusline-agents.mjs +0 -8
  45. package/src/ui/statusline.mjs +2 -4
  46. package/src/workflows/default/WORKFLOW.md +29 -20
  47. package/src/workflows/solo-review/WORKFLOW.md +47 -0
  48. package/src/workflows/bench/WORKFLOW.md +0 -60
@@ -3,9 +3,11 @@
3
3
  // runRecallFastTrackCompact stays in the loop (it drives the recall pipeline
4
4
  // against live session state).
5
5
  import {
6
+ contextMessagesSignature,
6
7
  estimateMessagesTokens,
7
8
  estimateRequestReserveTokens,
8
9
  resolveSessionCompactPolicy,
10
+ toolSchemaSignature,
9
11
  } from '../context-utils.mjs';
10
12
  import {
11
13
  compactTypeIsRecallFastTrack,
@@ -132,6 +134,7 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
132
134
  reserveTokens: requestReserve + configuredReserve,
133
135
  requestReserveTokens: requestReserve,
134
136
  configuredReserveTokens: configuredReserve,
137
+ toolSchemaSignature: toolSchemaSignature(tools),
135
138
  };
136
139
  }
137
140
  /** Transcript + request reserve fallback used until an aligned provider baseline exists. */
@@ -159,13 +162,20 @@ function providerPressureTokens(sessionRef, usage) {
159
162
  * covers. Later pressure checks add estimates only for messages after this
160
163
  * baseline, matching Claude Code's actual-usage-plus-growth accounting.
161
164
  */
162
- export function recordProviderContextBaseline(sessionRef, messages, usage, { boundary = 'complete' } = {}) {
165
+ export function recordProviderContextBaseline(sessionRef, messages, usage, {
166
+ boundary = 'complete',
167
+ sendTools = sessionRef?.tools,
168
+ } = {}) {
163
169
  if (!sessionRef || !Array.isArray(messages)) return false;
164
170
  const tokens = providerPressureTokens(sessionRef, usage);
165
171
  if (!tokens) return false;
166
172
  sessionRef.contextPressureBaselineTokens = tokens;
167
173
  sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.outputTokens) || 0));
168
174
  sessionRef.contextPressureBaselineMessageCount = messages.length;
175
+ sessionRef.contextPressureBaselinePrefixSignature = contextMessagesSignature(messages);
176
+ sessionRef.contextPressureBaselineProvider = sessionRef.provider || null;
177
+ sessionRef.contextPressureBaselineModel = sessionRef.model || null;
178
+ sessionRef.contextPressureBaselineToolSignature = toolSchemaSignature(sendTools);
169
179
  // provider_send usage arrives before the response's assistant message is
170
180
  // appended. Mark that request boundary so pressure resolution skips the
171
181
  // first subsequent assistant representation: its output (including opaque
@@ -183,11 +193,15 @@ export function invalidateProviderContextBaseline(sessionRef) {
183
193
  sessionRef.contextPressureBaselineOutputTokens = null;
184
194
  sessionRef.contextPressureBaselineMessageCount = null;
185
195
  sessionRef.contextPressureBaselineBoundary = null;
196
+ sessionRef.contextPressureBaselinePrefixSignature = null;
197
+ sessionRef.contextPressureBaselineProvider = null;
198
+ sessionRef.contextPressureBaselineModel = null;
199
+ sessionRef.contextPressureBaselineToolSignature = null;
186
200
  sessionRef.contextPressureBaselineUpdatedAt = null;
187
201
  sessionRef.lastContextTokensStaleAfterCompact = true;
188
202
  }
189
203
 
190
- function providerBaselinePressureTokens(messages, sessionRef) {
204
+ function providerBaselinePressureTokens(messages, sessionRef, policy) {
191
205
  if (!Array.isArray(messages) || !sessionRef
192
206
  || sessionRef.lastContextTokensStaleAfterCompact === true) return null;
193
207
  let tokens = positiveTokenInt(sessionRef.contextPressureBaselineTokens);
@@ -196,7 +210,11 @@ function providerBaselinePressureTokens(messages, sessionRef) {
196
210
  const baselineAt = Number(sessionRef.contextPressureBaselineUpdatedAt || 0);
197
211
  const compactAt = Number(sessionRef.compaction?.lastChangedAt || sessionRef.compaction?.lastCompactAt || 0);
198
212
  if (!tokens || !Number.isInteger(count) || count < 0 || count > messages.length
199
- || (compactAt > 0 && baselineAt > 0 && baselineAt < compactAt)) return null;
213
+ || (compactAt > 0 && baselineAt > 0 && baselineAt < compactAt)
214
+ || sessionRef.contextPressureBaselineProvider !== (sessionRef.provider || null)
215
+ || sessionRef.contextPressureBaselineModel !== (sessionRef.model || null)
216
+ || sessionRef.contextPressureBaselineToolSignature !== policy?.toolSchemaSignature
217
+ || sessionRef.contextPressureBaselinePrefixSignature !== contextMessagesSignature(messages, count)) return null;
200
218
  if (sessionRef.contextPressureBaselineBoundary === 'request') {
201
219
  const assistantOffset = messages.slice(count).findIndex(message => message?.role === 'assistant');
202
220
  if (assistantOffset >= 0) {
@@ -213,14 +231,14 @@ function providerBaselinePressureTokens(messages, sessionRef) {
213
231
  const growth = count < messages.length
214
232
  ? estimateMessagesTokens(messages.slice(count))
215
233
  : 0;
216
- return Math.max(0, tokens + growth);
234
+ return Math.max(0, tokens + growth + Math.max(0, Number(policy?.configuredReserveTokens) || 0));
217
235
  } catch {
218
236
  return null;
219
237
  }
220
238
  }
221
239
 
222
240
  export function resolveCompactionPressureTokens(messageTokensEst, policy, { messages, sessionRef } = {}) {
223
- return providerBaselinePressureTokens(messages, sessionRef)
241
+ return providerBaselinePressureTokens(messages, sessionRef, policy)
224
242
  ?? compactPressureTokens(messageTokensEst, policy);
225
243
  }
226
244
 
@@ -43,6 +43,9 @@ export function classifyTerminationReason(response, {
43
43
  // on its own contract.
44
44
  return 'iteration_cap';
45
45
  }
46
+ if (_finalStopReason === 'refusal') {
47
+ return 'refusal';
48
+ }
46
49
  if (_finalOutputLimitStop || (!_finalHasContent && _finalIncompleteStop)) {
47
50
  // Exhausted token-cap recovery is abnormal even with preserved partial
48
51
  // text. pause_turn/OTHER retain their prior non-empty completion
@@ -223,21 +223,30 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
223
223
  })();
224
224
  if (typeof afterToolHook === 'function') {
225
225
  try {
226
+ // Tool outcome metadata is runtime-internal. Hooks receive the same
227
+ // model-visible result value they received before transient
228
+ // envelopes existed, never the envelope object itself.
229
+ const {
230
+ result: __res,
231
+ newMessages: __nm,
232
+ explicitSuccess: __explicitSuccess,
233
+ } = normalizeToolEnvelope(__result);
226
234
  const hookResult = await afterToolHook({
227
235
  name,
228
236
  args,
229
237
  cwd,
230
238
  sessionId: callerSessionId,
231
239
  toolCallId: executeOpts.toolCallId || null,
232
- result: __result,
240
+ result: __res,
233
241
  });
234
242
  // Envelope-aware hook override: a PostToolUse hook may override the
235
243
  // model-VISIBLE tool output (the envelope's `result` / stub), but it
236
244
  // must NEVER drop the `newMessages` channel. Split first, apply the
237
245
  // override to `result` only, then re-wrap so newMessages survive.
238
- const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
239
246
  const __overridden = resolveToolResultAfterHook(__res, hookResult);
240
- if (__nm.length) return makeToolEnvelope(__overridden, __nm);
247
+ if (__nm.length || __explicitSuccess) {
248
+ return makeToolEnvelope(__overridden, __nm, { explicitSuccess: __explicitSuccess });
249
+ }
241
250
  return __overridden;
242
251
  } catch {
243
252
  // PostToolUse hooks are best-effort; never let one break the tool result.
@@ -341,7 +341,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
341
341
  promptTokens: d.deltaPrompt,
342
342
  cachedTokens: d.deltaCachedRead,
343
343
  cacheWriteTokens: d.deltaCacheWrite,
344
- }, { boundary: 'request' });
344
+ }, { boundary: 'request', sendTools: d.sendTools });
345
345
  }
346
346
  try { askOpts?.onUsageDelta?.(d); } catch {}
347
347
  },
@@ -482,7 +482,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
482
482
  applyAskTerminalUsageTotals(session, result, {
483
483
  skipTotalsIfIncremental: runtime?.usageMetricsTurnIncremental === true,
484
484
  });
485
- recordProviderContextBaseline(session, session.messages, result.lastTurnUsage || result.usage);
485
+ recordProviderContextBaseline(session, session.messages, result.lastTurnUsage || result.usage, {
486
+ sendTools: result.lastSendTools,
487
+ });
486
488
  // Agent Runtime cache stats — record hit/miss after every successful
487
489
  // ask so the registry reflects all agent traffic, not just
488
490
  // maintenance cycles. Guarded against any agent-runtime error so
@@ -40,6 +40,8 @@
40
40
  * "(no lines in range" — read offset out-of-range (builtin.mjs:739, 3571)
41
41
  *
42
42
  * @param {unknown} result
43
+ * @param {boolean} [explicitSuccess=false] true only when the tool handler
44
+ * explicitly returned `isError: false`
43
45
  * @returns {'normal' | 'error' | 'zero-match'}
44
46
  */
45
47
  const ZERO_MATCH_PREFIXES = [
@@ -56,7 +58,8 @@ const ZERO_MATCH_PREFIXES = [
56
58
  '(no lines in range',
57
59
  ];
58
60
 
59
- export function classifyResultKind(result) {
61
+ export function classifyResultKind(result, explicitSuccess = false) {
62
+ if (explicitSuccess === true) return 'normal';
60
63
  if (typeof result !== 'string') return 'normal';
61
64
  const trimmed = result.trimStart();
62
65
  if (/^error(?:\s+\[code\b|\s*:)/i.test(trimmed) || /^\[error/i.test(trimmed) || /^\[exit code:/i.test(trimmed)) return 'error';
@@ -32,6 +32,11 @@ import { crossTurnSignature, crossTurnDedupStub, isEditProgressTool } from './lo
32
32
  import { getToolKind, isEagerDispatchable, parseNativeToolSearchPayload } from './loop/tool-helpers.mjs';
33
33
  import { restoreToolCallBodyForId, dropCompactedBodyArgsForId } from './loop/stored-tool-args.mjs';
34
34
 
35
+ function classifyToolReturn(value) {
36
+ const normalized = normalizeToolEnvelope(value);
37
+ return classifyResultKind(normalized.result, normalized.explicitSuccess);
38
+ }
39
+
35
40
  export async function processToolBatch(ctx) {
36
41
  const {
37
42
  calls, messages, tools, cwd, sessionId, sessionRef, signal, opts,
@@ -258,7 +263,7 @@ export async function processToolBatch(ctx) {
258
263
  if (!settled.ok) throw settled.error;
259
264
  result = settled.value;
260
265
  toolEndedAt = eager.endedAt ?? Date.now();
261
- const _eagerKind = classifyResultKind(result);
266
+ const _eagerKind = classifyToolReturn(result);
262
267
  if (_eagerKind === 'error') {
263
268
  _resultKind = 'error';
264
269
  _executeOk = false;
@@ -286,7 +291,7 @@ export async function processToolBatch(ctx) {
286
291
  // Boundary: tool-return string convention → structural kind.
287
292
  // The only prefix check in this codebase; downstream layers
288
293
  // operate on _resultKind.
289
- if (classifyResultKind(result) === 'error') {
294
+ if (classifyToolReturn(result) === 'error') {
290
295
  _resultKind = 'error';
291
296
  _executeOk = false;
292
297
  } else {
@@ -5,7 +5,8 @@
5
5
  * - legacy: a string (or existing structured media object) — unchanged, OR
6
6
  * - an envelope object:
7
7
  * { __toolEnvelope: true, result: <string|structured>,
8
- * newMessages: [{ role:'user', content:'...' }, ...] }
8
+ * newMessages: [{ role:'user', content:'...' }, ...],
9
+ * explicitSuccess?: true }
9
10
  *
10
11
  * The `__toolEnvelope` marker is deliberately namespaced so it can NEVER be
11
12
  * confused with the existing structured media content objects that
@@ -37,17 +38,18 @@ function isValidNewMessage(m) {
37
38
  * sees as the tool_result; `newMessages` are appended (as their own
38
39
  * messages, e.g. role:'user') AFTER the batch's tool results.
39
40
  */
40
- export function makeToolEnvelope(result, newMessages = []) {
41
+ export function makeToolEnvelope(result, newMessages = [], options = {}) {
41
42
  return {
42
43
  [TOOL_ENVELOPE_MARKER]: true,
43
44
  result,
44
45
  newMessages: Array.isArray(newMessages) ? newMessages.filter(isValidNewMessage) : [],
46
+ ...(options.explicitSuccess === true ? { explicitSuccess: true } : {}),
45
47
  };
46
48
  }
47
49
 
48
50
  /**
49
- * Split a tool return value into `{ result, newMessages }`.
50
- * - legacy string/object → { result: value, newMessages: [] }
51
+ * Split a tool return value into `{ result, newMessages, explicitSuccess }`.
52
+ * - legacy string/object → { result: value, newMessages: [], explicitSuccess: false }
51
53
  * - envelope → { result, newMessages } (newMessages validated)
52
54
  */
53
55
  export function normalizeToolEnvelope(value) {
@@ -55,7 +57,7 @@ export function normalizeToolEnvelope(value) {
55
57
  const newMessages = Array.isArray(value.newMessages)
56
58
  ? value.newMessages.filter(isValidNewMessage)
57
59
  : [];
58
- return { result: value.result, newMessages };
60
+ return { result: value.result, newMessages, explicitSuccess: value.explicitSuccess === true };
59
61
  }
60
- return { result: value, newMessages: [] };
62
+ return { result: value, newMessages: [], explicitSuccess: false };
61
63
  }
@@ -344,6 +344,15 @@ function stripTrailingPatternArtifacts(v) {
344
344
  return out;
345
345
  }
346
346
 
347
+ function coercePatternStringValues(v) {
348
+ const coerce = (value) => (
349
+ (typeof value === 'number' && Number.isFinite(value)) || typeof value === 'boolean'
350
+ ? String(value)
351
+ : value
352
+ );
353
+ return Array.isArray(v) ? v.map(coerce) : coerce(v);
354
+ }
355
+
347
356
  // ---- per-tool guards ----
348
357
 
349
358
  function guardGrep(a) {
@@ -355,9 +364,10 @@ function guardGrep(a) {
355
364
  // Lossless cleanup of trailing artifacts before validation (item 5b).
356
365
  for (const k of patternKeys) {
357
366
  if (hasOwn(a, k)) {
358
- a[k] = Array.isArray(a[k])
359
- ? a[k].map(stripTrailingPatternArtifacts)
360
- : stripTrailingPatternArtifacts(a[k]);
367
+ const value = coercePatternStringValues(a[k]);
368
+ a[k] = Array.isArray(value)
369
+ ? value.map(stripTrailingPatternArtifacts)
370
+ : stripTrailingPatternArtifacts(value);
361
371
  }
362
372
  }
363
373
 
@@ -376,6 +386,7 @@ function guardGrep(a) {
376
386
  // rg's PCRE2 engine (-P/--pcre2) when the installed rg build supports it,
377
387
  // falling back to this same error text only when PCRE2 is unavailable.
378
388
  for (const k of globKeys) {
389
+ if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
379
390
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
380
391
  return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
381
392
  }
@@ -755,6 +766,7 @@ function guardGlob(a) {
755
766
  }
756
767
  }
757
768
  for (const k of globPatternKeys) {
769
+ if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
758
770
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
759
771
  return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
760
772
  }
@@ -773,7 +773,7 @@ export function foregroundLongCommandHint(command, timeoutMs, args = {}) {
773
773
  const longSleep = /\bsleep\s+(?:[3-9]\d|\d{3,}|\d+[mh])\b/i.test(cmd) || longPowerShellSleep;
774
774
  if (!watchLike && !longSleep) return '';
775
775
  if (!longTimeout && !watchLike && !longSleep) return '';
776
- return 'Error: long foreground command detected.';
776
+ return 'Error: long foreground command detected. Use mode:"async" (background task) or task wait instead of blocking sleeps/watch loops.';
777
777
  }
778
778
 
779
779
  // Commands that must NOT be promoted to background on a foreground timeout —
@@ -8,21 +8,22 @@ export const TOOL_DEFS = [
8
8
  name: 'memory',
9
9
  title: 'Memory Cycle',
10
10
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11
- description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
11
+ description: 'Core-memory mutation and status. Use recall for retrieval. Persist with action:"core" op:"add" when: user corrects/confirms your approach, states a durable preference, or says "remember". Core add/edit requires project_id + category + element + summary. Only what code/git/docs cannot derive; include the why. Never transient task state. No prior recall dedup needed — the cycle dedups.',
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
15
15
  action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
16
16
  op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
17
17
  id: { type: 'number', description: 'Exact memory id.' },
18
- element: { type: 'string', description: 'Memory key/title. Max 40 chars.' },
19
- summary: { type: 'string', description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
18
+ element: { type: 'string', maxLength: 40, description: 'Memory key/title. Max 40 chars.' },
19
+ summary: { type: 'string', maxLength: 100, description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
20
20
  category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
21
21
  status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
22
22
  limit: { type: 'number', description: 'Max rows/items.' },
23
23
  confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
24
24
  project_id: { type: 'string', description: 'Core pool: common, slug, or *. Required for core add/edit; there is no default pool.' },
25
25
  },
26
+ additionalProperties: false,
26
27
  required: ['action'],
27
28
  },
28
29
  },
@@ -514,8 +514,7 @@ export function toolWorkUnit(name, args = {}, category = '') {
514
514
  case 'memory': {
515
515
  const action = String(a.action || '').toLowerCase();
516
516
  const op = String(a.op || '').toLowerCase();
517
- const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss'
518
- || (action === 'core' && !op);
517
+ const isMutation = op === 'add' || op === 'edit' || op === 'delete' || op === 'promote' || op === 'dismiss';
519
518
  if (isMutation) return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Writing', done: 'Wrote', noun: 'memory item' });
520
519
  return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Checking', done: 'Checked', noun: 'memory item' });
521
520
  }
@@ -2,8 +2,10 @@ import {
2
2
  estimateRequestReserveTokens,
3
3
  estimateToolSchemaTokens,
4
4
  estimateTranscriptContextUsage,
5
+ contextMessagesRevision,
5
6
  resolveSessionCompactPolicy,
6
7
  summarizeContextMessages,
8
+ toolSchemaSignature,
7
9
  } from '../runtime/agent/orchestrator/session/context-utils.mjs';
8
10
  import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
9
11
 
@@ -16,7 +18,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
16
18
  let contextStatusCacheKey = null;
17
19
  let contextStatusCacheValue = null;
18
20
 
19
- function contextStatusCacheKeyFor({ messages, tools }) {
21
+ function contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature }) {
20
22
  const session = getSession();
21
23
  const route = getRoute();
22
24
  const compaction = session?.compaction || {};
@@ -30,11 +32,13 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
30
32
  mode: getMode(),
31
33
  messages,
32
34
  messageCount: messages.length,
35
+ messagesRevision,
33
36
  lastMessage,
34
37
  lastMessageRole: lastMessage?.role || null,
35
38
  lastMessageContent: lastMessage?.content || null,
36
39
  tools,
37
40
  toolCount: tools.length,
41
+ toolsSignature,
38
42
  contextWindow: session?.contextWindow || null,
39
43
  rawContextWindow: session?.rawContextWindow || null,
40
44
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
@@ -85,7 +89,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
85
89
  const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
86
90
  const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
87
91
  const tools = Array.isArray(session?.tools) ? session.tools : [];
88
- const cacheKey = contextStatusCacheKeyFor({ messages, tools });
92
+ const messagesRevision = contextMessagesRevision(messages);
93
+ const toolsSignature = toolSchemaSignature(tools);
94
+ const cacheKey = contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature });
89
95
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
90
96
  return contextStatusCacheValue;
91
97
  }
@@ -37,6 +37,8 @@ export function abnormalEmptyFinishError(result, agent) {
37
37
  // Only tagged for PUBLIC agents (hidden agents legitimately emit empty
38
38
  // terminal turns and are left untagged by the loop).
39
39
  return `agent '${agent}' finished without a final answer (stopReason=${stopReason ?? 'none'}, ${iterations} iterations, ${toolCallsTotal} tool calls)`;
40
+ case 'refusal':
41
+ return `agent '${agent}' response was refused by the safety classifier (${iterations} iterations, ${toolCallsTotal} tool calls)`;
40
42
  default:
41
43
  return null;
42
44
  }