@wrongstack/core 0.306.4 → 0.307.1

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 (113) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1803 -1701
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/agents/types.d.ts +6 -9
  14. package/dist/coordination/index.js +6051 -5880
  15. package/dist/coordination/mail-tools.d.ts +1 -1
  16. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  17. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  18. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  19. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  20. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  21. package/dist/coordination/mailbox-project-server.js +1 -1
  22. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  23. package/dist/coordination/mailbox-types.d.ts +6 -793
  24. package/dist/core/agent-loop-context.d.ts +28 -0
  25. package/dist/core/agent-loop-detector.d.ts +25 -0
  26. package/dist/core/fallback-doctor.d.ts +72 -0
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/index.js +1859 -1335
  29. package/dist/defaults/index.js +3647 -3559
  30. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  31. package/dist/execution/autonomy-brain.d.ts +3 -156
  32. package/dist/execution/compaction-budget.d.ts +38 -0
  33. package/dist/execution/compaction-core.d.ts +3 -170
  34. package/dist/execution/compaction-elision.d.ts +58 -0
  35. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  36. package/dist/execution/council-orchestrator.d.ts +3 -42
  37. package/dist/execution/council-response-parser.d.ts +56 -0
  38. package/dist/execution/index.js +2475 -2449
  39. package/dist/execution/prompt-enhancer.js +11 -3
  40. package/dist/execution/tool-executor-guard.d.ts +18 -0
  41. package/dist/execution/tool-executor-runner.d.ts +12 -0
  42. package/dist/execution/tool-executor.d.ts +0 -75
  43. package/dist/goal/index.js +19 -6
  44. package/dist/hq/index.js +8 -2
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +8021 -7405
  47. package/dist/infrastructure/index.js +3 -1
  48. package/dist/kernel/events/agent-events.d.ts +2 -0
  49. package/dist/kernel/events/provider-events.d.ts +13 -0
  50. package/dist/plugin/index.js +2590 -2561
  51. package/dist/plugins/auto-review-config.d.ts +53 -0
  52. package/dist/plugins/auto-review-git.d.ts +19 -0
  53. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  54. package/dist/plugins/review-finding-verification.d.ts +7 -0
  55. package/dist/prompts/index.d.ts +1 -0
  56. package/dist/prompts/index.js +601 -0
  57. package/dist/prompts/prompt-journal.d.ts +103 -0
  58. package/dist/security/index.js +627 -652
  59. package/dist/security/permission-explain.d.ts +19 -0
  60. package/dist/security/permission-policy.d.ts +0 -101
  61. package/dist/security/yolo-risk.d.ts +5 -4
  62. package/dist/session-catalog/index.js +220 -212
  63. package/dist/session-catalog/project-server.js +227 -219
  64. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  65. package/dist/session-catalog/store-schema.d.ts +48 -0
  66. package/dist/session-catalog/store.d.ts +0 -17
  67. package/dist/storage/director-state.d.ts +6 -0
  68. package/dist/storage/file-session-writer.d.ts +2 -58
  69. package/dist/storage/index.d.ts +2 -1
  70. package/dist/storage/index.js +2057 -1925
  71. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  72. package/dist/storage/queue-store.d.ts +6 -0
  73. package/dist/storage/session-recovery.d.ts +9 -0
  74. package/dist/storage/session-store/rename-session.d.ts +16 -0
  75. package/dist/storage/session-store/resume-session.d.ts +26 -0
  76. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  77. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  78. package/dist/storage/session-store.d.ts +1 -129
  79. package/dist/storage/session-summary-tracker.d.ts +39 -0
  80. package/dist/storage/session-write-buffer.d.ts +43 -0
  81. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  82. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  83. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  84. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  85. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  86. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  87. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  88. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  89. package/dist/tools/index.js +5124 -5071
  90. package/dist/types/config/root.d.ts +7 -2
  91. package/dist/types/default-config.d.ts +1 -1
  92. package/dist/types/index.d.ts +54 -55
  93. package/dist/types/index.js +953 -932
  94. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  95. package/dist/types/session-markers.d.ts +12 -0
  96. package/dist/types/session-markers.js +19 -0
  97. package/dist/worktree/worktree-manager.d.ts +1 -1
  98. package/instructions/agents/architect.md +2 -0
  99. package/instructions/agents/code-reviewer.md +1 -0
  100. package/instructions/agents/debugger.md +2 -0
  101. package/instructions/agents/executor.md +2 -0
  102. package/instructions/agents/explore.md +3 -1
  103. package/instructions/agents/refactor.md +2 -0
  104. package/instructions/agents/reviewer.md +1 -0
  105. package/instructions/coordination/subagent-baseline.md +68 -7
  106. package/instructions/llm/prompt-enhancer.md +2 -1
  107. package/instructions/sections/tool/common-patterns.md +30 -0
  108. package/instructions/sections/tool/delegation-compact.md +3 -1
  109. package/instructions/sections/tool/delegation-full.md +5 -1
  110. package/instructions/system-lite.md +32 -4
  111. package/instructions/system-pro.md +63 -9
  112. package/instructions/system.md +56 -11
  113. package/package.json +9 -5
@@ -1158,7 +1158,10 @@ var MAILBOX_TYPE_PROPERTIES = {
1158
1158
  }
1159
1159
  };
1160
1160
 
1161
- // src/coordination/mailbox-types.ts
1161
+ // src/coordination/mailbox-predicates.ts
1162
+ var SESSION_RECIPIENT_PREFIX = "@session:";
1163
+
1164
+ // src/coordination/mailbox-session-sync.ts
1162
1165
  function isAffectedBySessionAffinity(message) {
1163
1166
  return message.sessionAffinity !== void 0;
1164
1167
  }
@@ -1189,7 +1192,6 @@ async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
1189
1192
  if (ctx?.allowUnscoped === true) return true;
1190
1193
  return false;
1191
1194
  }
1192
- var SESSION_RECIPIENT_PREFIX = "@session:";
1193
1195
 
1194
1196
  // src/coordination/sqlite-mailbox.ts
1195
1197
  var SQLITE_MAILBOX_FILE = "_mailbox.sqlite";
@@ -5661,1281 +5663,1653 @@ function resolveEventSessionId(ctx) {
5661
5663
  return session?.id ?? "";
5662
5664
  }
5663
5665
 
5664
- // src/core/continue-to-next-iteration.ts
5665
- function parseContinueDirective(text) {
5666
- const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
5667
- const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
5668
- let match;
5669
- let lastDirective = "none";
5670
- while ((match = LINE_MARKERS.exec(tail)) !== null) {
5671
- const value = (match[1] ?? "").toLowerCase();
5672
- if (value === "continue" || value === "next step" || value === "proceed") {
5673
- lastDirective = "continue";
5674
- } else if (value === "done") {
5675
- lastDirective = "stop";
5676
- }
5666
+ // src/core/agent-loop-context.ts
5667
+ function createAgentLoopContextManager(a, handlers) {
5668
+ let _lastEmittedMsgCount = -1;
5669
+ let _lastEmittedToolCount = -1;
5670
+ let _lastEmittedRevision = -1;
5671
+ let _lastEmittedMaxContext = -1;
5672
+ let _lastPreFlightMsgCount = -1;
5673
+ let _lastPreFlightToolCount = -1;
5674
+ let _lastPreFlightRevision = -1;
5675
+ let _lastCompactionMsgCount = -1;
5676
+ let _lastCompactionRevision = -1;
5677
+ let _lastCompactionToolsRef = null;
5678
+ let _lastCompactionSystemRef = null;
5679
+ let _lastCompactionRequestTokens;
5680
+ let _lastCompactionMaxContext = -1;
5681
+ let _lastCompactionWasNoop = false;
5682
+ let _cachedSysRef = null;
5683
+ let _cachedToolsRef = null;
5684
+ let _cachedOverheadTokens = 0;
5685
+ const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
5686
+ function currentMaxContext() {
5687
+ const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
5688
+ const providerMax = a.ctx.provider.capabilities.maxContext;
5689
+ return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
5677
5690
  }
5678
- return lastDirective;
5679
- }
5680
- var DIRECTIVE_SCAN_WINDOW = 2048;
5681
- var META_KEY2 = "_autonomousContinue";
5682
- function consumeAutonomousContinue(ctx) {
5683
- const val = ctx.meta[META_KEY2] === true;
5684
- delete ctx.meta[META_KEY2];
5685
- return val;
5686
- }
5687
-
5688
- // src/core/iteration-limit.ts
5689
- function requestLimitExtension(opts) {
5690
- const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
5691
- return new Promise((resolve10) => {
5692
- let resolved = false;
5693
- const timerFired = () => {
5694
- if (!resolved) {
5695
- resolved = true;
5696
- resolve10(0);
5697
- }
5698
- };
5699
- const timer = setTimeout(timerFired, timeoutMs);
5700
- timer.unref();
5701
- const deny = () => {
5702
- if (!resolved) {
5703
- resolved = true;
5704
- clearTimeout(timer);
5705
- resolve10(0);
5706
- }
5691
+ async function refreshProviderContextLimit(provider, model, opts = {}) {
5692
+ a.ctx.meta ??= {};
5693
+ const routeKey = `${provider.id}/${model}`;
5694
+ const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
5695
+ const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
5696
+ const previousMaxContext = currentMaxContext();
5697
+ if (routeChanged) {
5698
+ delete a.ctx.meta["providerOverflowMaxContext"];
5699
+ delete a.ctx.meta["contextLimitBaseline"];
5700
+ delete a.ctx.meta["providerLimitEffectiveMaxContext"];
5701
+ delete a.ctx.meta["effectiveMaxContext"];
5702
+ delete a.ctx.meta["effectiveMaxContextSource"];
5703
+ delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
5704
+ delete a.ctx.meta["contextLimitLastEmittedSource"];
5705
+ }
5706
+ a.ctx.meta["contextLimitRouteKey"] = routeKey;
5707
+ const routeMeta = a.ctx.meta["effectiveMaxContext"];
5708
+ const routeProviderCap = provider.capabilities.maxContext;
5709
+ const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
5710
+ const emitEffectiveLimit = (effective2, source) => {
5711
+ const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
5712
+ const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
5713
+ if (lastEmitted === effective2 && lastEmittedSource === source) return;
5714
+ const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
5715
+ a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
5716
+ a.ctx.meta["contextLimitLastEmittedSource"] = source;
5717
+ a.events.emit("ctx.max_context", {
5718
+ sessionId: resolveEventSessionId(a.ctx),
5719
+ providerId: provider.id,
5720
+ modelId: model,
5721
+ maxContext: effective2,
5722
+ ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
5723
+ source,
5724
+ decreased: eventPrevious > 0 && effective2 < eventPrevious
5725
+ });
5707
5726
  };
5708
- const grant = (extra) => {
5709
- if (!resolved) {
5710
- resolved = true;
5711
- clearTimeout(timer);
5712
- resolve10(Math.max(0, extra));
5727
+ const emitNativeAfterRouteChange = () => {
5728
+ if (routeChanged && currentEffective > 0) {
5729
+ a.ctx.meta["effectiveMaxContext"] = currentEffective;
5730
+ a.ctx.meta["effectiveMaxContextSource"] = "configured";
5731
+ emitEffectiveLimit(currentEffective, "configured");
5713
5732
  }
5714
5733
  };
5715
- events.emit("iteration.limit_reached", {
5716
- sessionId,
5717
- currentIterations,
5718
- currentLimit,
5719
- grant,
5720
- deny
5721
- });
5722
- if (autoExtend) {
5723
- setImmediate(() => {
5724
- if (!resolved) {
5725
- resolved = true;
5726
- clearTimeout(timer);
5727
- resolve10(100);
5728
- }
5729
- });
5734
+ const refresh = provider.refreshContextLimit;
5735
+ if (opts.probe === false || !refresh) {
5736
+ emitNativeAfterRouteChange();
5737
+ return;
5730
5738
  }
5731
- });
5732
- }
5733
-
5734
- // src/core/next-steps-slot.ts
5735
- var SLOT_KEY = "nextsteps.pending";
5736
- var MAX_PENDING_NEXT_STEPS = 4;
5737
- var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
5738
- function writePendingNextSteps(ctx, steps) {
5739
- const cleaned = [];
5740
- for (const step of steps) {
5741
- const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
5742
- if (!text) continue;
5743
- cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
5744
- if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
5745
- }
5746
- if (cleaned.length === 0) {
5747
- clearPendingNextSteps(ctx);
5748
- return;
5749
- }
5750
- ctx.meta[SLOT_KEY] = cleaned;
5751
- }
5752
- function readPendingNextSteps(ctx) {
5753
- const raw = ctx.meta[SLOT_KEY];
5754
- if (!Array.isArray(raw) || raw.length === 0) return void 0;
5755
- const steps = raw.filter(
5756
- (s) => typeof s?.text === "string" && s.text.length > 0
5757
- );
5758
- return steps.length > 0 ? steps : void 0;
5759
- }
5760
- function clearPendingNextSteps(ctx) {
5761
- delete ctx.meta[SLOT_KEY];
5762
- }
5763
- function hasNextStepsTag(text) {
5764
- return NEXT_STEPS_TAG_RE.test(text);
5765
- }
5766
- function renderNextStepsBlock(steps) {
5767
- const lines = steps.map((step, i) => {
5768
- const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
5769
- return `${i + 1}. ${step.text}${auto}`;
5770
- });
5771
- return `<nextsteps>
5772
- ${lines.join("\n")}
5773
- </nextsteps>`;
5774
- }
5775
- function endsTurn(stopReason) {
5776
- return stopReason !== "tool_use" && stopReason !== "tool_call";
5777
- }
5778
- function maybeAppendPendingNextSteps(ctx, res) {
5779
- if (ctx.agentId !== "leader") return res;
5780
- if (!endsTurn(res.stopReason)) return res;
5781
- if (ctx.signal.aborted) return res;
5782
- const steps = readPendingNextSteps(ctx);
5783
- if (!steps) return res;
5784
- if (hasOpenTodos(ctx.todos)) {
5785
- clearPendingNextSteps(ctx);
5786
- return res;
5787
- }
5788
- const lastTextIndex = res.content.findLastIndex(isTextBlock);
5789
- if (lastTextIndex >= 0) {
5790
- const block = res.content[lastTextIndex];
5791
- if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
5792
- clearPendingNextSteps(ctx);
5793
- return res;
5739
+ let discovered;
5740
+ try {
5741
+ discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
5742
+ } catch {
5743
+ emitNativeAfterRouteChange();
5744
+ return;
5745
+ }
5746
+ const providerLimit = discovered?.maxContext;
5747
+ if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
5748
+ emitNativeAfterRouteChange();
5749
+ return;
5750
+ }
5751
+ const providerLimitFloored = Math.floor(providerLimit);
5752
+ const existingBaseline = a.ctx.meta["contextLimitBaseline"];
5753
+ const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
5754
+ const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
5755
+ const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
5756
+ const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
5757
+ a.ctx.meta["contextLimitBaseline"] = baseline;
5758
+ const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
5759
+ const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
5760
+ const ceilings = [baseline, providerLimitFloored];
5761
+ if (overflowFloored !== void 0) ceilings.push(overflowFloored);
5762
+ const effective = Math.min(...ceilings.filter((limit) => limit > 0));
5763
+ let effectiveSource;
5764
+ if (overflowFloored !== void 0 && effective === overflowFloored) {
5765
+ effectiveSource = "provider_overflow";
5766
+ } else if (effective === providerLimitFloored) {
5767
+ effectiveSource = "provider";
5768
+ } else {
5769
+ effectiveSource = "configured";
5794
5770
  }
5771
+ a.ctx.meta["effectiveMaxContext"] = effective;
5772
+ a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
5773
+ a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
5774
+ emitEffectiveLimit(effective, effectiveSource);
5795
5775
  }
5796
- const rendered = renderNextStepsBlock(steps);
5797
- const content = [...res.content];
5798
- const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
5799
- if (target && isTextBlock(target)) {
5800
- const separator = target.text.trim() ? "\n\n" : "";
5801
- content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
5802
- } else {
5803
- content.push({ type: "text", text: rendered });
5776
+ function systemAndToolsOverhead() {
5777
+ const sysRef = a.ctx.systemPrompt;
5778
+ const toolsRef = a.ctx.tools;
5779
+ if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
5780
+ return _cachedOverheadTokens;
5781
+ }
5782
+ const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
5783
+ _cachedSysRef = sysRef;
5784
+ _cachedToolsRef = toolsRef;
5785
+ _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
5786
+ return _cachedOverheadTokens;
5804
5787
  }
5805
- clearPendingNextSteps(ctx);
5806
- return { ...res, content };
5807
- }
5808
-
5809
- // src/core/streaming-response-builder.ts
5810
- import { randomUUID as randomUUID4 } from "node:crypto";
5811
-
5812
- // src/utils/json-repair.ts
5813
- function completePartialObject(s) {
5814
- if (!s.trim().startsWith("{")) return s;
5815
- if (tryParse(s).ok) return s;
5816
- return repairTruncated(s);
5817
- }
5818
- function repairTruncated(s) {
5819
- const stack = [];
5820
- let inString = false;
5821
- let escaped = false;
5822
- let sawKey = false;
5823
- let prevSig = "";
5824
- let contentEnd = 0;
5825
- let stringBraceDepth = 0;
5826
- for (let i = 0; i < s.length; i++) {
5827
- const ch = expectDefined(s[i]);
5828
- if (inString) {
5829
- contentEnd = i + 1;
5830
- if (escaped) {
5831
- escaped = false;
5832
- continue;
5833
- }
5834
- if (ch === "\\") {
5835
- escaped = true;
5836
- continue;
5837
- }
5838
- if (ch === '"') {
5839
- inString = false;
5840
- prevSig = '"';
5841
- stringBraceDepth = 0;
5842
- continue;
5788
+ function stashRequestTokens(req) {
5789
+ const preFlight = estimateRequestTokens(
5790
+ req.messages,
5791
+ req.system,
5792
+ req.tools ?? [],
5793
+ calibrationKey(req.model)
5794
+ );
5795
+ a.ctx.lastRequestTokens = preFlight.total;
5796
+ _lastPreFlightMsgCount = req.messages.length;
5797
+ _lastPreFlightToolCount = (req.tools ?? []).length;
5798
+ _lastPreFlightRevision = a.ctx.state.revision;
5799
+ _cachedSysRef = req.system;
5800
+ _cachedToolsRef = req.tools ?? [];
5801
+ _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
5802
+ a.ctx.meta["lastRequestTokensAt"] = {
5803
+ msgCount: req.messages.length,
5804
+ toolCount: (req.tools ?? []).length,
5805
+ revision: a.ctx.state.revision
5806
+ };
5807
+ return preFlight;
5808
+ }
5809
+ function refreshContextRequestTokenStash(opts = {}) {
5810
+ const msgCount = a.ctx.messages.length;
5811
+ const toolCount = (a.ctx.tools ?? []).length;
5812
+ const revision = a.ctx.state.revision;
5813
+ const stashed = a.ctx.lastRequestTokens;
5814
+ const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
5815
+ if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
5816
+ const meta = stashedAt;
5817
+ if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
5818
+ return stashed;
5843
5819
  }
5844
- if (ch === "{") stringBraceDepth++;
5845
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
5846
- continue;
5847
- }
5848
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
5849
- contentEnd = i + 1;
5850
- if (ch === '"') {
5851
- inString = true;
5852
- sawKey = true;
5853
- stringBraceDepth = 0;
5854
- prevSig = '"';
5855
- } else if (ch === "{" || ch === "[") {
5856
- stack.push(ch);
5857
- prevSig = ch;
5858
- } else if (ch === "}" || ch === "]") {
5859
- stack.pop();
5860
- prevSig = ch;
5861
- } else {
5862
- prevSig = ch;
5863
5820
  }
5821
+ const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
5822
+ a.ctx.lastRequestTokens = refreshed;
5823
+ _lastPreFlightMsgCount = msgCount;
5824
+ _lastPreFlightToolCount = toolCount;
5825
+ _lastPreFlightRevision = revision;
5826
+ a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
5827
+ return refreshed;
5864
5828
  }
5865
- if (!sawKey && !inString) return s;
5866
- let result = s.slice(0, contentEnd);
5867
- if (inString) {
5868
- if (escaped) {
5869
- result = result.slice(0, -1);
5870
- } else if (endsWithInvalidEscape(result)) {
5871
- result = result.slice(0, -2);
5829
+ async function compactContextIfNeeded() {
5830
+ const msgCount = a.ctx.messages.length;
5831
+ const maxContext = currentMaxContext();
5832
+ const revision = a.ctx.state.revision;
5833
+ const toolsRef = a.ctx.tools;
5834
+ const systemRef = a.ctx.systemPrompt;
5835
+ const requestTokens = a.ctx.lastRequestTokens;
5836
+ if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
5837
+ return false;
5872
5838
  }
5873
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
5874
- result += '"';
5875
- } else if (prevSig === ":") {
5876
- result += "null";
5839
+ const beforeMessages = a.ctx.messages;
5840
+ const beforeMsgCount = a.ctx.messages.length;
5841
+ const beforeRevision = a.ctx.state.revision;
5842
+ await a.pipelines.contextWindow.run(a.ctx);
5843
+ _lastCompactionMsgCount = a.ctx.messages.length;
5844
+ _lastCompactionRevision = a.ctx.state.revision;
5845
+ _lastCompactionToolsRef = a.ctx.tools;
5846
+ _lastCompactionSystemRef = a.ctx.systemPrompt;
5847
+ _lastCompactionMaxContext = maxContext;
5848
+ const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
5849
+ const tokens = refreshContextRequestTokenStash({ force: changed });
5850
+ _lastCompactionRequestTokens = tokens;
5851
+ const load = maxContext > 0 ? tokens / maxContext : 0;
5852
+ _lastCompactionWasNoop = tokens > 0 && load < 0.5;
5853
+ if (changed) {
5854
+ _lastEmittedMsgCount = -1;
5855
+ _lastEmittedToolCount = -1;
5856
+ _lastEmittedMaxContext = -1;
5857
+ }
5858
+ return changed;
5877
5859
  }
5878
- for (let k = stack.length - 1; k >= 0; k--) {
5879
- result += stack[k] === "{" ? "}" : "]";
5860
+ async function buildRequestWithPreflightCompaction(opts) {
5861
+ let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5862
+ let req = prepared.request;
5863
+ let preFlight = stashRequestTokens(req);
5864
+ let probedRouteKey = `${prepared.provider.id}/${req.model}`;
5865
+ await refreshProviderContextLimit(prepared.provider, req.model);
5866
+ if (await compactContextIfNeeded()) {
5867
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5868
+ req = prepared.request;
5869
+ preFlight = stashRequestTokens(req);
5870
+ const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
5871
+ if (rebuiltRouteKey !== probedRouteKey) {
5872
+ probedRouteKey = rebuiltRouteKey;
5873
+ await refreshProviderContextLimit(prepared.provider, req.model);
5874
+ if (await compactContextIfNeeded()) {
5875
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5876
+ req = prepared.request;
5877
+ preFlight = stashRequestTokens(req);
5878
+ }
5879
+ }
5880
+ }
5881
+ return { req, provider: prepared.provider, preFlight };
5880
5882
  }
5881
- if (!tryParse(result).ok) {
5882
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
5883
- if (tryParse(patched).ok) result = patched;
5883
+ function emitContextPct() {
5884
+ const msgCount = a.ctx.messages.length;
5885
+ const toolCount = (a.ctx.tools ?? []).length;
5886
+ const maxContext = currentMaxContext();
5887
+ const revision = a.ctx.state.revision;
5888
+ if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
5889
+ return;
5890
+ }
5891
+ _lastEmittedMsgCount = msgCount;
5892
+ _lastEmittedToolCount = toolCount;
5893
+ _lastEmittedRevision = revision;
5894
+ _lastEmittedMaxContext = maxContext;
5895
+ if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
5896
+ refreshContextRequestTokenStash({ force: true });
5897
+ }
5898
+ let total;
5899
+ const anchored = realAnchoredInputTokens(
5900
+ a.ctx.messages,
5901
+ a.ctx.lastRealInputTokens,
5902
+ typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
5903
+ );
5904
+ const stashed = a.ctx.lastRequestTokens;
5905
+ if (anchored !== null) {
5906
+ total = anchored;
5907
+ } else if (typeof stashed === "number" && stashed > 0) {
5908
+ const cal = getCalibrationState(calibrationKey());
5909
+ total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
5910
+ } else {
5911
+ const raw = refreshContextRequestTokenStash({ force: true });
5912
+ const cal = getCalibrationState(calibrationKey());
5913
+ total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
5914
+ }
5915
+ const rawLoad = maxContext > 0 ? total / maxContext : 0;
5916
+ const load = Math.max(0, Math.min(1, rawLoad));
5917
+ a.events.emit("ctx.pct", {
5918
+ sessionId: resolveEventSessionId(a.ctx),
5919
+ load,
5920
+ rawLoad,
5921
+ tokens: total,
5922
+ maxContext
5923
+ });
5884
5924
  }
5885
- return result;
5925
+ return {
5926
+ refreshProviderContextLimit,
5927
+ compactContextIfNeeded,
5928
+ stashRequestTokens,
5929
+ refreshContextRequestTokenStash,
5930
+ buildRequestWithPreflightCompaction,
5931
+ emitContextPct,
5932
+ currentMaxContext,
5933
+ calibrationKey,
5934
+ get lastPreFlightMsgCount() {
5935
+ return _lastPreFlightMsgCount;
5936
+ }
5937
+ };
5886
5938
  }
5887
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
5888
- function endsWithInvalidEscape(str) {
5889
- const last = str[str.length - 1];
5890
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
5891
- if (VALID_ESCAPE.has(last)) return false;
5892
- let backslashes = 0;
5893
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
5894
- return backslashes % 2 === 1;
5939
+
5940
+ // src/core/agent-loop-detector.ts
5941
+ function iterationFingerprint(blocks) {
5942
+ const toolUses = blocks.filter(isToolUseBlock);
5943
+ const texts = blocks.filter(isTextBlock);
5944
+ const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
5945
+ const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
5946
+ const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
5947
+ const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
5948
+ if (!hasContent) return "__empty__";
5949
+ return [
5950
+ `tools=${toolNameSet.join("+") || "-"}`,
5951
+ `in0=${firstInputHash}`,
5952
+ `txt=${textBlob}`
5953
+ ].join("\n");
5895
5954
  }
5896
- function tryParse(s) {
5897
- try {
5898
- return { ok: true, value: JSON.parse(s) };
5899
- } catch {
5900
- return { ok: false };
5955
+ function stableStringify(value) {
5956
+ return JSON.stringify(canonicalize(value));
5957
+ }
5958
+ function canonicalize(value) {
5959
+ if (Array.isArray(value)) return value.map(canonicalize);
5960
+ if (value && typeof value === "object") {
5961
+ const src = value;
5962
+ const out = {};
5963
+ for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
5964
+ return out;
5901
5965
  }
5966
+ return value;
5902
5967
  }
5903
-
5904
- // src/core/streaming-response-builder.ts
5905
- var STREAM_DRAIN_TIMEOUT_MS = 500;
5906
- function buildResponse(state) {
5907
- const content = [];
5908
- for (const b of state.blockOrder) {
5909
- if (b.kind === "text") {
5910
- const txt = state.textBuffers[b.idx] ?? "";
5911
- if (txt) content.push({ type: "text", text: txt });
5912
- } else if (b.kind === "thinking") {
5913
- const t2 = state.thinking[b.idx];
5914
- if (!t2) continue;
5915
- if (!t2.textBuf && !t2.signature) continue;
5916
- const block = { type: "thinking", thinking: t2.textBuf };
5917
- if (t2.signature) block.signature = t2.signature;
5918
- if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
5919
- block.providerMeta = t2.providerMeta;
5968
+ function hashSmall(s) {
5969
+ let h = 2166136261;
5970
+ for (let i = 0; i < s.length; i++) {
5971
+ h ^= s.charCodeAt(i);
5972
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
5973
+ }
5974
+ return h.toString(36);
5975
+ }
5976
+ var AgentLoopDetector = class {
5977
+ constructor(a) {
5978
+ this.a = a;
5979
+ }
5980
+ a;
5981
+ lastToolSignature = "";
5982
+ toolLoopCount = 0;
5983
+ iterationSteerDone = false;
5984
+ recentCallKeys = [];
5985
+ steeredCallKeys = /* @__PURE__ */ new Set();
5986
+ checkIteration(iterationIndex, content, toolUses, queueSteer) {
5987
+ const loopCfg = this.a.loopDetection;
5988
+ if (loopCfg.mode === "off") return { cut: false };
5989
+ const sig = iterationFingerprint(content);
5990
+ if (sig !== "__empty__") {
5991
+ if (sig === this.lastToolSignature) {
5992
+ this.toolLoopCount++;
5993
+ } else {
5994
+ this.lastToolSignature = sig;
5995
+ this.toolLoopCount = 1;
5996
+ this.iterationSteerDone = false;
5997
+ }
5998
+ const names = toolUses.map((t2) => t2.name).join(", ");
5999
+ const hasText2 = content.some(isTextBlock);
6000
+ const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
6001
+ const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
6002
+ const tool = this.a.tools.get(use.name);
6003
+ return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
6004
+ });
6005
+ const repeatMultiplier = observationRepeat ? 2 : 1;
6006
+ const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${this.toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${this.toolLoopCount} times in a row` : `same assistant text repeated ${this.toolLoopCount} times in a row`;
6007
+ const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
6008
+ if (this.toolLoopCount >= cutAt) {
6009
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
6010
+ this.a.events.emit("tool.loop_detected", {
6011
+ sessionId: resolveEventSessionId(this.a.ctx),
6012
+ ctx: this.a.ctx,
6013
+ tools: names,
6014
+ repeatCount: this.toolLoopCount,
6015
+ iteration: iterationIndex,
6016
+ kind,
6017
+ action: "cut",
6018
+ scope: "iteration"
6019
+ });
6020
+ const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${this.toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
6021
+ return { cut: true, cutSummary: summary };
6022
+ }
6023
+ if (loopCfg.mode === "steer-then-cut" && this.toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !this.iterationSteerDone) {
6024
+ this.iterationSteerDone = true;
6025
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
6026
+ this.a.events.emit("tool.loop_detected", {
6027
+ sessionId: resolveEventSessionId(this.a.ctx),
6028
+ ctx: this.a.ctx,
6029
+ tools: names,
6030
+ repeatCount: this.toolLoopCount,
6031
+ iteration: iterationIndex,
6032
+ kind,
6033
+ action: "steer",
6034
+ scope: "iteration"
6035
+ });
6036
+ queueSteer(
6037
+ `[loop-detector] Your last ${this.toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - this.toolLoopCount} more time(s) will terminate the turn.`
6038
+ );
5920
6039
  }
5921
- content.push(block);
5922
6040
  } else {
5923
- const tb = state.tools.get(b.id);
5924
- if (tb) {
5925
- const block = {
5926
- type: "tool_use",
5927
- id: b.id,
5928
- name: tb.name,
5929
- input: tb.input ?? {}
5930
- };
5931
- if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
5932
- block.providerMeta = tb.providerMeta;
5933
- }
5934
- content.push(block);
5935
- }
6041
+ this.lastToolSignature = "";
6042
+ this.toolLoopCount = 0;
6043
+ this.iterationSteerDone = false;
6044
+ }
6045
+ if (loopCfg.mode === "steer-then-cut") {
6046
+ for (const u of toolUses) {
6047
+ const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
6048
+ this.recentCallKeys.push(key);
6049
+ if (this.recentCallKeys.length > loopCfg.windowSize) this.recentCallKeys.shift();
6050
+ if (this.steeredCallKeys.has(key)) continue;
6051
+ let count = 0;
6052
+ for (const k of this.recentCallKeys) if (k === key) count++;
6053
+ const tool = this.a.tools.get(u.name);
6054
+ const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
6055
+ if (count < observationThreshold) continue;
6056
+ this.steeredCallKeys.add(key);
6057
+ const preview = JSON.stringify(u.input ?? {}).slice(0, 160);
6058
+ this.a.logger.warn(
6059
+ `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
6060
+ );
6061
+ this.a.events.emit("tool.loop_detected", {
6062
+ sessionId: resolveEventSessionId(this.a.ctx),
6063
+ ctx: this.a.ctx,
6064
+ tools: u.name,
6065
+ repeatCount: count,
6066
+ iteration: iterationIndex,
6067
+ kind: "tool",
6068
+ action: "steer",
6069
+ scope: "call"
6070
+ });
6071
+ queueSteer(
6072
+ `[loop-detector] You have called ${u.name}(${preview}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
6073
+ );
6074
+ }
5936
6075
  }
6076
+ return { cut: false };
5937
6077
  }
5938
- if (content.length === 0) content.push({ type: "text", text: "" });
5939
- return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
5940
- }
5941
- function createStreamingState(model) {
5942
- return {
5943
- model,
5944
- stopReason: "end_turn",
5945
- usage: { input: 0, output: 0 },
5946
- textBuffers: [],
5947
- currentTextIndex: -1,
5948
- tools: /* @__PURE__ */ new Map(),
5949
- thinking: [],
5950
- currentThinkingIndex: -1,
5951
- blockOrder: []
5952
- };
6078
+ };
6079
+
6080
+ // src/core/continue-to-next-iteration.ts
6081
+ function parseContinueDirective(text) {
6082
+ const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
6083
+ const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
6084
+ let match;
6085
+ let lastDirective = "none";
6086
+ while ((match = LINE_MARKERS.exec(tail)) !== null) {
6087
+ const value = (match[1] ?? "").toLowerCase();
6088
+ if (value === "continue" || value === "next step" || value === "proceed") {
6089
+ lastDirective = "continue";
6090
+ } else if (value === "done") {
6091
+ lastDirective = "stop";
6092
+ }
6093
+ }
6094
+ return lastDirective;
5953
6095
  }
5954
- function handleMessageStart(state, model) {
5955
- state.model = model;
6096
+ var DIRECTIVE_SCAN_WINDOW = 2048;
6097
+ var META_KEY2 = "_autonomousContinue";
6098
+ function consumeAutonomousContinue(ctx) {
6099
+ const val = ctx.meta[META_KEY2] === true;
6100
+ delete ctx.meta[META_KEY2];
6101
+ return val;
5956
6102
  }
5957
- function handleContentBlockStart(state, ev) {
5958
- const kind = ev.kind ?? "text";
5959
- if (kind === "text") {
5960
- state.currentTextIndex = state.textBuffers.length;
5961
- state.textBuffers.push("");
5962
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
5963
- } else if (kind === "tool_use") {
5964
- const id = ev.id ?? randomUUID4();
5965
- state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
5966
- state.blockOrder.push({ kind: "tool", id });
5967
- state.currentTextIndex = -1;
5968
- } else if (kind === "thinking") {
5969
- state.currentThinkingIndex = state.thinking.length;
5970
- state.thinking.push({
5971
- textBuf: "",
5972
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6103
+
6104
+ // src/core/iteration-limit.ts
6105
+ function requestLimitExtension(opts) {
6106
+ const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
6107
+ return new Promise((resolve10) => {
6108
+ let resolved = false;
6109
+ const timerFired = () => {
6110
+ if (!resolved) {
6111
+ resolved = true;
6112
+ resolve10(0);
6113
+ }
6114
+ };
6115
+ const timer = setTimeout(timerFired, timeoutMs);
6116
+ timer.unref();
6117
+ const deny = () => {
6118
+ if (!resolved) {
6119
+ resolved = true;
6120
+ clearTimeout(timer);
6121
+ resolve10(0);
6122
+ }
6123
+ };
6124
+ const grant = (extra) => {
6125
+ if (!resolved) {
6126
+ resolved = true;
6127
+ clearTimeout(timer);
6128
+ resolve10(Math.max(0, extra));
6129
+ }
6130
+ };
6131
+ events.emit("iteration.limit_reached", {
6132
+ sessionId,
6133
+ currentIterations,
6134
+ currentLimit,
6135
+ grant,
6136
+ deny
5973
6137
  });
5974
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
5975
- state.currentTextIndex = -1;
6138
+ if (autoExtend) {
6139
+ setImmediate(() => {
6140
+ if (!resolved) {
6141
+ resolved = true;
6142
+ clearTimeout(timer);
6143
+ resolve10(100);
6144
+ }
6145
+ });
6146
+ }
6147
+ });
6148
+ }
6149
+
6150
+ // src/core/next-steps-slot.ts
6151
+ var SLOT_KEY = "nextsteps.pending";
6152
+ var MAX_PENDING_NEXT_STEPS = 4;
6153
+ var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
6154
+ function writePendingNextSteps(ctx, steps) {
6155
+ const cleaned = [];
6156
+ for (const step of steps) {
6157
+ const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
6158
+ if (!text) continue;
6159
+ cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
6160
+ if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
6161
+ }
6162
+ if (cleaned.length === 0) {
6163
+ clearPendingNextSteps(ctx);
6164
+ return;
5976
6165
  }
6166
+ ctx.meta[SLOT_KEY] = cleaned;
5977
6167
  }
5978
- function handleContentBlockStop(state, ev) {
5979
- void state;
5980
- void ev;
6168
+ function readPendingNextSteps(ctx) {
6169
+ const raw = ctx.meta[SLOT_KEY];
6170
+ if (!Array.isArray(raw) || raw.length === 0) return void 0;
6171
+ const steps = raw.filter(
6172
+ (s) => typeof s?.text === "string" && s.text.length > 0
6173
+ );
6174
+ return steps.length > 0 ? steps : void 0;
5981
6175
  }
5982
- function handleTextDelta(state, text) {
5983
- if (state.currentTextIndex === -1) {
5984
- state.currentTextIndex = state.textBuffers.length;
5985
- state.textBuffers.push("");
5986
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
5987
- }
5988
- state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
6176
+ function clearPendingNextSteps(ctx) {
6177
+ delete ctx.meta[SLOT_KEY];
5989
6178
  }
5990
- function handleToolUseStart(state, ev) {
5991
- state.currentTextIndex = -1;
5992
- state.tools.set(ev.id, { name: ev.name, partial: "" });
5993
- state.blockOrder.push({ kind: "tool", id: ev.id });
6179
+ function hasNextStepsTag(text) {
6180
+ return NEXT_STEPS_TAG_RE.test(text);
5994
6181
  }
5995
- function handleToolUseInputDelta(state, ev) {
5996
- const t2 = state.tools.get(ev.id);
5997
- if (t2) t2.partial += ev.partial;
6182
+ function renderNextStepsBlock(steps) {
6183
+ const lines = steps.map((step, i) => {
6184
+ const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
6185
+ return `${i + 1}. ${step.text}${auto}`;
6186
+ });
6187
+ return `<nextsteps>
6188
+ ${lines.join("\n")}
6189
+ </nextsteps>`;
5998
6190
  }
5999
- function safeJsonOrRaw(s) {
6000
- if (!s) return {};
6191
+ function endsTurn(stopReason) {
6192
+ return stopReason !== "tool_use" && stopReason !== "tool_call";
6193
+ }
6194
+ function maybeAppendPendingNextSteps(ctx, res) {
6195
+ if (ctx.agentId !== "leader") return res;
6196
+ if (!endsTurn(res.stopReason)) return res;
6197
+ if (ctx.signal.aborted) return res;
6198
+ const steps = readPendingNextSteps(ctx);
6199
+ if (!steps) return res;
6200
+ if (hasOpenTodos(ctx.todos)) {
6201
+ clearPendingNextSteps(ctx);
6202
+ return res;
6203
+ }
6204
+ const lastTextIndex = res.content.findLastIndex(isTextBlock);
6205
+ if (lastTextIndex >= 0) {
6206
+ const block = res.content[lastTextIndex];
6207
+ if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
6208
+ clearPendingNextSteps(ctx);
6209
+ return res;
6210
+ }
6211
+ }
6212
+ const rendered = renderNextStepsBlock(steps);
6213
+ const content = [...res.content];
6214
+ const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
6215
+ if (target && isTextBlock(target)) {
6216
+ const separator = target.text.trim() ? "\n\n" : "";
6217
+ content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
6218
+ } else {
6219
+ content.push({ type: "text", text: rendered });
6220
+ }
6221
+ clearPendingNextSteps(ctx);
6222
+ return { ...res, content };
6223
+ }
6224
+
6225
+ // src/prompts/prompt-journal.ts
6226
+ import * as fs5 from "node:fs/promises";
6227
+ import * as path11 from "node:path";
6228
+ async function ensureGitignore(projectRoot) {
6229
+ const gitignorePath = path11.join(projectRoot, ".gitignore");
6001
6230
  try {
6002
- return JSON.parse(s);
6003
- } catch {
6004
- const repaired = completePartialObject(s);
6231
+ let content = "";
6005
6232
  try {
6006
- return JSON.parse(repaired);
6233
+ content = await fs5.readFile(gitignorePath, "utf8");
6007
6234
  } catch {
6008
- return { _raw: repaired };
6235
+ content = "";
6236
+ }
6237
+ if (!content.includes(".wrongstack") && !content.includes(".wrongstack/")) {
6238
+ const addition = content.endsWith("\n") || content.length === 0 ? ".wrongstack/\n" : "\n.wrongstack/\n";
6239
+ await fs5.writeFile(gitignorePath, content + addition, "utf8");
6009
6240
  }
6241
+ } catch {
6010
6242
  }
6011
6243
  }
6012
- function handleToolUseStop(state, ev) {
6013
- const t2 = state.tools.get(ev.id);
6014
- if (t2) {
6015
- t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
6016
- if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
6244
+ function sessionFileId(sessionId) {
6245
+ const leaf = sessionId.split(/[\\/]/u).pop();
6246
+ return leaf && leaf.trim().length > 0 ? leaf : "general";
6247
+ }
6248
+ async function recordPromptJournalEntry(opts) {
6249
+ const now = /* @__PURE__ */ new Date();
6250
+ const timestamp = now.toISOString();
6251
+ const dateStr = timestamp.slice(0, 10);
6252
+ const monthStr = dateStr.slice(0, 7);
6253
+ const sessionId = opts.sessionId && opts.sessionId.trim() ? opts.sessionId.trim() : "general";
6254
+ const id = `pmt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
6255
+ const content = opts.content ?? "";
6256
+ const lines = content.split("\n");
6257
+ const characterCount = content.length;
6258
+ const lineCount = lines.length;
6259
+ const tokenEstimate = Math.ceil(characterCount / 4);
6260
+ const entry = {
6261
+ id,
6262
+ timestamp,
6263
+ sessionId,
6264
+ projectRoot: opts.projectRoot,
6265
+ role: opts.role ?? (opts.category === "system_prompt" ? "system" : "user"),
6266
+ category: opts.category,
6267
+ content,
6268
+ rawContent: opts.rawContent,
6269
+ metadata: {
6270
+ model: opts.model,
6271
+ provider: opts.provider,
6272
+ iterationIndex: opts.iterationIndex,
6273
+ tokenEstimate,
6274
+ characterCount,
6275
+ lineCount,
6276
+ activeTools: opts.activeTools,
6277
+ contextFiles: opts.contextFiles,
6278
+ durationMs: opts.durationMs,
6279
+ decisionReason: opts.decisionReason,
6280
+ tags: opts.tags
6281
+ }
6282
+ };
6283
+ const basePromptsDir = path11.join(opts.projectRoot, ".wrongstack", "prompts");
6284
+ const dayDir = path11.join(basePromptsDir, monthStr, dateStr);
6285
+ try {
6286
+ await fs5.mkdir(dayDir, { recursive: true });
6287
+ await ensureGitignore(opts.projectRoot);
6288
+ const sessionJsonlFile = path11.join(dayDir, `session-${sessionFileId(sessionId)}.jsonl`);
6289
+ await fs5.appendFile(sessionJsonlFile, JSON.stringify(entry) + "\n", "utf8");
6290
+ const sessionMdFile = path11.join(dayDir, `session-${sessionFileId(sessionId)}.md`);
6291
+ const mdSection = formatEntryMarkdown(entry);
6292
+ await fs5.appendFile(sessionMdFile, mdSection, "utf8");
6293
+ const dailySummaryFile = path11.join(dayDir, "daily-summary.md");
6294
+ await updateDailySummary(dailySummaryFile, dateStr, entry);
6295
+ await updateRootCatalog(basePromptsDir, monthStr, dateStr, sessionId, entry);
6296
+ } catch (err) {
6297
+ console.error?.(`Failed to write hierarchical prompt journal: ${err}`);
6298
+ }
6299
+ return entry;
6300
+ }
6301
+ function formatEntryMarkdown(entry) {
6302
+ const tagList = [
6303
+ `**Category:** \`${entry.category}\``,
6304
+ `**Role:** \`${entry.role}\``,
6305
+ entry.metadata.model ? `**Model:** \`${entry.metadata.model}\`` : null,
6306
+ `**Tokens (est):** ~${entry.metadata.tokenEstimate}`,
6307
+ entry.metadata.iterationIndex !== void 0 ? `**Iteration:** #${entry.metadata.iterationIndex}` : null,
6308
+ `**Session:** \`${entry.sessionId}\``
6309
+ ].filter(Boolean).join(" | ");
6310
+ let md = `
6311
+ ### \u{1F4DD} [${entry.timestamp}] \`${entry.id}\`
6312
+ ${tagList}
6313
+
6314
+ `;
6315
+ if (entry.metadata.decisionReason) {
6316
+ md += `> **Rationale:** ${entry.metadata.decisionReason}
6317
+
6318
+ `;
6017
6319
  }
6018
- state.currentTextIndex = -1;
6019
- }
6020
- function handleThinkingStart(state, ev) {
6021
- state.currentThinkingIndex = state.thinking.length;
6022
- state.thinking.push({
6023
- textBuf: "",
6024
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6025
- });
6026
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6027
- state.currentTextIndex = -1;
6028
- }
6029
- function handleThinkingDelta(state, text) {
6030
- if (state.currentThinkingIndex === -1) {
6031
- handleThinkingStart(state, {});
6320
+ if (entry.metadata.activeTools && entry.metadata.activeTools.length > 0) {
6321
+ md += `*Active Tools:* \`${entry.metadata.activeTools.join("`, `")}\`
6322
+
6323
+ `;
6032
6324
  }
6033
- const t2 = state.thinking[state.currentThinkingIndex];
6034
- if (t2) t2.textBuf += text;
6035
- }
6036
- function handleThinkingSignature(state, signature) {
6037
- if (state.currentThinkingIndex === -1) {
6038
- handleThinkingStart(state, {});
6325
+ if (entry.rawContent && entry.rawContent !== entry.content) {
6326
+ md += `**Raw Input:**
6327
+ \`\`\`text
6328
+ ${entry.rawContent.trim()}
6329
+ \`\`\`
6330
+
6331
+ `;
6332
+ md += `**Refined / Injected Prompt:**
6333
+ \`\`\`text
6334
+ ${entry.content.trim()}
6335
+ \`\`\`
6336
+
6337
+ `;
6338
+ } else {
6339
+ md += `**Prompt Content:**
6340
+ \`\`\`text
6341
+ ${entry.content.trim()}
6342
+ \`\`\`
6343
+
6344
+ `;
6039
6345
  }
6040
- const t2 = state.thinking[state.currentThinkingIndex];
6041
- if (t2) t2.signature = signature;
6042
- }
6043
- function handleThinkingStop(state) {
6044
- state.currentThinkingIndex = -1;
6346
+ md += `---
6347
+ `;
6348
+ return md;
6045
6349
  }
6046
- function handleMessageStop(state, ev) {
6047
- state.stopReason = ev.stopReason ?? "end_turn";
6048
- state.usage = ev.usage ?? { input: 0, output: 0 };
6049
- }
6050
- async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
6051
- const state = createStreamingState(req.model);
6052
- const sessionId = resolveEventSessionId(ctx);
6053
- logger.debug("Stream started", { providerId: provider.id, model: req.model });
6054
- const TEXT_BATCH_SIZE = 4;
6055
- let pendingText = "";
6056
- let pendingCount = 0;
6057
- const flushText = () => {
6058
- if (pendingCount > 0) {
6059
- events.emit("provider.text_delta", { sessionId, ctx, text: pendingText });
6060
- pendingText = "";
6061
- pendingCount = 0;
6062
- }
6063
- };
6064
- const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
6350
+ async function updateDailySummary(summaryFile, dateStr, entry) {
6065
6351
  try {
6066
- for (; ; ) {
6067
- const next = await iter.next();
6068
- if (next.done) break;
6069
- const ev = next.value;
6070
- try {
6071
- switch (ev.type) {
6072
- case "message_start":
6073
- handleMessageStart(state, ev.model);
6074
- break;
6075
- case "content_block_start":
6076
- handleContentBlockStart(state, ev);
6077
- break;
6078
- case "content_block_stop":
6079
- handleContentBlockStop(state, ev);
6080
- break;
6081
- case "text_delta":
6082
- handleTextDelta(state, ev.text);
6083
- pendingText += ev.text;
6084
- pendingCount++;
6085
- if (pendingCount >= TEXT_BATCH_SIZE) flushText();
6086
- break;
6087
- case "tool_use_start": {
6088
- flushText();
6089
- const idVal = ev.id;
6090
- const nameVal = ev.name;
6091
- handleToolUseStart(state, { id: idVal, name: nameVal });
6092
- const emittedPayload = { sessionId, ctx, id: idVal ?? "unknown", name: nameVal ?? "unknown" };
6093
- events.emit("provider.tool_use_start", emittedPayload);
6094
- break;
6095
- }
6096
- case "tool_use_input_delta":
6097
- handleToolUseInputDelta(state, ev);
6098
- break;
6099
- case "tool_use_stop": {
6100
- flushText();
6101
- const stoppedName = state.tools.get(ev.id)?.name ?? "unknown";
6102
- handleToolUseStop(state, ev);
6103
- events.emit("provider.tool_use_stop", { sessionId, ctx, id: ev.id, name: stoppedName });
6104
- break;
6105
- }
6106
- case "thinking_start":
6107
- handleThinkingStart(state, ev);
6108
- break;
6109
- case "thinking_delta":
6110
- flushText();
6111
- handleThinkingDelta(state, ev.text);
6112
- events.emit("provider.thinking_delta", { sessionId, ctx, text: ev.text });
6113
- break;
6114
- case "thinking_signature":
6115
- handleThinkingSignature(state, ev.signature);
6116
- break;
6117
- case "thinking_stop":
6118
- handleThinkingStop(state);
6119
- break;
6120
- case "message_stop":
6121
- handleMessageStop(state, ev);
6122
- break;
6123
- default: {
6124
- const unknownEv = ev;
6125
- logger.warn(`Stream received unknown event type: "${String(unknownEv.type)}"`, {
6126
- providerId: provider.id,
6127
- model: req.model,
6128
- eventType: String(unknownEv.type)
6129
- });
6130
- break;
6131
- }
6132
- }
6133
- } catch (handlerErr) {
6134
- const errMsg = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
6135
- const evAny = ev;
6136
- logger.warn(`Stream handler error for event type "${String(evAny.type)}": ${errMsg}`, {
6137
- providerId: provider.id,
6138
- model: req.model,
6139
- eventType: String(evAny.type),
6140
- errorMessage: errMsg
6141
- });
6142
- flushText();
6143
- events.emit("provider.stream_error", {
6144
- sessionId,
6145
- ctx,
6146
- eventType: String(evAny.type),
6147
- msg: errMsg
6148
- });
6149
- }
6150
- }
6151
- } catch (err) {
6152
- if (signal.aborted) {
6153
- state.stopReason = "end_turn";
6154
- flushText();
6155
- logger.debug("Stream aborted \u2014 returning partial state", {
6156
- providerId: provider.id,
6157
- model: req.model,
6158
- textBlockCount: state.textBuffers.length,
6159
- toolBlockCount: state.tools.size,
6160
- thinkingBlockCount: state.thinking.length
6161
- });
6162
- return buildResponse(state);
6163
- }
6164
- throw err;
6165
- } finally {
6352
+ let content = "";
6166
6353
  try {
6167
- let drainTimer = null;
6168
- try {
6169
- const drainPromise = Promise.resolve(iter.return?.()).catch(() => {
6170
- });
6171
- await Promise.race([
6172
- drainPromise,
6173
- new Promise((resolve10) => {
6174
- drainTimer = setTimeout(resolve10, STREAM_DRAIN_TIMEOUT_MS);
6175
- })
6176
- ]);
6177
- } finally {
6178
- if (drainTimer) clearTimeout(drainTimer);
6179
- }
6354
+ content = await fs5.readFile(summaryFile, "utf8");
6180
6355
  } catch {
6356
+ content = `# \u{1F4C5} Daily Prompt Summary \u2014 ${dateStr}
6357
+
6358
+ | Time | ID | Session | Category | Model | Tokens | Rationale |
6359
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
6360
+ `;
6181
6361
  }
6362
+ const time = entry.timestamp.slice(11, 19);
6363
+ const model = entry.metadata.model ?? "-";
6364
+ const reason = entry.metadata.decisionReason ? entry.metadata.decisionReason.slice(0, 40) : "-";
6365
+ const row = `| ${time} | [\`${entry.id}\`](session-${sessionFileId(entry.sessionId)}.md) | \`${entry.sessionId}\` | \`${entry.category}\` | ${model} | ~${entry.metadata.tokenEstimate} | ${reason} |
6366
+ `;
6367
+ await fs5.writeFile(summaryFile, content + row, "utf8");
6368
+ } catch {
6182
6369
  }
6183
- flushText();
6184
- logger.debug("Stream completed", {
6185
- providerId: provider.id,
6186
- model: req.model,
6187
- stopReason: state.stopReason,
6188
- textBlockCount: state.textBuffers.length,
6189
- toolBlockCount: state.tools.size,
6190
- thinkingBlockCount: state.thinking.length,
6191
- usageInput: state.usage.input,
6192
- usageOutput: state.usage.output
6193
- });
6194
- return buildResponse(state);
6195
6370
  }
6371
+ async function updateRootCatalog(baseDir, monthStr, dateStr, sessionId, entry) {
6372
+ const indexJsonFile = path11.join(baseDir, "index.json");
6373
+ const indexMdFile = path11.join(baseDir, "index.md");
6374
+ let catalog;
6375
+ try {
6376
+ const raw = await fs5.readFile(indexJsonFile, "utf8");
6377
+ catalog = JSON.parse(raw);
6378
+ } catch {
6379
+ catalog = {
6380
+ updatedAt: entry.timestamp,
6381
+ totalPrompts: 0,
6382
+ totalTokensEstimated: 0,
6383
+ months: {}
6384
+ };
6385
+ }
6386
+ catalog.updatedAt = entry.timestamp;
6387
+ catalog.totalPrompts += 1;
6388
+ catalog.totalTokensEstimated += entry.metadata.tokenEstimate;
6389
+ if (!catalog.months[monthStr]) {
6390
+ catalog.months[monthStr] = { days: {} };
6391
+ }
6392
+ const monthData = catalog.months[monthStr];
6393
+ if (!monthData.days[dateStr]) {
6394
+ monthData.days[dateStr] = { sessions: {} };
6395
+ }
6396
+ const dayData = monthData.days[dateStr];
6397
+ if (!dayData.sessions[sessionId]) {
6398
+ dayData.sessions[sessionId] = {
6399
+ promptCount: 0,
6400
+ tokenEstimate: 0,
6401
+ lastTimestamp: entry.timestamp,
6402
+ categories: {}
6403
+ };
6404
+ }
6405
+ const sessionData = dayData.sessions[sessionId];
6406
+ sessionData.promptCount += 1;
6407
+ sessionData.tokenEstimate += entry.metadata.tokenEstimate;
6408
+ sessionData.lastTimestamp = entry.timestamp;
6409
+ sessionData.categories[entry.category] = (sessionData.categories[entry.category] ?? 0) + 1;
6410
+ try {
6411
+ await fs5.writeFile(indexJsonFile, JSON.stringify(catalog, null, 2), "utf8");
6412
+ let md = `# \u{1F5C2}\uFE0F Prompt Journal Navigation Index
6196
6413
 
6197
- // src/chronicle/prompt-manifest.ts
6198
- import { createHash as createHash5 } from "node:crypto";
6199
- function createChroniclePromptManifest(request) {
6200
- const roleCounts = {}, blockCounts = {};
6201
- const messages = request.messages.map((message, index) => messageSummary(message, index, roleCounts, blockCounts));
6202
- const systemText = (request.system ?? []).map((block) => block.text).join("\n");
6203
- const toolRecords = (request.tools ?? []).map((tool) => ({
6204
- name: tool.name,
6205
- schemaHash: hash(stable(tool.inputSchema)),
6206
- permission: tool.permission,
6207
- mutating: tool.mutating,
6208
- riskTier: tool.riskTier,
6209
- capabilities: tool.capabilities,
6210
- estimatedTokens: tool._estDefTokens ?? 0
6211
- }));
6212
- const core = {
6213
- systemHash: hash(systemText),
6214
- messages: messages.map(({ hash: contentHash, ...rest }) => ({ ...rest, contentHash })),
6215
- tools: toolRecords,
6216
- model: request.model,
6217
- maxTokens: request.maxTokens,
6218
- temperature: request.temperature,
6219
- topP: request.topP,
6220
- topK: request.topK,
6221
- seed: request.seed,
6222
- toolChoice: request.toolChoice,
6223
- reasoning: request.reasoning,
6224
- cache: request.cache,
6225
- responseFormat: request.responseFormat?.type
6226
- };
6227
- return {
6228
- manifestId: `prompt_${hash(stable(core))}`,
6229
- messageCount: messages.length,
6230
- estimatedMessageTokens: messages.reduce((sum, message) => sum + (message.estimatedTokens ?? 0), 0),
6231
- contentBytes: messages.reduce((sum, message) => sum + message.bytes, 0) + Buffer.byteLength(systemText),
6232
- roleCounts,
6233
- blockCounts,
6234
- system: { blockCount: request.system?.length ?? 0, bytes: Buffer.byteLength(systemText), hash: hash(systemText) },
6235
- messages,
6236
- tools: {
6237
- count: toolRecords.length,
6238
- estimatedDefinitionTokens: toolRecords.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
6239
- manifestHash: hash(stable(toolRecords)),
6240
- names: toolRecords.map((tool) => tool.name),
6241
- mutating: toolRecords.filter((tool) => tool.mutating).length,
6242
- destructive: toolRecords.filter((tool) => tool.riskTier === "destructive").length
6243
- },
6244
- request: {
6245
- maxTokens: request.maxTokens,
6246
- temperature: request.temperature,
6247
- topP: request.topP,
6248
- topK: request.topK,
6249
- frequencyPenalty: request.frequencyPenalty,
6250
- presencePenalty: request.presencePenalty,
6251
- seed: request.seed,
6252
- candidateCount: request.candidateCount,
6253
- logprobs: request.logprobs,
6254
- topLogprobs: request.topLogprobs,
6255
- stopSequenceCount: request.stopSequences?.length ?? 0,
6256
- toolChoice: request.toolChoice,
6257
- reasoning: request.reasoning,
6258
- cache: request.cache,
6259
- responseFormat: request.responseFormat?.type,
6260
- safetySettingCount: request.safetySettings?.length ?? 0,
6261
- userHash: request.user ? hash(request.user) : void 0
6414
+ `;
6415
+ md += `* **Total Prompts Logged:** ${catalog.totalPrompts}
6416
+ `;
6417
+ md += `* **Total Tokens (est):** ~${catalog.totalTokensEstimated.toLocaleString()}
6418
+ `;
6419
+ md += `* **Last Recorded Activity:** ${catalog.updatedAt}
6420
+
6421
+ `;
6422
+ md += `## \u{1F4C5} Recorded Dates & Sessions
6423
+
6424
+ `;
6425
+ md += `| Date | Session | Prompts | Tokens (est) | Daily Log | Session Log |
6426
+ `;
6427
+ md += `| :--- | :--- | :--- | :--- | :--- | :--- |
6428
+ `;
6429
+ for (const [m, mObj] of Object.entries(catalog.months).sort().reverse()) {
6430
+ for (const [d, dObj] of Object.entries(mObj.days).sort().reverse()) {
6431
+ for (const [sId, sData] of Object.entries(dObj.sessions)) {
6432
+ const dailyLink = `[daily-summary.md](./${m}/${d}/daily-summary.md)`;
6433
+ const sessionLink = `[session-${sessionFileId(sId)}.md](./${m}/${d}/session-${sessionFileId(sId)}.md)`;
6434
+ md += `| **${d}** | \`${sId}\` | ${sData.promptCount} | ~${sData.tokenEstimate.toLocaleString()} | ${dailyLink} | ${sessionLink} |
6435
+ `;
6436
+ }
6437
+ }
6262
6438
  }
6263
- };
6439
+ await fs5.writeFile(indexMdFile, md, "utf8");
6440
+ } catch {
6441
+ }
6264
6442
  }
6265
- function messageSummary(message, index, roles, totals) {
6266
- roles[message.role] = (roles[message.role] ?? 0) + 1;
6267
- const blocks = typeof message.content === "string" ? { text: 1 } : countBlocks(message.content);
6268
- for (const [type, count] of Object.entries(blocks)) totals[type] = (totals[type] ?? 0) + count;
6269
- const content = contentIdentity(message.content);
6270
- return {
6271
- index,
6272
- role: message.role,
6273
- bytes: content.bytes,
6274
- ...message._estTokens !== void 0 ? { estimatedTokens: message._estTokens } : {},
6275
- hash: content.hash,
6276
- blocks
6277
- };
6443
+
6444
+ // src/core/streaming-response-builder.ts
6445
+ import { randomUUID as randomUUID4 } from "node:crypto";
6446
+
6447
+ // src/utils/json-repair.ts
6448
+ function completePartialObject(s) {
6449
+ if (!s.trim().startsWith("{")) return s;
6450
+ if (tryParse(s).ok) return s;
6451
+ return repairTruncated(s);
6278
6452
  }
6279
- function countBlocks(blocks) {
6280
- const counts = {};
6281
- for (const block of blocks) counts[block.type] = (counts[block.type] ?? 0) + 1;
6282
- return counts;
6283
- }
6284
- function hash(value) {
6285
- return createHash5("sha256").update(value).digest("hex");
6286
- }
6287
- function contentIdentity(content) {
6288
- if (typeof content === "string") return { hash: hash(content), bytes: Buffer.byteLength(content) };
6289
- const digest = createHash5("sha256");
6290
- let bytes = 0;
6291
- const add = (value) => {
6292
- if (!value) return;
6293
- digest.update(value);
6294
- bytes += Buffer.byteLength(value);
6295
- };
6296
- for (const block of content) {
6297
- add(block.type);
6298
- if (block.type === "text") add(block.text);
6299
- else if (block.type === "thinking") {
6300
- add(block.thinking);
6301
- add(block.signature);
6302
- } else if (block.type === "tool_use") {
6303
- add(block.id);
6304
- add(block.name);
6305
- add(stable(block.input));
6306
- } else if (block.type === "tool_result") {
6307
- add(block.tool_use_id);
6308
- add(block.name);
6309
- add(block.content);
6310
- add(String(block.is_error ?? false));
6311
- } else if (block.type === "image") {
6312
- add(block.source.type);
6313
- add(block.source.media_type);
6314
- add(block.source.url);
6315
- add(block.source.data);
6453
+ function repairTruncated(s) {
6454
+ const stack = [];
6455
+ let inString = false;
6456
+ let escaped = false;
6457
+ let sawKey = false;
6458
+ let prevSig = "";
6459
+ let contentEnd = 0;
6460
+ let stringBraceDepth = 0;
6461
+ for (let i = 0; i < s.length; i++) {
6462
+ const ch = expectDefined(s[i]);
6463
+ if (inString) {
6464
+ contentEnd = i + 1;
6465
+ if (escaped) {
6466
+ escaped = false;
6467
+ continue;
6468
+ }
6469
+ if (ch === "\\") {
6470
+ escaped = true;
6471
+ continue;
6472
+ }
6473
+ if (ch === '"') {
6474
+ inString = false;
6475
+ prevSig = '"';
6476
+ stringBraceDepth = 0;
6477
+ continue;
6478
+ }
6479
+ if (ch === "{") stringBraceDepth++;
6480
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
6481
+ continue;
6482
+ }
6483
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
6484
+ contentEnd = i + 1;
6485
+ if (ch === '"') {
6486
+ inString = true;
6487
+ sawKey = true;
6488
+ stringBraceDepth = 0;
6489
+ prevSig = '"';
6490
+ } else if (ch === "{" || ch === "[") {
6491
+ stack.push(ch);
6492
+ prevSig = ch;
6493
+ } else if (ch === "}" || ch === "]") {
6494
+ stack.pop();
6495
+ prevSig = ch;
6496
+ } else {
6497
+ prevSig = ch;
6316
6498
  }
6317
6499
  }
6318
- return { hash: digest.digest("hex"), bytes };
6319
- }
6320
- function stable(value) {
6321
- if (value === void 0) return "undefined";
6322
- if (value === null || typeof value !== "object") return JSON.stringify(value);
6323
- if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
6324
- return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
6325
- }
6326
-
6327
- // src/observability/network-telemetry.ts
6328
- import { AsyncLocalStorage } from "node:async_hooks";
6329
- var storage = new AsyncLocalStorage();
6330
- function runWithNetworkTelemetry(context, run) {
6331
- return storage.run(context, run);
6332
- }
6333
-
6334
- // src/security/error-sanitize.ts
6335
- import { homedir as homedir2 } from "node:os";
6336
- var scrubber = new DefaultSecretScrubber();
6337
- function scrubErrorText(text) {
6338
- if (!text) return text;
6339
- let out = scrubber.scrub(text);
6340
- const home = safeHomedir();
6341
- if (home && home.length > 2) {
6342
- out = replaceAllCaseInsensitive(out, home, "~");
6343
- const alt = home.includes("\\") ? home.replace(/\\/g, "/") : home.replace(/\//g, "\\");
6344
- if (alt !== home) out = replaceAllCaseInsensitive(out, alt, "~");
6500
+ if (!sawKey && !inString) return s;
6501
+ let result = s.slice(0, contentEnd);
6502
+ if (inString) {
6503
+ if (escaped) {
6504
+ result = result.slice(0, -1);
6505
+ } else if (endsWithInvalidEscape(result)) {
6506
+ result = result.slice(0, -2);
6507
+ }
6508
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
6509
+ result += '"';
6510
+ } else if (prevSig === ":") {
6511
+ result += "null";
6345
6512
  }
6346
- return out;
6513
+ for (let k = stack.length - 1; k >= 0; k--) {
6514
+ result += stack[k] === "{" ? "}" : "]";
6515
+ }
6516
+ if (!tryParse(result).ok) {
6517
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
6518
+ if (tryParse(patched).ok) result = patched;
6519
+ }
6520
+ return result;
6347
6521
  }
6348
- function safeHomedir() {
6522
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
6523
+ function endsWithInvalidEscape(str) {
6524
+ const last = str[str.length - 1];
6525
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
6526
+ if (VALID_ESCAPE.has(last)) return false;
6527
+ let backslashes = 0;
6528
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
6529
+ return backslashes % 2 === 1;
6530
+ }
6531
+ function tryParse(s) {
6349
6532
  try {
6350
- return homedir2();
6533
+ return { ok: true, value: JSON.parse(s) };
6351
6534
  } catch {
6352
- return "";
6535
+ return { ok: false };
6353
6536
  }
6354
6537
  }
6355
- function replaceAllCaseInsensitive(haystack, needle, replacement) {
6356
- const lowerHay = haystack.toLowerCase();
6357
- const lowerNeedle = needle.toLowerCase();
6358
- let idx = lowerHay.indexOf(lowerNeedle);
6359
- if (idx === -1) return haystack;
6360
- let out = "";
6361
- let from = 0;
6362
- while (idx !== -1) {
6363
- out += haystack.slice(from, idx) + replacement;
6364
- from = idx + needle.length;
6365
- idx = lowerHay.indexOf(lowerNeedle, from);
6538
+
6539
+ // src/core/streaming-response-builder.ts
6540
+ var STREAM_DRAIN_TIMEOUT_MS = 500;
6541
+ function buildResponse(state) {
6542
+ const content = [];
6543
+ for (const b of state.blockOrder) {
6544
+ if (b.kind === "text") {
6545
+ const txt = state.textBuffers[b.idx] ?? "";
6546
+ if (txt) content.push({ type: "text", text: txt });
6547
+ } else if (b.kind === "thinking") {
6548
+ const t2 = state.thinking[b.idx];
6549
+ if (!t2) continue;
6550
+ if (!t2.textBuf && !t2.signature) continue;
6551
+ const block = { type: "thinking", thinking: t2.textBuf };
6552
+ if (t2.signature) block.signature = t2.signature;
6553
+ if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
6554
+ block.providerMeta = t2.providerMeta;
6555
+ }
6556
+ content.push(block);
6557
+ } else {
6558
+ const tb = state.tools.get(b.id);
6559
+ if (tb) {
6560
+ const block = {
6561
+ type: "tool_use",
6562
+ id: b.id,
6563
+ name: tb.name,
6564
+ input: tb.input ?? {}
6565
+ };
6566
+ if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
6567
+ block.providerMeta = tb.providerMeta;
6568
+ }
6569
+ content.push(block);
6570
+ }
6571
+ }
6366
6572
  }
6367
- return out + haystack.slice(from);
6573
+ if (content.length === 0) content.push({ type: "text", text: "" });
6574
+ return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
6368
6575
  }
6369
-
6370
- // src/core/provider-runner.ts
6371
- import { randomUUID as randomUUID5 } from "node:crypto";
6372
- function scrubProviderBody(body) {
6373
- if (!body) return void 0;
6576
+ function createStreamingState(model) {
6374
6577
  return {
6375
- ...body,
6376
- ...body.type !== void 0 ? { type: scrubErrorText(body.type) } : {},
6377
- ...body.message !== void 0 ? { message: scrubErrorText(body.message) } : {},
6378
- ...body.raw !== void 0 ? { raw: scrubErrorText(body.raw) } : {},
6379
- ...body.requestId !== void 0 ? { requestId: scrubErrorText(body.requestId) } : {}
6578
+ model,
6579
+ stopReason: "end_turn",
6580
+ usage: { input: 0, output: 0 },
6581
+ textBuffers: [],
6582
+ currentTextIndex: -1,
6583
+ tools: /* @__PURE__ */ new Map(),
6584
+ thinking: [],
6585
+ currentThinkingIndex: -1,
6586
+ blockOrder: []
6380
6587
  };
6381
6588
  }
6382
- function providerLogCtx(p, r) {
6383
- return {
6384
- providerId: p.id,
6385
- model: r.model,
6386
- streaming: p.capabilities.streaming,
6387
- msgCount: r.messages.length,
6388
- toolCount: r.tools?.length ?? 0
6389
- };
6589
+ function handleMessageStart(state, model) {
6590
+ state.model = model;
6390
6591
  }
6391
- async function runProviderWithRetry(opts) {
6392
- const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
6393
- const logicalRequestId = randomUUID5();
6394
- const promptManifest = createChroniclePromptManifest(request);
6395
- let attempt = 0;
6396
- for (; ; ) {
6397
- const attemptId = randomUUID5();
6398
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6399
- const startedNs = process.hrtime.bigint();
6400
- const correlation = {
6401
- sessionId: resolveEventSessionId(ctx),
6402
- ...ctx.traceId ? { traceId: ctx.traceId } : {},
6403
- ...ctx.agentId ? { agentId: ctx.agentId } : {},
6404
- logicalRequestId,
6405
- attemptId,
6406
- attempt,
6407
- providerId: provider.id,
6408
- model: request.model,
6409
- taskId: ctx.currentKanbanTaskId,
6410
- boardId: ctx.currentKanbanBoardId
6411
- };
6412
- events.emit("provider.attempt.started", {
6413
- ...correlation,
6414
- streaming: provider.capabilities.streaming,
6415
- messageCount: request.messages.length,
6416
- toolCount: request.tools?.length ?? 0,
6417
- promptManifest,
6418
- startedAt
6419
- });
6420
- const span = tracer?.startSpan("provider.complete", {
6421
- "provider.id": provider.id,
6422
- "provider.model": request.model,
6423
- "provider.streaming": provider.capabilities.streaming,
6424
- "provider.attempt": attempt
6425
- });
6426
- logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
6427
- try {
6428
- const res = await runWithNetworkTelemetry({
6429
- events,
6430
- sessionId: resolveEventSessionId(ctx),
6431
- ...ctx.traceId ? { traceId: ctx.traceId } : {},
6432
- ...ctx.agentId ? { agentId: ctx.agentId } : {},
6433
- attemptId,
6434
- initiator: "provider",
6435
- operationName: `${provider.id}.complete`
6436
- }, () => provider.capabilities.streaming ? streamProviderToResponse(provider, request, signal, ctx, events, logger) : provider.complete(request, { signal }));
6437
- span?.setAttribute("provider.stopReason", res.stopReason);
6438
- span?.setAttribute("provider.usage_in", res.usage.input);
6439
- span?.setAttribute("provider.usage_out", res.usage.output);
6440
- span?.end();
6441
- events.emit("provider.attempt.completed", {
6442
- ...correlation,
6443
- startedAt,
6444
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
6445
- durationMs: elapsedMs(startedNs),
6446
- stopReason: res.stopReason,
6447
- usage: res.usage
6448
- });
6449
- logger.debug("Provider call succeeded", {
6450
- ...providerLogCtx(provider, request),
6451
- stopReason: res.stopReason,
6452
- usageInput: res.usage.input,
6453
- usageOutput: res.usage.output,
6454
- cacheRead: res.usage.cacheRead,
6455
- cacheWrite: res.usage.cacheWrite,
6456
- attempts: attempt + 1
6457
- });
6458
- return res;
6459
- } catch (err) {
6460
- if (err instanceof Error) span?.recordError(err);
6461
- span?.end();
6462
- if (signal.aborted) throw err;
6463
- const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
6464
- const errAsErr = err instanceof Error ? err : new Error(String(err));
6465
- const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
6466
- const providerErrorBody = isProviderErr ? scrubProviderBody(err.body) : void 0;
6467
- const description = scrubErrorText(
6468
- isProviderErr ? err.describe() : errAsErr.message
6469
- );
6470
- const delay2 = canRetry ? Math.round(retry.delayMs(attempt, isProviderErr ? err : errAsErr)) : void 0;
6471
- events.emit("provider.attempt.failed", {
6472
- ...correlation,
6473
- startedAt,
6474
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
6475
- durationMs: elapsedMs(startedNs),
6476
- status: isProviderErr ? err.status : 0,
6477
- failureKind: isProviderErr ? err.kind : "unknown",
6478
- description,
6479
- retryable: canRetry,
6480
- retryScheduled: canRetry,
6481
- ...delay2 !== void 0 ? { retryDelayMs: delay2 } : {},
6482
- ...providerErrorBody?.requestId ? { providerRequestId: providerErrorBody.requestId } : {},
6483
- ...providerErrorBody ? { errorBody: providerErrorBody } : {}
6484
- });
6485
- if (!canRetry) {
6486
- events.emit("provider.error", {
6487
- sessionId: resolveEventSessionId(ctx),
6488
- providerId: isProviderErr ? err.providerId : provider.id,
6489
- status: isProviderErr ? err.status : 0,
6490
- description,
6491
- retryable: false,
6492
- ...providerErrorBody ? { errorBody: providerErrorBody } : {}
6493
- });
6494
- logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
6495
- ...providerLogCtx(provider, request),
6496
- attempts: attempt + 1,
6497
- errorDescription: description,
6498
- status: isProviderErr ? err.status : void 0,
6499
- errorBody: providerErrorBody,
6500
- errorName: err instanceof Error ? err.name : void 0,
6501
- errorStack: err instanceof Error ? err.stack?.split("\n").slice(0, 3).join("\n") : void 0
6502
- });
6503
- throw toWrongStackError(err);
6504
- }
6505
- const attemptNum = attempt + 1;
6506
- const maxAttempts = retry.maxAttempts(isProviderErr ? err : errAsErr);
6507
- logger.warn(`Provider retry ${attemptNum}/${maxAttempts} in ${delay2}ms \u2014 ${description}`, {
6508
- ...providerLogCtx(provider, request),
6509
- attempt: attemptNum,
6510
- maxAttempts,
6511
- delayMs: delay2,
6512
- errorDescription: description,
6513
- status: isProviderErr ? err.status : void 0,
6514
- errorBody: providerErrorBody
6515
- });
6516
- events.emit("provider.retry", {
6517
- sessionId: resolveEventSessionId(ctx),
6518
- providerId: isProviderErr ? err.providerId : provider.id,
6519
- attempt: attemptNum,
6520
- delayMs: delay2,
6521
- status: isProviderErr ? err.status : 0,
6522
- description,
6523
- ...providerErrorBody ? { errorBody: providerErrorBody } : {}
6524
- });
6525
- await new Promise((resolve10, reject) => {
6526
- let settled = false;
6527
- const cleanup = () => {
6528
- clearTimeout(t2);
6529
- signal.removeEventListener("abort", onAbort);
6530
- };
6531
- const onAbort = () => {
6532
- if (settled) return;
6533
- settled = true;
6534
- cleanup();
6535
- reject(new Error("aborted"));
6536
- };
6537
- const t2 = setTimeout(() => {
6538
- if (settled) return;
6539
- settled = true;
6540
- cleanup();
6541
- resolve10();
6542
- }, delay2);
6543
- if (signal.aborted) {
6544
- onAbort();
6545
- return;
6546
- }
6547
- signal.addEventListener("abort", onAbort);
6548
- });
6549
- attempt++;
6550
- }
6592
+ function handleContentBlockStart(state, ev) {
6593
+ const kind = ev.kind ?? "text";
6594
+ if (kind === "text") {
6595
+ state.currentTextIndex = state.textBuffers.length;
6596
+ state.textBuffers.push("");
6597
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
6598
+ } else if (kind === "tool_use") {
6599
+ const id = ev.id ?? randomUUID4();
6600
+ state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
6601
+ state.blockOrder.push({ kind: "tool", id });
6602
+ state.currentTextIndex = -1;
6603
+ } else if (kind === "thinking") {
6604
+ state.currentThinkingIndex = state.thinking.length;
6605
+ state.thinking.push({
6606
+ textBuf: "",
6607
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6608
+ });
6609
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6610
+ state.currentTextIndex = -1;
6551
6611
  }
6552
6612
  }
6553
- function elapsedMs(startedNs) {
6554
- return Number(process.hrtime.bigint() - startedNs) / 1e6;
6613
+ function handleContentBlockStop(state, ev) {
6614
+ void state;
6615
+ void ev;
6555
6616
  }
6556
-
6557
- // src/core/queued-messages.ts
6558
- var META_KEY3 = "_queuedMessagesAwareness";
6559
- var MAX_ITEMS = 20;
6560
- var MAX_PREVIEW_CHARS = 500;
6561
- function read(ctx) {
6562
- const raw = ctx.meta[META_KEY3];
6563
- if (!raw || typeof raw !== "object") return void 0;
6564
- const s = raw;
6565
- return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
6617
+ function handleTextDelta(state, text) {
6618
+ if (state.currentTextIndex === -1) {
6619
+ state.currentTextIndex = state.textBuffers.length;
6620
+ state.textBuffers.push("");
6621
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
6622
+ }
6623
+ state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
6566
6624
  }
6567
- function sameList(a, b) {
6568
- return a.length === b.length && a.every((v, i) => v === b[i]);
6625
+ function handleToolUseStart(state, ev) {
6626
+ state.currentTextIndex = -1;
6627
+ state.tools.set(ev.id, { name: ev.name, partial: "" });
6628
+ state.blockOrder.push({ kind: "tool", id: ev.id });
6569
6629
  }
6570
- function toPreview(text) {
6571
- const trimmed = text.trim();
6572
- if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
6573
- return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
6630
+ function handleToolUseInputDelta(state, ev) {
6631
+ const t2 = state.tools.get(ev.id);
6632
+ if (t2) t2.partial += ev.partial;
6574
6633
  }
6575
- function setQueuedMessagesSnapshot(ctx, texts) {
6576
- const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
6577
- const prev = read(ctx);
6578
- if (items.length === 0 && (!prev || prev.seen.length === 0)) {
6579
- if (prev) delete ctx.meta[META_KEY3];
6580
- return;
6634
+ function safeJsonOrRaw(s) {
6635
+ if (!s) return {};
6636
+ try {
6637
+ return JSON.parse(s);
6638
+ } catch {
6639
+ const repaired = completePartialObject(s);
6640
+ try {
6641
+ return JSON.parse(repaired);
6642
+ } catch {
6643
+ return { _raw: repaired };
6644
+ }
6581
6645
  }
6582
- ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
6583
6646
  }
6584
- function consumeQueuedMessagesUpdate(ctx) {
6585
- const s = read(ctx);
6586
- if (!s) return null;
6587
- if (sameList(s.items, s.seen)) return null;
6588
- if (s.items.length === 0) {
6589
- delete ctx.meta[META_KEY3];
6590
- return null;
6647
+ function handleToolUseStop(state, ev) {
6648
+ const t2 = state.tools.get(ev.id);
6649
+ if (t2) {
6650
+ t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
6651
+ if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
6591
6652
  }
6592
- ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
6593
- return s.items;
6653
+ state.currentTextIndex = -1;
6594
6654
  }
6595
- function buildQueuedMessagesBlock(items) {
6596
- const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
6597
- return [
6598
- "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
6599
- "waiting in the input queue and each will be delivered as its own turn",
6600
- "after you finish, so do NOT answer or act on them as new tasks now. Read",
6601
- "them as a heads-up: if one changes what you should be doing right now",
6602
- "(new constraint, invalidated approach), adapt your current work; if one",
6603
- "is just an FYI, fold it in. The queue may still change before delivery.",
6604
- "This notice replaces any earlier one \u2014 the queue now holds exactly:",
6605
- "",
6606
- body,
6607
- "]"
6608
- ].join("\n");
6655
+ function handleThinkingStart(state, ev) {
6656
+ state.currentThinkingIndex = state.thinking.length;
6657
+ state.thinking.push({
6658
+ textBuf: "",
6659
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6660
+ });
6661
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6662
+ state.currentTextIndex = -1;
6609
6663
  }
6610
-
6611
- // src/core/request-provider-binding.ts
6612
- var requestProviders = /* @__PURE__ */ new WeakMap();
6613
- function bindRequestProvider(request, provider) {
6614
- requestProviders.set(request, provider);
6664
+ function handleThinkingDelta(state, text) {
6665
+ if (state.currentThinkingIndex === -1) {
6666
+ handleThinkingStart(state, {});
6667
+ }
6668
+ const t2 = state.thinking[state.currentThinkingIndex];
6669
+ if (t2) t2.textBuf += text;
6615
6670
  }
6616
- function providerBoundToRequest(request) {
6617
- return requestProviders.get(request);
6671
+ function handleThinkingSignature(state, signature) {
6672
+ if (state.currentThinkingIndex === -1) {
6673
+ handleThinkingStart(state, {});
6674
+ }
6675
+ const t2 = state.thinking[state.currentThinkingIndex];
6676
+ if (t2) t2.signature = signature;
6618
6677
  }
6619
-
6620
- // src/core/agent-loop.ts
6621
- function toError(err) {
6622
- return err instanceof Error ? err : new Error(String(err));
6678
+ function handleThinkingStop(state) {
6679
+ state.currentThinkingIndex = -1;
6623
6680
  }
6624
- function signalAbortReason(signal) {
6625
- const r = signal.reason;
6626
- if (r instanceof Error) return r.message || r.name;
6627
- if (typeof r === "string" && r.length > 0) return r;
6628
- return "aborted";
6681
+ function handleMessageStop(state, ev) {
6682
+ state.stopReason = ev.stopReason ?? "end_turn";
6683
+ state.usage = ev.usage ?? { input: 0, output: 0 };
6629
6684
  }
6630
- function createAgentLoopHandler(a, handlers) {
6631
- const checkMailbox = attachMailboxChecker(a);
6632
- const fleetPulseCfg = (() => {
6633
- try {
6634
- return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
6635
- } catch {
6636
- return void 0;
6685
+ async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
6686
+ const state = createStreamingState(req.model);
6687
+ const sessionId = resolveEventSessionId(ctx);
6688
+ logger.debug("Stream started", { providerId: provider.id, model: req.model });
6689
+ const TEXT_BATCH_SIZE = 4;
6690
+ let pendingText = "";
6691
+ let pendingCount = 0;
6692
+ const flushText = () => {
6693
+ if (pendingCount > 0) {
6694
+ events.emit("provider.text_delta", { sessionId, ctx, text: pendingText });
6695
+ pendingText = "";
6696
+ pendingCount = 0;
6637
6697
  }
6638
- })();
6639
- const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
6640
- const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
6641
- const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
6642
- async function refreshProviderContextLimit(provider, model, opts = {}) {
6643
- a.ctx.meta ??= {};
6644
- const routeKey = `${provider.id}/${model}`;
6645
- const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
6646
- const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
6647
- const previousMaxContext = currentMaxContext();
6648
- if (routeChanged) {
6649
- delete a.ctx.meta["providerOverflowMaxContext"];
6650
- delete a.ctx.meta["contextLimitBaseline"];
6651
- delete a.ctx.meta["providerLimitEffectiveMaxContext"];
6652
- delete a.ctx.meta["effectiveMaxContext"];
6653
- delete a.ctx.meta["effectiveMaxContextSource"];
6654
- delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
6655
- delete a.ctx.meta["contextLimitLastEmittedSource"];
6698
+ };
6699
+ const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
6700
+ try {
6701
+ for (; ; ) {
6702
+ const next = await iter.next();
6703
+ if (next.done) break;
6704
+ const ev = next.value;
6705
+ try {
6706
+ switch (ev.type) {
6707
+ case "message_start":
6708
+ handleMessageStart(state, ev.model);
6709
+ break;
6710
+ case "content_block_start":
6711
+ handleContentBlockStart(state, ev);
6712
+ break;
6713
+ case "content_block_stop":
6714
+ handleContentBlockStop(state, ev);
6715
+ break;
6716
+ case "text_delta":
6717
+ handleTextDelta(state, ev.text);
6718
+ pendingText += ev.text;
6719
+ pendingCount++;
6720
+ if (pendingCount >= TEXT_BATCH_SIZE) flushText();
6721
+ break;
6722
+ case "tool_use_start": {
6723
+ flushText();
6724
+ const idVal = ev.id;
6725
+ const nameVal = ev.name;
6726
+ handleToolUseStart(state, { id: idVal, name: nameVal });
6727
+ const emittedPayload = { sessionId, ctx, id: idVal ?? "unknown", name: nameVal ?? "unknown" };
6728
+ events.emit("provider.tool_use_start", emittedPayload);
6729
+ break;
6730
+ }
6731
+ case "tool_use_input_delta":
6732
+ handleToolUseInputDelta(state, ev);
6733
+ break;
6734
+ case "tool_use_stop": {
6735
+ flushText();
6736
+ const stoppedName = state.tools.get(ev.id)?.name ?? "unknown";
6737
+ handleToolUseStop(state, ev);
6738
+ events.emit("provider.tool_use_stop", { sessionId, ctx, id: ev.id, name: stoppedName });
6739
+ break;
6740
+ }
6741
+ case "thinking_start":
6742
+ handleThinkingStart(state, ev);
6743
+ break;
6744
+ case "thinking_delta":
6745
+ flushText();
6746
+ handleThinkingDelta(state, ev.text);
6747
+ events.emit("provider.thinking_delta", { sessionId, ctx, text: ev.text });
6748
+ break;
6749
+ case "thinking_signature":
6750
+ handleThinkingSignature(state, ev.signature);
6751
+ break;
6752
+ case "thinking_stop":
6753
+ handleThinkingStop(state);
6754
+ break;
6755
+ case "message_stop":
6756
+ handleMessageStop(state, ev);
6757
+ break;
6758
+ default: {
6759
+ const unknownEv = ev;
6760
+ logger.warn(`Stream received unknown event type: "${String(unknownEv.type)}"`, {
6761
+ providerId: provider.id,
6762
+ model: req.model,
6763
+ eventType: String(unknownEv.type)
6764
+ });
6765
+ break;
6766
+ }
6767
+ }
6768
+ } catch (handlerErr) {
6769
+ const errMsg = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
6770
+ const evAny = ev;
6771
+ logger.warn(`Stream handler error for event type "${String(evAny.type)}": ${errMsg}`, {
6772
+ providerId: provider.id,
6773
+ model: req.model,
6774
+ eventType: String(evAny.type),
6775
+ errorMessage: errMsg
6776
+ });
6777
+ flushText();
6778
+ events.emit("provider.stream_error", {
6779
+ sessionId,
6780
+ ctx,
6781
+ eventType: String(evAny.type),
6782
+ msg: errMsg
6783
+ });
6784
+ }
6656
6785
  }
6657
- a.ctx.meta["contextLimitRouteKey"] = routeKey;
6658
- const routeMeta = a.ctx.meta["effectiveMaxContext"];
6659
- const routeProviderCap = provider.capabilities.maxContext;
6660
- const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
6661
- const emitEffectiveLimit = (effective2, source) => {
6662
- const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
6663
- const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
6664
- if (lastEmitted === effective2 && lastEmittedSource === source) return;
6665
- const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
6666
- a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
6667
- a.ctx.meta["contextLimitLastEmittedSource"] = source;
6668
- a.events.emit("ctx.max_context", {
6669
- sessionId: resolveEventSessionId(a.ctx),
6786
+ } catch (err) {
6787
+ if (signal.aborted) {
6788
+ state.stopReason = "end_turn";
6789
+ flushText();
6790
+ logger.debug("Stream aborted \u2014 returning partial state", {
6670
6791
  providerId: provider.id,
6671
- modelId: model,
6672
- maxContext: effective2,
6673
- ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
6674
- source,
6675
- decreased: eventPrevious > 0 && effective2 < eventPrevious
6792
+ model: req.model,
6793
+ textBlockCount: state.textBuffers.length,
6794
+ toolBlockCount: state.tools.size,
6795
+ thinkingBlockCount: state.thinking.length
6676
6796
  });
6677
- };
6678
- const emitNativeAfterRouteChange = () => {
6679
- if (routeChanged && currentEffective > 0) {
6680
- a.ctx.meta["effectiveMaxContext"] = currentEffective;
6681
- a.ctx.meta["effectiveMaxContextSource"] = "configured";
6682
- emitEffectiveLimit(currentEffective, "configured");
6683
- }
6684
- };
6685
- const refresh = provider.refreshContextLimit;
6686
- if (opts.probe === false || !refresh) {
6687
- emitNativeAfterRouteChange();
6688
- return;
6797
+ return buildResponse(state);
6689
6798
  }
6690
- let discovered;
6799
+ throw err;
6800
+ } finally {
6691
6801
  try {
6692
- discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
6802
+ let drainTimer = null;
6803
+ try {
6804
+ const drainPromise = Promise.resolve(iter.return?.()).catch(() => {
6805
+ });
6806
+ await Promise.race([
6807
+ drainPromise,
6808
+ new Promise((resolve10) => {
6809
+ drainTimer = setTimeout(resolve10, STREAM_DRAIN_TIMEOUT_MS);
6810
+ })
6811
+ ]);
6812
+ } finally {
6813
+ if (drainTimer) clearTimeout(drainTimer);
6814
+ }
6693
6815
  } catch {
6694
- emitNativeAfterRouteChange();
6695
- return;
6696
- }
6697
- const providerLimit = discovered?.maxContext;
6698
- if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
6699
- emitNativeAfterRouteChange();
6700
- return;
6701
- }
6702
- const providerLimitFloored = Math.floor(providerLimit);
6703
- const existingBaseline = a.ctx.meta["contextLimitBaseline"];
6704
- const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
6705
- const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
6706
- const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
6707
- const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
6708
- a.ctx.meta["contextLimitBaseline"] = baseline;
6709
- const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
6710
- const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
6711
- const ceilings = [baseline, providerLimitFloored];
6712
- if (overflowFloored !== void 0) ceilings.push(overflowFloored);
6713
- const effective = Math.min(...ceilings.filter((limit) => limit > 0));
6714
- let effectiveSource;
6715
- if (overflowFloored !== void 0 && effective === overflowFloored) {
6716
- effectiveSource = "provider_overflow";
6717
- } else if (effective === providerLimitFloored) {
6718
- effectiveSource = "provider";
6719
- } else {
6720
- effectiveSource = "configured";
6721
- }
6722
- a.ctx.meta["effectiveMaxContext"] = effective;
6723
- a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
6724
- a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
6725
- emitEffectiveLimit(effective, effectiveSource);
6726
- }
6727
- async function compactContextIfNeeded() {
6728
- const msgCount = a.ctx.messages.length;
6729
- const maxContext = currentMaxContext();
6730
- const revision = a.ctx.state.revision;
6731
- const toolsRef = a.ctx.tools;
6732
- const systemRef = a.ctx.systemPrompt;
6733
- const requestTokens = a.ctx.lastRequestTokens;
6734
- if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
6735
- return false;
6736
- }
6737
- const beforeMessages = a.ctx.messages;
6738
- const beforeMsgCount = a.ctx.messages.length;
6739
- const beforeRevision = a.ctx.state.revision;
6740
- await a.pipelines.contextWindow.run(a.ctx);
6741
- _lastCompactionMsgCount = a.ctx.messages.length;
6742
- _lastCompactionRevision = a.ctx.state.revision;
6743
- _lastCompactionToolsRef = a.ctx.tools;
6744
- _lastCompactionSystemRef = a.ctx.systemPrompt;
6745
- _lastCompactionMaxContext = maxContext;
6746
- const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
6747
- const tokens = refreshContextRequestTokenStash({ force: changed });
6748
- _lastCompactionRequestTokens = tokens;
6749
- const load = maxContext > 0 ? tokens / maxContext : 0;
6750
- _lastCompactionWasNoop = tokens > 0 && load < 0.5;
6751
- if (changed) {
6752
- _lastEmittedMsgCount = -1;
6753
- _lastEmittedToolCount = -1;
6754
- _lastEmittedMaxContext = -1;
6755
6816
  }
6756
- return changed;
6757
6817
  }
6758
- const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
6759
- let _cachedSysRef = null;
6760
- let _cachedToolsRef = null;
6761
- let _cachedOverheadTokens = 0;
6762
- function systemAndToolsOverhead() {
6763
- const sysRef = a.ctx.systemPrompt;
6764
- const toolsRef = a.ctx.tools;
6765
- if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
6766
- return _cachedOverheadTokens;
6818
+ flushText();
6819
+ logger.debug("Stream completed", {
6820
+ providerId: provider.id,
6821
+ model: req.model,
6822
+ stopReason: state.stopReason,
6823
+ textBlockCount: state.textBuffers.length,
6824
+ toolBlockCount: state.tools.size,
6825
+ thinkingBlockCount: state.thinking.length,
6826
+ usageInput: state.usage.input,
6827
+ usageOutput: state.usage.output
6828
+ });
6829
+ return buildResponse(state);
6830
+ }
6831
+
6832
+ // src/chronicle/prompt-manifest.ts
6833
+ import { createHash as createHash5 } from "node:crypto";
6834
+ function createChroniclePromptManifest(request) {
6835
+ const roleCounts = {}, blockCounts = {};
6836
+ const messages = request.messages.map((message, index) => messageSummary(message, index, roleCounts, blockCounts));
6837
+ const systemText = (request.system ?? []).map((block) => block.text).join("\n");
6838
+ const toolRecords = (request.tools ?? []).map((tool) => ({
6839
+ name: tool.name,
6840
+ schemaHash: hash(stable(tool.inputSchema)),
6841
+ permission: tool.permission,
6842
+ mutating: tool.mutating,
6843
+ riskTier: tool.riskTier,
6844
+ capabilities: tool.capabilities,
6845
+ estimatedTokens: tool._estDefTokens ?? 0
6846
+ }));
6847
+ const core = {
6848
+ systemHash: hash(systemText),
6849
+ messages: messages.map(({ hash: contentHash, ...rest }) => ({ ...rest, contentHash })),
6850
+ tools: toolRecords,
6851
+ model: request.model,
6852
+ maxTokens: request.maxTokens,
6853
+ temperature: request.temperature,
6854
+ topP: request.topP,
6855
+ topK: request.topK,
6856
+ seed: request.seed,
6857
+ toolChoice: request.toolChoice,
6858
+ reasoning: request.reasoning,
6859
+ cache: request.cache,
6860
+ responseFormat: request.responseFormat?.type
6861
+ };
6862
+ return {
6863
+ manifestId: `prompt_${hash(stable(core))}`,
6864
+ messageCount: messages.length,
6865
+ estimatedMessageTokens: messages.reduce((sum, message) => sum + (message.estimatedTokens ?? 0), 0),
6866
+ contentBytes: messages.reduce((sum, message) => sum + message.bytes, 0) + Buffer.byteLength(systemText),
6867
+ roleCounts,
6868
+ blockCounts,
6869
+ system: { blockCount: request.system?.length ?? 0, bytes: Buffer.byteLength(systemText), hash: hash(systemText) },
6870
+ messages,
6871
+ tools: {
6872
+ count: toolRecords.length,
6873
+ estimatedDefinitionTokens: toolRecords.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
6874
+ manifestHash: hash(stable(toolRecords)),
6875
+ names: toolRecords.map((tool) => tool.name),
6876
+ mutating: toolRecords.filter((tool) => tool.mutating).length,
6877
+ destructive: toolRecords.filter((tool) => tool.riskTier === "destructive").length
6878
+ },
6879
+ request: {
6880
+ maxTokens: request.maxTokens,
6881
+ temperature: request.temperature,
6882
+ topP: request.topP,
6883
+ topK: request.topK,
6884
+ frequencyPenalty: request.frequencyPenalty,
6885
+ presencePenalty: request.presencePenalty,
6886
+ seed: request.seed,
6887
+ candidateCount: request.candidateCount,
6888
+ logprobs: request.logprobs,
6889
+ topLogprobs: request.topLogprobs,
6890
+ stopSequenceCount: request.stopSequences?.length ?? 0,
6891
+ toolChoice: request.toolChoice,
6892
+ reasoning: request.reasoning,
6893
+ cache: request.cache,
6894
+ responseFormat: request.responseFormat?.type,
6895
+ safetySettingCount: request.safetySettings?.length ?? 0,
6896
+ userHash: request.user ? hash(request.user) : void 0
6897
+ }
6898
+ };
6899
+ }
6900
+ function messageSummary(message, index, roles, totals) {
6901
+ roles[message.role] = (roles[message.role] ?? 0) + 1;
6902
+ const blocks = typeof message.content === "string" ? { text: 1 } : countBlocks(message.content);
6903
+ for (const [type, count] of Object.entries(blocks)) totals[type] = (totals[type] ?? 0) + count;
6904
+ const content = contentIdentity(message.content);
6905
+ return {
6906
+ index,
6907
+ role: message.role,
6908
+ bytes: content.bytes,
6909
+ ...message._estTokens !== void 0 ? { estimatedTokens: message._estTokens } : {},
6910
+ hash: content.hash,
6911
+ blocks
6912
+ };
6913
+ }
6914
+ function countBlocks(blocks) {
6915
+ const counts = {};
6916
+ for (const block of blocks) counts[block.type] = (counts[block.type] ?? 0) + 1;
6917
+ return counts;
6918
+ }
6919
+ function hash(value) {
6920
+ return createHash5("sha256").update(value).digest("hex");
6921
+ }
6922
+ function contentIdentity(content) {
6923
+ if (typeof content === "string") return { hash: hash(content), bytes: Buffer.byteLength(content) };
6924
+ const digest = createHash5("sha256");
6925
+ let bytes = 0;
6926
+ const add = (value) => {
6927
+ if (!value) return;
6928
+ digest.update(value);
6929
+ bytes += Buffer.byteLength(value);
6930
+ };
6931
+ for (const block of content) {
6932
+ add(block.type);
6933
+ if (block.type === "text") add(block.text);
6934
+ else if (block.type === "thinking") {
6935
+ add(block.thinking);
6936
+ add(block.signature);
6937
+ } else if (block.type === "tool_use") {
6938
+ add(block.id);
6939
+ add(block.name);
6940
+ add(stable(block.input));
6941
+ } else if (block.type === "tool_result") {
6942
+ add(block.tool_use_id);
6943
+ add(block.name);
6944
+ add(block.content);
6945
+ add(String(block.is_error ?? false));
6946
+ } else if (block.type === "image") {
6947
+ add(block.source.type);
6948
+ add(block.source.media_type);
6949
+ add(block.source.url);
6950
+ add(block.source.data);
6767
6951
  }
6768
- const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
6769
- _cachedSysRef = sysRef;
6770
- _cachedToolsRef = toolsRef;
6771
- _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
6772
- return _cachedOverheadTokens;
6773
6952
  }
6774
- function stashRequestTokens(req) {
6775
- const preFlight = estimateRequestTokens(
6776
- req.messages,
6777
- req.system,
6778
- req.tools ?? [],
6779
- calibrationKey(req.model)
6780
- );
6781
- a.ctx.lastRequestTokens = preFlight.total;
6782
- _lastPreFlightMsgCount = req.messages.length;
6783
- _lastPreFlightToolCount = (req.tools ?? []).length;
6784
- _lastPreFlightRevision = a.ctx.state.revision;
6785
- _cachedSysRef = req.system;
6786
- _cachedToolsRef = req.tools ?? [];
6787
- _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
6788
- a.ctx.meta["lastRequestTokensAt"] = {
6789
- msgCount: req.messages.length,
6790
- toolCount: (req.tools ?? []).length,
6791
- revision: a.ctx.state.revision
6792
- };
6793
- return preFlight;
6953
+ return { hash: digest.digest("hex"), bytes };
6954
+ }
6955
+ function stable(value) {
6956
+ if (value === void 0) return "undefined";
6957
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
6958
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
6959
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`).join(",")}}`;
6960
+ }
6961
+
6962
+ // src/observability/network-telemetry.ts
6963
+ import { AsyncLocalStorage } from "node:async_hooks";
6964
+ var storage = new AsyncLocalStorage();
6965
+ function runWithNetworkTelemetry(context, run) {
6966
+ return storage.run(context, run);
6967
+ }
6968
+
6969
+ // src/security/error-sanitize.ts
6970
+ import { homedir as homedir2 } from "node:os";
6971
+ var scrubber = new DefaultSecretScrubber();
6972
+ function scrubErrorText(text) {
6973
+ if (!text) return text;
6974
+ let out = scrubber.scrub(text);
6975
+ const home = safeHomedir();
6976
+ if (home && home.length > 2) {
6977
+ out = replaceAllCaseInsensitive(out, home, "~");
6978
+ const alt = home.includes("\\") ? home.replace(/\\/g, "/") : home.replace(/\//g, "\\");
6979
+ if (alt !== home) out = replaceAllCaseInsensitive(out, alt, "~");
6794
6980
  }
6795
- function refreshContextRequestTokenStash(opts = {}) {
6796
- const msgCount = a.ctx.messages.length;
6797
- const toolCount = (a.ctx.tools ?? []).length;
6798
- const revision = a.ctx.state.revision;
6799
- const stashed = a.ctx.lastRequestTokens;
6800
- const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
6801
- if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
6802
- const meta = stashedAt;
6803
- if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
6804
- return stashed;
6805
- }
6806
- }
6807
- const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
6808
- a.ctx.lastRequestTokens = refreshed;
6809
- _lastPreFlightMsgCount = msgCount;
6810
- _lastPreFlightToolCount = toolCount;
6811
- _lastPreFlightRevision = revision;
6812
- a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
6813
- return refreshed;
6981
+ return out;
6982
+ }
6983
+ function safeHomedir() {
6984
+ try {
6985
+ return homedir2();
6986
+ } catch {
6987
+ return "";
6814
6988
  }
6815
- async function buildRequestWithPreflightCompaction(opts) {
6816
- let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6817
- let req = prepared.request;
6818
- let preFlight = stashRequestTokens(req);
6819
- let probedRouteKey = `${prepared.provider.id}/${req.model}`;
6820
- await refreshProviderContextLimit(prepared.provider, req.model);
6821
- if (await compactContextIfNeeded()) {
6822
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6823
- req = prepared.request;
6824
- preFlight = stashRequestTokens(req);
6825
- const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
6826
- if (rebuiltRouteKey !== probedRouteKey) {
6827
- probedRouteKey = rebuiltRouteKey;
6828
- await refreshProviderContextLimit(prepared.provider, req.model);
6829
- if (await compactContextIfNeeded()) {
6830
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6831
- req = prepared.request;
6832
- preFlight = stashRequestTokens(req);
6833
- }
6834
- }
6835
- }
6836
- return { req, provider: prepared.provider, preFlight };
6989
+ }
6990
+ function replaceAllCaseInsensitive(haystack, needle, replacement) {
6991
+ const lowerHay = haystack.toLowerCase();
6992
+ const lowerNeedle = needle.toLowerCase();
6993
+ let idx = lowerHay.indexOf(lowerNeedle);
6994
+ if (idx === -1) return haystack;
6995
+ let out = "";
6996
+ let from = 0;
6997
+ while (idx !== -1) {
6998
+ out += haystack.slice(from, idx) + replacement;
6999
+ from = idx + needle.length;
7000
+ idx = lowerHay.indexOf(lowerNeedle, from);
6837
7001
  }
6838
- function emitContextPct() {
6839
- const msgCount = a.ctx.messages.length;
6840
- const toolCount = (a.ctx.tools ?? []).length;
6841
- const maxContext = currentMaxContext();
6842
- const revision = a.ctx.state.revision;
6843
- if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
6844
- return;
6845
- }
6846
- _lastEmittedMsgCount = msgCount;
6847
- _lastEmittedToolCount = toolCount;
6848
- _lastEmittedRevision = revision;
6849
- _lastEmittedMaxContext = maxContext;
6850
- if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
6851
- refreshContextRequestTokenStash({ force: true });
6852
- }
6853
- let total;
6854
- const anchored = realAnchoredInputTokens(
6855
- a.ctx.messages,
6856
- a.ctx.lastRealInputTokens,
6857
- typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
6858
- );
6859
- const stashed = a.ctx.lastRequestTokens;
6860
- if (anchored !== null) {
6861
- total = anchored;
6862
- } else if (typeof stashed === "number" && stashed > 0) {
6863
- const cal = getCalibrationState(calibrationKey());
6864
- total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
6865
- } else {
6866
- const raw = refreshContextRequestTokenStash({ force: true });
6867
- const cal = getCalibrationState(calibrationKey());
6868
- total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
7002
+ return out + haystack.slice(from);
7003
+ }
7004
+
7005
+ // src/core/provider-runner.ts
7006
+ import { randomUUID as randomUUID5 } from "node:crypto";
7007
+ function scrubProviderBody(body) {
7008
+ if (!body) return void 0;
7009
+ return {
7010
+ ...body,
7011
+ ...body.type !== void 0 ? { type: scrubErrorText(body.type) } : {},
7012
+ ...body.message !== void 0 ? { message: scrubErrorText(body.message) } : {},
7013
+ ...body.raw !== void 0 ? { raw: scrubErrorText(body.raw) } : {},
7014
+ ...body.requestId !== void 0 ? { requestId: scrubErrorText(body.requestId) } : {}
7015
+ };
7016
+ }
7017
+ function providerLogCtx(p, r) {
7018
+ return {
7019
+ providerId: p.id,
7020
+ model: r.model,
7021
+ streaming: p.capabilities.streaming,
7022
+ msgCount: r.messages.length,
7023
+ toolCount: r.tools?.length ?? 0
7024
+ };
7025
+ }
7026
+ async function runProviderWithRetry(opts) {
7027
+ const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
7028
+ const logicalRequestId = randomUUID5();
7029
+ const promptManifest = createChroniclePromptManifest(request);
7030
+ let attempt = 0;
7031
+ for (; ; ) {
7032
+ const attemptId = randomUUID5();
7033
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
7034
+ const startedNs = process.hrtime.bigint();
7035
+ const correlation = {
7036
+ sessionId: resolveEventSessionId(ctx),
7037
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
7038
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
7039
+ logicalRequestId,
7040
+ attemptId,
7041
+ attempt,
7042
+ providerId: provider.id,
7043
+ model: request.model,
7044
+ taskId: ctx.currentKanbanTaskId,
7045
+ boardId: ctx.currentKanbanBoardId
7046
+ };
7047
+ events.emit("provider.attempt.started", {
7048
+ ...correlation,
7049
+ streaming: provider.capabilities.streaming,
7050
+ messageCount: request.messages.length,
7051
+ toolCount: request.tools?.length ?? 0,
7052
+ promptManifest,
7053
+ startedAt
7054
+ });
7055
+ const span = tracer?.startSpan("provider.complete", {
7056
+ "provider.id": provider.id,
7057
+ "provider.model": request.model,
7058
+ "provider.streaming": provider.capabilities.streaming,
7059
+ "provider.attempt": attempt
7060
+ });
7061
+ logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
7062
+ try {
7063
+ const res = await runWithNetworkTelemetry({
7064
+ events,
7065
+ sessionId: resolveEventSessionId(ctx),
7066
+ ...ctx.traceId ? { traceId: ctx.traceId } : {},
7067
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
7068
+ attemptId,
7069
+ initiator: "provider",
7070
+ operationName: `${provider.id}.complete`
7071
+ }, () => provider.capabilities.streaming ? streamProviderToResponse(provider, request, signal, ctx, events, logger) : provider.complete(request, { signal }));
7072
+ span?.setAttribute("provider.stopReason", res.stopReason);
7073
+ span?.setAttribute("provider.usage_in", res.usage.input);
7074
+ span?.setAttribute("provider.usage_out", res.usage.output);
7075
+ span?.end();
7076
+ events.emit("provider.attempt.completed", {
7077
+ ...correlation,
7078
+ startedAt,
7079
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
7080
+ durationMs: elapsedMs(startedNs),
7081
+ stopReason: res.stopReason,
7082
+ usage: res.usage
7083
+ });
7084
+ logger.debug("Provider call succeeded", {
7085
+ ...providerLogCtx(provider, request),
7086
+ stopReason: res.stopReason,
7087
+ usageInput: res.usage.input,
7088
+ usageOutput: res.usage.output,
7089
+ cacheRead: res.usage.cacheRead,
7090
+ cacheWrite: res.usage.cacheWrite,
7091
+ attempts: attempt + 1
7092
+ });
7093
+ return res;
7094
+ } catch (err) {
7095
+ if (err instanceof Error) span?.recordError(err);
7096
+ span?.end();
7097
+ if (signal.aborted) throw err;
7098
+ const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
7099
+ const errAsErr = err instanceof Error ? err : new Error(String(err));
7100
+ const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
7101
+ const providerErrorBody = isProviderErr ? scrubProviderBody(err.body) : void 0;
7102
+ const description = scrubErrorText(
7103
+ isProviderErr ? err.describe() : errAsErr.message
7104
+ );
7105
+ const delay2 = canRetry ? Math.round(retry.delayMs(attempt, isProviderErr ? err : errAsErr)) : void 0;
7106
+ events.emit("provider.attempt.failed", {
7107
+ ...correlation,
7108
+ startedAt,
7109
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
7110
+ durationMs: elapsedMs(startedNs),
7111
+ status: isProviderErr ? err.status : 0,
7112
+ failureKind: isProviderErr ? err.kind : "unknown",
7113
+ description,
7114
+ retryable: canRetry,
7115
+ retryScheduled: canRetry,
7116
+ ...delay2 !== void 0 ? { retryDelayMs: delay2 } : {},
7117
+ ...providerErrorBody?.requestId ? { providerRequestId: providerErrorBody.requestId } : {},
7118
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
7119
+ });
7120
+ if (!canRetry) {
7121
+ events.emit("provider.error", {
7122
+ sessionId: resolveEventSessionId(ctx),
7123
+ providerId: isProviderErr ? err.providerId : provider.id,
7124
+ status: isProviderErr ? err.status : 0,
7125
+ description,
7126
+ retryable: false,
7127
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
7128
+ });
7129
+ logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
7130
+ ...providerLogCtx(provider, request),
7131
+ attempts: attempt + 1,
7132
+ errorDescription: description,
7133
+ status: isProviderErr ? err.status : void 0,
7134
+ errorBody: providerErrorBody,
7135
+ errorName: err instanceof Error ? err.name : void 0,
7136
+ errorStack: err instanceof Error ? err.stack?.split("\n").slice(0, 3).join("\n") : void 0
7137
+ });
7138
+ throw toWrongStackError(err);
7139
+ }
7140
+ const attemptNum = attempt + 1;
7141
+ const maxAttempts = retry.maxAttempts(isProviderErr ? err : errAsErr);
7142
+ logger.warn(`Provider retry ${attemptNum}/${maxAttempts} in ${delay2}ms \u2014 ${description}`, {
7143
+ ...providerLogCtx(provider, request),
7144
+ attempt: attemptNum,
7145
+ maxAttempts,
7146
+ delayMs: delay2,
7147
+ errorDescription: description,
7148
+ status: isProviderErr ? err.status : void 0,
7149
+ errorBody: providerErrorBody
7150
+ });
7151
+ events.emit("provider.retry", {
7152
+ sessionId: resolveEventSessionId(ctx),
7153
+ providerId: isProviderErr ? err.providerId : provider.id,
7154
+ attempt: attemptNum,
7155
+ delayMs: delay2,
7156
+ status: isProviderErr ? err.status : 0,
7157
+ description,
7158
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
7159
+ });
7160
+ await new Promise((resolve10, reject) => {
7161
+ let settled = false;
7162
+ const cleanup = () => {
7163
+ clearTimeout(t2);
7164
+ signal.removeEventListener("abort", onAbort);
7165
+ };
7166
+ const onAbort = () => {
7167
+ if (settled) return;
7168
+ settled = true;
7169
+ cleanup();
7170
+ reject(new Error("aborted"));
7171
+ };
7172
+ const t2 = setTimeout(() => {
7173
+ if (settled) return;
7174
+ settled = true;
7175
+ cleanup();
7176
+ resolve10();
7177
+ }, delay2);
7178
+ if (signal.aborted) {
7179
+ onAbort();
7180
+ return;
7181
+ }
7182
+ signal.addEventListener("abort", onAbort);
7183
+ });
7184
+ attempt++;
6869
7185
  }
6870
- const rawLoad = maxContext > 0 ? total / maxContext : 0;
6871
- const load = Math.max(0, Math.min(1, rawLoad));
6872
- a.events.emit("ctx.pct", {
6873
- sessionId: resolveEventSessionId(a.ctx),
6874
- load,
6875
- rawLoad,
6876
- tokens: total,
6877
- maxContext
6878
- });
6879
7186
  }
6880
- function currentMaxContext() {
6881
- const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
6882
- const providerMax = a.ctx.provider.capabilities.maxContext;
6883
- return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
7187
+ }
7188
+ function elapsedMs(startedNs) {
7189
+ return Number(process.hrtime.bigint() - startedNs) / 1e6;
7190
+ }
7191
+
7192
+ // src/core/queued-messages.ts
7193
+ var META_KEY3 = "_queuedMessagesAwareness";
7194
+ var MAX_ITEMS = 20;
7195
+ var MAX_PREVIEW_CHARS = 500;
7196
+ function read(ctx) {
7197
+ const raw = ctx.meta[META_KEY3];
7198
+ if (!raw || typeof raw !== "object") return void 0;
7199
+ const s = raw;
7200
+ return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
7201
+ }
7202
+ function sameList(a, b) {
7203
+ return a.length === b.length && a.every((v, i) => v === b[i]);
7204
+ }
7205
+ function toPreview(text) {
7206
+ const trimmed = text.trim();
7207
+ if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
7208
+ return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
7209
+ }
7210
+ function setQueuedMessagesSnapshot(ctx, texts) {
7211
+ const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
7212
+ const prev = read(ctx);
7213
+ if (items.length === 0 && (!prev || prev.seen.length === 0)) {
7214
+ if (prev) delete ctx.meta[META_KEY3];
7215
+ return;
6884
7216
  }
6885
- let _lastEmittedMsgCount = -1;
6886
- let _lastEmittedToolCount = -1;
6887
- let _lastEmittedRevision = -1;
6888
- let _lastEmittedMaxContext = -1;
6889
- let _lastPreFlightMsgCount = -1;
6890
- let _lastPreFlightToolCount = -1;
6891
- let _lastPreFlightRevision = -1;
6892
- let _lastCompactionMsgCount = -1;
6893
- let _lastCompactionRevision = -1;
6894
- let _lastCompactionToolsRef = null;
6895
- let _lastCompactionSystemRef = null;
6896
- let _lastCompactionRequestTokens;
6897
- let _lastCompactionMaxContext = -1;
6898
- let _lastCompactionWasNoop = false;
7217
+ ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
7218
+ }
7219
+ function consumeQueuedMessagesUpdate(ctx) {
7220
+ const s = read(ctx);
7221
+ if (!s) return null;
7222
+ if (sameList(s.items, s.seen)) return null;
7223
+ if (s.items.length === 0) {
7224
+ delete ctx.meta[META_KEY3];
7225
+ return null;
7226
+ }
7227
+ ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
7228
+ return s.items;
7229
+ }
7230
+ function buildQueuedMessagesBlock(items) {
7231
+ const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
7232
+ return [
7233
+ "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
7234
+ "waiting in the input queue and each will be delivered as its own turn",
7235
+ "after you finish, so do NOT answer or act on them as new tasks now. Read",
7236
+ "them as a heads-up: if one changes what you should be doing right now",
7237
+ "(new constraint, invalidated approach), adapt your current work; if one",
7238
+ "is just an FYI, fold it in. The queue may still change before delivery.",
7239
+ "This notice replaces any earlier one \u2014 the queue now holds exactly:",
7240
+ "",
7241
+ body,
7242
+ "]"
7243
+ ].join("\n");
7244
+ }
7245
+
7246
+ // src/core/request-provider-binding.ts
7247
+ var requestProviders = /* @__PURE__ */ new WeakMap();
7248
+ function bindRequestProvider(request, provider) {
7249
+ requestProviders.set(request, provider);
7250
+ }
7251
+ function providerBoundToRequest(request) {
7252
+ return requestProviders.get(request);
7253
+ }
7254
+
7255
+ // src/core/agent-loop.ts
7256
+ function toError(err) {
7257
+ return err instanceof Error ? err : new Error(String(err));
7258
+ }
7259
+ function recordSelfHealingRetry(a, err, reason) {
7260
+ const projectRoot = a.ctx.projectRoot;
7261
+ if (!projectRoot) return;
7262
+ void recordPromptJournalEntry({
7263
+ projectRoot,
7264
+ sessionId: resolveEventSessionId(a.ctx),
7265
+ category: "self_healing_retry",
7266
+ content: toErrorMessage(err),
7267
+ decisionReason: reason,
7268
+ model: a.ctx.model,
7269
+ provider: a.ctx.provider?.id,
7270
+ activeTools: a.ctx.tools?.map((tool) => tool.name) ?? []
7271
+ }).catch(() => {
7272
+ });
7273
+ }
7274
+ function recordAutonomousContinue(a, text) {
7275
+ const projectRoot = a.ctx.projectRoot;
7276
+ if (!projectRoot) return;
7277
+ void recordPromptJournalEntry({
7278
+ projectRoot,
7279
+ sessionId: resolveEventSessionId(a.ctx),
7280
+ category: "autonomous_next_step",
7281
+ content: text,
7282
+ decisionReason: "text-marker autonomous continue",
7283
+ model: a.ctx.model,
7284
+ provider: a.ctx.provider?.id,
7285
+ activeTools: a.ctx.tools?.map((tool) => tool.name) ?? []
7286
+ }).catch(() => {
7287
+ });
7288
+ }
7289
+ function signalAbortReason(signal) {
7290
+ const r = signal.reason;
7291
+ if (r instanceof Error) return r.message || r.name;
7292
+ if (typeof r === "string" && r.length > 0) return r;
7293
+ return "aborted";
7294
+ }
7295
+ function createAgentLoopHandler(a, handlers) {
7296
+ const checkMailbox = attachMailboxChecker(a);
7297
+ const fleetPulseCfg = (() => {
7298
+ try {
7299
+ return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
7300
+ } catch {
7301
+ return void 0;
7302
+ }
7303
+ })();
7304
+ const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
7305
+ const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
7306
+ const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
7307
+ const loopContext = createAgentLoopContextManager(a, handlers);
6899
7308
  function foldBlockIntoConversation(block) {
6900
7309
  if (!a.ctx.state.appendBlockToLastUserMessage(block)) {
6901
7310
  a.ctx.state.appendMessage({ role: "user", content: [block], origin: "runtime" });
6902
7311
  }
6903
7312
  }
6904
- function iterationFingerprint(blocks) {
6905
- const toolUses = blocks.filter(isToolUseBlock);
6906
- const texts = blocks.filter(isTextBlock);
6907
- const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
6908
- const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
6909
- const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
6910
- const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
6911
- if (!hasContent) return "__empty__";
6912
- return [
6913
- `tools=${toolNameSet.join("+") || "-"}`,
6914
- `in0=${firstInputHash}`,
6915
- `txt=${textBlob}`
6916
- ].join("\n");
6917
- }
6918
- function stableStringify(value) {
6919
- return JSON.stringify(canonicalize(value));
6920
- }
6921
- function canonicalize(value) {
6922
- if (Array.isArray(value)) return value.map(canonicalize);
6923
- if (value && typeof value === "object") {
6924
- const src = value;
6925
- const out = {};
6926
- for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
6927
- return out;
6928
- }
6929
- return value;
6930
- }
6931
- function hashSmall(s) {
6932
- let h = 2166136261;
6933
- for (let i = 0; i < s.length; i++) {
6934
- h ^= s.charCodeAt(i);
6935
- h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
6936
- }
6937
- return h.toString(36);
6938
- }
6939
7313
  function injectPendingBtwNotes(onMailboxBlock) {
6940
7314
  const notes = consumeBtwNotes(a.ctx);
6941
7315
  if (notes.length === 0) return;
@@ -7015,15 +7389,10 @@ function createAgentLoopHandler(a, handlers) {
7015
7389
  a.ctx.state.replaceMessages(cleaned.messages);
7016
7390
  a.ctx.lastRealInputTokens = void 0;
7017
7391
  delete a.ctx.meta["realAnchorMsgCount"];
7018
- refreshContextRequestTokenStash({ force: true });
7392
+ loopContext.refreshContextRequestTokenStash({ force: true });
7019
7393
  }
7020
7394
  }
7021
- const loopCfg = a.loopDetection;
7022
- let lastToolSignature = "";
7023
- let toolLoopCount = 0;
7024
- let iterationSteerDone = false;
7025
- const recentCallKeys = [];
7026
- const steeredCallKeys = /* @__PURE__ */ new Set();
7395
+ const loopDetector = new AgentLoopDetector(a);
7027
7396
  let pendingLoopSteer = null;
7028
7397
  let todoReconcileSteers = 0;
7029
7398
  function queueLoopSteer(text) {
@@ -7130,7 +7499,7 @@ ${text}` : text;
7130
7499
  req,
7131
7500
  provider: requestProvider,
7132
7501
  preFlight
7133
- } = await buildRequestWithPreflightCompaction(opts);
7502
+ } = await loopContext.buildRequestWithPreflightCompaction(opts);
7134
7503
  await a.ctx.session.append({
7135
7504
  type: "llm_request",
7136
7505
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7143,7 +7512,7 @@ ${text}` : text;
7143
7512
  let res;
7144
7513
  try {
7145
7514
  res = await customRunner(a.ctx, req);
7146
- const key = calibrationKey(req.model);
7515
+ const key = loopContext.calibrationKey(req.model);
7147
7516
  const cal = getCalibrationState(key);
7148
7517
  const calibratedTotal = cal.calibrated ? Math.round(preFlight.total * Math.min(1.5, Math.max(0.5, cal.ratio))) : preFlight.total;
7149
7518
  const realInputTokens = effectiveInputTokens(res.usage);
@@ -7151,10 +7520,10 @@ ${text}` : text;
7151
7520
  const previousRealInput = a.ctx.lastRealInputTokens;
7152
7521
  const previousAnchorMsgCount = typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0;
7153
7522
  const anchorIsPlausible = realInputTokens >= calibratedTotal * 0.5;
7154
- const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && _lastPreFlightMsgCount < previousAnchorMsgCount;
7523
+ const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && loopContext.lastPreFlightMsgCount < previousAnchorMsgCount;
7155
7524
  if (realInputTokens > 0 && anchorIsPlausible && anchorAdvanced) {
7156
7525
  a.ctx.lastRealInputTokens = realInputTokens;
7157
- a.ctx.meta["realAnchorMsgCount"] = _lastPreFlightMsgCount;
7526
+ a.ctx.meta["realAnchorMsgCount"] = loopContext.lastPreFlightMsgCount;
7158
7527
  }
7159
7528
  recoveryRetries = 0;
7160
7529
  } catch (err) {
@@ -7207,6 +7576,7 @@ ${text}` : text;
7207
7576
  }
7208
7577
  if (extDecision.model) a.ctx.model = extDecision.model;
7209
7578
  a.logger.info("Extension requested retry; retrying turn");
7579
+ recordSelfHealingRetry(a, err, "extension-requested provider retry");
7210
7580
  continue;
7211
7581
  }
7212
7582
  }
@@ -7236,6 +7606,7 @@ ${text}` : text;
7236
7606
  }
7237
7607
  if (recovered.model) a.ctx.model = recovered.model;
7238
7608
  a.logger.info(`Recovered provider error via ${recovered.reason}; retrying turn`);
7609
+ recordSelfHealingRetry(a, err, recovered.reason);
7239
7610
  continue;
7240
7611
  }
7241
7612
  recoveryRetries = 0;
@@ -7244,7 +7615,9 @@ ${text}` : text;
7244
7615
  clearEvaluatedMailboxBlocks();
7245
7616
  const responseProvider = providerBoundToRequest(req) ?? requestProvider;
7246
7617
  const responseResult = await handlers.response.processResponse(res, req, responseProvider);
7247
- await refreshProviderContextLimit(responseProvider, req.model, { probe: false });
7618
+ await loopContext.refreshProviderContextLimit(responseProvider, req.model, {
7619
+ probe: false
7620
+ });
7248
7621
  if (responseResult.finalText) {
7249
7622
  a.ctx.meta["lastAgentOutput"] = responseResult.finalText;
7250
7623
  }
@@ -7267,103 +7640,18 @@ ${text}` : text;
7267
7640
  }
7268
7641
  finalText = responseResult.finalText;
7269
7642
  const toolUses = res.content.filter(isToolUseBlock);
7270
- if (loopCfg.mode !== "off") {
7271
- const sig = iterationFingerprint(res.content);
7272
- if (sig !== "__empty__") {
7273
- if (sig === lastToolSignature) {
7274
- toolLoopCount++;
7275
- } else {
7276
- lastToolSignature = sig;
7277
- toolLoopCount = 1;
7278
- iterationSteerDone = false;
7279
- }
7280
- const names = toolUses.map((t2) => t2.name).join(", ");
7281
- const hasText2 = res.content.some(isTextBlock);
7282
- const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
7283
- const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
7284
- const tool = a.tools.get(use.name);
7285
- return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
7286
- });
7287
- const repeatMultiplier = observationRepeat ? 2 : 1;
7288
- const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${toolLoopCount} times in a row` : `same assistant text repeated ${toolLoopCount} times in a row`;
7289
- const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
7290
- if (toolLoopCount >= cutAt) {
7291
- a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
7292
- a.events.emit("tool.loop_detected", {
7293
- sessionId: resolveEventSessionId(a.ctx),
7294
- ctx: a.ctx,
7295
- tools: names,
7296
- repeatCount: toolLoopCount,
7297
- iteration: i,
7298
- kind,
7299
- action: "cut",
7300
- scope: "iteration"
7301
- });
7302
- const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
7303
- return {
7304
- status: "max_iterations",
7305
- iterations,
7306
- finalText: finalText || summary,
7307
- delegateSummaries
7308
- };
7309
- }
7310
- if (loopCfg.mode === "steer-then-cut" && toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !iterationSteerDone) {
7311
- iterationSteerDone = true;
7312
- a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
7313
- a.events.emit("tool.loop_detected", {
7314
- sessionId: resolveEventSessionId(a.ctx),
7315
- ctx: a.ctx,
7316
- tools: names,
7317
- repeatCount: toolLoopCount,
7318
- iteration: i,
7319
- kind,
7320
- action: "steer",
7321
- scope: "iteration"
7322
- });
7323
- queueLoopSteer(
7324
- `[loop-detector] Your last ${toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - toolLoopCount} more time(s) will terminate the turn.`
7325
- );
7326
- }
7327
- } else {
7328
- lastToolSignature = "";
7329
- toolLoopCount = 0;
7330
- iterationSteerDone = false;
7331
- }
7332
- if (loopCfg.mode === "steer-then-cut") {
7333
- for (const u of toolUses) {
7334
- const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
7335
- recentCallKeys.push(key);
7336
- if (recentCallKeys.length > loopCfg.windowSize) recentCallKeys.shift();
7337
- if (steeredCallKeys.has(key)) continue;
7338
- let count = 0;
7339
- for (const k of recentCallKeys) if (k === key) count++;
7340
- const tool = a.tools.get(u.name);
7341
- const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
7342
- if (count < observationThreshold) continue;
7343
- steeredCallKeys.add(key);
7344
- const preview2 = JSON.stringify(u.input ?? {}).slice(0, 160);
7345
- a.logger.warn(
7346
- `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
7347
- );
7348
- a.events.emit("tool.loop_detected", {
7349
- sessionId: resolveEventSessionId(a.ctx),
7350
- ctx: a.ctx,
7351
- tools: u.name,
7352
- repeatCount: count,
7353
- iteration: i,
7354
- kind: "tool",
7355
- action: "steer",
7356
- scope: "call"
7357
- });
7358
- queueLoopSteer(
7359
- `[loop-detector] You have called ${u.name}(${preview2}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
7360
- );
7361
- }
7362
- }
7643
+ const loopCheck = loopDetector.checkIteration(i, res.content, toolUses, queueLoopSteer);
7644
+ if (loopCheck.cut) {
7645
+ return {
7646
+ status: "max_iterations",
7647
+ iterations,
7648
+ finalText: finalText || loopCheck.cutSummary || "",
7649
+ delegateSummaries
7650
+ };
7363
7651
  }
7364
7652
  if (toolUses.length === 0) {
7365
- await compactContextIfNeeded();
7366
- emitContextPct();
7653
+ await loopContext.compactContextIfNeeded();
7654
+ loopContext.emitContextPct();
7367
7655
  a.events.emit("iteration.completed", {
7368
7656
  sessionId: resolveEventSessionId(a.ctx),
7369
7657
  ctx: a.ctx,
@@ -7378,6 +7666,7 @@ ${text}` : text;
7378
7666
  continue;
7379
7667
  }
7380
7668
  if (autonomousContinue && responseResult.directive === "continue") {
7669
+ recordAutonomousContinue(a, finalText);
7381
7670
  await a.extensions.runAfterIteration(a.ctx, i);
7382
7671
  continue;
7383
7672
  }
@@ -7401,8 +7690,8 @@ ${text}` : text;
7401
7690
  throw toolErr;
7402
7691
  }
7403
7692
  if (autonomousContinue && consumeAutonomousContinue(a.ctx)) {
7404
- await compactContextIfNeeded();
7405
- emitContextPct();
7693
+ await loopContext.compactContextIfNeeded();
7694
+ loopContext.emitContextPct();
7406
7695
  a.events.emit("iteration.completed", {
7407
7696
  sessionId: resolveEventSessionId(a.ctx),
7408
7697
  ctx: a.ctx,
@@ -7411,8 +7700,8 @@ ${text}` : text;
7411
7700
  await a.extensions.runAfterIteration(a.ctx, i);
7412
7701
  continue;
7413
7702
  }
7414
- await compactContextIfNeeded();
7415
- emitContextPct();
7703
+ await loopContext.compactContextIfNeeded();
7704
+ loopContext.emitContextPct();
7416
7705
  a.events.emit("iteration.completed", {
7417
7706
  sessionId: resolveEventSessionId(a.ctx),
7418
7707
  ctx: a.ctx,
@@ -7611,6 +7900,10 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7611
7900
  "codebase-incoming-calls",
7612
7901
  "codebase-outgoing-calls",
7613
7902
  "codebase-index",
7903
+ "codebase-skeleton",
7904
+ "codebase-repo-map",
7905
+ "codebase-impact-analysis",
7906
+ "codebase-invariant-check",
7614
7907
  "dead-code-scan",
7615
7908
  "diff",
7616
7909
  "json",
@@ -7621,7 +7914,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7621
7914
  id: "filesystem.write",
7622
7915
  pack: "development",
7623
7916
  exposure: "direct",
7624
- tools: ["write", "edit", "replace", "patch"]
7917
+ tools: ["write", "edit", "replace", "patch", "codebase-ast-replace"]
7625
7918
  },
7626
7919
  {
7627
7920
  id: "execution.shell",
@@ -7633,7 +7926,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7633
7926
  id: "verification.run",
7634
7927
  pack: "development",
7635
7928
  exposure: "direct",
7636
- tools: ["lint", "format", "typecheck", "test", "e2e_plan"]
7929
+ tools: ["lint", "format", "typecheck", "test", "e2e_plan", "codebase-targeted-test"]
7637
7930
  },
7638
7931
  {
7639
7932
  id: "version-control.manage",
@@ -7770,7 +8063,13 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7770
8063
  id: "quality.analyze",
7771
8064
  pack: "development",
7772
8065
  exposure: "on-demand",
7773
- tools: ["dead_code_scan", "detect_duplicate_code", "secret_scanner_test", "error_lens_history"]
8066
+ tools: [
8067
+ "dead_code_scan",
8068
+ "detect_duplicate_code",
8069
+ "secret_scanner_test",
8070
+ "error_lens_history",
8071
+ "security-ast-scan"
8072
+ ]
7774
8073
  },
7775
8074
  {
7776
8075
  id: "release.manage",
@@ -7788,7 +8087,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7788
8087
  id: "tools.discover",
7789
8088
  pack: "admin",
7790
8089
  exposure: "direct",
7791
- tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use"]
8090
+ tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use", "clarify"]
7792
8091
  },
7793
8092
  {
7794
8093
  id: "runtime.admin",
@@ -8228,7 +8527,7 @@ function createAgentResponseHandler(a) {
8228
8527
  ...repaired.report
8229
8528
  });
8230
8529
  a.logger.warn(
8231
- `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
8530
+ `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)` + formatAdjacencyRepairIds(repaired.report.removedToolUses, repaired.report.removedToolResults)
8232
8531
  );
8233
8532
  }
8234
8533
  }
@@ -8348,6 +8647,10 @@ function createAgentResponseHandler(a) {
8348
8647
  }
8349
8648
  return { buildAndRunRequestPipeline, processResponse };
8350
8649
  }
8650
+ function formatAdjacencyRepairIds(toolUses, toolResults) {
8651
+ const ids = [.../* @__PURE__ */ new Set([...toolUses, ...toolResults])].slice(0, 8);
8652
+ return ids.length > 0 ? `; affected tool ids: ${ids.join(", ")}${toolUses.length + toolResults.length > ids.length ? ", \u2026" : ""}` : "";
8653
+ }
8351
8654
 
8352
8655
  // src/utils/sage-output-block.ts
8353
8656
  var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
@@ -8919,7 +9222,7 @@ var Agent = class {
8919
9222
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
8920
9223
  this.executionStrategy = init.executionStrategy ?? "smart";
8921
9224
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
8922
- this.autoExtendLimit = init.autoExtendLimit ?? true;
9225
+ this.autoExtendLimit = init.autoExtendLimit ?? false;
8923
9226
  this.loopDetection = resolveLoopDetection(init.loopDetection);
8924
9227
  this.autonomousContinue = init.autonomousContinue ?? false;
8925
9228
  this.refreshSystemPrompt = init.refreshSystemPrompt ?? false;
@@ -9286,9 +9589,6 @@ function resolveContinuation(input) {
9286
9589
  return { source: "open", text, label: "\u25B6 Continue \u2192 (no pending task \u2014 choosing next step)" };
9287
9590
  }
9288
9591
 
9289
- // src/core/fallback-model.ts
9290
- import { randomUUID as randomUUID6 } from "node:crypto";
9291
-
9292
9592
  // src/core/model-availability-calendar.ts
9293
9593
  function logicalCalendarTarget(providerId, model) {
9294
9594
  if (providerId !== "omniroute") return { providerId, model };
@@ -9776,7 +10076,206 @@ function dedupeChain(entries, current) {
9776
10076
  );
9777
10077
  }
9778
10078
 
10079
+ // src/core/fallback-doctor.ts
10080
+ function diagnoseFallbackConfig(config, tracker, opts) {
10081
+ const mgr = new FallbackProfileManager(config, { statusTracker: tracker });
10082
+ const primary = { providerId: config.provider, model: config.model };
10083
+ const activeProfile = mgr.activeProfileName();
10084
+ const explicitChain = Object.freeze([...config.fallbackModels ?? []]);
10085
+ const effectiveCandidates = mgr.resolveCandidates(primary, {
10086
+ fallbackProfile: activeProfile
10087
+ });
10088
+ const effectiveOrder = Object.freeze(
10089
+ effectiveCandidates.map((c) => `${c.providerId}/${c.model}`)
10090
+ );
10091
+ const autoEnabled = config.fallbackAuto !== false;
10092
+ const warnings = [];
10093
+ const providersInChain = /* @__PURE__ */ new Set();
10094
+ for (const entry of effectiveCandidates) {
10095
+ providersInChain.add(entry.providerId);
10096
+ }
10097
+ if (effectiveCandidates.length === 0) {
10098
+ warnings.push({
10099
+ code: "EMPTY_CHAIN",
10100
+ severity: "critical",
10101
+ message: "The effective fallback chain is empty. Any rate limit or outage on the primary model will immediately fail without recovery.",
10102
+ recommendation: "Add fallback models using /fallback add <provider/model> or enable smart defaults with /fallback auto on."
10103
+ });
10104
+ }
10105
+ if (effectiveCandidates.length > 0) {
10106
+ const onlyPrimaryProvider = providersInChain.size === 1 && providersInChain.has(primary.providerId);
10107
+ if (onlyPrimaryProvider) {
10108
+ warnings.push({
10109
+ code: "SIBLING_QUARANTINE_RISK",
10110
+ severity: "warning",
10111
+ message: `All fallback models belong to the same provider ("${primary.providerId}"). An account-level quota or token exhaustion will quarantine all sibling models at once.`,
10112
+ recommendation: "Add at least one cross-provider model (e.g. OpenAI, Anthropic, or Google) to your fallback profile."
10113
+ });
10114
+ }
10115
+ }
10116
+ for (const entry of effectiveCandidates) {
10117
+ const health = mgr.checkProvider(entry.providerId);
10118
+ if (!health.usable) {
10119
+ warnings.push({
10120
+ code: "PROVIDER_UNUSABLE",
10121
+ severity: "critical",
10122
+ target: `${entry.providerId}/${entry.model}`,
10123
+ message: `Provider "${entry.providerId}" is missing an API key or baseUrl in configuration.`,
10124
+ recommendation: `Configure API credentials for "${entry.providerId}" or remove it from the fallback chain.`
10125
+ });
10126
+ }
10127
+ if (tracker && !tracker.isAvailable(entry.providerId, entry.model)) {
10128
+ const status2 = tracker.getStatus(entry.providerId, entry.model);
10129
+ const remainingMs = status2?.stateExpiresAt ? Math.max(0, status2.stateExpiresAt - Date.now()) : 0;
10130
+ const remainingSec = Math.round(remainingMs / 1e3);
10131
+ warnings.push({
10132
+ code: "MODEL_QUARANTINED",
10133
+ severity: "warning",
10134
+ target: `${entry.providerId}/${entry.model}`,
10135
+ message: `Model is currently in quarantine (waiting room) due to ${status2?.lastErrorKind ?? "failures"}.${remainingSec > 0 ? ` Resets in ${remainingSec}s.` : ""}`,
10136
+ recommendation: "Model will be automatically re-admitted once cooldown/reset expires."
10137
+ });
10138
+ }
10139
+ const cal = evaluateModelCalendar(
10140
+ config.modelAvailabilitySchedule,
10141
+ entry.providerId,
10142
+ entry.model
10143
+ );
10144
+ if (!cal.allowed) {
10145
+ warnings.push({
10146
+ code: "CALENDAR_BLOCKED",
10147
+ severity: "warning",
10148
+ target: `${entry.providerId}/${entry.model}`,
10149
+ message: `Model is blocked by the model availability calendar (${cal.rule?.label ?? "schedule restriction"}).`
10150
+ });
10151
+ }
10152
+ if (opts?.modelContextLimitMap) {
10153
+ const primaryLimit = opts.modelContextLimitMap[`${primary.providerId}/${primary.model}`] ?? opts.modelContextLimitMap[primary.model];
10154
+ const entryLimit = opts.modelContextLimitMap[`${entry.providerId}/${entry.model}`] ?? opts.modelContextLimitMap[entry.model];
10155
+ if (typeof primaryLimit === "number" && typeof entryLimit === "number" && entryLimit > 0 && primaryLimit > 0 && entryLimit < primaryLimit) {
10156
+ warnings.push({
10157
+ code: "CONTEXT_WINDOW_WARNING",
10158
+ severity: "warning",
10159
+ target: `${entry.providerId}/${entry.model}`,
10160
+ message: `Context window downgrade: primary model has ${primaryLimit.toLocaleString()} tokens capacity, but fallback candidate has only ${entryLimit.toLocaleString()} tokens. Long sessions will fail failover or require aggressive compaction.`,
10161
+ recommendation: `Ensure fallback models have equal or greater context capacity (>= ${primaryLimit.toLocaleString()} tokens) or keep sessions below ${entryLimit.toLocaleString()} tokens.`
10162
+ });
10163
+ }
10164
+ }
10165
+ }
10166
+ const uniqueProviders = Object.freeze([...providersInChain]);
10167
+ const crossProviderCount = uniqueProviders.filter((p) => p !== primary.providerId).length;
10168
+ const hasCritical = warnings.some((w) => w.severity === "critical");
10169
+ const hasWarning = warnings.some((w) => w.severity === "warning");
10170
+ const status = hasCritical ? "critical" : hasWarning ? "warning" : "healthy";
10171
+ return Object.freeze({
10172
+ primary: Object.freeze(primary),
10173
+ activeProfile,
10174
+ explicitChain,
10175
+ effectiveOrder,
10176
+ autoEnabled,
10177
+ status,
10178
+ warnings: Object.freeze(warnings),
10179
+ crossProviderCount,
10180
+ uniqueProviders
10181
+ });
10182
+ }
10183
+ function simulateFallbackFailover(config, opts = {}) {
10184
+ const errorKind = opts.errorKind ?? "rate_limit";
10185
+ const isEligible = isFallbackWorthy(errorKind);
10186
+ const primary = { providerId: config.provider, model: config.model };
10187
+ if (!isEligible) {
10188
+ return Object.freeze({
10189
+ triggeringError: errorKind,
10190
+ isFallbackEligible: false,
10191
+ primary: Object.freeze(primary),
10192
+ steps: Object.freeze([]),
10193
+ finalTarget: null,
10194
+ summary: `Error kind "${errorKind}" is not fallback-worthy (request-shaped error, e.g. context_overflow/auth). The agent loop will not attempt fallback.`
10195
+ });
10196
+ }
10197
+ const mgr = new FallbackProfileManager(config, { statusTracker: opts.statusTracker });
10198
+ const candidates = mgr.resolveCandidates(primary);
10199
+ const steps = [];
10200
+ let finalTarget = null;
10201
+ for (let i = 0; i < candidates.length; i++) {
10202
+ const entry = candidates[i];
10203
+ const key = `${entry.providerId}/${entry.model}`;
10204
+ const providerSwitched = entry.providerId !== primary.providerId;
10205
+ const cal = evaluateModelCalendar(
10206
+ config.modelAvailabilitySchedule,
10207
+ entry.providerId,
10208
+ entry.model
10209
+ );
10210
+ if (!cal.allowed) {
10211
+ steps.push({
10212
+ index: i + 1,
10213
+ providerId: entry.providerId,
10214
+ model: entry.model,
10215
+ skipped: true,
10216
+ reason: `Blocked by model availability calendar (${cal.rule?.label ?? "schedule"})`,
10217
+ providerSwitched
10218
+ });
10219
+ continue;
10220
+ }
10221
+ if (opts.statusTracker && !opts.statusTracker.isAvailable(entry.providerId, entry.model)) {
10222
+ steps.push({
10223
+ index: i + 1,
10224
+ providerId: entry.providerId,
10225
+ model: entry.model,
10226
+ skipped: true,
10227
+ reason: "Quarantined in status tracker waiting room",
10228
+ providerSwitched
10229
+ });
10230
+ continue;
10231
+ }
10232
+ const contextLimit = opts.modelContextLimitMap?.[key];
10233
+ if (typeof opts.currentTokens === "number" && opts.currentTokens > 0 && typeof contextLimit === "number" && contextLimit > 0 && opts.currentTokens > contextLimit) {
10234
+ steps.push({
10235
+ index: i + 1,
10236
+ providerId: entry.providerId,
10237
+ model: entry.model,
10238
+ skipped: true,
10239
+ reason: `Context pre-filter: request tokens (${opts.currentTokens}) exceeds model window (${contextLimit})`,
10240
+ providerSwitched
10241
+ });
10242
+ continue;
10243
+ }
10244
+ const health = mgr.checkProvider(entry.providerId);
10245
+ if (!health.usable) {
10246
+ steps.push({
10247
+ index: i + 1,
10248
+ providerId: entry.providerId,
10249
+ model: entry.model,
10250
+ skipped: true,
10251
+ reason: `Cannot construct provider "${entry.providerId}" (missing API key/endpoint)`,
10252
+ providerSwitched
10253
+ });
10254
+ continue;
10255
+ }
10256
+ steps.push({
10257
+ index: i + 1,
10258
+ providerId: entry.providerId,
10259
+ model: entry.model,
10260
+ skipped: false,
10261
+ providerSwitched
10262
+ });
10263
+ finalTarget = { providerId: entry.providerId, model: entry.model };
10264
+ break;
10265
+ }
10266
+ const summary = finalTarget ? `Failover succeeded: will rotate from ${primary.providerId}/${primary.model} to ${finalTarget.providerId}/${finalTarget.model}.` : "Failover failed: no usable fallback candidate could be reached.";
10267
+ return Object.freeze({
10268
+ triggeringError: errorKind,
10269
+ isFallbackEligible: true,
10270
+ primary: Object.freeze(primary),
10271
+ steps: Object.freeze(steps),
10272
+ finalTarget: finalTarget ? Object.freeze(finalTarget) : null,
10273
+ summary
10274
+ });
10275
+ }
10276
+
9779
10277
  // src/core/fallback-model.ts
10278
+ import { randomUUID as randomUUID6 } from "node:crypto";
9780
10279
  function fallbackProfileChain(config, profileName) {
9781
10280
  if (!profileName) return [];
9782
10281
  const mgr = new FallbackProfileManager(config);
@@ -9911,7 +10410,25 @@ function createFallbackModelExtension(deps) {
9911
10410
  if (!evaluateModelCalendar(cfg.modelAvailabilitySchedule, primary.providerId, primary.model).allowed || deps.statusTracker && !deps.statusTracker.isAvailable(primary.providerId, primary.model))
9912
10411
  return;
9913
10412
  try {
9914
- ctx.provider = await deps.buildProvider(primary.providerId, primary.model);
10413
+ const primaryProvider = await deps.buildProvider(primary.providerId, primary.model);
10414
+ const currentTokens = ctx.lastRequestTokens;
10415
+ const maxContext = maxContextOf(primaryProvider);
10416
+ if (maxContext > 0 && typeof currentTokens === "number" && currentTokens > maxContext) {
10417
+ deps.events.emit("provider.primary_probe_context_blocked", {
10418
+ sessionId: resolveEventSessionId(ctx),
10419
+ providerId: primary.providerId,
10420
+ model: primary.model,
10421
+ currentTokens,
10422
+ maxContext,
10423
+ timestamp: now()
10424
+ });
10425
+ deps.logger?.warn(
10426
+ `fallback-model: deferring primary probe "${primary.providerId}/${primary.model}" \u2014 context (${currentTokens}) exceeds target window (${maxContext})`
10427
+ );
10428
+ markPrimaryFailure(cfg);
10429
+ return;
10430
+ }
10431
+ ctx.provider = primaryProvider;
9915
10432
  ctx.model = primary.model;
9916
10433
  await deps.onModelSwitch?.(primary.providerId, primary.model);
9917
10434
  primaryBlockedUntil = 0;
@@ -10025,9 +10542,10 @@ function createFallbackModelExtension(deps) {
10025
10542
  const status = shouldFallback(firstErr_);
10026
10543
  if (status === null) throw firstErr_;
10027
10544
  let gateRequestId;
10028
- if (deps.fallbackGate && usableChain.length > 0) {
10545
+ const configuredGateSeconds = cfg.fallbackGateSeconds ?? deps.fallbackGateSeconds;
10546
+ if (configuredGateSeconds !== 0 && deps.fallbackGate && usableChain.length > 0) {
10029
10547
  gateRequestId = randomUUID6();
10030
- const autoSwitchSeconds = Math.max(1, deps.fallbackGateSeconds ?? 7);
10548
+ const autoSwitchSeconds = Math.max(1, configuredGateSeconds ?? 7);
10031
10549
  const gateCandidates = usableChain.map((e) => ({
10032
10550
  providerId: e.providerId,
10033
10551
  model: e.model
@@ -10270,8 +10788,8 @@ var InputBuilder = class {
10270
10788
  async registerFile(input) {
10271
10789
  const ref = await this.store.add({ ...input, kind: "file" });
10272
10790
  this.refs.push(ref);
10273
- const path15 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
10274
- return `[file:${path15}]`;
10791
+ const path16 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
10792
+ return `[file:${path16}]`;
10275
10793
  }
10276
10794
  /**
10277
10795
  * Whether `appendPaste(text)` would collapse the text to a placeholder
@@ -10318,8 +10836,8 @@ function paragraphLabel(text) {
10318
10836
 
10319
10837
  // src/core/instruction-bundle.ts
10320
10838
  import { statSync as statSync2 } from "node:fs";
10321
- import * as fs5 from "node:fs/promises";
10322
- import * as path11 from "node:path";
10839
+ import * as fs6 from "node:fs/promises";
10840
+ import * as path12 from "node:path";
10323
10841
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10324
10842
  async function loadInstructionBundle(paths) {
10325
10843
  let bundle = {};
@@ -10375,17 +10893,17 @@ function resolveSystemInstructionFile(paths) {
10375
10893
  }
10376
10894
  function sanitizeSystemInstructionFile(file) {
10377
10895
  const trimmed = file.trim();
10378
- if (trimmed.length === 0 || trimmed !== path11.basename(trimmed) || path11.extname(trimmed).toLowerCase() !== ".md") {
10896
+ if (trimmed.length === 0 || trimmed !== path12.basename(trimmed) || path12.extname(trimmed).toLowerCase() !== ".md") {
10379
10897
  throw new Error(`Invalid system instruction file: ${file}`);
10380
10898
  }
10381
10899
  return trimmed;
10382
10900
  }
10383
10901
  async function readInstructionDir(dir, options) {
10384
10902
  const [json, identity, leaderAfterTask, sections] = await Promise.all([
10385
- readInstructionJson(path11.join(dir, "instructions.json")),
10386
- readOptionalText(path11.join(dir, options.systemFile)),
10387
- readOptionalText(path11.join(dir, "leader-after-task.md")),
10388
- readSections(path11.join(dir, "sections"))
10903
+ readInstructionJson(path12.join(dir, "instructions.json")),
10904
+ readOptionalText(path12.join(dir, options.systemFile)),
10905
+ readOptionalText(path12.join(dir, "leader-after-task.md")),
10906
+ readSections(path12.join(dir, "sections"))
10389
10907
  ]);
10390
10908
  const fromMarkdown = {
10391
10909
  system: {
@@ -10404,13 +10922,13 @@ async function readSections(root) {
10404
10922
  async function readSectionsInto(root, dir, out) {
10405
10923
  let entries;
10406
10924
  try {
10407
- entries = await fs5.readdir(dir, { withFileTypes: true });
10925
+ entries = await fs6.readdir(dir, { withFileTypes: true });
10408
10926
  } catch {
10409
10927
  return;
10410
10928
  }
10411
10929
  await Promise.all(
10412
10930
  entries.map(async (entry) => {
10413
- const file = path11.join(dir, entry.name);
10931
+ const file = path12.join(dir, entry.name);
10414
10932
  if (entry.isDirectory()) {
10415
10933
  await readSectionsInto(root, file, out);
10416
10934
  return;
@@ -10418,7 +10936,7 @@ async function readSectionsInto(root, dir, out) {
10418
10936
  if (!entry.isFile() || !entry.name.endsWith(".md")) return;
10419
10937
  const text = await readOptionalText(file);
10420
10938
  if (text === void 0) return;
10421
- const rel = path11.relative(root, file).replace(/\\/g, "/").replace(/\.md$/i, "");
10939
+ const rel = path12.relative(root, file).replace(/\\/g, "/").replace(/\.md$/i, "");
10422
10940
  const key = rel.split("/").join(".").replace(/-/g, ".");
10423
10941
  out[key] = text;
10424
10942
  })
@@ -10427,7 +10945,7 @@ async function readSectionsInto(root, dir, out) {
10427
10945
  async function readInstructionJson(file) {
10428
10946
  let raw;
10429
10947
  try {
10430
- raw = await fs5.readFile(file, "utf8");
10948
+ raw = await fs6.readFile(file, "utf8");
10431
10949
  } catch {
10432
10950
  return {};
10433
10951
  }
@@ -10459,18 +10977,18 @@ function normalizeInstructionBundle(value) {
10459
10977
  }
10460
10978
  async function readOptionalText(file) {
10461
10979
  try {
10462
- const text = await fs5.readFile(file, "utf8");
10980
+ const text = await fs6.readFile(file, "utf8");
10463
10981
  return text.trimEnd();
10464
10982
  } catch {
10465
10983
  return void 0;
10466
10984
  }
10467
10985
  }
10468
10986
  function defaultBundledInstructionDir() {
10469
- const here = path11.dirname(fileURLToPath2(import.meta.url));
10987
+ const here = path12.dirname(fileURLToPath2(import.meta.url));
10470
10988
  return firstExistingDirSync([
10471
- path11.resolve(here, "../../instructions"),
10472
- path11.resolve(here, "../instructions"),
10473
- path11.resolve(here, "instructions")
10989
+ path12.resolve(here, "../../instructions"),
10990
+ path12.resolve(here, "../instructions"),
10991
+ path12.resolve(here, "instructions")
10474
10992
  ]);
10475
10993
  }
10476
10994
  function definedPick(obj, keys) {
@@ -10524,13 +11042,13 @@ function flattenSystemPromptRegions(regions) {
10524
11042
 
10525
11043
  // src/core/modes/default.ts
10526
11044
  import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
10527
- import * as path12 from "node:path";
11045
+ import * as path13 from "node:path";
10528
11046
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10529
11047
  var PROMPT = readBundledInstructionFile("system.md");
10530
11048
  var LEADER_AFTER_TASK_PROMPT = readBundledInstructionFile("leader-after-task.md");
10531
11049
  function readBundledInstructionFile(name) {
10532
11050
  for (const dir of bundledInstructionDirCandidates()) {
10533
- const file = path12.join(dir, name);
11051
+ const file = path13.join(dir, name);
10534
11052
  try {
10535
11053
  return readFileSync6(file, "utf8").trimEnd();
10536
11054
  } catch {
@@ -10539,12 +11057,12 @@ function readBundledInstructionFile(name) {
10539
11057
  return "";
10540
11058
  }
10541
11059
  function bundledInstructionDirCandidates() {
10542
- const here = path12.dirname(fileURLToPath3(import.meta.url));
11060
+ const here = path13.dirname(fileURLToPath3(import.meta.url));
10543
11061
  const candidates = [
10544
- path12.resolve(here, "../../../instructions"),
10545
- path12.resolve(here, "../../instructions"),
10546
- path12.resolve(here, "../instructions"),
10547
- path12.resolve(here, "instructions")
11062
+ path13.resolve(here, "../../../instructions"),
11063
+ path13.resolve(here, "../../instructions"),
11064
+ path13.resolve(here, "../instructions"),
11065
+ path13.resolve(here, "instructions")
10548
11066
  ];
10549
11067
  return candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
10550
11068
  }
@@ -10558,12 +11076,12 @@ function isDirectory(candidate) {
10558
11076
 
10559
11077
  // src/core/system-prompt-environment.ts
10560
11078
  import * as os3 from "node:os";
10561
- import * as path14 from "node:path";
11079
+ import * as path15 from "node:path";
10562
11080
 
10563
11081
  // src/core/system-prompt-environment-probes.ts
10564
11082
  import { spawn as spawn2 } from "node:child_process";
10565
- import * as fs6 from "node:fs/promises";
10566
- import * as path13 from "node:path";
11083
+ import * as fs7 from "node:fs/promises";
11084
+ import * as path14 from "node:path";
10567
11085
 
10568
11086
  // src/utils/child-env.ts
10569
11087
  var ALLOWED_KEYS = /* @__PURE__ */ new Set([
@@ -10704,7 +11222,7 @@ function buildChildEnv(optsOrSessionId) {
10704
11222
  // src/core/system-prompt-environment-probes.ts
10705
11223
  async function dirExists(p) {
10706
11224
  try {
10707
- const stat3 = await fs6.stat(p);
11225
+ const stat3 = await fs7.stat(p);
10708
11226
  return stat3.isDirectory();
10709
11227
  } catch {
10710
11228
  return false;
@@ -10791,7 +11309,7 @@ async function detectLanguages(root) {
10791
11309
  const hits = await Promise.all(
10792
11310
  checks.map(async ([marker, lang]) => {
10793
11311
  try {
10794
- await fs6.access(path13.join(root, marker));
11312
+ await fs7.access(path14.join(root, marker));
10795
11313
  return lang;
10796
11314
  } catch {
10797
11315
  return null;
@@ -10854,6 +11372,7 @@ function shellGuidanceBlock(shell, detail) {
10854
11372
  var MAX_ENVIRONMENT_CACHE_ENTRIES = 16;
10855
11373
  async function buildEnvironment(ctx, env) {
10856
11374
  const modelCapabilities = env.modelCapabilities;
11375
+ const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10857
11376
  const cacheKey = [
10858
11377
  ctx.projectRoot,
10859
11378
  ctx.provider ?? "",
@@ -10862,7 +11381,9 @@ async function buildEnvironment(ctx, env) {
10862
11381
  modelCapabilities?.supportsTools ? 1 : 0,
10863
11382
  modelCapabilities?.supportsVision ? 1 : 0,
10864
11383
  modelCapabilities?.supportsReasoning ? 1 : 0,
10865
- env.skillCache ?? ""
11384
+ env.skillCache ?? "",
11385
+ today,
11386
+ env.modeId ?? ""
10866
11387
  ].join("\0");
10867
11388
  const cached = env.envCacheByRoot.get(cacheKey);
10868
11389
  if (cached) {
@@ -10870,12 +11391,11 @@ async function buildEnvironment(ctx, env) {
10870
11391
  env.envCacheByRoot.set(cacheKey, cached);
10871
11392
  return cached;
10872
11393
  }
10873
- const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10874
11394
  const platform2 = `${os3.platform()} ${os3.release()}`;
10875
11395
  const effShell = effectiveShell(os3.platform(), process.env["WRONGSTACK_SHELL"]);
10876
11396
  const shell = effShell === "posix" ? process.env.SHELL ?? process.env.ComSpec ?? "unknown" : SHELL_DISPLAY[effShell];
10877
11397
  const node = process.version;
10878
- const isGit = await dirExists(path14.join(ctx.projectRoot, ".git"));
11398
+ const isGit = await dirExists(path15.join(ctx.projectRoot, ".git"));
10879
11399
  const [git, langs] = await Promise.all([
10880
11400
  isGit ? gitStatus(ctx.projectRoot) : Promise.resolve("not a git repo"),
10881
11401
  detectLanguages(ctx.projectRoot)
@@ -11351,16 +11871,16 @@ ${skillBodyCache}`);
11351
11871
  }
11352
11872
 
11353
11873
  // src/core/system-prompt-plan.ts
11354
- import * as fs7 from "node:fs/promises";
11874
+ import * as fs8 from "node:fs/promises";
11355
11875
  async function readActivePlanBlock(args) {
11356
11876
  const { planPath, cache } = args;
11357
11877
  if (!planPath) return { text: "", cache };
11358
11878
  try {
11359
- const stat3 = await fs7.stat(planPath);
11879
+ const stat3 = await fs8.stat(planPath);
11360
11880
  if (cache && cache.path === planPath && cache.mtimeMs === stat3.mtimeMs) {
11361
11881
  return { text: cache.text, cache };
11362
11882
  }
11363
- const raw = await fs7.readFile(planPath, "utf8");
11883
+ const raw = await fs8.readFile(planPath, "utf8");
11364
11884
  const text = formatActivePlan(raw);
11365
11885
  return { text, cache: { path: planPath, mtimeMs: stat3.mtimeMs, text } };
11366
11886
  } catch {
@@ -11716,7 +12236,9 @@ ${peers}
11716
12236
  const tpl = tplCtx ?? this.templateContext(ctx);
11717
12237
  const section = (key, vars = {}) => instructionSection(instructions, key, vars, tpl);
11718
12238
  const tier = this.tier;
11719
- if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier) {
12239
+ const subagent = ctx.subagent === true;
12240
+ const maxContextTokens = this.modelCapabilities()?.maxContextTokens ?? 0;
12241
+ if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier && this._toolsUsageCache?.subagent === subagent && this._toolsUsageCache?.maxContextTokens === maxContextTokens) {
11720
12242
  return this._toolsUsageCache.text;
11721
12243
  }
11722
12244
  const byCat = /* @__PURE__ */ new Map();
@@ -11843,7 +12365,7 @@ ${hint.trim()}`);
11843
12365
  }
11844
12366
  }
11845
12367
  const text = lines.join("\n");
11846
- this._toolsUsageCache = { toolsRef: tools, tier, text };
12368
+ this._toolsUsageCache = { toolsRef: tools, tier, subagent, maxContextTokens, text };
11847
12369
  return text;
11848
12370
  }
11849
12371
  renderOnlineAgents(agents) {
@@ -11906,6 +12428,7 @@ export {
11906
12428
  createDefaultPipelines,
11907
12429
  createFallbackModelExtension,
11908
12430
  detectContinueIntent,
12431
+ diagnoseFallbackConfig,
11909
12432
  effectiveFallbackChain,
11910
12433
  fallbackProfileChain,
11911
12434
  formatModelRef,
@@ -11924,6 +12447,7 @@ export {
11924
12447
  runtimeFallbackChain,
11925
12448
  setBtwNote,
11926
12449
  setQueuedMessagesSnapshot,
12450
+ simulateFallbackFailover,
11927
12451
  smartDefaultFallbackChain,
11928
12452
  wrapAsState,
11929
12453
  writePendingNextSteps