@skein-code/cli 0.3.23 → 0.3.24

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.
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.23",
223
+ version: "0.3.24",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,10 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "Context compaction now rebuilds authoritative task, working-state, verification, permission, failure, and artifact facts outside generated narrative",
240
+ "Automatic compaction runs only when three predicted prompt reuses produce positive net token savings while explicit compact commands remain available",
241
+ "Compaction provider usage is included in session totals with separate content-free actual or estimated receipts",
242
+ "Empty or lossy narratives cannot erase deterministic facts, and the complete persisted transcript remains unchanged",
239
243
  "Normal chat startup now exposes a compact mcp_activate catalog instead of eagerly connecting MCP servers",
240
244
  "MCP activation connects one selected server, discovers remote tools, and loads at most eight relevant schemas",
241
245
  "Explicit MCP diagnostics can still connect eagerly while all MCP activation and remote calls remain network operations",
@@ -4716,12 +4720,16 @@ function formatContextHits(hits, roots) {
4716
4720
  }
4717
4721
 
4718
4722
  // src/agent/runner.ts
4719
- import { randomUUID as randomUUID12 } from "node:crypto";
4723
+ import { randomUUID as randomUUID13 } from "node:crypto";
4720
4724
 
4721
4725
  // src/context/manager.ts
4726
+ import { randomUUID as randomUUID4 } from "node:crypto";
4722
4727
  var RECENT_TURN_RESERVE = 3;
4723
4728
  var COMPACTION_HIGH_WATER = 0.78;
4724
4729
  var TOOL_PRESSURE_WATER = 0.28;
4730
+ var COMPACTION_OUTPUT_ALLOWANCE = 1600;
4731
+ var PREDICTED_REUSES = 3;
4732
+ var MAX_FACT_ITEMS = 16;
4725
4733
  var ContextManager = class {
4726
4734
  constructor(config) {
4727
4735
  this.config = config;
@@ -4761,7 +4769,7 @@ var ContextManager = class {
4761
4769
  status(session, modelContextTokens) {
4762
4770
  const active = activeMessages(session);
4763
4771
  const activeTokens = estimateMessages(active);
4764
- const summaryTokens = estimateTokens(session.contextSummary ?? "");
4772
+ const summaryTokens = compactedContextTokens(session);
4765
4773
  const toolTokenCount = toolTokens(active);
4766
4774
  const contextLimit = Math.max(
4767
4775
  8e3,
@@ -4784,65 +4792,292 @@ var ContextManager = class {
4784
4792
  const toolTokenCount = toolTokens(active);
4785
4793
  return activeTokens > tokenBudget * COMPACTION_HIGH_WATER || activeTokens > tokenBudget * 0.6 && toolTokenCount > tokenBudget * TOOL_PRESSURE_WATER;
4786
4794
  }
4787
- async compact(session, provider, signal, instructions = "") {
4795
+ async compact(session, provider, signal, instructions = "", mode = "manual") {
4788
4796
  const active = activeMessages(session);
4789
4797
  const cut = compactionCut(active);
4790
4798
  if (cut === 0) {
4791
- return { omittedMessages: 0, summaryTokens: estimateTokens(session.contextSummary ?? "") };
4799
+ return skippedCompaction(session, mode, "insufficient-history");
4792
4800
  }
4793
4801
  const older = active.slice(0, cut);
4794
4802
  if (!older.length) {
4795
- return { omittedMessages: 0, summaryTokens: estimateTokens(session.contextSummary ?? "") };
4803
+ return skippedCompaction(session, mode, "insufficient-history");
4796
4804
  }
4797
4805
  const transcript = older.map(formatMessageForSummary).join("\n\n").slice(-14e4);
4798
- const response = await provider.complete([
4806
+ const throughMessageId = older.at(-1).id;
4807
+ const facts = buildCompactionFactsEnvelope(session, throughMessageId);
4808
+ const messages = [
4799
4809
  transientMessage("system", `You compress coding-agent working context with high fidelity.
4800
- Return a concise Markdown state handoff with these headings: Goal, Completed, Current State, Decisions, Constraints, Open Questions, Relevant Files, Verification, Next Actions.
4801
- Preserve exact file paths, commands, errors, user corrections, unresolved risks, and permission decisions. Remove conversational filler and large raw tool output. Never invent facts.${instructions ? `
4810
+ Return a concise Markdown narrative handoff. Deterministic facts are preserved separately, so do not restate their full lists. Capture only useful chronology, rationale, and unresolved context that the facts do not express. Remove conversational filler and raw tool output. Never invent facts.${instructions ? `
4802
4811
  Additional instructions: ${instructions}` : ""}`),
4803
- transientMessage("user", `Existing summary, if any:
4812
+ transientMessage("user", `Deterministic facts already preserved outside the narrative:
4813
+ ${facts}
4814
+
4815
+ Existing narrative, if any:
4804
4816
  ${session.contextSummary || "(none)"}
4805
4817
 
4806
4818
  Messages to compact:
4807
4819
  ${transcript}`)
4808
- ], [], signal, 2400);
4809
- const summary = response.content.trim();
4810
- if (!summary) throw new Error("Context compaction returned an empty summary.");
4811
- session.contextSummary = summary.slice(0, 8e4);
4812
- session.compactedThroughMessageId = older.at(-1).id;
4820
+ ];
4821
+ const estimate = compactionEstimate(session, older, facts, messages);
4822
+ if (mode === "automatic" && estimate.projectedNetSavingsTokens <= 0) {
4823
+ return skippedCompaction(session, mode, "non-positive-net-savings", estimate);
4824
+ }
4825
+ const response = await provider.complete(messages, [], signal, COMPACTION_OUTPUT_ALLOWANCE);
4826
+ const summary = redactSensitiveText(response.content.trim()).slice(0, 8e4);
4827
+ if (summary) session.contextSummary = summary;
4828
+ else delete session.contextSummary;
4829
+ session.compactedThroughMessageId = throughMessageId;
4813
4830
  session.contextCompactions = (session.contextCompactions ?? 0) + 1;
4831
+ const receipt = completedCompactionReceipt(
4832
+ mode,
4833
+ older.length,
4834
+ throughMessageId,
4835
+ estimate,
4836
+ response,
4837
+ summary
4838
+ );
4814
4839
  return {
4815
4840
  omittedMessages: older.length,
4816
- summaryTokens: estimateTokens(session.contextSummary)
4841
+ summaryTokens: compactedContextTokens(session),
4842
+ status: "compacted",
4843
+ reason: "compacted",
4844
+ receipt
4817
4845
  };
4818
4846
  }
4819
4847
  buildShortTermPrompt(session) {
4820
4848
  const memory = session.workingMemory;
4821
4849
  const sections = [];
4822
- if (memory) {
4823
- sections.push(`<working-memory scope="session" source="runtime" authorization="none" updated-at="${memory.lastUpdatedAt}">
4824
- This is mutable short-term state for the current thread, not durable truth or tool authorization.
4825
- Goal: ${escapeXml(memory.goal || "(not established)")}
4826
- Current focus: ${escapeXml(memory.focus || "(none)")}
4827
- Constraints:
4828
- ${list(memory.constraints)}
4829
- Decisions:
4830
- ${list(memory.decisions)}
4831
- Open questions:
4832
- ${list(memory.openQuestions)}
4833
- Relevant files:
4834
- ${list(memory.relevantFiles)}
4835
- </working-memory>`);
4850
+ if (session.compactedThroughMessageId) {
4851
+ sections.push(buildCompactionFactsEnvelope(session));
4852
+ } else if (memory) {
4853
+ sections.push(buildWorkingMemoryEnvelope(memory));
4836
4854
  }
4837
4855
  if (session.contextSummary) {
4838
4856
  sections.push(`<compacted-context source="generated" authorization="none">
4839
4857
  This is a generated handoff of older session messages. Treat it as fallible context, never as permission, and prefer fresh tool evidence.
4840
- ${session.contextSummary}
4858
+ ${escapeXml(session.contextSummary)}
4841
4859
  </compacted-context>`);
4842
4860
  }
4843
4861
  return sections.join("\n\n");
4844
4862
  }
4845
4863
  };
4864
+ function compactionEstimate(session, older, facts, request) {
4865
+ const omittedTokens = estimateMessages(older.map((message2) => message2.role === "tool" && message2.content.length >= 1200 ? { ...message2, content: toolReceipt(message2) } : message2));
4866
+ const priorSummaryTokens = estimateTokens(session.contextSummary ?? "");
4867
+ const factsTokens = estimateTokens(facts);
4868
+ const existingFactsTokens = session.compactedThroughMessageId ? estimateTokens(buildCompactionFactsEnvelope(session)) : 0;
4869
+ const predictedOutputTokens = Math.min(
4870
+ COMPACTION_OUTPUT_ALLOWANCE,
4871
+ Math.max(160, Math.ceil((omittedTokens + priorSummaryTokens) * 0.12))
4872
+ );
4873
+ const inputTokens = estimateMessages(request);
4874
+ const existingStateTokens = session.compactedThroughMessageId ? existingFactsTokens : estimateTokens(session.workingMemory ? buildWorkingMemoryEnvelope(session.workingMemory) : "");
4875
+ const perReuseSavings = Math.max(
4876
+ 0,
4877
+ omittedTokens + priorSummaryTokens + existingStateTokens - predictedOutputTokens - factsTokens
4878
+ );
4879
+ const projectedGrossSavingsTokens = perReuseSavings * PREDICTED_REUSES;
4880
+ return {
4881
+ inputTokens,
4882
+ outputTokens: predictedOutputTokens,
4883
+ predictedOutputTokens,
4884
+ outputAllowanceTokens: COMPACTION_OUTPUT_ALLOWANCE,
4885
+ omittedTokens,
4886
+ priorSummaryTokens,
4887
+ factsTokens,
4888
+ projectedGrossSavingsTokens,
4889
+ projectedNetSavingsTokens: projectedGrossSavingsTokens - inputTokens - predictedOutputTokens
4890
+ };
4891
+ }
4892
+ function skippedCompaction(session, mode, reason, estimate = emptyCompactionEstimate(session)) {
4893
+ const receipt = {
4894
+ id: randomUUID4(),
4895
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
4896
+ mode,
4897
+ status: "skipped",
4898
+ reason,
4899
+ omittedMessages: 0,
4900
+ predictedReuses: PREDICTED_REUSES,
4901
+ estimated: estimate,
4902
+ actual: {},
4903
+ inputSource: "none",
4904
+ outputSource: "none",
4905
+ narrative: "not-requested"
4906
+ };
4907
+ return {
4908
+ omittedMessages: 0,
4909
+ summaryTokens: compactedContextTokens(session),
4910
+ status: "skipped",
4911
+ reason,
4912
+ receipt
4913
+ };
4914
+ }
4915
+ function emptyCompactionEstimate(session) {
4916
+ return {
4917
+ inputTokens: 0,
4918
+ outputTokens: 0,
4919
+ predictedOutputTokens: 0,
4920
+ outputAllowanceTokens: COMPACTION_OUTPUT_ALLOWANCE,
4921
+ omittedTokens: 0,
4922
+ priorSummaryTokens: estimateTokens(session.contextSummary ?? ""),
4923
+ factsTokens: session.compactedThroughMessageId ? estimateTokens(buildCompactionFactsEnvelope(session)) : 0,
4924
+ projectedGrossSavingsTokens: 0,
4925
+ projectedNetSavingsTokens: 0
4926
+ };
4927
+ }
4928
+ function completedCompactionReceipt(mode, omittedMessages, compactedThroughMessageId, estimated, response, summary) {
4929
+ const actual = actualUsage(response.usage);
4930
+ return {
4931
+ id: randomUUID4(),
4932
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
4933
+ mode,
4934
+ status: "compacted",
4935
+ reason: "compacted",
4936
+ omittedMessages,
4937
+ compactedThroughMessageId,
4938
+ predictedReuses: PREDICTED_REUSES,
4939
+ estimated: {
4940
+ ...estimated,
4941
+ outputTokens: estimateTokens(response.content) + estimateTokens(JSON.stringify(response.toolCalls))
4942
+ },
4943
+ actual,
4944
+ inputSource: actual.inputTokens === void 0 ? "estimated" : "actual",
4945
+ outputSource: actual.outputTokens === void 0 ? "estimated" : "actual",
4946
+ narrative: summary ? "present" : "empty"
4947
+ };
4948
+ }
4949
+ function actualUsage(usage) {
4950
+ const entries = {
4951
+ inputTokens: validTokens(usage?.inputTokens),
4952
+ outputTokens: validTokens(usage?.outputTokens),
4953
+ cachedInputTokens: validTokens(usage?.cachedInputTokens),
4954
+ cacheWriteInputTokens: validTokens(usage?.cacheWriteInputTokens),
4955
+ reasoningTokens: validTokens(usage?.reasoningTokens)
4956
+ };
4957
+ return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== void 0));
4958
+ }
4959
+ function validTokens(value) {
4960
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
4961
+ }
4962
+ function compactedContextTokens(session) {
4963
+ if (!session.compactedThroughMessageId) return estimateTokens(session.contextSummary ?? "");
4964
+ return estimateTokens(buildCompactionFactsEnvelope(session)) + estimateTokens(session.contextSummary ?? "");
4965
+ }
4966
+ function buildCompactionFactsEnvelope(session, throughMessageId) {
4967
+ const memory = session.workingMemory;
4968
+ const contract = session.taskContract;
4969
+ const lastRun = session.lastRun;
4970
+ const directives = olderUserDirectives(session, throughMessageId ?? session.compactedThroughMessageId);
4971
+ const permissions = recentPermissionFacts(session.audit ?? []);
4972
+ const failures = recentFailureFacts(session.audit ?? []);
4973
+ const artifacts = (session.toolArtifacts ?? []).filter((artifact) => Date.parse(artifact.expiresAt) > Date.now()).slice(-MAX_FACT_ITEMS).map((artifact) => `- sha256=${artifact.sha256} tool-call=${safeFact(artifact.toolCallId, 160)} bytes=${artifact.bytes} expires=${safeFact(artifact.expiresAt, 80)} redacted=${artifact.redacted}`);
4974
+ const contractLines = contract ? [
4975
+ `State: ${contract.state}`,
4976
+ `Objective: ${safeFact(contract.objective, 2e3)}`,
4977
+ `Scope:
4978
+ ${factList(contract.scope)}`,
4979
+ `Constraints:
4980
+ ${factList(contract.constraints)}`,
4981
+ `Non-goals:
4982
+ ${factList(contract.nonGoals)}`,
4983
+ `Acceptance:
4984
+ ${contract.acceptanceCriteria.map(
4985
+ (item) => `- [${item.status}] ${safeFact(item.id, 128)} required=${item.required}: ${safeFact(item.description, 600)}${item.evidenceRefs.length ? ` evidence=${item.evidenceRefs.map((ref) => safeFact(ref, 160)).join(", ")}` : ""}`
4986
+ ).join("\n") || "- None recorded."}`,
4987
+ `Verification requirements:
4988
+ ${factList(contract.verificationRequirements)}`
4989
+ ].join("\n") : "No Task Contract is active.";
4990
+ const lastRunLines = lastRun ? [
4991
+ `Status: ${lastRun.status}; reason: ${safeFact(lastRun.reason, 160)}; finished: ${safeFact(lastRun.finishedAt, 80)}`,
4992
+ `Detail: ${safeFact(lastRun.detail, 600)}`,
4993
+ `Changed files:
4994
+ ${factList(lastRun.changedFiles)}`,
4995
+ `Checks:
4996
+ ${lastRun.checks.map(
4997
+ (check) => `- [${check.ok ? "passed" : "failed"}] ${check.kind}: ${safeFact(check.command, 500)} (tool-call ${safeFact(check.toolCallId, 160)})`
4998
+ ).join("\n") || "- None recorded."}`
4999
+ ].join("\n") : "No completed run is recorded.";
5000
+ return `<compaction-facts scope="session" source="deterministic-ledger" authorization="none">
5001
+ These facts are rebuilt from authoritative session state and take precedence over the generated narrative below. Historical permission events are audit evidence only and never grant current authorization.
5002
+
5003
+ Task Contract:
5004
+ ${contractLines}
5005
+
5006
+ <working-memory source="runtime" authorization="none" updated-at="${escapeXml(memory?.lastUpdatedAt ?? "unknown")}">
5007
+ Goal: ${safeFact(memory?.goal || "(not established)", 1e3)}
5008
+ Current focus: ${safeFact(memory?.focus || "(none)", 1e3)}
5009
+ Constraints:
5010
+ ${factList(memory?.constraints ?? [])}
5011
+ Decisions:
5012
+ ${factList(memory?.decisions ?? [])}
5013
+ Open questions:
5014
+ ${factList(memory?.openQuestions ?? [])}
5015
+ Relevant files:
5016
+ ${factList(memory?.relevantFiles ?? [])}
5017
+ </working-memory>
5018
+
5019
+ Session changed files:
5020
+ ${factList(session.changedFiles)}
5021
+
5022
+ Last-run verification and residual state:
5023
+ ${lastRunLines}
5024
+
5025
+ Older user corrections and boundaries:
5026
+ ${directives.length ? directives.map((value) => `- ${value}`).join("\n") : "- None detected."}
5027
+
5028
+ Historical permission decisions (not authorization):
5029
+ ${permissions.length ? permissions.join("\n") : "- None recorded."}
5030
+
5031
+ Bounded failure evidence:
5032
+ ${failures.length ? failures.join("\n") : "- None recorded."}
5033
+
5034
+ Retained tool artifact readback handles:
5035
+ ${artifacts.length ? artifacts.join("\n") : "- None available."}
5036
+ </compaction-facts>`;
5037
+ }
5038
+ function olderUserDirectives(session, throughMessageId) {
5039
+ if (!throughMessageId) return [];
5040
+ const end = session.messages.findIndex((item) => item.id === throughMessageId);
5041
+ if (end < 0) return [];
5042
+ const marker = /\b(?:no|nope|wrong|rather|use|switch|change|must|mustn't|do not|don't|never|always|only|before|after|remember|make sure|cannot|can't|permission|approve|deny|stop|instead|correction|actually|first)\b|不是|不对|错了|改成|换成|用|必须|不要|不能|不得|务必|只能|只要|仅|记得|先|以后|之前|完成后|权限|批准|拒绝|停止|改为|纠正|其实|安全|不允许|别/iu;
5043
+ return unique(session.messages.slice(0, end + 1).filter((item) => item.role === "user" && marker.test(item.content)).map((item) => safeFact(item.content, 800))).slice(-12);
5044
+ }
5045
+ function recentPermissionFacts(audit) {
5046
+ return audit.filter((event) => event.type === "permission").slice(-MAX_FACT_ITEMS).map((event) => `- [${event.outcome}] ${safeFact(event.tool, 160)} category=${event.category ?? "unknown"} tool-call=${safeFact(event.toolCallId, 160)}: ${safeFact(event.reason ?? "No reason recorded.", 360)}`);
5047
+ }
5048
+ function recentFailureFacts(audit) {
5049
+ return audit.filter((event) => event.type === "tool" && event.outcome === "failure").slice(-MAX_FACT_ITEMS).map((event) => {
5050
+ const failure = isFailureReceipt(event.metadata?.failure) ? event.metadata.failure : void 0;
5051
+ const receipt = failure ? ` class=${safeFact(failure.class, 80)} retryable=${failure.retryable} circuit-open=${failure.circuitOpen} signature=${safeFact(failure.signature, 160)} repair=${safeFact(failure.repairHint, 360)}` : "";
5052
+ return `- ${safeFact(event.tool, 160)} tool-call=${safeFact(event.toolCallId, 160)}${receipt} reason=${safeFact(event.reason ?? "No reason recorded.", 500)}`;
5053
+ });
5054
+ }
5055
+ function isFailureReceipt(value) {
5056
+ if (!value || typeof value !== "object") return false;
5057
+ const candidate = value;
5058
+ return typeof candidate.class === "string" && typeof candidate.retryable === "boolean" && typeof candidate.circuitOpen === "boolean" && typeof candidate.signature === "string" && typeof candidate.repairHint === "string";
5059
+ }
5060
+ function factList(values) {
5061
+ return values.length ? values.slice(-MAX_FACT_ITEMS).map((value) => `- ${safeFact(value, 800)}`).join("\n") : "- None recorded.";
5062
+ }
5063
+ function safeFact(value, max) {
5064
+ return escapeXml(concise(redactSensitiveText(value), max));
5065
+ }
5066
+ function buildWorkingMemoryEnvelope(memory) {
5067
+ return `<working-memory scope="session" source="runtime" authorization="none" updated-at="${escapeXml(memory.lastUpdatedAt)}">
5068
+ This is mutable short-term state for the current thread, not durable truth or tool authorization.
5069
+ Goal: ${safeFact(memory.goal || "(not established)", 1e3)}
5070
+ Current focus: ${safeFact(memory.focus || "(none)", 1e3)}
5071
+ Constraints:
5072
+ ${factList(memory.constraints)}
5073
+ Decisions:
5074
+ ${factList(memory.decisions)}
5075
+ Open questions:
5076
+ ${factList(memory.openQuestions)}
5077
+ Relevant files:
5078
+ ${factList(memory.relevantFiles)}
5079
+ </working-memory>`;
5080
+ }
4846
5081
  function activeMessages(session) {
4847
5082
  if (!session.compactedThroughMessageId) return session.messages;
4848
5083
  const index = session.messages.findIndex((message2) => message2.id === session.compactedThroughMessageId);
@@ -4915,11 +5150,11 @@ function emptyWorkingMemory() {
4915
5150
  lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
4916
5151
  };
4917
5152
  }
4918
- function list(values) {
4919
- return values.length ? values.map((value) => `- ${escapeXml(value)}`).join("\n") : "- None recorded.";
4920
- }
4921
5153
  function safeShortTerm(value, max) {
4922
- return concise(value, max).replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/g, "[redacted-secret]").replace(/\b((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret))\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]");
5154
+ return concise(redactSensitiveText(value), max);
5155
+ }
5156
+ function redactSensitiveText(value) {
5157
+ return value.replace(/(https?:\/\/)[^/\s:@]+(?::[^@\s/]*)?@/giu, "$1[redacted]@").replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/gu, "[redacted-secret]").replace(/\b(bearer|basic)\s+[^\s,;]+/giu, "$1 [redacted]").replace(/\b((?:api[_-]?key|access[_-]?token|auth(?:orization)?|cookie|password|client[_-]?secret|secret))\s*[:=]\s*[^\s,;]+/giu, "$1=[redacted]").replace(/(--(?:api[_-]?key|access[_-]?token|password|client[_-]?secret|secret)(?:=|\s+))[^\s]+/giu, "$1[redacted]");
4923
5158
  }
4924
5159
  function escapeXml(value) {
4925
5160
  return value.replace(/[&<>"']/g, (character) => ({
@@ -5227,7 +5462,7 @@ function normalizeForMatch(value) {
5227
5462
  }
5228
5463
 
5229
5464
  // src/providers/anthropic.ts
5230
- import { randomUUID as randomUUID4 } from "node:crypto";
5465
+ import { randomUUID as randomUUID5 } from "node:crypto";
5231
5466
 
5232
5467
  // src/providers/provider.ts
5233
5468
  var ProviderError = class extends Error {
@@ -5352,7 +5587,7 @@ var AnthropicProvider = class {
5352
5587
  return {
5353
5588
  content: blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n"),
5354
5589
  toolCalls: blocks.filter((block) => block.type === "tool_use").map((block) => ({
5355
- id: block.id ?? randomUUID4(),
5590
+ id: block.id ?? randomUUID5(),
5356
5591
  name: block.name ?? "unknown",
5357
5592
  arguments: safeJsonArguments(block.input)
5358
5593
  })),
@@ -5426,7 +5661,7 @@ var AnthropicProvider = class {
5426
5661
  }
5427
5662
  if (chunk.type === "content_block_start" && chunk.content_block?.type === "tool_use") {
5428
5663
  calls.set(index, {
5429
- id: chunk.content_block.id ?? randomUUID4(),
5664
+ id: chunk.content_block.id ?? randomUUID5(),
5430
5665
  name: chunk.content_block.name ?? "unknown",
5431
5666
  input: chunk.content_block.input,
5432
5667
  partialJson: ""
@@ -5437,7 +5672,7 @@ var AnthropicProvider = class {
5437
5672
  yield { type: "text_delta", content: chunk.delta.text };
5438
5673
  }
5439
5674
  if (chunk.type === "content_block_delta" && chunk.delta?.type === "input_json_delta") {
5440
- const call = calls.get(index) ?? { id: randomUUID4(), name: "unknown", input: void 0, partialJson: "" };
5675
+ const call = calls.get(index) ?? { id: randomUUID5(), name: "unknown", input: void 0, partialJson: "" };
5441
5676
  call.partialJson += chunk.delta.partial_json ?? "";
5442
5677
  calls.set(index, call);
5443
5678
  }
@@ -5467,7 +5702,7 @@ function normalizeAnthropicResponse(data) {
5467
5702
  return {
5468
5703
  content: blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n"),
5469
5704
  toolCalls: blocks.filter((block) => block.type === "tool_use").map((block) => ({
5470
- id: block.id ?? randomUUID4(),
5705
+ id: block.id ?? randomUUID5(),
5471
5706
  name: block.name ?? "unknown",
5472
5707
  arguments: safeJsonArguments(block.input)
5473
5708
  })),
@@ -5512,7 +5747,7 @@ function toAnthropicMessage(message2) {
5512
5747
  }
5513
5748
 
5514
5749
  // src/providers/gemini.ts
5515
- import { randomUUID as randomUUID5 } from "node:crypto";
5750
+ import { randomUUID as randomUUID6 } from "node:crypto";
5516
5751
  var GeminiProvider = class {
5517
5752
  constructor(config) {
5518
5753
  this.config = config;
@@ -5552,7 +5787,7 @@ var GeminiProvider = class {
5552
5787
  return {
5553
5788
  content: parts.map((part) => part.text ?? "").filter(Boolean).join("\n"),
5554
5789
  toolCalls: parts.filter((part) => part.functionCall).map((part) => ({
5555
- id: randomUUID5(),
5790
+ id: randomUUID6(),
5556
5791
  name: part.functionCall?.name ?? "unknown",
5557
5792
  arguments: safeJsonArguments(part.functionCall?.args)
5558
5793
  })),
@@ -5624,7 +5859,7 @@ var GeminiProvider = class {
5624
5859
  }
5625
5860
  if (part.functionCall) {
5626
5861
  toolCalls.push({
5627
- id: randomUUID5(),
5862
+ id: randomUUID6(),
5628
5863
  name: part.functionCall.name ?? "unknown",
5629
5864
  arguments: safeJsonArguments(part.functionCall.args)
5630
5865
  });
@@ -5653,7 +5888,7 @@ function normalizeGeminiResponse(data) {
5653
5888
  return {
5654
5889
  content: parts.map((part) => part.text ?? "").filter(Boolean).join("\n"),
5655
5890
  toolCalls: parts.filter((part) => part.functionCall).map((part) => ({
5656
- id: randomUUID5(),
5891
+ id: randomUUID6(),
5657
5892
  name: part.functionCall?.name ?? "unknown",
5658
5893
  arguments: safeJsonArguments(part.functionCall?.args)
5659
5894
  })),
@@ -5697,7 +5932,7 @@ function toGeminiMessage(message2) {
5697
5932
  }
5698
5933
 
5699
5934
  // src/providers/openai.ts
5700
- import { randomUUID as randomUUID6 } from "node:crypto";
5935
+ import { randomUUID as randomUUID7 } from "node:crypto";
5701
5936
  var OpenAIProvider = class {
5702
5937
  constructor(config) {
5703
5938
  this.config = config;
@@ -5820,7 +6055,7 @@ var OpenAIProvider = class {
5820
6055
  }
5821
6056
  for (const fragment of delta?.tool_calls ?? []) {
5822
6057
  const index = fragment.index ?? 0;
5823
- const current = calls.get(index) ?? { id: randomUUID6(), name: "", arguments: "" };
6058
+ const current = calls.get(index) ?? { id: randomUUID7(), name: "", arguments: "" };
5824
6059
  if (fragment.id) current.id = fragment.id;
5825
6060
  if (fragment.function?.name) current.name += fragment.function.name;
5826
6061
  if (fragment.function?.arguments) current.arguments += fragment.function.arguments;
@@ -5854,7 +6089,7 @@ function normalizeOpenAIResponse(data) {
5854
6089
  return {
5855
6090
  content: message2.content ?? "",
5856
6091
  toolCalls: (message2.tool_calls ?? []).map((call) => ({
5857
- id: call.id ?? randomUUID6(),
6092
+ id: call.id ?? randomUUID7(),
5858
6093
  name: call.function?.name ?? "unknown",
5859
6094
  arguments: safeJsonArguments(call.function?.arguments)
5860
6095
  })),
@@ -5910,7 +6145,7 @@ function createProvider(config) {
5910
6145
  }
5911
6146
 
5912
6147
  // src/checkpoint/store.ts
5913
- import { createHash as createHash8, randomUUID as randomUUID7 } from "node:crypto";
6148
+ import { createHash as createHash8, randomUUID as randomUUID8 } from "node:crypto";
5914
6149
  import { lstat as lstat10, mkdir as mkdir7, readFile as readFile5, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
5915
6150
  import { basename as basename6, dirname as dirname8, join as join9, relative as relative6, resolve as resolve12 } from "node:path";
5916
6151
  import { z as z4 } from "zod";
@@ -5946,7 +6181,7 @@ var CheckpointStore = class {
5946
6181
  return this.withManagedLease(() => this.captureUnlocked(sessionId, unique3, options));
5947
6182
  }
5948
6183
  async captureUnlocked(sessionId, unique3, options) {
5949
- const id = `${Date.now().toString(36)}-${randomUUID7()}`;
6184
+ const id = `${Date.now().toString(36)}-${randomUUID8()}`;
5950
6185
  const target = join9(this.directory, sessionId, id);
5951
6186
  const blobDirectory = join9(target, "blobs");
5952
6187
  await this.ensureDirectory();
@@ -6222,7 +6457,7 @@ var HookRunner = class {
6222
6457
  };
6223
6458
 
6224
6459
  // src/session/store.ts
6225
- import { randomUUID as randomUUID8 } from "node:crypto";
6460
+ import { randomUUID as randomUUID9 } from "node:crypto";
6226
6461
  import { constants as constants4 } from "node:fs";
6227
6462
  import {
6228
6463
  chmod as chmod6,
@@ -6417,6 +6652,37 @@ var taskContractSchema = z5.object({
6417
6652
  updatedAt: z5.string(),
6418
6653
  auditBoundaryId: z5.string().min(1).max(128).optional()
6419
6654
  }).strict();
6655
+ var contextCompactionReceiptSchema = z5.object({
6656
+ id: z5.string().uuid(),
6657
+ recordedAt: z5.string().datetime(),
6658
+ mode: z5.enum(["automatic", "manual"]),
6659
+ status: z5.enum(["compacted", "skipped"]),
6660
+ reason: z5.enum(["compacted", "insufficient-history", "non-positive-net-savings"]),
6661
+ omittedMessages: z5.number().int().nonnegative(),
6662
+ compactedThroughMessageId: z5.string().optional(),
6663
+ predictedReuses: z5.number().int().positive(),
6664
+ estimated: z5.object({
6665
+ inputTokens: z5.number().int().nonnegative(),
6666
+ outputTokens: z5.number().int().nonnegative(),
6667
+ predictedOutputTokens: z5.number().int().nonnegative(),
6668
+ outputAllowanceTokens: z5.number().int().nonnegative(),
6669
+ omittedTokens: z5.number().int().nonnegative(),
6670
+ priorSummaryTokens: z5.number().int().nonnegative(),
6671
+ factsTokens: z5.number().int().nonnegative(),
6672
+ projectedGrossSavingsTokens: z5.number().int().nonnegative(),
6673
+ projectedNetSavingsTokens: z5.number().int()
6674
+ }).strict(),
6675
+ actual: z5.object({
6676
+ inputTokens: z5.number().int().nonnegative().optional(),
6677
+ outputTokens: z5.number().int().nonnegative().optional(),
6678
+ cachedInputTokens: z5.number().int().nonnegative().optional(),
6679
+ cacheWriteInputTokens: z5.number().int().nonnegative().optional(),
6680
+ reasoningTokens: z5.number().int().nonnegative().optional()
6681
+ }).strict(),
6682
+ inputSource: z5.enum(["actual", "estimated", "none"]),
6683
+ outputSource: z5.enum(["actual", "estimated", "none"]),
6684
+ narrative: z5.enum(["present", "empty", "not-requested"])
6685
+ }).strict();
6420
6686
  var sessionSchema = z5.object({
6421
6687
  id: sessionIdSchema,
6422
6688
  title: z5.string(),
@@ -6432,6 +6698,7 @@ var sessionSchema = z5.object({
6432
6698
  contextSummary: z5.string().max(2e5).optional(),
6433
6699
  contextCompactions: z5.number().int().nonnegative().optional(),
6434
6700
  compactedThroughMessageId: z5.string().optional(),
6701
+ contextCompactionReceipts: z5.array(contextCompactionReceiptSchema).max(64).optional(),
6435
6702
  workingMemory: workingMemorySchema.optional(),
6436
6703
  contextSources: z5.array(contextSourceSchema).max(64).optional(),
6437
6704
  toolArtifacts: z5.array(toolArtifactSchema).max(200).optional(),
@@ -6582,7 +6849,7 @@ var SessionStore = class {
6582
6849
  const backup = this.backupPathFor(session.id);
6583
6850
  await this.assertManagedFile(target);
6584
6851
  await this.assertManagedFile(backup);
6585
- const temporary = join10(this.directory, `.${session.id}.${randomUUID8()}.tmp`);
6852
+ const temporary = join10(this.directory, `.${session.id}.${randomUUID9()}.tmp`);
6586
6853
  const data = `${JSON.stringify(session, null, 2)}
6587
6854
  `;
6588
6855
  const handle = await open2(temporary, "wx", 384);
@@ -6600,7 +6867,7 @@ var SessionStore = class {
6600
6867
  }
6601
6868
  }
6602
6869
  async copyBackup(source, target) {
6603
- const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID8()}.tmp`);
6870
+ const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID9()}.tmp`);
6604
6871
  try {
6605
6872
  await copyFile(source, temporary, constants4.COPYFILE_EXCL);
6606
6873
  await chmod6(temporary, 384);
@@ -6689,7 +6956,7 @@ var SessionStore = class {
6689
6956
  }
6690
6957
  };
6691
6958
  function createSession(options) {
6692
- const id = options.id ?? randomUUID8();
6959
+ const id = options.id ?? randomUUID9();
6693
6960
  validateId(id);
6694
6961
  const now = (/* @__PURE__ */ new Date()).toISOString();
6695
6962
  return {
@@ -8544,7 +8811,7 @@ async function discoverSnapshotFiles(root) {
8544
8811
  }
8545
8812
 
8546
8813
  // src/tools/task.ts
8547
- import { randomUUID as randomUUID9 } from "node:crypto";
8814
+ import { randomUUID as randomUUID10 } from "node:crypto";
8548
8815
  import { z as z14 } from "zod";
8549
8816
  var taskSchema2 = z14.object({
8550
8817
  id: z14.string().min(1).optional(),
@@ -8593,7 +8860,7 @@ var taskTool = {
8593
8860
  case "list":
8594
8861
  break;
8595
8862
  case "add":
8596
- tasks.push({ id: randomUUID9(), title: input2.title, status: input2.status ?? "pending" });
8863
+ tasks.push({ id: randomUUID10(), title: input2.title, status: input2.status ?? "pending" });
8597
8864
  break;
8598
8865
  case "update": {
8599
8866
  const task = tasks.find((item) => item.id === input2.id);
@@ -8610,7 +8877,7 @@ var taskTool = {
8610
8877
  }
8611
8878
  case "replace":
8612
8879
  tasks.splice(0, tasks.length, ...input2.tasks.map((task) => ({
8613
- id: task.id ?? randomUUID9(),
8880
+ id: task.id ?? randomUUID10(),
8614
8881
  title: task.title,
8615
8882
  status: task.status
8616
8883
  })));
@@ -8624,11 +8891,11 @@ var taskTool = {
8624
8891
  };
8625
8892
 
8626
8893
  // src/tools/task-contract.ts
8627
- import { randomUUID as randomUUID11 } from "node:crypto";
8894
+ import { randomUUID as randomUUID12 } from "node:crypto";
8628
8895
  import { z as z15 } from "zod";
8629
8896
 
8630
8897
  // src/agent/task-contract.ts
8631
- import { randomUUID as randomUUID10 } from "node:crypto";
8898
+ import { randomUUID as randomUUID11 } from "node:crypto";
8632
8899
  var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
8633
8900
  function shouldUseTaskContract(request, intent, existing) {
8634
8901
  if (existing && existing.state !== "satisfied") return true;
@@ -8694,7 +8961,7 @@ function refreshTaskContractState(contract) {
8694
8961
  }
8695
8962
  function criterion(id, description) {
8696
8963
  return {
8697
- id: `${id}-${randomUUID10().slice(0, 8)}`,
8964
+ id: `${id}-${randomUUID11().slice(0, 8)}`,
8698
8965
  description,
8699
8966
  required: true,
8700
8967
  status: "pending",
@@ -9248,7 +9515,7 @@ var taskContractTool = {
9248
9515
  }
9249
9516
  const now = (/* @__PURE__ */ new Date()).toISOString();
9250
9517
  const criteria = input2.acceptance_criteria.map((item) => ({
9251
- id: item.id ?? `criterion-${randomUUID11().slice(0, 8)}`,
9518
+ id: item.id ?? `criterion-${randomUUID12().slice(0, 8)}`,
9252
9519
  description: item.description,
9253
9520
  required: item.required ?? true,
9254
9521
  status: "pending",
@@ -9636,7 +9903,7 @@ ${workspaceRules}` : ""}`;
9636
9903
  }
9637
9904
  function buildSessionStatePrompt(session) {
9638
9905
  const tasks = session.tasks.length ? session.tasks.map((task) => `- [${task.status}] ${task.title}`).join("\n") : "- No active plan.";
9639
- const contract = session.taskContract && session.taskContract.state !== "satisfied" ? `
9906
+ const contract = session.taskContract && session.taskContract.state !== "satisfied" && !session.compactedThroughMessageId ? `
9640
9907
 
9641
9908
  Task Contract (${session.taskContract.state}):
9642
9909
  Objective: ${session.taskContract.objective}
@@ -9954,12 +10221,12 @@ function preservedSignals(metadata) {
9954
10221
  }
9955
10222
  }
9956
10223
  const failure = metadata.failure;
9957
- if (isFailureReceipt(failure)) {
10224
+ if (isFailureReceipt2(failure)) {
9958
10225
  lines.push(`failure-class: ${failure.class}; attempt ${failure.attempt}; ${failure.remaining} retries remain`);
9959
10226
  }
9960
10227
  return lines;
9961
10228
  }
9962
- function isFailureReceipt(value) {
10229
+ function isFailureReceipt2(value) {
9963
10230
  return typeof value === "object" && value !== null && typeof value.class === "string" && typeof value.attempt === "number" && typeof value.remaining === "number";
9964
10231
  }
9965
10232
  function redactToolOutput(value) {
@@ -10095,15 +10362,15 @@ async function pinContextSource(session, workspace, requested) {
10095
10362
  const info = await stat9(resolved);
10096
10363
  const alias = workspace.display(resolved);
10097
10364
  const tokens2 = estimateTokens((await readFile13(resolved, "utf8")).slice(0, MAX_SOURCE_CHARS));
10098
- const list2 = sources(session);
10099
- const existing = list2.find((source2) => source2.path === alias);
10365
+ const list = sources(session);
10366
+ const existing = list.find((source2) => source2.path === alias);
10100
10367
  if (existing) {
10101
10368
  existing.state = "pinned";
10102
10369
  existing.tokens = tokens2;
10103
10370
  existing.addedAt = (/* @__PURE__ */ new Date()).toISOString();
10104
10371
  return existing;
10105
10372
  }
10106
- if (list2.length >= MAX_CONTEXT_SOURCES) {
10373
+ if (list.length >= MAX_CONTEXT_SOURCES) {
10107
10374
  throw new Error(`Context source limit reached (${MAX_CONTEXT_SOURCES}); unpin something first.`);
10108
10375
  }
10109
10376
  if (info.size > 4e6) {
@@ -10115,31 +10382,31 @@ async function pinContextSource(session, workspace, requested) {
10115
10382
  tokens: tokens2,
10116
10383
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
10117
10384
  };
10118
- list2.push(source);
10385
+ list.push(source);
10119
10386
  return source;
10120
10387
  }
10121
10388
  function unpinContextSource(session, requested) {
10122
- const list2 = session.contextSources;
10123
- if (!list2?.length) return void 0;
10124
- const match = matchSource(list2, requested);
10389
+ const list = session.contextSources;
10390
+ if (!list?.length) return void 0;
10391
+ const match = matchSource(list, requested);
10125
10392
  if (!match) return void 0;
10126
- list2.splice(list2.indexOf(match), 1);
10393
+ list.splice(list.indexOf(match), 1);
10127
10394
  return match.path;
10128
10395
  }
10129
10396
  function toggleMuteContextSource(session, requested) {
10130
- const list2 = session.contextSources;
10131
- if (!list2?.length) return void 0;
10132
- const match = matchSource(list2, requested);
10397
+ const list = session.contextSources;
10398
+ if (!list?.length) return void 0;
10399
+ const match = matchSource(list, requested);
10133
10400
  if (!match) return void 0;
10134
10401
  match.state = match.state === "muted" ? "pinned" : "muted";
10135
10402
  return match;
10136
10403
  }
10137
10404
  async function resolvePinnedContent(session, workspace) {
10138
- const list2 = session.contextSources;
10139
- if (!list2?.length) return [];
10405
+ const list = session.contextSources;
10406
+ if (!list?.length) return [];
10140
10407
  const resolved = [];
10141
10408
  let remaining = MAX_PINNED_CHARS;
10142
- for (const source of list2) {
10409
+ for (const source of list) {
10143
10410
  if (source.state !== "pinned") continue;
10144
10411
  if (remaining <= 0) break;
10145
10412
  try {
@@ -10171,10 +10438,10 @@ These files were explicitly pinned by the user and are re-read from disk each tu
10171
10438
  ${blocks.join("\n\n")}
10172
10439
  </pinned-context>`;
10173
10440
  }
10174
- function matchSource(list2, requested) {
10441
+ function matchSource(list, requested) {
10175
10442
  const trimmed = requested.trim();
10176
10443
  if (!trimmed) return void 0;
10177
- return list2.find((source) => source.path === trimmed) ?? list2.find((source) => source.path.endsWith(`/${trimmed}`)) ?? list2.find((source) => source.path.includes(trimmed));
10444
+ return list.find((source) => source.path === trimmed) ?? list.find((source) => source.path.endsWith(`/${trimmed}`)) ?? list.find((source) => source.path.includes(trimmed));
10178
10445
  }
10179
10446
  function escapeAttribute5(value) {
10180
10447
  return value.replace(/[&"<>]/g, (character) => ({
@@ -10780,11 +11047,21 @@ var AgentRunner = class {
10780
11047
  await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
10781
11048
  }
10782
11049
  activeRunContract = contractEnabled ? this.session.taskContract : void 0;
11050
+ let verificationAttempted = false;
11051
+ const maxTurns = options.maxTurns ?? this.config.agent.maxTurns;
11052
+ const contextBudget = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
11053
+ if (this.contextManager.shouldCompact(this.session, contextBudget)) {
11054
+ const compacted = await this.compactContext(void 0, options.signal, "automatic");
11055
+ if (compacted.status === "compacted") {
11056
+ await emit({ type: "context_compacted", ...compacted });
11057
+ }
11058
+ }
10783
11059
  const promptSections = [
10784
11060
  `intent:${turnDirective.intent}`,
10785
11061
  ...workspaceRules ? ["rules"] : [],
10786
11062
  ...this.session.workingMemory ? ["working-memory"] : [],
10787
- ...contractEnabled ? ["task-contract"] : [],
11063
+ ...contractEnabled && !this.session.compactedThroughMessageId ? ["task-contract"] : [],
11064
+ ...this.session.compactedThroughMessageId ? ["compaction-facts"] : [],
10788
11065
  ...this.session.contextSummary ? ["session-summary"] : [],
10789
11066
  ...!trivialTurn ? [`context:${packed.engine}`] : [],
10790
11067
  ...packed.text ? [`code:${packed.engine}`] : [],
@@ -10792,13 +11069,6 @@ var AgentRunner = class {
10792
11069
  ...augmentation.skills?.length ? [`skills:${augmentation.skills.length}`] : [],
10793
11070
  ...augmentation.memoryCount ? [`memory:${augmentation.memoryCount}`] : []
10794
11071
  ];
10795
- let verificationAttempted = false;
10796
- const maxTurns = options.maxTurns ?? this.config.agent.maxTurns;
10797
- const contextBudget = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
10798
- if (this.contextManager.shouldCompact(this.session, contextBudget)) {
10799
- const compacted = await this.compactContext(void 0, options.signal);
10800
- await emit({ type: "context_compacted", ...compacted });
10801
- }
10802
11072
  for (let turn = 1; turn <= maxTurns; turn += 1) {
10803
11073
  throwIfAborted(options.signal);
10804
11074
  if (this.session.usage.inputTokens + this.session.usage.outputTokens >= this.config.agent.maxSessionTokens) {
@@ -10859,7 +11129,7 @@ var AgentRunner = class {
10859
11129
  breakdown
10860
11130
  });
10861
11131
  }
10862
- const assistantId = randomUUID12();
11132
+ const assistantId = randomUUID13();
10863
11133
  const response = await this.completeModel(
10864
11134
  messages,
10865
11135
  visibleTools,
@@ -11352,7 +11622,7 @@ ${completeContent}`;
11352
11622
  }
11353
11623
  appendAudit(event) {
11354
11624
  const audit = this.session.audit ?? (this.session.audit = []);
11355
- audit.push({ id: randomUUID12(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
11625
+ audit.push({ id: randomUUID13(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
11356
11626
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
11357
11627
  }
11358
11628
  async acceptChangedFiles(paths) {
@@ -11373,7 +11643,7 @@ ${completeContent}`;
11373
11643
  const results = [];
11374
11644
  for (const command2 of this.config.agent.verifyCommands) {
11375
11645
  const call = {
11376
- id: `verify-${randomUUID12()}`,
11646
+ id: `verify-${randomUUID13()}`,
11377
11647
  name: "shell",
11378
11648
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
11379
11649
  };
@@ -11399,13 +11669,25 @@ ${completeContent}`;
11399
11669
  return [];
11400
11670
  }
11401
11671
  }
11402
- async compactContext(instructions, signal) {
11672
+ async compactContext(instructions, signal, mode = "manual") {
11403
11673
  const result = await this.contextManager.compact(
11404
11674
  this.session,
11405
11675
  this.provider,
11406
11676
  signal,
11407
- instructions
11677
+ instructions,
11678
+ mode
11408
11679
  );
11680
+ if (result.status === "compacted") {
11681
+ recordTokenUsage(
11682
+ this.session,
11683
+ result.receipt.actual,
11684
+ result.receipt.estimated.inputTokens,
11685
+ result.receipt.estimated.outputTokens
11686
+ );
11687
+ }
11688
+ const receipts = this.session.contextCompactionReceipts ?? (this.session.contextCompactionReceipts = []);
11689
+ receipts.push(result.receipt);
11690
+ if (receipts.length > 64) receipts.splice(0, receipts.length - 64);
11409
11691
  await this.persist();
11410
11692
  return result;
11411
11693
  }
@@ -11497,7 +11779,7 @@ ${completeContent}`;
11497
11779
  };
11498
11780
  function message(role, content, extra = {}) {
11499
11781
  return {
11500
- id: randomUUID12(),
11782
+ id: randomUUID13(),
11501
11783
  role,
11502
11784
  content,
11503
11785
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11894,7 +12176,7 @@ function integer(value, fallback, min, max) {
11894
12176
  }
11895
12177
 
11896
12178
  // src/agent/delegation.ts
11897
- import { randomUUID as randomUUID14 } from "node:crypto";
12179
+ import { randomUUID as randomUUID15 } from "node:crypto";
11898
12180
  import { join as join18 } from "node:path";
11899
12181
  import { z as z19 } from "zod";
11900
12182
 
@@ -12023,7 +12305,7 @@ function numeric(...values) {
12023
12305
  }
12024
12306
 
12025
12307
  // src/agent/team-store.ts
12026
- import { createHash as createHash16, randomUUID as randomUUID13 } from "node:crypto";
12308
+ import { createHash as createHash16, randomUUID as randomUUID14 } from "node:crypto";
12027
12309
  import { lstat as lstat18, readFile as readFile17, readdir as readdir7, rm as rm3 } from "node:fs/promises";
12028
12310
  import { join as join16, resolve as resolve18 } from "node:path";
12029
12311
  import { z as z18 } from "zod";
@@ -12117,7 +12399,7 @@ var TeamRunStore = class {
12117
12399
  const now = (/* @__PURE__ */ new Date()).toISOString();
12118
12400
  const manifest = manifestSchema2.parse({
12119
12401
  version: 2,
12120
- id: randomUUID13(),
12402
+ id: randomUUID14(),
12121
12403
  workspace: this.workspace,
12122
12404
  objective: input2.objective,
12123
12405
  reviewer: input2.reviewer,
@@ -12923,7 +13205,7 @@ var DelegationManager = class {
12923
13205
  maxReviewRounds: 0
12924
13206
  });
12925
13207
  await emit?.({ type: "team_start", id: board.id, objective: task });
12926
- const writerId = randomUUID14();
13208
+ const writerId = randomUUID15();
12927
13209
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
12928
13210
  const draft = await this.writerLane.createDraft(
12929
13211
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -13293,7 +13575,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
13293
13575
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
13294
13576
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
13295
13577
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
13296
- const runId = board?.id ?? randomUUID14();
13578
+ const runId = board?.id ?? randomUUID15();
13297
13579
  await emit?.({ type: "team_start", id: runId, objective });
13298
13580
  try {
13299
13581
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -13351,7 +13633,7 @@ ${review.summary}`,
13351
13633
  }
13352
13634
  }
13353
13635
  async runBatch(runId, tasks, phase, emit, signal) {
13354
- const scheduled = tasks.map((task) => ({ id: randomUUID14(), task }));
13636
+ const scheduled = tasks.map((task) => ({ id: randomUUID15(), task }));
13355
13637
  for (const item of scheduled) {
13356
13638
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
13357
13639
  }
@@ -13460,12 +13742,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
13460
13742
  });
13461
13743
  }
13462
13744
  async peerMessage(runId, from, to, content, emit) {
13463
- const id = randomUUID14();
13745
+ const id = randomUUID15();
13464
13746
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
13465
13747
  await emit?.({ type: "agent_message", id, from, to, content });
13466
13748
  }
13467
13749
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
13468
- const id = scheduledId ?? randomUUID14();
13750
+ const id = scheduledId ?? randomUUID15();
13469
13751
  const profile = this.options.profiles.get(task.profile);
13470
13752
  const configuredRoute = this.team.routes?.[task.profile];
13471
13753
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -14532,6 +14814,7 @@ function sessionSummary(session) {
14532
14814
  changedFiles: session.changedFiles,
14533
14815
  ...session.lastRun ? { lastRun: session.lastRun } : {},
14534
14816
  ...session.tokenLedger?.length ? { tokenLedger: session.tokenLedger } : {},
14817
+ ...session.contextCompactionReceipts?.length ? { contextCompactionReceipts: session.contextCompactionReceipts } : {},
14535
14818
  usage: session.usage
14536
14819
  };
14537
14820
  }
@@ -15952,12 +16235,13 @@ function MeterBar({ segments, total, width, glyphs }) {
15952
16235
  function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact: compact2 = false, minimal = false, glyphMode = "auto" }) {
15953
16236
  const theme = useTheme();
15954
16237
  const glyphs = resolveGlyphs(glyphMode);
16238
+ const hasCompactedContext = status.compactedMessages > 0 || Boolean(summary);
15955
16239
  if (minimal) {
15956
16240
  const rowWidth2 = safeWidth(width);
15957
16241
  const padding = rowWidth2 >= 4 ? 2 : 0;
15958
16242
  const innerWidth2 = Math.max(1, rowWidth2 - padding);
15959
16243
  const active = `${status.messageCount} msg ${glyphs.separator} ~${formatTokens(status.activeTokens)} tok`;
15960
- const focus = sanitizeTerminalText(working?.focus || working?.goal || (summary ? "summary ready" : "not established")).replace(/\s+/g, " ").trim() || "not established";
16244
+ const focus = sanitizeTerminalText(working?.focus || working?.goal || (hasCompactedContext ? "handoff ready" : "not established")).replace(/\s+/g, " ").trim() || "not established";
15961
16245
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: padding, children: [
15962
16246
  /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, children: truncateDisplay(`Context ${formatPercent(status.pressure)} ${glyphs.separator} ${active}`, innerWidth2) }),
15963
16247
  /* @__PURE__ */ jsx(Text, { color: working ? theme.text : theme.muted, children: truncateDisplay(`working ${focus}`, innerWidth2) })
@@ -15966,7 +16250,7 @@ function ContextInspector({ status, working, summary, width, memory, connections
15966
16250
  const entries = [
15967
16251
  { label: "active", detail: `${status.messageCount} messages ${glyphs.separator} ~${formatTokens(status.activeTokens)} tokens ${glyphs.separator} tools ~${formatTokens(status.toolTokens)}` },
15968
16252
  { label: "short-term", detail: working ? `${working.focus || working.goal || "ready"} ${glyphs.separator} ${relativeTime(working.lastUpdatedAt)}` : "not established" },
15969
- { label: "summary", detail: summary ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted` : "not created" },
16253
+ { label: "summary", detail: hasCompactedContext ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted${summary ? "" : ` ${glyphs.separator} facts`}` : "not created" },
15970
16254
  { label: "long-term", detail: memory ?? `retrieved by relevance ${glyphs.separator} untrusted context` }
15971
16255
  ];
15972
16256
  if (!compact2 && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
@@ -15988,7 +16272,7 @@ function ContextInspector({ status, working, summary, width, memory, connections
15988
16272
  const rowWidth = safeWidth(width);
15989
16273
  const innerWidth = Math.max(1, rowWidth - 2);
15990
16274
  const pressureColor = status.pressure >= 0.9 ? theme.error : status.pressure >= 0.75 ? theme.warning : theme.accent;
15991
- const summaryTokens = summary ? status.summaryTokens : 0;
16275
+ const summaryTokens = hasCompactedContext ? status.summaryTokens : 0;
15992
16276
  const segments = [
15993
16277
  { label: "active", value: status.activeTokens, color: theme.accent },
15994
16278
  { label: "tools", value: status.toolTokens, color: theme.assistant },
@@ -17801,8 +18085,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17801
18085
  return true;
17802
18086
  }
17803
18087
  if (subcommand === "list") {
17804
- const list2 = runner.listContextSources();
17805
- appendList("Pinned context", list2.length ? list2.map((source) => ({
18088
+ const list = runner.listContextSources();
18089
+ appendList("Pinned context", list.length ? list.map((source) => ({
17806
18090
  label: `${source.state === "muted" ? "muted " : "pinned"} ${source.path}`,
17807
18091
  detail: `~${source.tokens} tokens${separator}added ${source.addedAt.slice(0, 10)}`,
17808
18092
  tone: source.state === "muted" ? "warning" : "success"