@sema-agent/core 7.23.5 → 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 (52) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/agents/list-agents-tool.d.ts +7 -4
  3. package/dist/agents/list-agents-tool.js +12 -1
  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/degrading.js +8 -9
  8. package/dist/brain/route-adjudicator.d.ts +25 -12
  9. package/dist/brain/route-adjudicator.js +89 -23
  10. package/dist/brain/route-conformance.js +9 -0
  11. package/dist/brain/stream-engine.js +11 -9
  12. package/dist/core/arg-summary.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +42 -1
  14. package/dist/core/auto-compaction.js +21 -1
  15. package/dist/core/auto-mode.js +2 -8
  16. package/dist/core/exec-gate.js +3 -5
  17. package/dist/core/file-history-store.js +11 -10
  18. package/dist/core/hooks.d.ts +7 -0
  19. package/dist/core/mcp.js +7 -5
  20. package/dist/core/memory-engine/distiller.d.ts +7 -4
  21. package/dist/core/memory-engine/distiller.js +11 -3
  22. package/dist/core/memory.js +1 -0
  23. package/dist/core/runner/assemble-result.js +12 -19
  24. package/dist/core/runner/contracts.d.ts +1 -0
  25. package/dist/core/runner/prepare-file-history.js +19 -0
  26. package/dist/core/runner/run-recovery-lanes.js +4 -20
  27. package/dist/core/runner/run-settle-and-teardown.js +2 -16
  28. package/dist/core/runner/run-turn-boundary.js +4 -20
  29. package/dist/core/runner/runtask.js +3 -5
  30. package/dist/core/runner/stream-settle-backstop.js +5 -5
  31. package/dist/core/runner/tool-end-body.js +5 -4
  32. package/dist/core/runner/tool-output-projection.js +3 -2
  33. package/dist/core/runner/turn-attachments.d.ts +0 -20
  34. package/dist/core/runner/turn-attachments.js +41 -36
  35. package/dist/core/session-store.js +2 -4
  36. package/dist/core/session.d.ts +4 -1
  37. package/dist/core/session.js +2 -1
  38. package/dist/core/task-event.d.ts +19 -9
  39. package/dist/core/task-registry-shared.js +1 -0
  40. package/dist/core/thrown-value.d.ts +24 -13
  41. package/dist/core/thrown-value.js +13 -1
  42. package/dist/core/tool-errors.d.ts +6 -0
  43. package/dist/core/tool-errors.js +6 -14
  44. package/dist/core/tools.js +8 -9
  45. package/dist/engine/llm/types.d.ts +4 -4
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/orchestration/workflow.js +44 -23
  49. package/dist/stores/file/file-history-store.js +12 -11
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +7 -3
  52. package/wire-changes/core@7.24.0.json +249 -0
@@ -1,13 +1,13 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
2
  import { STREAM_TORN_WARNING } from "./stream-shared.js";
3
3
  import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
4
- import { abortLikeName, BrainError, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
4
+ import { abortLikeName, BrainError, brainErrorCodeOf, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
5
5
  import { stripUrlCredentials } from "./route-adjudicator.js";
6
6
  import { classifyInputTooLong } from "./input-too-long.js";
7
7
  import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
8
8
  import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
9
9
  import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
10
- import { thrownText } from "../core/thrown-value.js";
10
+ import { readMember, thrownText } from "../core/thrown-value.js";
11
11
  import { redactSecrets } from "../core/untrusted-egress.js";
12
12
  const DEFAULT_MAX_RETRIES = 10;
13
13
  const MAX_RETRIES_ENV_CEILING = 15;
@@ -274,15 +274,17 @@ export function runStreamingBrain(args) {
274
274
  const errorMsg = emptyAssistant(model);
275
275
  errorMsg.stopReason = aborted ? "aborted" : "error";
276
276
  errorMsg.errorMessage = thrownText(err);
277
- if (!aborted && err instanceof BrainError) {
278
- if (err.inputTooLong === true)
277
+ if (!aborted && brainErrorCodeOf(err) !== undefined) {
278
+ if (readMember(err, "inputTooLong") === true)
279
279
  errorMsg.errorKind = "input_too_long";
280
- else if (err.inputTooLongRuledOut === true)
280
+ else if (readMember(err, "inputTooLongRuledOut") === true)
281
281
  errorMsg.inputTooLongRuledOut = true;
282
- if (err.apiError === true) {
282
+ if (readMember(err, "apiError") === true) {
283
283
  errorMsg.isApiErrorMessage = true;
284
- if (namesTheFailure(err.status))
285
- errorMsg.apiErrorStatus = err.status;
284
+ const status = readMember(err, "status");
285
+ const statusNum = typeof status === "number" ? status : undefined;
286
+ if (namesTheFailure(statusNum))
287
+ errorMsg.apiErrorStatus = statusNum;
286
288
  if (requestIdSeen !== undefined)
287
289
  errorMsg.requestId = requestIdSeen;
288
290
  }
@@ -581,7 +583,7 @@ export function runStreamingBrain(args) {
581
583
  return false;
582
584
  }
583
585
  catch (e) {
584
- if (!(e instanceof BrainError) || e.code !== "server" || !parser.snapshot().hasSubstantiveText)
586
+ if (brainErrorCodeOf(e) !== "server" || !parser.snapshot().hasSubstantiveText)
585
587
  throw e;
586
588
  return true;
587
589
  }
@@ -196,14 +196,15 @@ export function runRedactionPasses(input, passes, report) {
196
196
  return result;
197
197
  }
198
198
  export const SECRET_PASSES = [
199
- { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
199
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur]|github_pat)[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
200
200
  { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
201
201
  { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
202
202
  { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
203
203
  { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
204
204
  { kind: "jwt", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
205
- { kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
205
+ { kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----|$)/g, replace: "[redacted]" },
206
206
  { kind: "bearer-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
207
+ { kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(AccountKey=)[A-Za-z0-9+/]{40,}={0,2}/g, replace: "$1[redacted]" },
207
208
  {
208
209
  kind: "keyword-value",
209
210
  confidence: "medium",
@@ -247,12 +248,22 @@ export function primaryActivityArg(args) {
247
248
  val = basename(path);
248
249
  }
249
250
  else if (url !== undefined) {
250
- try {
251
- const u = new URL(url);
252
- val = `${u.origin}${u.pathname}`;
251
+ const parsed = (() => { try {
252
+ return new URL(url);
253
253
  }
254
254
  catch {
255
- val = url.split(/[?#]/)[0].replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, "$1");
255
+ return undefined;
256
+ } })();
257
+ if (parsed !== undefined && parsed.host !== "") {
258
+ val = `${parsed.origin}${parsed.pathname}`;
259
+ }
260
+ else {
261
+ const head = url.split(/[?#]/, 1)[0] ?? url;
262
+ const sep = head.indexOf("://");
263
+ const scheme = sep > 0 && /^[a-z][a-z0-9+.-]*$/i.test(head.slice(0, sep)) ? head.slice(0, sep + 3) : "";
264
+ const rest = head.slice(scheme.length);
265
+ const at = rest.lastIndexOf("@");
266
+ val = scheme + (at === -1 ? rest : rest.slice(at + 1));
256
267
  }
257
268
  }
258
269
  else {
@@ -3,6 +3,7 @@ import type { Message, Model, Tool } from "../internal/llm.js";
3
3
  import type { Hooks } from "./hooks.js";
4
4
  import type { TraceEvent } from "./trace.js";
5
5
  import type { Brain } from "./types.js";
6
+ import type { TaskEvent, TaskEventIdentity } from "./task-event.js";
6
7
  /**
7
8
  * Default summarization focus, applied when a task does not provide `compaction.instructions`.
8
9
  * Addresses the common fidelity failure where summaries drop specific facts.
@@ -431,10 +432,13 @@ export interface MaybeCompactOptions {
431
432
  * return). Does NOT alter control flow. `reused` distinguishes a provider-supplied summary from a real
432
433
  * LLM `generateSummary`. `freedTokens` is the §25.2 structural delta (may be ~0 when a reused summary
433
434
  * frees little — see reuse guard (b): such a compaction does NOT raise the caller's anti-thrash floor).
435
+ * ABSENT when the post measurement failed — never a stand-in ("fully effective" is not true of a
436
+ * landing that leaves a summary behind). Every consumer reads absence as "no evidence": the floor
437
+ * stays where it is and the backoff does not flip.
434
438
  */
435
439
  onCompaction?: (info: {
436
440
  messagesCompacted: number;
437
- freedTokens: number;
441
+ freedTokens?: number;
438
442
  tokensBefore: number;
439
443
  reused: boolean;
440
444
  }) => void;
@@ -517,6 +521,8 @@ export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
517
521
  * (`compaction_outcome{outcome:"noop"}`) reads this flag. */
518
522
  noop?: true;
519
523
  tokensBefore?: number;
524
+ /** The §25.2 STRUCTURAL pre/post delta in ONE coordinate — the only subtractable quantity here.
525
+ * Absent ⇒ the post measurement failed and no delta was taken. */
520
526
  freedTokens?: number;
521
527
  triggerTokens?: number;
522
528
  /** P-13: post-compaction context size in the SAME coordinate as `triggerTokens` (estimate + anchor-less
@@ -627,6 +633,41 @@ export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
627
633
  * ordinary truncation (disclosed, not silent). */
628
634
  clampReason?: "budget" | "tolerance";
629
635
  }>;
636
+ /** What a landed compaction hands the wire frame. Structural, so both the full `maybeCompact` result
637
+ * and the settle lane's narrower `EndOfTaskCompaction` satisfy it without either importing the other. */
638
+ export interface CompactedFrameSource {
639
+ tokensBefore?: number;
640
+ freedTokens?: number;
641
+ postTriggerTokens?: number;
642
+ triggerTokens?: number;
643
+ durationMs?: number;
644
+ phaseDurations?: CompactionPhaseDurations;
645
+ firstKeptEntryId?: string;
646
+ attachedFiles?: Array<{
647
+ path: string;
648
+ chars: number;
649
+ truncated: boolean;
650
+ }>;
651
+ modelFallback?: true;
652
+ fallbackReason?: "window";
653
+ clampedRatio?: number;
654
+ clampReason?: "budget" | "tolerance";
655
+ }
656
+ /**
657
+ * THE mint of the `compacted` wire frame — one function, three lanes (the turn boundary, the
658
+ * prompt-too-long recovery, the end-of-task settle). The three used to carry a verbatim copy of the
659
+ * same fourteen-line literal, which is how a field reaches the wire on two lanes out of three: a key
660
+ * added to one copy is invisible in the others' diffs, and the only guard against it was a test that
661
+ * grepped the SOURCE TEXT of all three for a matching line. A behavior can be pinned; a copy can only
662
+ * be spelled.
663
+ *
664
+ * What differs per lane is exactly `trigger` and the identity envelope, so those are the parameters;
665
+ * everything else is derived here, once. Absence semantics live on the event type
666
+ * ({@link import("./task-event.js").TaskEvent}) and are the same whichever lane fires.
667
+ */
668
+ export declare function mintCompactedEvent(comp: CompactedFrameSource, trigger: "auto" | "manual" | "forced", identity: TaskEventIdentity): Extract<TaskEvent, {
669
+ type: "compacted";
670
+ }>;
630
671
  /**
631
672
  * design/123 D4 (r5) — trim-force BACKOFF transition (pure; the run-scoped bit lives in the caller).
632
673
  * Spiral protection replacing the §25.2 floor's role on the trim-force path: trim pressure re-arms
@@ -478,7 +478,7 @@ export async function maybeCompact(opts) {
478
478
  catch {
479
479
  }
480
480
  }
481
- let freedTokens = preChars;
481
+ let freedTokens;
482
482
  let postTriggerTokens;
483
483
  try {
484
484
  const post = (await opts.session.buildContext()).messages;
@@ -502,6 +502,7 @@ export async function maybeCompact(opts) {
502
502
  trigger,
503
503
  summary,
504
504
  tokensBefore: summaryTokensBefore,
505
+ ...(freedTokens !== undefined ? { freedTokens } : {}),
505
506
  ...(postTriggerTokens !== undefined ? { tokensAfter: postTriggerTokens } : {}),
506
507
  }), "compaction.postCompact");
507
508
  }
@@ -534,6 +535,25 @@ export async function maybeCompact(opts) {
534
535
  ...(clampReason !== undefined ? { clampReason } : {}),
535
536
  };
536
537
  }
538
+ export function mintCompactedEvent(comp, trigger, identity) {
539
+ return {
540
+ type: "compacted",
541
+ trigger,
542
+ tokensBefore: comp.tokensBefore ?? 0,
543
+ ...(comp.freedTokens !== undefined ? { freedTokens: comp.freedTokens } : {}),
544
+ ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
545
+ ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
546
+ ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
547
+ ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
548
+ ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
549
+ ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
550
+ ...(comp.modelFallback ? { modelFallback: true } : {}),
551
+ ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
552
+ ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
553
+ ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
554
+ ...identity,
555
+ };
556
+ }
537
557
  export function nextTrimForceBackoff(prev, comp, trimForced = false) {
538
558
  if (trimForced && comp.naturalTrigger !== true && comp.compacted && comp.freedTokens !== undefined && comp.freedTokens <= 0) {
539
559
  return true;
@@ -1,4 +1,5 @@
1
1
  import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
2
+ import { thrownIs } from "./thrown-value.js";
2
3
  import { describeRejectedValue } from "./tool-search-setting.js";
3
4
  export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
4
5
  const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
@@ -286,14 +287,7 @@ class AutoModeTimeout extends Error {
286
287
  super("auto-mode classify timeout");
287
288
  }
288
289
  }
289
- function isAutoModeTimeout(e) {
290
- try {
291
- return e instanceof AutoModeTimeout;
292
- }
293
- catch {
294
- return false;
295
- }
296
- }
290
+ const isAutoModeTimeout = (e) => thrownIs(e, AutoModeTimeout);
297
291
  export function readDenialLimitFallback(v) {
298
292
  if (typeof v !== "object" || v === null)
299
293
  return undefined;
@@ -1,4 +1,5 @@
1
1
  import { isRemoteExecutionEnv, RemoteExecutionError } from "./remote-env.js";
2
+ import { readMember, thrownIs } from "./thrown-value.js";
2
3
  const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
3
4
  function clampBytes(s, maxBytes) {
4
5
  const total = Buffer.byteLength(s, "utf8");
@@ -47,11 +48,8 @@ async function runStreamStep(env, step, options, maxOutputBytes) {
47
48
  }
48
49
  }
49
50
  catch (e) {
50
- const code = e instanceof RemoteExecutionError
51
- ? e.code
52
- : e?.name === "AbortError"
53
- ? "aborted"
54
- : "unknown";
51
+ const remoteCode = thrownIs(e, RemoteExecutionError) ? readMember(e, "code") : undefined;
52
+ const code = typeof remoteCode === "string" ? remoteCode : readMember(e, "name") === "AbortError" ? "aborted" : "unknown";
55
53
  return { label, command: step.command, exitCode: null, stdout: out.join(""), stderr: errOut.join(""), ok: false, errorCode: code, truncated };
56
54
  }
57
55
  return { label, command: step.command, exitCode, stdout: out.join(""), stderr: errOut.join(""), ok: exitCode === 0, truncated };
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { thrownText } from "./thrown-value.js";
2
3
  import { isAbsoluteForFamily, isAbsolutePathForm, isWinFormPath, pathFamilyOf } from "../tools/fs/safety.js";
3
4
  import { fileHistoryBoundariesToKeep, resolveFileHistoryRetention } from "./file-history-retention.js";
4
5
  const normSlash = (p) => p.replace(/\\/g, "/");
@@ -489,7 +490,7 @@ export class InMemoryFileHistoryStore {
489
490
  return { ok: true, minted: true };
490
491
  }
491
492
  catch (err) {
492
- return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} could not be persisted: ${err.message}` } };
493
+ return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} could not be persisted: ${thrownText(err)}` } };
493
494
  }
494
495
  }
495
496
  async annulTrack(scope, absPath, root, verify) {
@@ -530,7 +531,7 @@ export class InMemoryFileHistoryStore {
530
531
  return { ok: true };
531
532
  }
532
533
  catch (err) {
533
- return { ok: false, error: { code: "track_failed", message: `annulling the first-touch record for ${absPath} failed: ${err.message}` } };
534
+ return { ok: false, error: { code: "track_failed", message: `annulling the first-touch record for ${absPath} failed: ${thrownText(err)}` } };
534
535
  }
535
536
  }
536
537
  async snapshot(scope, entryId, env, root, signal) {
@@ -599,14 +600,14 @@ export class InMemoryFileHistoryStore {
599
600
  }
600
601
  catch (err) {
601
602
  sc.spentAttempts.add(entryId);
602
- return { ok: false, error: { code: "snapshot_failed", message: `boundary for entry "${entryId}" could not be committed: ${err.message}` } };
603
+ return { ok: false, error: { code: "snapshot_failed", message: `boundary for entry "${entryId}" could not be committed: ${thrownText(err)}` } };
603
604
  }
604
605
  this.applyBoundaryRetention(scope, entryId);
605
606
  return { ok: true };
606
607
  }
607
608
  catch (err) {
608
609
  sc.spentAttempts.add(entryId);
609
- return { ok: false, error: { code: "snapshot_failed", message: err.message } };
610
+ return { ok: false, error: { code: "snapshot_failed", message: thrownText(err) } };
610
611
  }
611
612
  finally {
612
613
  sc.activeAttempts.delete(entryId);
@@ -669,7 +670,7 @@ export class InMemoryFileHistoryStore {
669
670
  });
670
671
  }
671
672
  catch (err) {
672
- applied = { outcome: "failed", reason: err.message };
673
+ applied = { outcome: "failed", reason: thrownText(err) };
673
674
  }
674
675
  if (applied.outcome === "applied")
675
676
  ledger.filesChanged.push(key);
@@ -686,7 +687,7 @@ export class InMemoryFileHistoryStore {
686
687
  }
687
688
  catch (err) {
688
689
  const touched = empty.filesChanged.length > 0 || empty.identicalSkipped.length > 0 || empty.refused.length > 0 || empty.failed.length > 0;
689
- return { outcome: touched ? "partial" : "failed", ...empty, error: { code: "restore_failed", message: err.message } };
690
+ return { outcome: touched ? "partial" : "failed", ...empty, error: { code: "restore_failed", message: thrownText(err) } };
690
691
  }
691
692
  }
692
693
  async canRestore(scope, entryId) {
@@ -793,7 +794,7 @@ export class InMemoryFileHistoryStore {
793
794
  return { ok: true };
794
795
  }
795
796
  catch (err) {
796
- return { ok: false, error: { code: "adopt_failed", message: `adoptScope failed: ${err.message}` } };
797
+ return { ok: false, error: { code: "adopt_failed", message: `adoptScope failed: ${thrownText(err)}` } };
797
798
  }
798
799
  }
799
800
  async exportHistory(scope) {
@@ -846,7 +847,7 @@ export class InMemoryFileHistoryStore {
846
847
  }
847
848
  }
848
849
  catch (err) {
849
- return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${err.message}` } };
850
+ return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}` } };
850
851
  }
851
852
  }
852
853
  async importStaged(scope, data, srcGetBlob, needed) {
@@ -860,7 +861,7 @@ export class InMemoryFileHistoryStore {
860
861
  bytes = await srcGetBlob(hash);
861
862
  }
862
863
  catch (err) {
863
- return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fetch failed: ${err.message}` } };
864
+ return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fetch failed: ${thrownText(err)}` } };
864
865
  }
865
866
  if (bytes === undefined)
866
867
  return { ok: false, error: { code: "not_found", message: `source blob ${hash} is missing — the envelope's graph cannot be completed, so nothing is published` } };
@@ -892,7 +893,7 @@ export class InMemoryFileHistoryStore {
892
893
  return { ok: true };
893
894
  }
894
895
  catch (err) {
895
- return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${err.message}` } };
896
+ return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}` } };
896
897
  }
897
898
  }
898
899
  }
@@ -515,8 +515,15 @@ export interface PostCompactContext {
515
515
  trigger: "auto" | "manual" | "forced";
516
516
  /** The conversation summary the compaction produced (CC `compact_summary` parity). */
517
517
  summary: string;
518
+ /** DIAGNOSTIC, NOT subtractable: prepareCompaction's coordinate, LACKING the overhead term
519
+ * {@link tokensAfter} carries. Use {@link freedTokens}. */
518
520
  tokensBefore?: number;
521
+ /** DIAGNOSTIC, NOT subtractable: always structural + prompt overhead, a different coordinate from
522
+ * {@link tokensBefore}, so the difference can go negative. Absent when the post measurement failed. */
519
523
  tokensAfter?: number;
524
+ /** The engine's measured delta (clamped ≥ 0) — the ONE subtractable number here, and the same value
525
+ * this pass's `compacted` frame carries. Absent ⇒ the post measurement failed. */
526
+ freedTokens?: number;
520
527
  /** {@link HookSeatSignal} — this invocation's own abort signal. */
521
528
  signal?: AbortSignal;
522
529
  }
package/dist/core/mcp.js CHANGED
@@ -21,7 +21,7 @@ import { withContentOrigin } from "./memory-engine/content-origin.js";
21
21
  import { snapshotMcpEntries } from "./locked-config.js";
22
22
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
23
23
  import { schemaKeyBoundProblem } from "./tool-roster.js";
24
- import { thrownText } from "./thrown-value.js";
24
+ import { readMember, thrownIs, thrownName, thrownText } from "./thrown-value.js";
25
25
  export const MCP_PREFIX = MCP_NAMESPACE.prefix;
26
26
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
27
27
  const MCP_CHARS_PER_TOKEN = 4;
@@ -187,7 +187,7 @@ function sanitizeMcpErrorTextInPlace(err) {
187
187
  }
188
188
  function namedMcpFailureText(err) {
189
189
  const detail = remoteErrorText(err, { collapse: true });
190
- const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
190
+ const condition = thrownIs(err, McpError) ? describeMcpSpecErrorCode(readMember(err, "code")) : undefined;
191
191
  return condition !== undefined ? `${condition}: ${detail}` : detail;
192
192
  }
193
193
  function remoteErrorText(err, opts) {
@@ -1769,9 +1769,11 @@ function asServerWarning(spec, err) {
1769
1769
  }
1770
1770
  function sanitizedCause(err) {
1771
1771
  const copy = new Error(namedMcpFailureText(err));
1772
- if (err instanceof Error) {
1773
- copy.name = err.name;
1774
- const code = err.code;
1772
+ const name = thrownName(err);
1773
+ if (name !== undefined) {
1774
+ if (name !== "")
1775
+ copy.name = name;
1776
+ const code = readMember(err, "code");
1775
1777
  if (typeof code === "string" || typeof code === "number")
1776
1778
  copy.code = code;
1777
1779
  }
@@ -131,10 +131,13 @@ export type DistillerChatFn = (req: DistillerChatRequest) => Promise<DistillerCh
131
131
  export declare function isAliasModelId(model: string): boolean;
132
132
  /**
133
133
  * An OpenAI-compatible chat seat over an explicit endpoint. Screened at CONSTRUCTION (the
134
- * bad-value loudness law): a missing key or an alias model id refuses with a code before any call
135
- * is made — an alias can be re-pointed upstream and would move the driven model with no trace in
136
- * the archived plan.
137
- */
134
+ * bad-value loudness law): a missing key, an alias model id or an unservable request root refuses
135
+ * with a code before any call is made — an alias can be re-pointed upstream and would move the
136
+ * driven model with no trace in the archived plan.
137
+ *
138
+ * The root is judged by {@link assertServableRequestRoot} — the SAME law the brains apply, because
139
+ * emptiness alone let userinfo, a non-http(s) scheme and a non-URL spelling reach `fetch` as a
140
+ * TRANSPORT error. Only the ENVELOPE is this seat's; the driver routes config throws apart. */
138
141
  export declare function openAiCompatChatSeat(seat: {
139
142
  baseUrl: string;
140
143
  apiKey: string;
@@ -1,4 +1,5 @@
1
1
  import { readMember, thrownText } from "../thrown-value.js";
2
+ import { assertServableRequestRoot } from "../../brain/route-adjudicator.js";
2
3
  export const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32;
3
4
  const SIZE_CLAUSE = {
4
5
  "dl-2": "- the index should be as small as it can be made without losing either of the above.",
@@ -132,8 +133,8 @@ function renderCandidate(n, c) {
132
133
  return `[${n}] ${head}\n${String(c.entry?.body ?? "").trim()}`;
133
134
  }
134
135
  const fill = (tpl, vars) => Object.entries(vars).reduce((s, [k, v]) => s.split(`{{${k}}}`).join(String(v)), tpl);
135
- function distillerConfigRefusal(message) {
136
- const e = new Error(message);
136
+ function distillerConfigRefusal(message, cause) {
137
+ const e = (cause === undefined ? new Error(message) : new Error(message, { cause }));
137
138
  e.code = "config.memory_consolidation_driver";
138
139
  return e;
139
140
  }
@@ -143,13 +144,20 @@ export function isAliasModelId(model) {
143
144
  export function openAiCompatChatSeat(seat) {
144
145
  if (typeof seat.baseUrl !== "string" || seat.baseUrl.trim() === "")
145
146
  throw distillerConfigRefusal("memory consolidation driver chat seat needs a non-empty baseUrl.");
147
+ let requestRoot;
148
+ try {
149
+ requestRoot = (assertServableRequestRoot(seat.baseUrl, "memory consolidation driver chat seat baseUrl") ?? "").replace(/\/+$/, "");
150
+ }
151
+ catch (e) {
152
+ throw distillerConfigRefusal(thrownText(e), e);
153
+ }
146
154
  if (typeof seat.apiKey !== "string" || seat.apiKey === "")
147
155
  throw distillerConfigRefusal("memory consolidation driver chat seat needs an api key — a driver that silently skips would publish an empty plan as a model's answer.");
148
156
  if (typeof seat.model !== "string" || seat.model.trim() === "" || isAliasModelId(seat.model)) {
149
157
  throw distillerConfigRefusal(`memory consolidation driver chat seat needs an EXPLICIT model id, got ${JSON.stringify(seat.model)} — an alias can be re-pointed upstream and would move the driver with no trace in the plan.`);
150
158
  }
151
159
  return async (req) => {
152
- const res = await fetch(`${String(seat.baseUrl).replace(/\/+$/, "")}/chat/completions`, {
160
+ const res = await fetch(`${requestRoot}/chat/completions`, {
153
161
  method: "POST",
154
162
  headers: { "content-type": "application/json", authorization: `Bearer ${seat.apiKey}` },
155
163
  body: JSON.stringify({
@@ -46,6 +46,7 @@ const SECRET_PATTERNS = [
46
46
  { label: "stripe_secret_key", re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{24,}\b/ },
47
47
  { label: "slack_token", re: /\bxox[baprs]-(?:[A-Za-z0-9]+-){1,3}[A-Za-z0-9]{16,}\b/ },
48
48
  { label: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
49
+ { label: "huggingface_token", re: /\bhf_(?=[A-Za-z0-9]*[0-9])[A-Za-z0-9]{20,}\b/ },
49
50
  { label: "private_key", re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
50
51
  { label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
51
52
  { label: "azure_storage_account_key", re: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
@@ -2,6 +2,7 @@ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
4
  import { haltConsumed } from "./halt-attribution.js";
5
+ import { isErrorLike, readMember } from "../thrown-value.js";
5
6
  import { describeRejectedValue } from "../tool-search-setting.js";
6
7
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
7
8
  "output.degenerate",
@@ -13,33 +14,25 @@ const SALVAGE_ELIGIBLE_TERMINALS = new Set([
13
14
  "usage.window_exhausted",
14
15
  ]);
15
16
  function thrownMessage(threw) {
16
- try {
17
- if (threw instanceof Error) {
18
- const m = threw.message;
19
- if (typeof m === "string")
20
- return m;
21
- }
22
- }
23
- catch {
17
+ if (isErrorLike(threw)) {
18
+ const m = readMember(threw, "message");
19
+ if (typeof m === "string")
20
+ return m;
24
21
  }
25
22
  return describeRejectedValue(threw);
26
23
  }
27
24
  export function errorCodeOf(err) {
28
25
  let cur = err;
29
26
  let fallback;
30
- try {
31
- for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
32
- const code = cur.code;
33
- if (typeof code === "string" && code) {
34
- if (code !== "unknown") {
35
- return code;
36
- }
37
- fallback ??= code;
27
+ for (let depth = 0; cur !== null && cur !== undefined && typeof cur === "object" && depth < 8; depth++) {
28
+ const code = readMember(cur, "code");
29
+ if (typeof code === "string" && code) {
30
+ if (code !== "unknown") {
31
+ return code;
38
32
  }
39
- cur = cur.cause;
33
+ fallback ??= code;
40
34
  }
41
- }
42
- catch {
35
+ cur = readMember(cur, "cause");
43
36
  }
44
37
  return fallback;
45
38
  }
@@ -2620,6 +2620,7 @@ export interface ManualCompactRef {
2620
2620
  export type EndOfTaskCompaction = {
2621
2621
  compacted: boolean;
2622
2622
  tokensBefore?: number;
2623
+ freedTokens?: number;
2623
2624
  triggerTokens?: number;
2624
2625
  postTriggerTokens?: number;
2625
2626
  durationMs?: number;
@@ -43,6 +43,16 @@ export async function prepareFileHistory(input) {
43
43
  });
44
44
  }
45
45
  const fileHistoryEnabled = fileHistoryStore !== undefined && handsEnabled;
46
+ if (!fileHistoryEnabled && (handsEnabled || fileHistoryStore !== undefined) && deps.onError !== undefined && !toldAlready(deps.onError, fileHistoryStore === undefined ? "no-store" : "no-env")) {
47
+ const reason = fileHistoryStore === undefined
48
+ ? `this deployment wired no RunnerDeps.fileHistoryStore, so NOTHING is recorded: no first-touch snapshot before an edit, no turn boundary, and a later restoreFiles / rewindFilesTo has nothing to restore from and will refuse (rewind.store_unconfigured). Supply a store, or accept an unrecoverable working tree knowingly`
49
+ : `RunnerDeps.fileHistoryStore is wired but this run mounts no filesystem-capable ExecutionEnv, so the store is INERT: there is no working tree to snapshot and nothing will ever be written to it. Wire RunnerDeps.executionEnv, or drop the store from this deployment shape`;
50
+ try {
51
+ deps.onError(new Error(`file history is not live for this run — ${reason}.`), { phase: "config", sessionId });
52
+ }
53
+ catch {
54
+ }
55
+ }
46
56
  const { historyRoot, historyScope, historyFs } = await resolveFileHistoryCoordinates(fileHistoryEnabled, executionEnv, taskRootFinal, internals?.fileHistoryLineage, sessionId);
47
57
  if (fileHistoryStore !== undefined && internals?.fileHistoryLineage === undefined)
48
58
  await adoptForkedFileHistory(fileHistoryStore, session, sessionId, deps.onError);
@@ -197,6 +207,15 @@ function rewindRefusalTail(form) {
197
207
  : `Either the boundary does not exist (its turns ran without a RunnerDeps.fileHistoryStore, or it was reaped), or it was produced in the retired whole-tree snapshot era, which this engine's per-edited-file seat does not restore.`;
198
208
  }
199
209
  const FILE_HISTORY_BOUNDARY_TIMEOUT_MS = 30_000;
210
+ const FILE_HISTORY_LIVENESS_MEMO = new WeakMap();
211
+ function toldAlready(sink, arm) {
212
+ const heard = FILE_HISTORY_LIVENESS_MEMO.get(sink) ?? new Set();
213
+ FILE_HISTORY_LIVENESS_MEMO.set(sink, heard);
214
+ if (heard.has(arm))
215
+ return true;
216
+ heard.add(arm);
217
+ return false;
218
+ }
200
219
  const FILE_HISTORY_CARRY_MEMO = new WeakMap();
201
220
  async function adoptForkedFileHistory(store, session, sessionId, onError) {
202
221
  const memo = FILE_HISTORY_CARRY_MEMO.get(store) ?? { settled: new Set(), disclosed: new Set(), inFlight: new Map() };
@@ -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;