@sema-agent/core 7.23.4 → 7.24.0

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 (89) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/dist/agents/list-agents-tool.d.ts +8 -1
  3. package/dist/agents/list-agents-tool.js +26 -3
  4. package/dist/agents/observer.js +5 -8
  5. package/dist/agents/subagent.js +2 -2
  6. package/dist/agents/team.js +9 -0
  7. package/dist/brain/anthropic.js +3 -2
  8. package/dist/brain/circuit-breaker.js +3 -3
  9. package/dist/brain/degrading.js +11 -14
  10. package/dist/brain/errors.d.ts +35 -18
  11. package/dist/brain/errors.js +49 -31
  12. package/dist/brain/failover.js +2 -3
  13. package/dist/brain/open-responses.js +6 -5
  14. package/dist/brain/openai.js +6 -5
  15. package/dist/brain/route-adjudicator.d.ts +50 -15
  16. package/dist/brain/route-adjudicator.js +112 -20
  17. package/dist/brain/route-conformance.js +9 -0
  18. package/dist/brain/stream-engine.js +18 -10
  19. package/dist/core/arg-summary.js +17 -6
  20. package/dist/core/auto-compaction.d.ts +42 -1
  21. package/dist/core/auto-compaction.js +21 -1
  22. package/dist/core/auto-mode.js +2 -8
  23. package/dist/core/checkpoint-store.d.ts +4 -6
  24. package/dist/core/checkpoint-store.js +2 -2
  25. package/dist/core/exec-gate.js +3 -5
  26. package/dist/core/file-history-store.js +11 -10
  27. package/dist/core/hooks.d.ts +7 -0
  28. package/dist/core/mcp.js +7 -5
  29. package/dist/core/memory-engine/distiller.d.ts +7 -4
  30. package/dist/core/memory-engine/distiller.js +11 -3
  31. package/dist/core/memory.js +1 -0
  32. package/dist/core/permission-rule-model.js +8 -5
  33. package/dist/core/runner/assemble-result.js +12 -19
  34. package/dist/core/runner/contracts.d.ts +1 -0
  35. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
  36. package/dist/core/runner/prepare-delegation-surface.js +1 -0
  37. package/dist/core/runner/prepare-file-history.js +19 -0
  38. package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
  39. package/dist/core/runner/prepare-hands-readface.js +4 -0
  40. package/dist/core/runner/prepare-task.js +2 -2
  41. package/dist/core/runner/run-leg.js +2 -13
  42. package/dist/core/runner/run-recovery-lanes.js +4 -20
  43. package/dist/core/runner/run-settle-and-teardown.js +2 -16
  44. package/dist/core/runner/run-turn-boundary.js +4 -20
  45. package/dist/core/runner/runtask.js +3 -5
  46. package/dist/core/runner/stream-settle-backstop.js +5 -5
  47. package/dist/core/runner/tool-end-body.js +5 -4
  48. package/dist/core/runner/tool-output-projection.js +3 -2
  49. package/dist/core/runner/turn-attachments.d.ts +0 -20
  50. package/dist/core/runner/turn-attachments.js +41 -36
  51. package/dist/core/session-store.js +2 -4
  52. package/dist/core/session.d.ts +4 -1
  53. package/dist/core/session.js +2 -1
  54. package/dist/core/task-event.d.ts +19 -9
  55. package/dist/core/task-registry-shared.js +1 -0
  56. package/dist/core/thrown-value.d.ts +52 -11
  57. package/dist/core/thrown-value.js +35 -1
  58. package/dist/core/tool-catalog-entries.js +1 -1
  59. package/dist/core/tool-errors.d.ts +6 -0
  60. package/dist/core/tool-errors.js +6 -14
  61. package/dist/core/tool-model-gate.js +3 -15
  62. package/dist/core/tools.js +15 -16
  63. package/dist/core/untrusted-text.d.ts +6 -0
  64. package/dist/core/untrusted-text.js +1 -0
  65. package/dist/core/workflow-run-store-contract.js +13 -0
  66. package/dist/core/workflow-run-store.d.ts +15 -0
  67. package/dist/core/workflow-run-store.js +1 -0
  68. package/dist/engine/llm/types.d.ts +4 -4
  69. package/dist/engine/loop/agent-loop.js +10 -24
  70. package/dist/index.d.ts +1 -1
  71. package/dist/index.js +1 -1
  72. package/dist/orchestration/workflow.js +44 -23
  73. package/dist/stores/file/file-history-store.js +12 -11
  74. package/dist/tools/fs/bash-program-position.d.ts +23 -0
  75. package/dist/tools/fs/bash-program-position.js +11 -0
  76. package/dist/tools/fs/bash-readonly-classifier.d.ts +30 -7
  77. package/dist/tools/fs/bash-readonly-classifier.js +31 -10
  78. package/dist/tools/fs/fs-bash.d.ts +30 -1
  79. package/dist/tools/fs/fs-bash.js +123 -54
  80. package/dist/tools/fs/read-deny.d.ts +14 -0
  81. package/dist/tools/fs/read-deny.js +5 -0
  82. package/dist/tools/fs/safety.d.ts +1 -1
  83. package/dist/tools/fs/safety.js +2 -1
  84. package/dist/tools/monitor.d.ts +5 -0
  85. package/dist/tools/monitor.js +7 -0
  86. package/package.json +1 -1
  87. package/test/export-surface.snapshot.json +7 -3
  88. package/wire-changes/core@7.23.5.json +133 -0
  89. package/wire-changes/core@7.24.0.json +249 -0
@@ -29,7 +29,7 @@ import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
29
29
  import { announceListingTruncation } from "../skill-listing-budget.js";
30
30
  import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
31
31
  import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
32
- import { asThrownError, thrownText } from "../thrown-value.js";
32
+ import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
33
33
  export async function runLeg(input) {
34
34
  const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
35
35
  let final;
@@ -693,18 +693,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
693
693
  }
694
694
  catch (err) {
695
695
  const execError = `Error: ${thrownText(err)}`;
696
- const marks = (() => {
697
- if (err === null || typeof err !== "object")
698
- return undefined;
699
- const src = err;
700
- let d;
701
- if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
702
- d = { ...src.details };
703
- }
704
- if (typeof src.errorKind === "string")
705
- d = { ...(d ?? {}), errorKind: src.errorKind };
706
- return d;
707
- })();
696
+ const marks = thrownMarksOf(err);
708
697
  await settleEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
709
698
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
710
699
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
2
2
  import { planRejectionClears } from "../context-edit.js";
3
3
  import { landStaleResultClears } from "./prepare-context-lane.js";
4
- import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact } from "../auto-compaction.js";
4
+ import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact, mintCompactedEvent } from "../auto-compaction.js";
5
5
  import { emitTrace } from "../trace.js";
6
6
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
7
7
  import { rebaseCadenceWindows } from "./turn-attachments.js";
@@ -48,26 +48,10 @@ export function runRecoveryLanes(input) {
48
48
  if (comp.compacted) {
49
49
  compactionBreaker.failures = 0;
50
50
  recordCompactionReuse(runner, prepared, comp);
51
- if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
52
- const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
53
- rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
51
+ if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON && comp.postTriggerTokens !== undefined) {
52
+ rs.counters.compactionFloor = Math.ceil(comp.postTriggerTokens * COMPACTION_REGROWTH_FACTOR);
54
53
  }
55
- queue.push({
56
- type: "compacted",
57
- trigger: "forced",
58
- tokensBefore: comp.tokensBefore ?? 0,
59
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
60
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
61
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
62
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
63
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
64
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
65
- ...(comp.modelFallback ? { modelFallback: true } : {}),
66
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
67
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
68
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
69
- ...ident(),
70
- });
54
+ queue.push(mintCompactedEvent(comp, "forced", ident()));
71
55
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
72
56
  const pd = comp.phaseDurations;
73
57
  const pdDur = comp.durationMs;
@@ -20,27 +20,13 @@ import { generatePromptSuggestions } from "./prompt-suggestions.js";
20
20
  import { settleTeardownLeg } from "./teardown-bounded.js";
21
21
  import { autoRunConsolidationFromRecommendation, autoRunOnRecommendationEffective, scopeHasUnconsumedRecommendation } from "./memory-consolidation-driver.js";
22
22
  import { amendTerminal, terminalProjection } from "./terminal-projection.js";
23
+ import { mintCompactedEvent } from "../auto-compaction.js";
23
24
  import { asThrownError } from "../thrown-value.js";
24
25
  export async function runSettleAndTeardown(input) {
25
26
  const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, owedUserInput, setResult, onSuggestions, onSuspend, runner, sessions } = input;
26
27
  try {
27
28
  if (comp?.compacted) {
28
- queue.push({
29
- type: "compacted",
30
- trigger: "auto",
31
- tokensBefore: comp.tokensBefore ?? 0,
32
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
33
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
34
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
35
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
36
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
37
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
38
- ...(comp.modelFallback ? { modelFallback: true } : {}),
39
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
40
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
41
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
42
- ...ident(),
43
- });
29
+ queue.push(mintCompactedEvent(comp, "auto", ident()));
44
30
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
45
31
  const pd = comp.phaseDurations;
46
32
  const pdDur = comp.durationMs;
@@ -1,7 +1,7 @@
1
1
  import { mintSystemReminder } from "../reminder-mint.js";
2
2
  import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
3
3
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
4
- import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
4
+ import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, mintCompactedEvent, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
5
5
  import { emitTrace } from "../trace.js";
6
6
  import { redactThenCut } from "../../agents/subagent-steps.js";
7
7
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, isIntactSource, rebaseCadenceWindows, renderAgentListingFrame, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingFrame, renderToolsDelta } from "./turn-attachments.js";
@@ -643,22 +643,7 @@ export function createTurnBoundary(input) {
643
643
  }
644
644
  recordCompactionReuse(runner, prepared, comp);
645
645
  if (comp.compacted) {
646
- queue.push({
647
- type: "compacted",
648
- trigger: passTrigger,
649
- tokensBefore: comp.tokensBefore ?? 0,
650
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
651
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
652
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
653
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
654
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
655
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
656
- ...(comp.modelFallback ? { modelFallback: true } : {}),
657
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
658
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
659
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
660
- ...ident(),
661
- });
646
+ queue.push(mintCompactedEvent(comp, passTrigger, ident()));
662
647
  if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
663
648
  compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
664
649
  queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
@@ -674,9 +659,8 @@ export function createTurnBoundary(input) {
674
659
  rs.attach.attachState.postCompactPending = true;
675
660
  rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
676
661
  }
677
- if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
678
- const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
679
- rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
662
+ if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON && comp.postTriggerTokens !== undefined) {
663
+ rs.counters.compactionFloor = Math.ceil(comp.postTriggerTokens * COMPACTION_REGROWTH_FACTOR);
680
664
  }
681
665
  }
682
666
  }
@@ -6,7 +6,7 @@ import { createRapidRefillState } from "../auto-compaction.js";
6
6
  import { boundInputHashOf } from "../canonical-json.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { emitTaskOutcome } from "../task-outcome.js";
9
- import { authCarrierFingerprint, normalizeBaseUrl } from "../../brain/route-adjudicator.js";
9
+ import { authCarrierFingerprint, displayUrlOf, routeKeyOf } from "../../brain/route-adjudicator.js";
10
10
  import { runWithReasoningWireFacts } from "../../brain/status-sink.js";
11
11
  import { expandTiers } from "../roles.js";
12
12
  import { screenSwappableDeps } from "../swappable-deps.js";
@@ -208,10 +208,8 @@ export class Runner {
208
208
  const prior = this.deps.models?.[name];
209
209
  if (!prior)
210
210
  continue;
211
- const from = normalizeBaseUrl(prior.baseUrl);
212
- const to = normalizeBaseUrl(nextModel.baseUrl);
213
- if (from !== to && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
214
- movedEntries.push({ modelId: name, from, to });
211
+ if (routeKeyOf(prior.baseUrl) !== routeKeyOf(nextModel.baseUrl) && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
212
+ movedEntries.push({ modelId: name, from: displayUrlOf(prior.baseUrl), to: displayUrlOf(nextModel.baseUrl) });
215
213
  }
216
214
  }
217
215
  }
@@ -6,7 +6,7 @@ import { createSafeNotifier } from "../safe-notify.js";
6
6
  import { settleTeardownLeg } from "./teardown-bounded.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { deliverDelegationLifecycle } from "../types.js";
9
- import { asThrownError, thrownText } from "../thrown-value.js";
9
+ import { asThrownError, readMember, thrownText } from "../thrown-value.js";
10
10
  export function streamSettleBackstop(input) {
11
11
  const { spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner, sessions, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
12
12
  const onRunRejected = async (err) => {
@@ -57,11 +57,11 @@ export function streamSettleBackstop(input) {
57
57
  }
58
58
  const remoteEnvFailure = (() => {
59
59
  let cur = err;
60
- for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
61
- const note = cur.remoteEnvFailure;
60
+ for (let depth = 0; cur !== null && cur !== undefined && typeof cur === "object" && depth < 8; depth++) {
61
+ const note = readMember(cur, "remoteEnvFailure");
62
62
  if (note !== undefined)
63
63
  return [note];
64
- cur = cur.cause;
64
+ cur = readMember(cur, "cause");
65
65
  }
66
66
  return undefined;
67
67
  })();
@@ -79,7 +79,7 @@ export function streamSettleBackstop(input) {
79
79
  return observed !== undefined && observed.length > 0 ? { editedFiles: observed } : {};
80
80
  })(),
81
81
  ...(() => {
82
- const hinted = err.retryAfterMs;
82
+ const hinted = readMember(err, "retryAfterMs");
83
83
  return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
84
84
  ? { retryAfterMs: hinted }
85
85
  : {};
@@ -1,4 +1,5 @@
1
1
  import {} from "../../internal/harness.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
3
4
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
4
5
  import { toolOutputFrom } from "./tool-output-projection.js";
@@ -35,11 +36,11 @@ export function batchResponseDigest(result) {
35
36
  export function toolEndBodyFrom(result, isError, structured, gate, gatedCallIdOfRun) {
36
37
  const o = toolOutputFrom(result);
37
38
  const st = structured(result);
38
- const det = isError ? result?.details : undefined;
39
- const codeRaw = det?.code;
40
- const kindRaw = det?.errorKind;
39
+ const det = isError ? readMember(result, "details") : undefined;
40
+ const codeRaw = readMember(det, "code");
41
+ const kindRaw = readMember(det, "errorKind");
41
42
  const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
42
- const deliveredRaw = det?.delivered;
43
+ const deliveredRaw = readMember(det, "delivered");
43
44
  const delivered = typeof deliveredRaw === "string" && MCP_DELIVERY_VERDICTS.includes(deliveredRaw) ? deliveredRaw : undefined;
44
45
  const gatedCallId = code === "gate.parked" ? gatedCallIdOfRun : undefined;
45
46
  return {
@@ -1,4 +1,5 @@
1
1
  import { engineCardTypes } from "../tool-registry.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
3
4
  const safeJsonLength = (v) => {
4
5
  try {
@@ -62,10 +63,10 @@ export function createStructuredProjector(opts = {}) {
62
63
  const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
63
64
  const announced = new Set();
64
65
  return (result) => {
65
- const details = result !== null && typeof result === "object" ? result.details : undefined;
66
+ const details = readMember(result, "details");
66
67
  if (details === null || typeof details !== "object")
67
68
  return undefined;
68
- const t = details.type;
69
+ const t = readMember(details, "type");
69
70
  if (typeof t !== "string")
70
71
  return undefined;
71
72
  if (!allowed.has(t)) {
@@ -346,26 +346,6 @@ export declare const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
346
346
  * `steering_injected` echo are all keyed on that wordlist.
347
347
  */
348
348
  export declare function createAttachmentState(): AttachmentState;
349
- /**
350
- * H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire card projector
351
- * projection and its 4×16K caps: a huge todo list must shrink into a bounded snapshot here, never
352
- * disarm the reminder by being dropped wholesale). Only the first-party discriminants `todo` /
353
- * `task` / `task-list` are recognized — every other card type (subagent `agent`/`fork`/`task-output`,
354
- * bash, fs, mcp, …) is structurally ignored, which is also the child-isolation contract: a subagent's
355
- * own todo state never reaches the parent's onToolEnd, and its delegation-card types don't parse here.
356
- *
357
- * task #51 F3 (窗口复位/投影摄入拆分): this reducer is PROJECTION INGEST ONLY — it
358
- * never touches the write windows. CC's window anchor is the tool_use block's PRESENCE in the
359
- * transcript (qFm/BFm match the block by name, :480217-480219/:480253-480254, with no execution-
360
- * outcome check), so the run loop stamps the windows on INITIATION via {@link stampWriteAnchor}
361
- * (inside {@link advanceCadenceClock}'s message walk); ingest stays success-only — a failed write
362
- * has no trustworthy `details`, exactly like CC's reminder body rendering from the authoritative
363
- * store rather than from tool results.
364
- *
365
- * Idempotent (same event twice ⇒ same state) and commutative across DISTINCT task ids (F10); two
366
- * whole-list writes (TodoWrite / TaskList) racing in one batch are last-write-wins by nature — the
367
- * later list IS the truer snapshot, documented rather than fought.
368
- */
369
349
  export declare function reduceToolEnd(state: AttachmentState, details: unknown): void;
370
350
  /** task #51 F2/F3 — which reminder window a tool_use block resets. Resolved by the run loop (alias-aware:
371
351
  * ToolSpec.aliases over the mounted roster; retired-name normalization is gone); the producers
@@ -1,4 +1,5 @@
1
1
  import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
3
4
  import { protocolOf } from "../protocol-table.js";
4
5
  import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
@@ -24,6 +25,7 @@ export const CHANGED_FILES_MAX = 20;
24
25
  export const CHANGED_FILES_MTIME_EPS_MS = 2000;
25
26
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
26
27
  const PROJECTION_ITEMS_MAX = 50;
28
+ const PROJECTION_SCAN_MAX = 10_000;
27
29
  const PROJECTION_CONTENT_MAX = 80;
28
30
  const LISTING_FRAME_FLOOR_BYTES = 768;
29
31
  const DATE_CHANGE_MAX_BYTES = 160;
@@ -72,52 +74,55 @@ export function createAttachmentState() {
72
74
  postCompactPending: false,
73
75
  };
74
76
  }
77
+ function readListLength(value) {
78
+ const n = value !== null && typeof value === "object" ? readMember(value, "length") : undefined;
79
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 ? Math.min(n, PROJECTION_SCAN_MAX) : undefined;
80
+ }
81
+ function readTaskEntry(raw) {
82
+ const id = readMember(raw, "id");
83
+ if (typeof id !== "string")
84
+ return undefined;
85
+ const subject = readMember(raw, "subject");
86
+ const status = readMember(raw, "status");
87
+ return { id, item: { content: typeof subject === "string" ? subject.slice(0, PROJECTION_CONTENT_MAX) : "", status: typeof status === "string" ? status : "pending" } };
88
+ }
75
89
  export function reduceToolEnd(state, details) {
76
- if (details === null || typeof details !== "object")
77
- return;
78
- const d = details;
79
- if (d.type === "todo") {
80
- const newTodos = d.newTodos;
81
- if (!Array.isArray(newTodos))
90
+ const type = readMember(details, "type");
91
+ if (type === "todo") {
92
+ const newTodos = readMember(details, "newTodos");
93
+ const count = readListLength(newTodos);
94
+ if (count === undefined)
82
95
  return;
83
- const shaped = newTodos.flatMap((t) => {
84
- const item = t;
85
- return item && typeof item === "object" && typeof item.content === "string" && typeof item.status === "string"
86
- ? [{ content: item.content.slice(0, PROJECTION_CONTENT_MAX), status: item.status }]
87
- : [];
88
- });
96
+ const shaped = [];
97
+ for (let i = 0; i < count; i++) {
98
+ const item = readMember(newTodos, String(i));
99
+ const content = readMember(item, "content");
100
+ const status = readMember(item, "status");
101
+ if (typeof content === "string" && typeof status === "string")
102
+ shaped.push({ content: content.slice(0, PROJECTION_CONTENT_MAX), status });
103
+ }
89
104
  const allDone = shaped.length > 0 && shaped.every((t) => t.status === "completed");
90
105
  state.todo = allDone ? { items: [] } : { items: shaped.slice(0, PROJECTION_ITEMS_MAX) };
91
106
  }
92
- else if (d.type === "task") {
107
+ else if (type === "task") {
93
108
  const map = state.taskItems ?? (state.taskItems = new Map());
94
- const deleted = d.deleted;
109
+ const deleted = readMember(details, "deleted");
95
110
  if (typeof deleted === "string")
96
111
  map.delete(deleted);
97
- const task = d.task;
98
- if (task !== null && typeof task === "object") {
99
- const t = task;
100
- if (typeof t.id === "string") {
101
- map.set(t.id, {
102
- content: typeof t.subject === "string" ? t.subject.slice(0, PROJECTION_CONTENT_MAX) : "",
103
- status: typeof t.status === "string" ? t.status : "pending",
104
- });
105
- }
106
- }
107
- }
108
- else if (d.type === "task-list") {
109
- const tasks = d.tasks;
110
- if (!Array.isArray(tasks))
112
+ const entry = readTaskEntry(readMember(details, "task"));
113
+ if (entry !== undefined)
114
+ map.set(entry.id, entry.item);
115
+ }
116
+ else if (type === "task-list") {
117
+ const tasks = readMember(details, "tasks");
118
+ const count = readListLength(tasks);
119
+ if (count === undefined)
111
120
  return;
112
121
  const map = new Map();
113
- for (const raw of tasks) {
114
- const t = raw;
115
- if (t && typeof t === "object" && typeof t.id === "string") {
116
- map.set(t.id, {
117
- content: typeof t.subject === "string" ? t.subject.slice(0, PROJECTION_CONTENT_MAX) : "",
118
- status: typeof t.status === "string" ? t.status : "pending",
119
- });
120
- }
122
+ for (let i = 0; i < count; i++) {
123
+ const entry = readTaskEntry(readMember(tasks, String(i)));
124
+ if (entry !== undefined)
125
+ map.set(entry.id, entry.item);
121
126
  }
122
127
  state.taskItems = map;
123
128
  }
@@ -1,5 +1,6 @@
1
1
  import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js";
2
2
  import { isSessionConflict } from "./session.js";
3
+ import { readMember } from "./thrown-value.js";
3
4
  const DAY_MS = 24 * 60 * 60 * 1000;
4
5
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
5
6
  export { SESSION_DEFAULT_TTL_DAYS };
@@ -285,8 +286,5 @@ async function settledForkOptions(source) {
285
286
  return {};
286
287
  }
287
288
  function isNotFound(err) {
288
- return (typeof err === "object" &&
289
- err !== null &&
290
- "code" in err &&
291
- err.code === "not_found");
289
+ return readMember(err, "code") === "not_found";
292
290
  }
@@ -8,7 +8,10 @@ export type { SessionWriteOptions } from "../internal/harness.js";
8
8
  import type { Session } from "../internal/harness.js";
9
9
  import type { SessionTreeEntry } from "../internal/harness.js";
10
10
  export type { SessionPlacementRecord } from "../internal/harness.js";
11
- /** True when `err` is a session optimistic-lock conflict (a concurrent writer won the branch leaf). */
11
+ /** True when `err` is a session optimistic-lock conflict (a concurrent writer won the branch leaf). Both
12
+ * halves read what a HOST `SessionStorage` threw, so both are guarded — unreadable is not a conflict,
13
+ * never a raise inside the classify-and-rethrow guard. The class test is KEPT: widening the verdict to
14
+ * any value carrying the word would arm the CAS retry loop on a foreign error (KL-942b). */
12
15
  export declare function isSessionConflict(err: unknown): boolean;
13
16
  /**
14
17
  * Subagent transcript persistence — the ACQUIRE-form placement declaration a trusted spawner passes
@@ -3,8 +3,9 @@ export { InMemorySessionRepo, InMemorySessionStorage, uuidv7, validateEntriesFor
3
3
  export { BaseSessionStorage, leafIdAfterEntry, } from "../internal/harness.js";
4
4
  export { SessionError } from "../internal/harness.js";
5
5
  import { SessionError } from "../internal/harness.js";
6
+ import { readMember, thrownIs } from "./thrown-value.js";
6
7
  export function isSessionConflict(err) {
7
- return err instanceof SessionError && err.code === "conflict";
8
+ return thrownIs(err, SessionError) && readMember(err, "code") === "conflict";
8
9
  }
9
10
  export function hasSessionFork(store) {
10
11
  return typeof store.fork === "function";
@@ -592,16 +592,23 @@ export type TaskEvent = ({
592
592
  type: "compacted";
593
593
  trigger: "auto" | "manual" | "forced";
594
594
  tokensBefore: number;
595
- /** TB telemetry B4: post-compaction context size in the TRIGGER-GATE coordinate
596
- * (`postTriggerTokens` = estimate + anchor-less overhead). Pairs with {@link triggerTokensBefore}
597
- * — NOT with `tokensBefore` (which is the CC `pre_tokens` value in prepareCompaction's coordinate,
598
- * lacking the overhead term; subtracting across the two coordinates would mis-state the delta,
599
- * the audit-B-1 / P-13 class). Absent when the post measurement failed (best-effort, never fails a
600
- * successful compaction). */
595
+ /** How much context this compaction FREED, as the engine measured it: one structural sum over
596
+ * the pre-messages minus the same sum over the post-messages, inside the pass. **The only
597
+ * subtractable number on this event**, and what a context meter should render. Clamped ≥ 0 at
598
+ * the mint, so `0` means "freed nothing", never "could not tell". Absent ⇒ the post
599
+ * measurement failed and no delta exists — never a stand-in. */
600
+ freedTokens?: number;
601
+ /** DIAGNOSTIC, NOT subtractable. Post-compaction size in the TRIGGER-GATE coordinate
602
+ * (structural estimate + the fixed prompt overhead). {@link triggerTokensBefore} is not its
603
+ * matching pre-value: the gate reads a usage-ANCHORED figure whenever the provider's last
604
+ * usage is fresh, while this is always structural + overhead, so the difference mixes
605
+ * coordinates and goes NEGATIVE on ordinary auto passes (3037 − 4591 = −1554, measured on a
606
+ * 4k window). Use {@link freedTokens}. Absent when the post measurement failed. */
601
607
  tokensAfter?: number;
602
- /** TB telemetry B4: pre-compaction size in the SAME trigger-gate coordinate as {@link tokensAfter}
603
- * (`triggerTokens` — the exact quantity the compaction gate compared). `triggerTokensBefore −
604
- * tokensAfter` is the self-consistent freed delta. Absent on the legacy emitter. */
608
+ /** DIAGNOSTIC, NOT subtractable. The quantity the gate compared against its threshold —
609
+ * usage-anchored when the anchor is fresh, structural + overhead when it is not. It answers
610
+ * WHAT tripped the trigger, a different question from how much was freed; left un-clamped and
611
+ * un-paired rather than made to look subtractable. Absent on the legacy emitter. */
605
612
  triggerTokensBefore?: number;
606
613
  /** TB telemetry B4: wall time of the whole compaction pass (summary call included) — what the
607
614
  * boundary pause actually cost. Absent on legacy emitters. */
@@ -823,6 +830,9 @@ export type TaskEvent = ({
823
830
  * Live/ephemeral — NOT persisted (a resumable trace re-derives usage from the result), like `status`.
824
831
  * Maps to CC `SDKTaskProgress` (camelCase here → snake_case `task_id`/`total_tokens`/`tool_uses`/`duration_ms`
825
832
  * at the SDK/service projection, the same convention as `toolCallId`→`tool_use_id`).
833
+ * `taskId` is the child RUN's id (`spec.taskId ?? sessionId`), NOT the registry handle: the
834
+ * background lanes launch with no explicit `spec.taskId`, so it equals the child's session id and
835
+ * hence its `transcriptId`. Both domains are tabled in `docs/INTEGRATION-CORE.md` §1.6a.
826
836
  */
827
837
  type: "task_progress";
828
838
  taskId: string;
@@ -227,6 +227,7 @@ export function formatWorkflowRun(run) {
227
227
  : {}),
228
228
  ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
229
229
  ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
230
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
230
231
  ...(run.status === "completed" && run.result !== undefined
231
232
  ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
232
233
  : {}),
@@ -1,18 +1,40 @@
1
+ /**
2
+ * Is this an Error, including one minted in ANOTHER REALM?
3
+ *
4
+ * `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` minted outside it — by
5
+ * a script the engine ran through a deployment-supplied runner, or a duplicated module instance — fails and would be
6
+ * described as an anonymous object, losing the message that is the entire point of reporting it. The brand
7
+ * check is the realm-independent one: an object with the `[[ErrorData]]` internal slot answers `[object Error]`
8
+ * no matter where it was made, and a proxy forwards it. A plain object can forge the tag through
9
+ * `Symbol.toStringTag`, which costs nothing — the only thing this decides is whether we look for a `message`,
10
+ * and a forger that supplies a string message has told us what it wanted rendered anyway.
11
+ *
12
+ * Guarded, because both operations are attacker-reachable: `instanceof` walks a prototype chain a revoked
13
+ * proxy traps, and the tag is a getter that can throw.
14
+ */
15
+ export declare function isErrorLike(value: unknown): boolean;
16
+ /** `value instanceof ctor`, TOTAL — the class half of {@link readMember}'s rule. The prototype-chain walk
17
+ * runs two proxy traps and a revoked proxy throws on both, while every caller tests a value someone else
18
+ * threw inside the handler that decides how a failure settles. Unrecognizable is not that class, and
19
+ * {@link isErrorLike} is this same rule for `Error`. */
20
+ export declare function thrownIs<T>(value: unknown, ctor: abstract new (...args: never[]) => T): value is T;
21
+ /** The NAME a thrown value carries (`Error.prototype.name` and its subclasses' overrides), read
22
+ * realm-independently and guarded — `undefined` when the value is not error-like or its `name` is not a
23
+ * string. Callers that compare against a name CONSTANT need exactly this: the comparison is on the WORD
24
+ * precisely because the value crossed a seam, so binding the test to this realm's `Error.prototype`
25
+ * defeats it. Whether an EMPTY name folds to `undefined` is policy and stays at the call site. */
26
+ export declare function thrownName(err: unknown): string | undefined;
1
27
  /**
2
28
  * Turning a value that was THROWN at us into text — the ONE rule, for the whole engine.
3
29
  *
4
- * The idiom this replaces is `err instanceof Error ? err.message : String(err)` (and its template-literal
5
- * twin, `` `${err}` ``). Both spell a PARTIAL function on the non-Error arm: `String` calls the value's own
6
- * `toString` / `Symbol.toPrimitive`, so it throws on a value that has neither (a null-prototype object) and
7
- * on one whose own hook throws. The thrower is not ours — a deployment hook, a plugin callback, a model-
8
- * supplied tool body may reject with any value at all — so the rendering step was the one step that could
9
- * destroy the report it exists to produce. Where the render sat inside a promise rejection handler, the
10
- * resulting `TypeError` was an unhandled rejection and took the whole process down: a caller's choice of
11
- * rejection value decided whether the engine stayed alive.
30
+ * The idiom it replaces — `err instanceof Error ? err.message : String(err)` — is PARTIAL on its non-Error
31
+ * arm: `String` calls the value's own `toString` / `Symbol.toPrimitive`, so it throws on a value that has
32
+ * neither and on one whose hook throws. The thrower is not ours (a deployment hook, a plugin callback, a
33
+ * model-supplied tool body may reject with anything), so the rendering step was the one step that could
34
+ * destroy the report it exists to produce — and inside a promise rejection handler it took the process down.
12
35
  *
13
- * Totality is therefore the contract, and it is met the same way the engine's structural describer already
14
- * meets it: the value is never asked to say what it is. Three arms, in this order, chosen so that the
15
- * dominant shapes keep their EXACT previous bytes and only the shapes that used to be useless or fatal move:
36
+ * TOTALITY is therefore the contract, met the way the structural describer meets it: the value is never
37
+ * asked to say what it is. Three arms, ordered so the dominant shapes keep their EXACT previous bytes:
16
38
  * - a thrown STRING is its own message (a bare `throw "…"`), returned verbatim — the describer would quote
17
39
  * it, which is right when rejecting a config value and wrong when relaying a message someone threw;
18
40
  * - an ERROR hands over `message` verbatim — no cap, no one-lining, no quoting. This is the arm nearly all
@@ -75,5 +97,24 @@ export declare function asThrownError(err: unknown): Error;
75
97
  * is `undefined` for BOTH "absent" and "unreadable" — deliberately, because a caller that must tell those
76
98
  * apart is asking a question an untrusted value is not obliged to answer; what it does instead is fall back
77
99
  * to describing the value, which {@link thrownText} already does totally.
100
+ *
101
+ * The read walks the PROTOTYPE CHAIN deliberately — `name` lives on `Error.prototype`. `"__proto__"` is
102
+ * the one key where that differs from an own read, and its only caller is {@link readOwnMembers}, which
103
+ * passes it only when it IS an own key (a data property shadows the accessor; both readings agree).
78
104
  */
79
105
  export declare function readMember(value: unknown, key: string): unknown;
106
+ /** The PLURAL of {@link readMember}: read a value's own enumerable members, once each, into an inert
107
+ * snapshot — for sites that want a COPY of a whole bag an untrusted value handed over (a thrown
108
+ * error's `details` mark, on its way onto a tool result). The idiom it replaces is `{ ...value }`,
109
+ * a second way of running the value's code: the spread asks for its own keys and evaluates every
110
+ * getter behind them, so one hostile member took the copy down with it, and a revoked proxy traps the
111
+ * enumeration itself (even `Array.isArray` on one throws). Non-objects, arrays (spreading one yields
112
+ * index keys) and anything whose keys cannot be enumerated answer `undefined` — "there is no bag
113
+ * here"; a member that refuses to be read comes back `undefined` in a PRESENT key. */
114
+ export declare function readOwnMembers(value: unknown): Record<string, unknown> | undefined;
115
+ /** The MACHINE-READABLE marks a thrown value carries (`details`, with a string `errorKind` folded in) —
116
+ * the one extractor for both legs that lift them onto a tool result. The live leg and the resume leg
117
+ * each had their own copy, and the resume copy still spread the bag and called `Array.isArray` on it,
118
+ * so a hostile `details` failed the whole leg where the live one had stopped losing anything but the
119
+ * mark. `undefined` = nothing to carry; a caller that must always present a bag folds that itself. */
120
+ export declare function thrownMarksOf(err: unknown): Record<string, unknown> | undefined;
@@ -1,5 +1,5 @@
1
1
  import { describeRejectedValue } from "./tool-search-setting.js";
2
- function isErrorLike(value) {
2
+ export function isErrorLike(value) {
3
3
  try {
4
4
  if (value instanceof Error)
5
5
  return true;
@@ -9,6 +9,18 @@ function isErrorLike(value) {
9
9
  return false;
10
10
  }
11
11
  }
12
+ export function thrownIs(value, ctor) {
13
+ try {
14
+ return value instanceof ctor;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function thrownName(err) {
21
+ const name = isErrorLike(err) ? readMember(err, "name") : undefined;
22
+ return typeof name === "string" ? name : undefined;
23
+ }
12
24
  export function thrownText(err) {
13
25
  try {
14
26
  if (typeof err === "string")
@@ -54,3 +66,25 @@ export function readMember(value, key) {
54
66
  return undefined;
55
67
  }
56
68
  }
69
+ export function readOwnMembers(value) {
70
+ let keys;
71
+ try {
72
+ if (value === null || typeof value !== "object" || Array.isArray(value))
73
+ return undefined;
74
+ keys = Object.keys(value);
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ const out = {};
80
+ for (const key of keys)
81
+ Object.defineProperty(out, key, { value: readMember(value, key), enumerable: true, writable: true, configurable: true });
82
+ return out;
83
+ }
84
+ export function thrownMarksOf(err) {
85
+ const details = readOwnMembers(readMember(err, "details"));
86
+ const errorKind = readMember(err, "errorKind");
87
+ if (typeof errorKind !== "string")
88
+ return details;
89
+ return { ...(details ?? {}), errorKind };
90
+ }