omk-agent-core 1.2.0 → 1.2.2

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 (65) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +8 -0
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +27 -24
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/agent-state.d.ts +9 -0
  7. package/dist/agent-state.d.ts.map +1 -0
  8. package/dist/agent-state.js +40 -0
  9. package/dist/agent-state.js.map +1 -0
  10. package/dist/agent.d.ts.map +1 -1
  11. package/dist/agent.js +4 -39
  12. package/dist/agent.js.map +1 -1
  13. package/dist/commit-conflicts.d.ts +4 -0
  14. package/dist/commit-conflicts.d.ts.map +1 -0
  15. package/dist/commit-conflicts.js +43 -0
  16. package/dist/commit-conflicts.js.map +1 -0
  17. package/dist/commit-graph.d.ts +5 -0
  18. package/dist/commit-graph.d.ts.map +1 -0
  19. package/dist/commit-graph.js +93 -0
  20. package/dist/commit-graph.js.map +1 -0
  21. package/dist/commit-input.d.ts +4 -0
  22. package/dist/commit-input.d.ts.map +1 -0
  23. package/dist/commit-input.js +105 -0
  24. package/dist/commit-input.js.map +1 -0
  25. package/dist/commit-planner.d.ts +4 -0
  26. package/dist/commit-planner.d.ts.map +1 -0
  27. package/dist/commit-planner.js +136 -0
  28. package/dist/commit-planner.js.map +1 -0
  29. package/dist/commit-types.d.ts +55 -0
  30. package/dist/commit-types.d.ts.map +1 -0
  31. package/dist/commit-types.js +7 -0
  32. package/dist/commit-types.js.map +1 -0
  33. package/dist/harness/compaction/compaction.d.ts.map +1 -1
  34. package/dist/harness/compaction/compaction.js +0 -11
  35. package/dist/harness/compaction/compaction.js.map +1 -1
  36. package/dist/harness/session/jsonl-labels.d.ts +4 -0
  37. package/dist/harness/session/jsonl-labels.d.ts.map +1 -0
  38. package/dist/harness/session/jsonl-labels.js +16 -0
  39. package/dist/harness/session/jsonl-labels.js.map +1 -0
  40. package/dist/harness/session/jsonl-storage.d.ts.map +1 -1
  41. package/dist/harness/session/jsonl-storage.js +23 -39
  42. package/dist/harness/session/jsonl-storage.js.map +1 -1
  43. package/dist/harness/session/session.d.ts.map +1 -1
  44. package/dist/harness/session/session.js +2 -1
  45. package/dist/harness/session/session.js.map +1 -1
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/tool-dag-deferred.d.ts +17 -0
  51. package/dist/tool-dag-deferred.d.ts.map +1 -0
  52. package/dist/tool-dag-deferred.js +17 -0
  53. package/dist/tool-dag-deferred.js.map +1 -0
  54. package/dist/tool-dag-ecraf-exchange.d.ts +13 -0
  55. package/dist/tool-dag-ecraf-exchange.d.ts.map +1 -0
  56. package/dist/tool-dag-ecraf-exchange.js +68 -0
  57. package/dist/tool-dag-ecraf-exchange.js.map +1 -0
  58. package/dist/tool-dag-memo.d.ts +1 -1
  59. package/dist/tool-dag-memo.d.ts.map +1 -1
  60. package/dist/tool-dag-memo.js +3 -1
  61. package/dist/tool-dag-memo.js.map +1 -1
  62. package/dist/tool-dag-scheduler.d.ts.map +1 -1
  63. package/dist/tool-dag-scheduler.js +3 -0
  64. package/dist/tool-dag-scheduler.js.map +1 -1
  65. package/package.json +2 -2
@@ -0,0 +1,105 @@
1
+ const MAX_ATOMS = 20_000;
2
+ const MAX_RELATIONS = 100_000;
3
+ const MAX_PATHS = 100_000;
4
+ const MAX_PACKAGES = 100_000;
5
+ const MAX_TEXT_UNITS = 8 * 1024 * 1024;
6
+ function record(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ throw new TypeError("Invalid commit plan object");
9
+ return value;
10
+ }
11
+ function text(value, budget, maxLength = 512) {
12
+ if (typeof value !== "string" || value.length > maxLength)
13
+ throw new TypeError("Invalid commit plan text");
14
+ budget.textUnits += value.length;
15
+ if (budget.textUnits > MAX_TEXT_UNITS)
16
+ throw new TypeError("Commit text limit exceeded");
17
+ if (!value.trim() || /[\u0000-\u001f\u007f]/u.test(value))
18
+ throw new TypeError("Invalid commit plan text");
19
+ try {
20
+ encodeURIComponent(value);
21
+ }
22
+ catch {
23
+ throw new TypeError("Invalid commit plan Unicode");
24
+ }
25
+ return value;
26
+ }
27
+ function flag(value) {
28
+ if (typeof value !== "boolean")
29
+ throw new TypeError("Invalid commit plan boolean");
30
+ return value;
31
+ }
32
+ function list(value, limit) {
33
+ if (!Array.isArray(value) || value.length > limit)
34
+ throw new TypeError("Invalid commit plan array or size");
35
+ return value;
36
+ }
37
+ function path(value, budget) {
38
+ const result = text(value, budget, 4096);
39
+ if (/[\\:]/u.test(result) ||
40
+ result.split("/").some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git"))
41
+ throw new TypeError("Unsupported relative path");
42
+ return result;
43
+ }
44
+ function atom(value, budget) {
45
+ const raw = record(value);
46
+ const paths = list(raw.paths, 256);
47
+ const packages = list(raw.packages, 256);
48
+ budget.paths += paths.length;
49
+ budget.packages += packages.length;
50
+ if (budget.paths > MAX_PATHS)
51
+ throw new TypeError("Commit path limit exceeded");
52
+ if (budget.packages > MAX_PACKAGES)
53
+ throw new TypeError("Commit package limit exceeded");
54
+ if (!paths.length)
55
+ throw new TypeError("Missing commit paths");
56
+ const provenance = text(raw.provenance, budget);
57
+ if (provenance !== "verified" && provenance !== "unknown" && provenance !== "foreign")
58
+ throw new TypeError("Invalid commit provenance");
59
+ return {
60
+ id: text(raw.id, budget),
61
+ repoId: text(raw.repoId, budget),
62
+ sessionId: text(raw.sessionId, budget),
63
+ worktreeId: text(raw.worktreeId, budget),
64
+ intentId: text(raw.intentId, budget),
65
+ paths: Array.from(paths, (entry) => path(entry, budget)),
66
+ packages: Array.from(packages, (entry) => text(entry, budget)),
67
+ provenance,
68
+ receiptId: raw.receiptId === null ? null : text(raw.receiptId, budget),
69
+ closureComplete: flag(raw.closureComplete),
70
+ settled: flag(raw.settled),
71
+ reviewRequired: flag(raw.reviewRequired),
72
+ baseBlobId: text(raw.baseBlobId, budget),
73
+ patchDigest: text(raw.patchDigest, budget),
74
+ };
75
+ }
76
+ /** Validate shape, not authenticity. Only the host observer can establish these facts. */
77
+ export function parseCommitPlannerInput(value) {
78
+ const raw = record(value);
79
+ const budget = { paths: 0, packages: 0, textUnits: 0 };
80
+ const header = {
81
+ policyVersion: text(raw.policyVersion, budget),
82
+ repoId: text(raw.repoId, budget),
83
+ worktreeId: text(raw.worktreeId, budget),
84
+ sessionId: text(raw.sessionId, budget),
85
+ baseCommit: text(raw.baseCommit, budget),
86
+ };
87
+ const atomValues = list(raw.atoms, MAX_ATOMS);
88
+ const relationValues = list(raw.relations, MAX_RELATIONS);
89
+ const atoms = Array.from(atomValues, (entry) => atom(entry, budget));
90
+ const ids = new Set(atoms.map((entry) => entry.id));
91
+ if (ids.size !== atoms.length)
92
+ throw new TypeError("Duplicate commit atom ID");
93
+ const relations = Array.from(relationValues, (value) => {
94
+ const edge = record(value);
95
+ const kind = text(edge.kind, budget);
96
+ if (kind !== "together" && kind !== "depends" && kind !== "separate")
97
+ throw new TypeError("Invalid commit relation kind");
98
+ const from = text(edge.from, budget), to = text(edge.to, budget);
99
+ if (!ids.has(from) || !ids.has(to))
100
+ throw new TypeError("Unknown commit relation node");
101
+ return { kind, from, to, evidenceRef: text(edge.evidenceRef, budget) };
102
+ });
103
+ return { ...header, atoms, relations };
104
+ }
105
+ //# sourceMappingURL=commit-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-input.js","sourceRoot":"","sources":["../src/commit-input.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChangeAtom, ChangeRelation, CommitPlannerInput } from \"./commit-types.ts\";\n\nconst MAX_ATOMS = 20_000;\nconst MAX_RELATIONS = 100_000;\nconst MAX_PATHS = 100_000;\nconst MAX_PACKAGES = 100_000;\nconst MAX_TEXT_UNITS = 8 * 1024 * 1024;\n\ninterface InputBudget {\n\tpaths: number;\n\tpackages: number;\n\ttextUnits: number;\n}\n\nfunction record(value: unknown): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new TypeError(\"Invalid commit plan object\");\n\treturn value as Record<string, unknown>;\n}\nfunction text(value: unknown, budget: InputBudget, maxLength = 512): string {\n\tif (typeof value !== \"string\" || value.length > maxLength) throw new TypeError(\"Invalid commit plan text\");\n\tbudget.textUnits += value.length;\n\tif (budget.textUnits > MAX_TEXT_UNITS) throw new TypeError(\"Commit text limit exceeded\");\n\tif (!value.trim() || /[\\u0000-\\u001f\\u007f]/u.test(value)) throw new TypeError(\"Invalid commit plan text\");\n\ttry {\n\t\tencodeURIComponent(value);\n\t} catch {\n\t\tthrow new TypeError(\"Invalid commit plan Unicode\");\n\t}\n\treturn value;\n}\nfunction flag(value: unknown): boolean {\n\tif (typeof value !== \"boolean\") throw new TypeError(\"Invalid commit plan boolean\");\n\treturn value;\n}\nfunction list(value: unknown, limit: number): readonly unknown[] {\n\tif (!Array.isArray(value) || value.length > limit) throw new TypeError(\"Invalid commit plan array or size\");\n\treturn value;\n}\nfunction path(value: unknown, budget: InputBudget): string {\n\tconst result = text(value, budget, 4096);\n\tif (\n\t\t/[\\\\:]/u.test(result) ||\n\t\tresult.split(\"/\").some((part) => !part || part === \".\" || part === \"..\" || part.toLowerCase() === \".git\")\n\t)\n\t\tthrow new TypeError(\"Unsupported relative path\");\n\treturn result;\n}\nfunction atom(value: unknown, budget: InputBudget): ChangeAtom {\n\tconst raw = record(value);\n\tconst paths = list(raw.paths, 256);\n\tconst packages = list(raw.packages, 256);\n\tbudget.paths += paths.length;\n\tbudget.packages += packages.length;\n\tif (budget.paths > MAX_PATHS) throw new TypeError(\"Commit path limit exceeded\");\n\tif (budget.packages > MAX_PACKAGES) throw new TypeError(\"Commit package limit exceeded\");\n\tif (!paths.length) throw new TypeError(\"Missing commit paths\");\n\tconst provenance = text(raw.provenance, budget);\n\tif (provenance !== \"verified\" && provenance !== \"unknown\" && provenance !== \"foreign\")\n\t\tthrow new TypeError(\"Invalid commit provenance\");\n\treturn {\n\t\tid: text(raw.id, budget),\n\t\trepoId: text(raw.repoId, budget),\n\t\tsessionId: text(raw.sessionId, budget),\n\t\tworktreeId: text(raw.worktreeId, budget),\n\t\tintentId: text(raw.intentId, budget),\n\t\tpaths: Array.from(paths, (entry) => path(entry, budget)),\n\t\tpackages: Array.from(packages, (entry) => text(entry, budget)),\n\t\tprovenance,\n\t\treceiptId: raw.receiptId === null ? null : text(raw.receiptId, budget),\n\t\tclosureComplete: flag(raw.closureComplete),\n\t\tsettled: flag(raw.settled),\n\t\treviewRequired: flag(raw.reviewRequired),\n\t\tbaseBlobId: text(raw.baseBlobId, budget),\n\t\tpatchDigest: text(raw.patchDigest, budget),\n\t};\n}\n\n/** Validate shape, not authenticity. Only the host observer can establish these facts. */\nexport function parseCommitPlannerInput(value: unknown): CommitPlannerInput {\n\tconst raw = record(value);\n\tconst budget: InputBudget = { paths: 0, packages: 0, textUnits: 0 };\n\tconst header = {\n\t\tpolicyVersion: text(raw.policyVersion, budget),\n\t\trepoId: text(raw.repoId, budget),\n\t\tworktreeId: text(raw.worktreeId, budget),\n\t\tsessionId: text(raw.sessionId, budget),\n\t\tbaseCommit: text(raw.baseCommit, budget),\n\t};\n\tconst atomValues = list(raw.atoms, MAX_ATOMS);\n\tconst relationValues = list(raw.relations, MAX_RELATIONS);\n\tconst atoms = Array.from(atomValues, (entry) => atom(entry, budget));\n\tconst ids = new Set(atoms.map((entry) => entry.id));\n\tif (ids.size !== atoms.length) throw new TypeError(\"Duplicate commit atom ID\");\n\tconst relations = Array.from(relationValues, (value): ChangeRelation => {\n\t\tconst edge = record(value);\n\t\tconst kind = text(edge.kind, budget);\n\t\tif (kind !== \"together\" && kind !== \"depends\" && kind !== \"separate\")\n\t\t\tthrow new TypeError(\"Invalid commit relation kind\");\n\t\tconst from = text(edge.from, budget),\n\t\t\tto = text(edge.to, budget);\n\t\tif (!ids.has(from) || !ids.has(to)) throw new TypeError(\"Unknown commit relation node\");\n\t\treturn { kind, from, to, evidenceRef: text(edge.evidenceRef, budget) };\n\t});\n\treturn { ...header, atoms, relations };\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { type CommitPlan } from "./commit-types.ts";
2
+ /** Read-only planning. No Git, filesystem, clock, receipt authentication or commit authorization. */
3
+ export declare function planAtomicCommits(value: unknown): CommitPlan;
4
+ //# sourceMappingURL=commit-planner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-planner.d.ts","sourceRoot":"","sources":["../src/commit-planner.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,UAAU,EAGf,MAAM,mBAAmB,CAAC;AAQ3B,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAwH5D","sourcesContent":["import { ambiguousAtoms } from \"./commit-conflicts.ts\";\nimport { stronglyConnected, topologicalOrder } from \"./commit-graph.ts\";\nimport { parseCommitPlannerInput } from \"./commit-input.ts\";\nimport {\n\ttype ChangeAtom,\n\ttype ChangeRelation,\n\ttype CommitGroup,\n\ttype CommitPlan,\n\tcompareIds,\n\tsortedUnique,\n} from \"./commit-types.ts\";\n\nfunction required<K, V>(map: ReadonlyMap<K, V>, key: K): V {\n\tconst value = map.get(key);\n\tif (value === undefined) throw new Error(\"Invalid normalized commit graph\");\n\treturn value;\n}\n\n/** Read-only planning. No Git, filesystem, clock, receipt authentication or commit authorization. */\nexport function planAtomicCommits(value: unknown): CommitPlan {\n\tconst input = parseCommitPlannerInput(value);\n\tconst atoms = [...input.atoms].sort((a, b) => compareIds(a.id, b.id));\n\tconst byId = new Map(atoms.map((atom) => [atom.id, atom]));\n\tconst relationMap = new Map<string, ChangeRelation>();\n\tfor (const relation of input.relations) {\n\t\tconst normalized =\n\t\t\trelation.kind !== \"depends\" && compareIds(relation.from, relation.to) > 0\n\t\t\t\t? { ...relation, from: relation.to, to: relation.from }\n\t\t\t\t: relation;\n\t\trelationMap.set(\n\t\t\tJSON.stringify([normalized.kind, normalized.from, normalized.to, normalized.evidenceRef]),\n\t\t\tnormalized,\n\t\t);\n\t}\n\tconst relations = [...relationMap].sort(([a], [b]) => compareIds(a, b)).map(([, edge]) => edge);\n\tconst adjacency = new Map(atoms.map((atom) => [atom.id, [] as string[]]));\n\tfor (const edge of relations) {\n\t\tif (edge.kind === \"separate\") continue;\n\t\trequired(adjacency, edge.from).push(edge.to);\n\t\tif (edge.kind === \"together\") required(adjacency, edge.to).push(edge.from);\n\t}\n\tfor (const [id, edges] of adjacency) adjacency.set(id, sortedUnique(edges));\n\tconst components = stronglyConnected(\n\t\tatoms.map((atom) => atom.id),\n\t\tadjacency,\n\t);\n\tconst groupOf = new Map<string, string>();\n\tconst members = new Map<string, readonly string[]>();\n\tfor (const ids of components) {\n\t\tconst id = `g:${ids[0]}`;\n\t\tmembers.set(id, ids);\n\t\tfor (const atom of ids) groupOf.set(atom, id);\n\t}\n\tconst prerequisites = new Map([...members.keys()].map((id) => [id, [] as string[]]));\n\tconst conflicts = new Set<string>();\n\tfor (const edge of relations) {\n\t\tconst from = required(groupOf, edge.from),\n\t\t\tto = required(groupOf, edge.to);\n\t\tif (edge.kind === \"separate\" && from === to) conflicts.add(from);\n\t\tif (edge.kind === \"depends\" && from !== to) required(prerequisites, from).push(to);\n\t}\n\tfor (const [id, deps] of prerequisites) prerequisites.set(id, sortedUnique(deps));\n\tconst order = topologicalOrder([...members.keys()], prerequisites);\n\tconst local = (atom: ChangeAtom) =>\n\t\tatom.repoId === input.repoId && atom.sessionId === input.sessionId && atom.worktreeId === input.worktreeId;\n\tconst selected = new Set<string>();\n\tconst stack = atoms.filter(local).map((atom) => required(groupOf, atom.id));\n\twhile (stack.length) {\n\t\tconst id = stack.pop();\n\t\tif (id === undefined || selected.has(id)) continue;\n\t\tselected.add(id);\n\t\tfor (const dep of required(prerequisites, id)) stack.push(dep);\n\t}\n\tconst ambiguous = ambiguousAtoms(atoms);\n\tconst groups = new Map<string, CommitGroup>();\n\tfor (const id of order.filter((id) => selected.has(id))) {\n\t\tconst atomIds = required(members, id);\n\t\tconst groupAtoms = atomIds.map((id) => required(byId, id));\n\t\tconst reasons = new Set<string>();\n\t\tfor (const atom of groupAtoms) {\n\t\t\tif (!local(atom) || atom.provenance === \"foreign\") reasons.add(\"FOREIGN_OWNERSHIP\");\n\t\t\tif (atom.provenance !== \"verified\" || !atom.receiptId) reasons.add(\"PROVENANCE_UNVERIFIED\");\n\t\t\tif (!atom.settled) reasons.add(\"WRITER_UNSETTLED\");\n\t\t\tif (!atom.closureComplete) reasons.add(\"DEPENDENCY_CLOSURE_INCOMPLETE\");\n\t\t\tif (ambiguous.has(atom.id)) reasons.add(\"AMBIGUOUS_FILE_OWNERSHIP\");\n\t\t}\n\t\tif (conflicts.has(id)) reasons.add(\"CONTRADICTORY_BOUNDARIES\");\n\t\tconst dependsOn = required(prerequisites, id);\n\t\tfor (const dep of dependsOn)\n\t\t\tif (required(groups, dep).status !== \"candidate\") reasons.add(\"PREREQUISITE_NOT_ADMISSIBLE\");\n\t\tconst intentIds = sortedUnique(groupAtoms.map((atom) => atom.intentId));\n\t\tconst intents = new Set(intentIds);\n\t\tconst crossIntent =\n\t\t\tintentIds.length > 1 ||\n\t\t\tdependsOn.some((dep) => required(groups, dep).intentIds.some((intent) => !intents.has(intent)));\n\t\tlet status: CommitGroup[\"status\"] = reasons.size ? \"blocked\" : \"candidate\";\n\t\tif (status === \"candidate\" && (crossIntent || groupAtoms.some((atom) => atom.reviewRequired))) {\n\t\t\tstatus = \"review\";\n\t\t\tif (crossIntent) reasons.add(\"CROSS_INTENT_CLOSURE\");\n\t\t\tif (groupAtoms.some((atom) => atom.reviewRequired)) reasons.add(\"EXPLICIT_REVIEW_REQUIRED\");\n\t\t}\n\t\tgroups.set(\n\t\t\tid,\n\t\t\tObject.freeze({\n\t\t\t\tid,\n\t\t\t\tatomIds: Object.freeze([...atomIds]),\n\t\t\t\tpaths: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.paths))),\n\t\t\t\tpackages: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.packages))),\n\t\t\t\tintentIds: Object.freeze(intentIds),\n\t\t\t\tdependsOn: Object.freeze([...dependsOn]),\n\t\t\t\tstatus,\n\t\t\t\treasons: Object.freeze([...reasons].sort(compareIds)),\n\t\t\t}),\n\t\t);\n\t}\n\tconst result = [...groups.values()];\n\tconst canonicalInput = JSON.stringify({\n\t\tschemaVersion: \"omk.atomic-commit-plan.v1\",\n\t\tpolicyVersion: input.policyVersion,\n\t\trepoId: input.repoId,\n\t\tworktreeId: input.worktreeId,\n\t\tsessionId: input.sessionId,\n\t\tbaseCommit: input.baseCommit,\n\t\tatoms: atoms.map((atom) => ({ ...atom, paths: sortedUnique(atom.paths), packages: sortedUnique(atom.packages) })),\n\t\trelations: relations.map((edge) => ({\n\t\t\tkind: edge.kind,\n\t\t\tfrom: edge.from,\n\t\t\tto: edge.to,\n\t\t\tevidenceRef: edge.evidenceRef,\n\t\t})),\n\t});\n\treturn Object.freeze({\n\t\tcanonicalInput,\n\t\tgroups: Object.freeze(result),\n\t\tvalidationOrder: Object.freeze(result.filter((group) => group.status === \"candidate\").map((group) => group.id)),\n\t\tunrelatedAtomIds: Object.freeze(\n\t\t\tatoms.filter((atom) => !selected.has(required(groupOf, atom.id))).map((atom) => atom.id),\n\t\t),\n\t});\n}\n"]}
@@ -0,0 +1,136 @@
1
+ import { ambiguousAtoms } from "./commit-conflicts.js";
2
+ import { stronglyConnected, topologicalOrder } from "./commit-graph.js";
3
+ import { parseCommitPlannerInput } from "./commit-input.js";
4
+ import { compareIds, sortedUnique, } from "./commit-types.js";
5
+ function required(map, key) {
6
+ const value = map.get(key);
7
+ if (value === undefined)
8
+ throw new Error("Invalid normalized commit graph");
9
+ return value;
10
+ }
11
+ /** Read-only planning. No Git, filesystem, clock, receipt authentication or commit authorization. */
12
+ export function planAtomicCommits(value) {
13
+ const input = parseCommitPlannerInput(value);
14
+ const atoms = [...input.atoms].sort((a, b) => compareIds(a.id, b.id));
15
+ const byId = new Map(atoms.map((atom) => [atom.id, atom]));
16
+ const relationMap = new Map();
17
+ for (const relation of input.relations) {
18
+ const normalized = relation.kind !== "depends" && compareIds(relation.from, relation.to) > 0
19
+ ? { ...relation, from: relation.to, to: relation.from }
20
+ : relation;
21
+ relationMap.set(JSON.stringify([normalized.kind, normalized.from, normalized.to, normalized.evidenceRef]), normalized);
22
+ }
23
+ const relations = [...relationMap].sort(([a], [b]) => compareIds(a, b)).map(([, edge]) => edge);
24
+ const adjacency = new Map(atoms.map((atom) => [atom.id, []]));
25
+ for (const edge of relations) {
26
+ if (edge.kind === "separate")
27
+ continue;
28
+ required(adjacency, edge.from).push(edge.to);
29
+ if (edge.kind === "together")
30
+ required(adjacency, edge.to).push(edge.from);
31
+ }
32
+ for (const [id, edges] of adjacency)
33
+ adjacency.set(id, sortedUnique(edges));
34
+ const components = stronglyConnected(atoms.map((atom) => atom.id), adjacency);
35
+ const groupOf = new Map();
36
+ const members = new Map();
37
+ for (const ids of components) {
38
+ const id = `g:${ids[0]}`;
39
+ members.set(id, ids);
40
+ for (const atom of ids)
41
+ groupOf.set(atom, id);
42
+ }
43
+ const prerequisites = new Map([...members.keys()].map((id) => [id, []]));
44
+ const conflicts = new Set();
45
+ for (const edge of relations) {
46
+ const from = required(groupOf, edge.from), to = required(groupOf, edge.to);
47
+ if (edge.kind === "separate" && from === to)
48
+ conflicts.add(from);
49
+ if (edge.kind === "depends" && from !== to)
50
+ required(prerequisites, from).push(to);
51
+ }
52
+ for (const [id, deps] of prerequisites)
53
+ prerequisites.set(id, sortedUnique(deps));
54
+ const order = topologicalOrder([...members.keys()], prerequisites);
55
+ const local = (atom) => atom.repoId === input.repoId && atom.sessionId === input.sessionId && atom.worktreeId === input.worktreeId;
56
+ const selected = new Set();
57
+ const stack = atoms.filter(local).map((atom) => required(groupOf, atom.id));
58
+ while (stack.length) {
59
+ const id = stack.pop();
60
+ if (id === undefined || selected.has(id))
61
+ continue;
62
+ selected.add(id);
63
+ for (const dep of required(prerequisites, id))
64
+ stack.push(dep);
65
+ }
66
+ const ambiguous = ambiguousAtoms(atoms);
67
+ const groups = new Map();
68
+ for (const id of order.filter((id) => selected.has(id))) {
69
+ const atomIds = required(members, id);
70
+ const groupAtoms = atomIds.map((id) => required(byId, id));
71
+ const reasons = new Set();
72
+ for (const atom of groupAtoms) {
73
+ if (!local(atom) || atom.provenance === "foreign")
74
+ reasons.add("FOREIGN_OWNERSHIP");
75
+ if (atom.provenance !== "verified" || !atom.receiptId)
76
+ reasons.add("PROVENANCE_UNVERIFIED");
77
+ if (!atom.settled)
78
+ reasons.add("WRITER_UNSETTLED");
79
+ if (!atom.closureComplete)
80
+ reasons.add("DEPENDENCY_CLOSURE_INCOMPLETE");
81
+ if (ambiguous.has(atom.id))
82
+ reasons.add("AMBIGUOUS_FILE_OWNERSHIP");
83
+ }
84
+ if (conflicts.has(id))
85
+ reasons.add("CONTRADICTORY_BOUNDARIES");
86
+ const dependsOn = required(prerequisites, id);
87
+ for (const dep of dependsOn)
88
+ if (required(groups, dep).status !== "candidate")
89
+ reasons.add("PREREQUISITE_NOT_ADMISSIBLE");
90
+ const intentIds = sortedUnique(groupAtoms.map((atom) => atom.intentId));
91
+ const intents = new Set(intentIds);
92
+ const crossIntent = intentIds.length > 1 ||
93
+ dependsOn.some((dep) => required(groups, dep).intentIds.some((intent) => !intents.has(intent)));
94
+ let status = reasons.size ? "blocked" : "candidate";
95
+ if (status === "candidate" && (crossIntent || groupAtoms.some((atom) => atom.reviewRequired))) {
96
+ status = "review";
97
+ if (crossIntent)
98
+ reasons.add("CROSS_INTENT_CLOSURE");
99
+ if (groupAtoms.some((atom) => atom.reviewRequired))
100
+ reasons.add("EXPLICIT_REVIEW_REQUIRED");
101
+ }
102
+ groups.set(id, Object.freeze({
103
+ id,
104
+ atomIds: Object.freeze([...atomIds]),
105
+ paths: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.paths))),
106
+ packages: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.packages))),
107
+ intentIds: Object.freeze(intentIds),
108
+ dependsOn: Object.freeze([...dependsOn]),
109
+ status,
110
+ reasons: Object.freeze([...reasons].sort(compareIds)),
111
+ }));
112
+ }
113
+ const result = [...groups.values()];
114
+ const canonicalInput = JSON.stringify({
115
+ schemaVersion: "omk.atomic-commit-plan.v1",
116
+ policyVersion: input.policyVersion,
117
+ repoId: input.repoId,
118
+ worktreeId: input.worktreeId,
119
+ sessionId: input.sessionId,
120
+ baseCommit: input.baseCommit,
121
+ atoms: atoms.map((atom) => ({ ...atom, paths: sortedUnique(atom.paths), packages: sortedUnique(atom.packages) })),
122
+ relations: relations.map((edge) => ({
123
+ kind: edge.kind,
124
+ from: edge.from,
125
+ to: edge.to,
126
+ evidenceRef: edge.evidenceRef,
127
+ })),
128
+ });
129
+ return Object.freeze({
130
+ canonicalInput,
131
+ groups: Object.freeze(result),
132
+ validationOrder: Object.freeze(result.filter((group) => group.status === "candidate").map((group) => group.id)),
133
+ unrelatedAtomIds: Object.freeze(atoms.filter((atom) => !selected.has(required(groupOf, atom.id))).map((atom) => atom.id)),
134
+ });
135
+ }
136
+ //# sourceMappingURL=commit-planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-planner.js","sourceRoot":"","sources":["../src/commit-planner.ts"],"names":[],"mappings":"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","sourcesContent":["import { ambiguousAtoms } from \"./commit-conflicts.ts\";\nimport { stronglyConnected, topologicalOrder } from \"./commit-graph.ts\";\nimport { parseCommitPlannerInput } from \"./commit-input.ts\";\nimport {\n\ttype ChangeAtom,\n\ttype ChangeRelation,\n\ttype CommitGroup,\n\ttype CommitPlan,\n\tcompareIds,\n\tsortedUnique,\n} from \"./commit-types.ts\";\n\nfunction required<K, V>(map: ReadonlyMap<K, V>, key: K): V {\n\tconst value = map.get(key);\n\tif (value === undefined) throw new Error(\"Invalid normalized commit graph\");\n\treturn value;\n}\n\n/** Read-only planning. No Git, filesystem, clock, receipt authentication or commit authorization. */\nexport function planAtomicCommits(value: unknown): CommitPlan {\n\tconst input = parseCommitPlannerInput(value);\n\tconst atoms = [...input.atoms].sort((a, b) => compareIds(a.id, b.id));\n\tconst byId = new Map(atoms.map((atom) => [atom.id, atom]));\n\tconst relationMap = new Map<string, ChangeRelation>();\n\tfor (const relation of input.relations) {\n\t\tconst normalized =\n\t\t\trelation.kind !== \"depends\" && compareIds(relation.from, relation.to) > 0\n\t\t\t\t? { ...relation, from: relation.to, to: relation.from }\n\t\t\t\t: relation;\n\t\trelationMap.set(\n\t\t\tJSON.stringify([normalized.kind, normalized.from, normalized.to, normalized.evidenceRef]),\n\t\t\tnormalized,\n\t\t);\n\t}\n\tconst relations = [...relationMap].sort(([a], [b]) => compareIds(a, b)).map(([, edge]) => edge);\n\tconst adjacency = new Map(atoms.map((atom) => [atom.id, [] as string[]]));\n\tfor (const edge of relations) {\n\t\tif (edge.kind === \"separate\") continue;\n\t\trequired(adjacency, edge.from).push(edge.to);\n\t\tif (edge.kind === \"together\") required(adjacency, edge.to).push(edge.from);\n\t}\n\tfor (const [id, edges] of adjacency) adjacency.set(id, sortedUnique(edges));\n\tconst components = stronglyConnected(\n\t\tatoms.map((atom) => atom.id),\n\t\tadjacency,\n\t);\n\tconst groupOf = new Map<string, string>();\n\tconst members = new Map<string, readonly string[]>();\n\tfor (const ids of components) {\n\t\tconst id = `g:${ids[0]}`;\n\t\tmembers.set(id, ids);\n\t\tfor (const atom of ids) groupOf.set(atom, id);\n\t}\n\tconst prerequisites = new Map([...members.keys()].map((id) => [id, [] as string[]]));\n\tconst conflicts = new Set<string>();\n\tfor (const edge of relations) {\n\t\tconst from = required(groupOf, edge.from),\n\t\t\tto = required(groupOf, edge.to);\n\t\tif (edge.kind === \"separate\" && from === to) conflicts.add(from);\n\t\tif (edge.kind === \"depends\" && from !== to) required(prerequisites, from).push(to);\n\t}\n\tfor (const [id, deps] of prerequisites) prerequisites.set(id, sortedUnique(deps));\n\tconst order = topologicalOrder([...members.keys()], prerequisites);\n\tconst local = (atom: ChangeAtom) =>\n\t\tatom.repoId === input.repoId && atom.sessionId === input.sessionId && atom.worktreeId === input.worktreeId;\n\tconst selected = new Set<string>();\n\tconst stack = atoms.filter(local).map((atom) => required(groupOf, atom.id));\n\twhile (stack.length) {\n\t\tconst id = stack.pop();\n\t\tif (id === undefined || selected.has(id)) continue;\n\t\tselected.add(id);\n\t\tfor (const dep of required(prerequisites, id)) stack.push(dep);\n\t}\n\tconst ambiguous = ambiguousAtoms(atoms);\n\tconst groups = new Map<string, CommitGroup>();\n\tfor (const id of order.filter((id) => selected.has(id))) {\n\t\tconst atomIds = required(members, id);\n\t\tconst groupAtoms = atomIds.map((id) => required(byId, id));\n\t\tconst reasons = new Set<string>();\n\t\tfor (const atom of groupAtoms) {\n\t\t\tif (!local(atom) || atom.provenance === \"foreign\") reasons.add(\"FOREIGN_OWNERSHIP\");\n\t\t\tif (atom.provenance !== \"verified\" || !atom.receiptId) reasons.add(\"PROVENANCE_UNVERIFIED\");\n\t\t\tif (!atom.settled) reasons.add(\"WRITER_UNSETTLED\");\n\t\t\tif (!atom.closureComplete) reasons.add(\"DEPENDENCY_CLOSURE_INCOMPLETE\");\n\t\t\tif (ambiguous.has(atom.id)) reasons.add(\"AMBIGUOUS_FILE_OWNERSHIP\");\n\t\t}\n\t\tif (conflicts.has(id)) reasons.add(\"CONTRADICTORY_BOUNDARIES\");\n\t\tconst dependsOn = required(prerequisites, id);\n\t\tfor (const dep of dependsOn)\n\t\t\tif (required(groups, dep).status !== \"candidate\") reasons.add(\"PREREQUISITE_NOT_ADMISSIBLE\");\n\t\tconst intentIds = sortedUnique(groupAtoms.map((atom) => atom.intentId));\n\t\tconst intents = new Set(intentIds);\n\t\tconst crossIntent =\n\t\t\tintentIds.length > 1 ||\n\t\t\tdependsOn.some((dep) => required(groups, dep).intentIds.some((intent) => !intents.has(intent)));\n\t\tlet status: CommitGroup[\"status\"] = reasons.size ? \"blocked\" : \"candidate\";\n\t\tif (status === \"candidate\" && (crossIntent || groupAtoms.some((atom) => atom.reviewRequired))) {\n\t\t\tstatus = \"review\";\n\t\t\tif (crossIntent) reasons.add(\"CROSS_INTENT_CLOSURE\");\n\t\t\tif (groupAtoms.some((atom) => atom.reviewRequired)) reasons.add(\"EXPLICIT_REVIEW_REQUIRED\");\n\t\t}\n\t\tgroups.set(\n\t\t\tid,\n\t\t\tObject.freeze({\n\t\t\t\tid,\n\t\t\t\tatomIds: Object.freeze([...atomIds]),\n\t\t\t\tpaths: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.paths))),\n\t\t\t\tpackages: Object.freeze(sortedUnique(groupAtoms.flatMap((atom) => atom.packages))),\n\t\t\t\tintentIds: Object.freeze(intentIds),\n\t\t\t\tdependsOn: Object.freeze([...dependsOn]),\n\t\t\t\tstatus,\n\t\t\t\treasons: Object.freeze([...reasons].sort(compareIds)),\n\t\t\t}),\n\t\t);\n\t}\n\tconst result = [...groups.values()];\n\tconst canonicalInput = JSON.stringify({\n\t\tschemaVersion: \"omk.atomic-commit-plan.v1\",\n\t\tpolicyVersion: input.policyVersion,\n\t\trepoId: input.repoId,\n\t\tworktreeId: input.worktreeId,\n\t\tsessionId: input.sessionId,\n\t\tbaseCommit: input.baseCommit,\n\t\tatoms: atoms.map((atom) => ({ ...atom, paths: sortedUnique(atom.paths), packages: sortedUnique(atom.packages) })),\n\t\trelations: relations.map((edge) => ({\n\t\t\tkind: edge.kind,\n\t\t\tfrom: edge.from,\n\t\t\tto: edge.to,\n\t\t\tevidenceRef: edge.evidenceRef,\n\t\t})),\n\t});\n\treturn Object.freeze({\n\t\tcanonicalInput,\n\t\tgroups: Object.freeze(result),\n\t\tvalidationOrder: Object.freeze(result.filter((group) => group.status === \"candidate\").map((group) => group.id)),\n\t\tunrelatedAtomIds: Object.freeze(\n\t\t\tatoms.filter((atom) => !selected.has(required(groupOf, atom.id))).map((atom) => atom.id),\n\t\t),\n\t});\n}\n"]}
@@ -0,0 +1,55 @@
1
+ /** Facts supplied by a trusted edit-receipt adapter, never inferred from a dirty file or model claim. */
2
+ export interface ChangeAtom {
3
+ readonly id: string;
4
+ readonly repoId: string;
5
+ readonly sessionId: string;
6
+ readonly worktreeId: string;
7
+ readonly intentId: string;
8
+ /** File-level v1 atoms. Renames name both the old and new path. */
9
+ readonly paths: readonly string[];
10
+ readonly packages: readonly string[];
11
+ readonly provenance: "verified" | "unknown" | "foreign";
12
+ readonly receiptId: string | null;
13
+ readonly closureComplete: boolean;
14
+ readonly settled: boolean;
15
+ readonly reviewRequired: boolean;
16
+ readonly baseBlobId: string;
17
+ readonly patchDigest: string;
18
+ }
19
+ /** depends points from the dependent to its prerequisite; the other relations are symmetric. */
20
+ export interface ChangeRelation {
21
+ readonly kind: "together" | "depends" | "separate";
22
+ readonly from: string;
23
+ readonly to: string;
24
+ readonly evidenceRef: string;
25
+ }
26
+ export interface CommitPlannerInput {
27
+ readonly policyVersion: string;
28
+ readonly repoId: string;
29
+ readonly worktreeId: string;
30
+ readonly sessionId: string;
31
+ readonly baseCommit: string;
32
+ readonly atoms: readonly ChangeAtom[];
33
+ readonly relations: readonly ChangeRelation[];
34
+ }
35
+ export interface CommitGroup {
36
+ readonly id: string;
37
+ readonly atomIds: readonly string[];
38
+ readonly paths: readonly string[];
39
+ readonly packages: readonly string[];
40
+ readonly intentIds: readonly string[];
41
+ readonly dependsOn: readonly string[];
42
+ /** candidate only schedules snapshot validation; it never authorizes a commit. */
43
+ readonly status: "candidate" | "review" | "blocked";
44
+ readonly reasons: readonly string[];
45
+ }
46
+ export interface CommitPlan {
47
+ /** Canonical data binding, not a signed ownership or validation receipt. */
48
+ readonly canonicalInput: string;
49
+ readonly groups: readonly CommitGroup[];
50
+ readonly validationOrder: readonly string[];
51
+ readonly unrelatedAtomIds: readonly string[];
52
+ }
53
+ export declare function compareIds(a: string, b: string): number;
54
+ export declare function sortedUnique(values: readonly string[]): string[];
55
+ //# sourceMappingURL=commit-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-types.d.ts","sourceRoot":"","sources":["../src/commit-types.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,gGAAgG;AAChG,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,UAAU;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvD;AACD,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAEhE","sourcesContent":["/** Facts supplied by a trusted edit-receipt adapter, never inferred from a dirty file or model claim. */\nexport interface ChangeAtom {\n\treadonly id: string;\n\treadonly repoId: string;\n\treadonly sessionId: string;\n\treadonly worktreeId: string;\n\treadonly intentId: string;\n\t/** File-level v1 atoms. Renames name both the old and new path. */\n\treadonly paths: readonly string[];\n\treadonly packages: readonly string[];\n\treadonly provenance: \"verified\" | \"unknown\" | \"foreign\";\n\treadonly receiptId: string | null;\n\treadonly closureComplete: boolean;\n\treadonly settled: boolean;\n\treadonly reviewRequired: boolean;\n\treadonly baseBlobId: string;\n\treadonly patchDigest: string;\n}\n\n/** depends points from the dependent to its prerequisite; the other relations are symmetric. */\nexport interface ChangeRelation {\n\treadonly kind: \"together\" | \"depends\" | \"separate\";\n\treadonly from: string;\n\treadonly to: string;\n\treadonly evidenceRef: string;\n}\n\nexport interface CommitPlannerInput {\n\treadonly policyVersion: string;\n\treadonly repoId: string;\n\treadonly worktreeId: string;\n\treadonly sessionId: string;\n\treadonly baseCommit: string;\n\treadonly atoms: readonly ChangeAtom[];\n\treadonly relations: readonly ChangeRelation[];\n}\n\nexport interface CommitGroup {\n\treadonly id: string;\n\treadonly atomIds: readonly string[];\n\treadonly paths: readonly string[];\n\treadonly packages: readonly string[];\n\treadonly intentIds: readonly string[];\n\treadonly dependsOn: readonly string[];\n\t/** candidate only schedules snapshot validation; it never authorizes a commit. */\n\treadonly status: \"candidate\" | \"review\" | \"blocked\";\n\treadonly reasons: readonly string[];\n}\n\nexport interface CommitPlan {\n\t/** Canonical data binding, not a signed ownership or validation receipt. */\n\treadonly canonicalInput: string;\n\treadonly groups: readonly CommitGroup[];\n\treadonly validationOrder: readonly string[];\n\treadonly unrelatedAtomIds: readonly string[];\n}\n\nexport function compareIds(a: string, b: string): number {\n\treturn a < b ? -1 : a > b ? 1 : 0;\n}\nexport function sortedUnique(values: readonly string[]): string[] {\n\treturn [...new Set(values)].sort(compareIds);\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export function compareIds(a, b) {
2
+ return a < b ? -1 : a > b ? 1 : 0;
3
+ }
4
+ export function sortedUnique(values) {
5
+ return [...new Set(values)].sort(compareIds);
6
+ }
7
+ //# sourceMappingURL=commit-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-types.js","sourceRoot":"","sources":["../src/commit-types.ts"],"names":[],"mappings":"AAyDA,MAAM,UAAU,UAAU,CAAC,CAAS,EAAE,CAAS,EAAU;IACxD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAAA,CAClC;AACD,MAAM,UAAU,YAAY,CAAC,MAAyB,EAAY;IACjE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAAA,CAC7C","sourcesContent":["/** Facts supplied by a trusted edit-receipt adapter, never inferred from a dirty file or model claim. */\nexport interface ChangeAtom {\n\treadonly id: string;\n\treadonly repoId: string;\n\treadonly sessionId: string;\n\treadonly worktreeId: string;\n\treadonly intentId: string;\n\t/** File-level v1 atoms. Renames name both the old and new path. */\n\treadonly paths: readonly string[];\n\treadonly packages: readonly string[];\n\treadonly provenance: \"verified\" | \"unknown\" | \"foreign\";\n\treadonly receiptId: string | null;\n\treadonly closureComplete: boolean;\n\treadonly settled: boolean;\n\treadonly reviewRequired: boolean;\n\treadonly baseBlobId: string;\n\treadonly patchDigest: string;\n}\n\n/** depends points from the dependent to its prerequisite; the other relations are symmetric. */\nexport interface ChangeRelation {\n\treadonly kind: \"together\" | \"depends\" | \"separate\";\n\treadonly from: string;\n\treadonly to: string;\n\treadonly evidenceRef: string;\n}\n\nexport interface CommitPlannerInput {\n\treadonly policyVersion: string;\n\treadonly repoId: string;\n\treadonly worktreeId: string;\n\treadonly sessionId: string;\n\treadonly baseCommit: string;\n\treadonly atoms: readonly ChangeAtom[];\n\treadonly relations: readonly ChangeRelation[];\n}\n\nexport interface CommitGroup {\n\treadonly id: string;\n\treadonly atomIds: readonly string[];\n\treadonly paths: readonly string[];\n\treadonly packages: readonly string[];\n\treadonly intentIds: readonly string[];\n\treadonly dependsOn: readonly string[];\n\t/** candidate only schedules snapshot validation; it never authorizes a commit. */\n\treadonly status: \"candidate\" | \"review\" | \"blocked\";\n\treadonly reasons: readonly string[];\n}\n\nexport interface CommitPlan {\n\t/** Canonical data binding, not a signed ownership or validation receipt. */\n\treadonly canonicalInput: string;\n\treadonly groups: readonly CommitGroup[];\n\treadonly validationOrder: readonly string[];\n\treadonly unrelatedAtomIds: readonly string[];\n}\n\nexport function compareIds(a: string, b: string): number {\n\treturn a < b ? -1 : a > b ? 1 : 0;\n}\nexport function sortedUnique(values: readonly string[]): string[] {\n\treturn [...new Set(values)].sort(compareIds);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../../src/harness/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAssistantMessage,\n\tImageContent,\n\tMessage,\n\tModel,\n\tRetryCallbacks,\n\tRetryPolicy,\n\tTextContent,\n\tUsage,\n} from \"omk-ai\";\nimport { completeSimple, retryAssistantCall } from \"omk-ai\";\nimport type { AgentMessage, ThinkingLevel } from \"../../types.ts\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext } from \"../session/session.ts\";\nimport { type CompactionEntry, CompactionError, err, ok, type Result, type SessionTreeEntry } from \"../types.ts\";\nimport {\n\tSUMMARIZATION_PROMPT,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tTURN_PREFIX_SUMMARIZATION_PROMPT,\n\tUPDATE_SUMMARIZATION_PROMPT,\n} from \"./summarization-prompts.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tserializeConversation,\n} from \"./utils.ts\";\n\nexport { SUMMARIZATION_SYSTEM_PROMPT } from \"./summarization-prompts.ts\";\n\n/** File-operation details stored on generated compaction entries. */\nexport interface CompactionDetails {\n\t/** Files read in the compacted history. */\n\treadFiles: string[];\n\t/** Files modified in the compacted history. */\n\tmodifiedFiles: string[];\n}\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionTreeEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\nfunction getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message as AgentMessage;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(\n\t\t\tentry.customType,\n\t\t\tentry.content as string | (TextContent | ImageContent)[],\n\t\t\tentry.display,\n\t\t\tentry.details,\n\t\t\tentry.timestamp,\n\t\t);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionTreeEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Generated compaction data ready to be persisted as a compaction entry. */\nexport interface CompactionResult<T = unknown> {\n\t/** Summary text that replaces compacted history in future context. */\n\tsummary: string;\n\t/** Entry id where retained history starts. */\n\tfirstKeptEntryId: string;\n\t/** Estimated context tokens before compaction. */\n\ttokensBefore: number;\n\t/** Optional implementation-specific details stored with the compaction entry. */\n\tdetails?: T;\n}\n\nexport interface SummarizationRetryOptions {\n\treadonly retry?: RetryPolicy;\n\treadonly callbacks?: RetryCallbacks;\n}\n\n/** Compaction thresholds and retention settings. */\nexport interface CompactionSettings {\n\t/** Enable automatic compaction decisions. */\n\tenabled: boolean;\n\t/** Tokens reserved for summary prompt and output. */\n\treserveTokens: number;\n\t/** Approximate recent-context tokens to keep after compaction. */\n\tkeepRecentTokens: number;\n\t/** Maximum fraction of the context window to use before compaction. Default: 0.9. */\n\tmaxUsageRatio?: number;\n}\n\n/** Default context-window ratio used before compaction triggers. */\nexport const DEFAULT_COMPACTION_MAX_USAGE_RATIO = 0.9;\n\n/** Default compaction settings used by the harness. */\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\tmaxUsageRatio: DEFAULT_COMPACTION_MAX_USAGE_RATIO,\n};\n\n/** Calculate total context tokens from provider usage. */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Return usage from the last successful assistant message in session entries. */\nexport function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message as AgentMessage);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Estimated context-token usage for a message list. */\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/** Estimate context tokens for messages using provider usage when available. */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/** Estimate context tokens after adding messages that have not been sent yet. */\nexport function estimateProjectedContextTokens(\n\tmessages: AgentMessage[],\n\tpendingMessages: AgentMessage[],\n): ContextUsageEstimate {\n\treturn estimateContextTokens([...messages, ...pendingMessages]);\n}\n\nexport type CompactionHeadroomLimit = \"max_usage_ratio\" | \"reserve_tokens\";\n\n/** Threshold calculation details for automatic compaction. */\nexport interface CompactionHeadroomThreshold {\n\t/** Context-token count that triggers compaction. */\n\ttriggerTokens: number;\n\t/** Tokens kept free at the trigger point. */\n\theadroomTokens: number;\n\t/** Boundary derived from the configured max usage ratio. */\n\tmaxUsageRatioTokens: number;\n\t/** Boundary derived from the absolute reserve token setting. */\n\treserveBoundaryTokens: number;\n\t/** Which boundary triggered earlier. */\n\tlimitedBy: CompactionHeadroomLimit;\n}\n\n/** Return the earliest compaction threshold from ratio-based headroom and absolute reserve. */\nexport function getCompactionHeadroomThreshold(\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n): CompactionHeadroomThreshold | undefined {\n\tif (!settings.enabled || !Number.isFinite(contextWindow) || contextWindow <= 0) {\n\t\treturn undefined;\n\t}\n\n\tconst windowTokens = Math.floor(contextWindow);\n\tconst reserveTokens = Number.isFinite(settings.reserveTokens) ? Math.max(0, Math.floor(settings.reserveTokens)) : 0;\n\tconst configuredMaxUsageRatio = settings.maxUsageRatio ?? DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatio =\n\t\tNumber.isFinite(configuredMaxUsageRatio) && configuredMaxUsageRatio > 0 && configuredMaxUsageRatio < 1\n\t\t\t? configuredMaxUsageRatio\n\t\t\t: DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatioTokens = Math.max(1, Math.floor(windowTokens * maxUsageRatio));\n\tconst reserveBoundaryTokens =\n\t\treserveTokens >= windowTokens ? maxUsageRatioTokens : Math.max(1, windowTokens - reserveTokens);\n\tconst triggerTokens = Math.min(maxUsageRatioTokens, reserveBoundaryTokens);\n\n\treturn {\n\t\ttriggerTokens,\n\t\theadroomTokens: windowTokens - triggerTokens,\n\t\tmaxUsageRatioTokens,\n\t\treserveBoundaryTokens,\n\t\tlimitedBy: reserveBoundaryTokens <= maxUsageRatioTokens ? \"reserve_tokens\" : \"max_usage_ratio\",\n\t};\n}\n\n/** Return whether context usage exceeds the configured compaction threshold. */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tconst threshold = getCompactionHeadroomThreshold(contextWindow, settings);\n\treturn threshold !== undefined && Number.isFinite(contextTokens) && contextTokens >= threshold.triggerTokens;\n}\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\nconst ESTIMATED_MESSAGE_OVERHEAD_TOKENS = 4;\nconst ESTIMATED_UNKNOWN_MESSAGE_TOKENS = 4;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/** Estimate token count for one message using a conservative character heuristic. */\nexport function estimateTokens(message: AgentMessage): number {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn (\n\t\t\t\tESTIMATED_MESSAGE_OVERHEAD_TOKENS +\n\t\t\t\tMath.ceil(\n\t\t\t\t\testimateTextAndImageContentChars(\n\t\t\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t\t\t) / 4,\n\t\t\t\t)\n\t\t\t);\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tlet chars = 0;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(estimateTextAndImageContentChars(message.content) / 4);\n\t\tcase \"bashExecution\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil((message.command.length + message.output.length) / 4);\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(message.summary.length / 4);\n\t\tdefault:\n\t\t\t// Keep unknown runtime extensions conservative: compaction may trigger\n\t\t\t// earlier, never after the previous estimate would have triggered it.\n\t\t\treturn ESTIMATED_UNKNOWN_MESSAGE_TOKENS;\n\t}\n}\nfunction findValidCutPoints(entries: SessionTreeEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"toolResult\":\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"thinking_level_change\":\n\t\t\tcase \"model_change\":\n\t\t\tcase \"active_tools_change\":\n\t\t\tcase \"compaction\":\n\t\t\tcase \"branch_summary\":\n\t\t\tcase \"custom\":\n\t\t\tcase \"custom_message\":\n\t\t\tcase \"label\":\n\t\t\tcase \"session_info\":\n\t\t\tcase \"leaf\":\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/** Find the user-visible message that starts the turn containing an entry. */\nexport function findTurnStartIndex(entries: SessionTreeEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\n/** Cut point selected for compaction. */\nexport interface CutPointResult {\n\t/** Index of the first entry retained after compaction. */\n\tfirstKeptEntryIndex: number;\n\t/** Index of the turn-start entry when the cut splits a turn, otherwise -1. */\n\tturnStartIndex: number;\n\t/** Whether the selected cut point splits an in-progress turn. */\n\tisSplitTurn: boolean;\n}\n\n/** Find the compaction cut point that keeps approximately the requested recent-token budget. */\nexport function findCutPoint(\n\tentries: SessionTreeEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0];\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst messageTokens = estimateTokens(entry.message as AgentMessage);\n\t\taccumulatedTokens += messageTokens;\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\tbreak;\n\t\t}\n\t\tcutIndex--;\n\t}\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n/** Generate or update a conversation summary for compaction. */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<string, CompactionError>> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.8 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t);\n\tlet basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;\n\tif (customInstructions) {\n\t\tbasePrompt = `${basePrompt}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\tconst llmMessages = convertToLlm(currentMessages);\n\tconst conversationText = serializeConversationBounded(llmMessages, model, maxTokens);\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\tpromptText += basePrompt;\n\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst completionOptions =\n\t\tmodel.reasoning && thinkingLevel && thinkingLevel !== \"off\"\n\t\t\t? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }\n\t\t\t: { maxTokens, signal, apiKey, headers };\n\n\tconst response = await retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteSimple(\n\t\t\t\tmodel,\n\t\t\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t\t\tcompletionOptions,\n\t\t\t),\n\t\tretryOptions?.retry,\n\t\tsignal,\n\t\tretryOptions?.callbacks,\n\t);\n\tif (response.stopReason === \"aborted\") {\n\t\treturn err(new CompactionError(\"aborted\", response.errorMessage || \"Summarization aborted\"));\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn err(\n\t\t\tnew CompactionError(\n\t\t\t\t\"summarization_failed\",\n\t\t\t\t`Summarization failed: ${response.errorMessage || \"Unknown error\"}`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn ok(textContent);\n}\n\n/** Prepared inputs for a compaction run. */\nexport interface CompactionPreparation {\n\t/** Entry id where retained history starts. */\n\tfirstKeptEntryId: string;\n\t/** Messages summarized into the history summary. */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Prefix messages summarized separately when compaction splits a turn. */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether compaction splits a turn. */\n\tisSplitTurn: boolean;\n\t/** Estimated context tokens before compaction. */\n\ttokensBefore: number;\n\t/** Previous compaction summary used for iterative updates. */\n\tpreviousSummary?: string;\n\t/** File operations extracted from summarized history. */\n\tfileOps: FileOperations;\n\t/** Settings used to prepare compaction. */\n\tsettings: CompactionSettings;\n}\n\n/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */\nexport function prepareCompaction(\n\tpathEntries: SessionTreeEntry[],\n\tsettings: CompactionSettings,\n): Result<CompactionPreparation | undefined, CompactionError> {\n\tif (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn ok(undefined);\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\tconst dropsHistoryEntries = cutPoint.firstKeptEntryIndex > boundaryStart;\n\tconst summarizesSplitTurnPrefix = cutPoint.isSplitTurn && cutPoint.firstKeptEntryIndex > cutPoint.turnStartIndex;\n\t// Invariant: normal compactions that drop at least one entry, or summarize a\n\t// split-turn prefix, are unchanged. Only true no-ops are skipped.\n\tif (!dropsHistoryEntries && !summarizesSplitTurnPrefix) {\n\t\treturn ok(undefined);\n\t}\n\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn err(new CompactionError(\"invalid_session\", \"First kept entry has no UUID - session may need migration\"));\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\treturn ok({\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t});\n}\n\nexport { serializeConversation } from \"./utils.ts\";\n\n/**\n * Rough chars-per-token estimate for bounding summarization input.\n * Conservative on purpose: serialized code/JSON runs below 4 chars/token.\n */\nconst SUMMARY_CHARS_PER_TOKEN = 3.5;\n/**\n * Headroom subtracted on top of the output reserve: the summarization system\n * prompt, the <conversation>/prompt wrapper, and tokenizer drift between the\n * chars-per-token estimate and the real tokenizer.\n */\nconst SUMMARY_INPUT_SAFETY_TOKENS = 2048;\n\nfunction summaryElisionMessage(omittedCount: number): Message {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: `[... ${omittedCount} earlier message(s) omitted to fit the summarization context window ...]`,\n\t\ttimestamp: Date.now(),\n\t} as Message;\n}\n\n/**\n * Serialize a conversation for summarization, bounded to the model's context\n * window. When the full serialization would exceed the window, middle\n * messages are dropped (keeping the goal-bearing head and the recent-work\n * tail) with an explicit elision marker; oversized single messages fall back\n * to hard text truncation keeping both ends.\n *\n * Without this bound, a session that already overflows the model cannot be\n * compacted: the summarization request itself exceeds the window, the\n * provider rejects it (e.g. Codex context_length_exceeded), and overflow\n * recovery is stuck permanently.\n */\nexport function serializeConversationBounded(messages: Message[], model: Model<any>, maxOutputTokens: number): string {\n\tconst serialized = serializeConversation(messages);\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return serialized;\n\tconst budgetTokens = contextWindow - maxOutputTokens - SUMMARY_INPUT_SAFETY_TOKENS;\n\tconst budgetChars = Math.floor(Math.max(0, budgetTokens) * SUMMARY_CHARS_PER_TOKEN);\n\tif (serialized.length <= budgetChars) return serialized;\n\t// Window too small to bound meaningfully; leave it to the provider error path.\n\tif (budgetChars < 4096) return serialized;\n\n\t// Halve the kept head/tail spans until the elided conversation fits.\n\tfor (let keep = Math.floor(messages.length / 2); keep >= 1; keep = Math.floor(keep / 2)) {\n\t\tconst head = messages.slice(0, keep);\n\t\tconst tail = messages.slice(messages.length - keep);\n\t\tconst omitted = messages.length - head.length - tail.length;\n\t\tif (omitted <= 0) continue;\n\t\tconst bounded = serializeConversation([...head, summaryElisionMessage(omitted), ...tail]);\n\t\tif (bounded.length <= budgetChars) return bounded;\n\t}\n\n\t// Single oversized message(s): truncate the text itself, keeping both ends.\n\tconst headChars = Math.floor(budgetChars / 2);\n\tconst tailChars = Math.max(0, budgetChars - headChars - 128);\n\treturn `${serialized.slice(0, headChars)}\\n[... middle omitted to fit the summarization context window ...]\\n${serialized.slice(serialized.length - tailChars)}`;\n}\n\n/** Generate compaction summary data from prepared session history. */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<CompactionResult, CompactionError>> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t} = preparation;\n\n\tif (!firstKeptEntryId) {\n\t\treturn err(new CompactionError(\"invalid_session\", \"First kept entry has no UUID - session may need migration\"));\n\t}\n\n\tlet summary: string;\n\n\tif (isSplitTurn && turnPrefixMessages.length > 0) {\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\tmessagesToSummarize.length > 0\n\t\t\t\t? generateSummary(\n\t\t\t\t\t\tmessagesToSummarize,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tsettings.reserveTokens,\n\t\t\t\t\t\tapiKey,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\tpreviousSummary,\n\t\t\t\t\t\tthinkingLevel,\n\t\t\t\t\t\tretryOptions,\n\t\t\t\t\t)\n\t\t\t\t: Promise.resolve(ok<string, CompactionError>(\"No prior history.\")),\n\t\t\tgenerateTurnPrefixSummary(\n\t\t\t\tturnPrefixMessages,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tthinkingLevel,\n\t\t\t\tretryOptions,\n\t\t\t),\n\t\t]);\n\t\tif (!historyResult.ok) return err(historyResult.error);\n\t\tif (!turnPrefixResult.ok) return err(turnPrefixResult.error);\n\t\tsummary = `${historyResult.value}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixResult.value}`;\n\t} else {\n\t\tconst summaryResult = await generateSummary(\n\t\t\tmessagesToSummarize,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t\tpreviousSummary,\n\t\t\tthinkingLevel,\n\t\t\tretryOptions,\n\t\t);\n\t\tif (!summaryResult.ok) return err(summaryResult.error);\n\t\tsummary = summaryResult.value;\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\treturn ok({\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t});\n}\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<string, CompactionError>> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t);\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversationBounded(llmMessages, model, maxTokens);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteSimple(\n\t\t\t\tmodel,\n\t\t\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t\t\tmodel.reasoning && thinkingLevel && thinkingLevel !== \"off\"\n\t\t\t\t\t? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }\n\t\t\t\t\t: { maxTokens, signal, apiKey, headers },\n\t\t\t),\n\t\tretryOptions?.retry,\n\t\tsignal,\n\t\tretryOptions?.callbacks,\n\t);\n\tif (response.stopReason === \"aborted\") {\n\t\treturn err(new CompactionError(\"aborted\", response.errorMessage || \"Turn prefix summarization aborted\"));\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn err(\n\t\t\tnew CompactionError(\n\t\t\t\t\"summarization_failed\",\n\t\t\t\t`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn ok(\n\t\tresponse.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\\n\"),\n\t);\n}\n"]}
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../../src/harness/compaction/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAssistantMessage,\n\tImageContent,\n\tMessage,\n\tModel,\n\tRetryCallbacks,\n\tRetryPolicy,\n\tTextContent,\n\tUsage,\n} from \"omk-ai\";\nimport { completeSimple, retryAssistantCall } from \"omk-ai\";\nimport type { AgentMessage, ThinkingLevel } from \"../../types.ts\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { buildSessionContext } from \"../session/session.ts\";\nimport { type CompactionEntry, CompactionError, err, ok, type Result, type SessionTreeEntry } from \"../types.ts\";\nimport {\n\tSUMMARIZATION_PROMPT,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tTURN_PREFIX_SUMMARIZATION_PROMPT,\n\tUPDATE_SUMMARIZATION_PROMPT,\n} from \"./summarization-prompts.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tserializeConversation,\n} from \"./utils.ts\";\n\nexport { SUMMARIZATION_SYSTEM_PROMPT } from \"./summarization-prompts.ts\";\n\n/** File-operation details stored on generated compaction entries. */\nexport interface CompactionDetails {\n\t/** Files read in the compacted history. */\n\treadFiles: string[];\n\t/** Files modified in the compacted history. */\n\tmodifiedFiles: string[];\n}\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction extractFileOperations(\n\tmessages: AgentMessage[],\n\tentries: SessionTreeEntry[],\n\tprevCompactionIndex: number,\n): FileOperations {\n\tconst fileOps = createFileOps();\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\tif (!prevCompaction.fromHook && prevCompaction.details) {\n\t\t\tconst details = prevCompaction.details as CompactionDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\tfor (const f of details.modifiedFiles) fileOps.edited.add(f);\n\t\t\t}\n\t\t}\n\t}\n\tfor (const msg of messages) {\n\t\textractFileOpsFromMessage(msg, fileOps);\n\t}\n\n\treturn fileOps;\n}\nfunction getMessageFromEntry(entry: SessionTreeEntry): AgentMessage | undefined {\n\tif (entry.type === \"message\") {\n\t\treturn entry.message as AgentMessage;\n\t}\n\tif (entry.type === \"custom_message\") {\n\t\treturn createCustomMessage(\n\t\t\tentry.customType,\n\t\t\tentry.content as string | (TextContent | ImageContent)[],\n\t\t\tentry.display,\n\t\t\tentry.details,\n\t\t\tentry.timestamp,\n\t\t);\n\t}\n\tif (entry.type === \"branch_summary\") {\n\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\t}\n\tif (entry.type === \"compaction\") {\n\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\t}\n\treturn undefined;\n}\n\nfunction getMessageFromEntryForCompaction(entry: SessionTreeEntry): AgentMessage | undefined {\n\tif (entry.type === \"compaction\") {\n\t\treturn undefined;\n\t}\n\treturn getMessageFromEntry(entry);\n}\n\n/** Generated compaction data ready to be persisted as a compaction entry. */\nexport interface CompactionResult<T = unknown> {\n\t/** Summary text that replaces compacted history in future context. */\n\tsummary: string;\n\t/** Entry id where retained history starts. */\n\tfirstKeptEntryId: string;\n\t/** Estimated context tokens before compaction. */\n\ttokensBefore: number;\n\t/** Optional implementation-specific details stored with the compaction entry. */\n\tdetails?: T;\n}\n\nexport interface SummarizationRetryOptions {\n\treadonly retry?: RetryPolicy;\n\treadonly callbacks?: RetryCallbacks;\n}\n\n/** Compaction thresholds and retention settings. */\nexport interface CompactionSettings {\n\t/** Enable automatic compaction decisions. */\n\tenabled: boolean;\n\t/** Tokens reserved for summary prompt and output. */\n\treserveTokens: number;\n\t/** Approximate recent-context tokens to keep after compaction. */\n\tkeepRecentTokens: number;\n\t/** Maximum fraction of the context window to use before compaction. Default: 0.9. */\n\tmaxUsageRatio?: number;\n}\n\n/** Default context-window ratio used before compaction triggers. */\nexport const DEFAULT_COMPACTION_MAX_USAGE_RATIO = 0.9;\n\n/** Default compaction settings used by the harness. */\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\tmaxUsageRatio: DEFAULT_COMPACTION_MAX_USAGE_RATIO,\n};\n\n/** Calculate total context tokens from provider usage. */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\nfunction getAssistantUsage(msg: AgentMessage): Usage | undefined {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Return usage from the last successful assistant message in session entries. */\nexport function getLastAssistantUsage(entries: SessionTreeEntry[]): Usage | undefined {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message as AgentMessage);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Estimated context-token usage for a message list. */\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nfunction getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]);\n\t\tif (usage) return { usage, index: i };\n\t}\n\treturn undefined;\n}\n\n/** Estimate context tokens for messages using provider usage when available. */\nexport function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\n\tif (!usageInfo) {\n\t\tlet estimated = 0;\n\t\tfor (const message of messages) {\n\t\t\testimated += estimateTokens(message);\n\t\t}\n\t\treturn {\n\t\t\ttokens: estimated,\n\t\t\tusageTokens: 0,\n\t\t\ttrailingTokens: estimated,\n\t\t\tlastUsageIndex: null,\n\t\t};\n\t}\n\n\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\tlet trailingTokens = 0;\n\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\ttrailingTokens += estimateTokens(messages[i]);\n\t}\n\n\treturn {\n\t\ttokens: usageTokens + trailingTokens,\n\t\tusageTokens,\n\t\ttrailingTokens,\n\t\tlastUsageIndex: usageInfo.index,\n\t};\n}\n\n/** Estimate context tokens after adding messages that have not been sent yet. */\nexport function estimateProjectedContextTokens(\n\tmessages: AgentMessage[],\n\tpendingMessages: AgentMessage[],\n): ContextUsageEstimate {\n\treturn estimateContextTokens([...messages, ...pendingMessages]);\n}\n\nexport type CompactionHeadroomLimit = \"max_usage_ratio\" | \"reserve_tokens\";\n\n/** Threshold calculation details for automatic compaction. */\nexport interface CompactionHeadroomThreshold {\n\t/** Context-token count that triggers compaction. */\n\ttriggerTokens: number;\n\t/** Tokens kept free at the trigger point. */\n\theadroomTokens: number;\n\t/** Boundary derived from the configured max usage ratio. */\n\tmaxUsageRatioTokens: number;\n\t/** Boundary derived from the absolute reserve token setting. */\n\treserveBoundaryTokens: number;\n\t/** Which boundary triggered earlier. */\n\tlimitedBy: CompactionHeadroomLimit;\n}\n\n/** Return the earliest compaction threshold from ratio-based headroom and absolute reserve. */\nexport function getCompactionHeadroomThreshold(\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n): CompactionHeadroomThreshold | undefined {\n\tif (!settings.enabled || !Number.isFinite(contextWindow) || contextWindow <= 0) {\n\t\treturn undefined;\n\t}\n\n\tconst windowTokens = Math.floor(contextWindow);\n\tconst reserveTokens = Number.isFinite(settings.reserveTokens) ? Math.max(0, Math.floor(settings.reserveTokens)) : 0;\n\tconst configuredMaxUsageRatio = settings.maxUsageRatio ?? DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatio =\n\t\tNumber.isFinite(configuredMaxUsageRatio) && configuredMaxUsageRatio > 0 && configuredMaxUsageRatio < 1\n\t\t\t? configuredMaxUsageRatio\n\t\t\t: DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatioTokens = Math.max(1, Math.floor(windowTokens * maxUsageRatio));\n\tconst reserveBoundaryTokens =\n\t\treserveTokens >= windowTokens ? maxUsageRatioTokens : Math.max(1, windowTokens - reserveTokens);\n\tconst triggerTokens = Math.min(maxUsageRatioTokens, reserveBoundaryTokens);\n\n\treturn {\n\t\ttriggerTokens,\n\t\theadroomTokens: windowTokens - triggerTokens,\n\t\tmaxUsageRatioTokens,\n\t\treserveBoundaryTokens,\n\t\tlimitedBy: reserveBoundaryTokens <= maxUsageRatioTokens ? \"reserve_tokens\" : \"max_usage_ratio\",\n\t};\n}\n\n/** Return whether context usage exceeds the configured compaction threshold. */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tconst threshold = getCompactionHeadroomThreshold(contextWindow, settings);\n\treturn threshold !== undefined && Number.isFinite(contextTokens) && contextTokens >= threshold.triggerTokens;\n}\n\nconst ESTIMATED_IMAGE_CHARS = 4800;\nconst ESTIMATED_MESSAGE_OVERHEAD_TOKENS = 4;\nconst ESTIMATED_UNKNOWN_MESSAGE_TOKENS = 4;\n\nfunction estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {\n\tif (typeof content === \"string\") {\n\t\treturn content.length;\n\t}\n\n\tlet chars = 0;\n\tfor (const block of content) {\n\t\tif (block.type === \"text\" && block.text) {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"image\") {\n\t\t\tchars += ESTIMATED_IMAGE_CHARS;\n\t\t}\n\t}\n\treturn chars;\n}\n\n/** Estimate token count for one message using a conservative character heuristic. */\nexport function estimateTokens(message: AgentMessage): number {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn (\n\t\t\t\tESTIMATED_MESSAGE_OVERHEAD_TOKENS +\n\t\t\t\tMath.ceil(\n\t\t\t\t\testimateTextAndImageContentChars(\n\t\t\t\t\t\t(message as { content: string | Array<{ type: string; text?: string }> }).content,\n\t\t\t\t\t) / 4,\n\t\t\t\t)\n\t\t\t);\n\t\tcase \"assistant\": {\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\tlet chars = 0;\n\t\t\tfor (const block of assistant.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\tchars += block.thinking.length;\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(chars / 4);\n\t\t}\n\t\tcase \"custom\":\n\t\tcase \"toolResult\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(estimateTextAndImageContentChars(message.content) / 4);\n\t\tcase \"bashExecution\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil((message.command.length + message.output.length) / 4);\n\t\tcase \"branchSummary\":\n\t\tcase \"compactionSummary\":\n\t\t\treturn ESTIMATED_MESSAGE_OVERHEAD_TOKENS + Math.ceil(message.summary.length / 4);\n\t\tdefault:\n\t\t\t// Keep unknown runtime extensions conservative: compaction may trigger\n\t\t\t// earlier, never after the previous estimate would have triggered it.\n\t\t\treturn ESTIMATED_UNKNOWN_MESSAGE_TOKENS;\n\t}\n}\nfunction findValidCutPoints(entries: SessionTreeEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tswitch (entry.type) {\n\t\t\tcase \"message\": {\n\t\t\t\tconst role = entry.message.role;\n\t\t\t\tswitch (role) {\n\t\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\tcase \"custom\":\n\t\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\tcase \"user\":\n\t\t\t\t\tcase \"assistant\":\n\t\t\t\t\t\tcutPoints.push(i);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/** Find the user-visible message that starts the turn containing an entry. */\nexport function findTurnStartIndex(entries: SessionTreeEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"branch_summary\" || entry.type === \"custom_message\") {\n\t\t\treturn i;\n\t\t}\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\n/** Cut point selected for compaction. */\nexport interface CutPointResult {\n\t/** Index of the first entry retained after compaction. */\n\tfirstKeptEntryIndex: number;\n\t/** Index of the turn-start entry when the cut splits a turn, otherwise -1. */\n\tturnStartIndex: number;\n\t/** Whether the selected cut point splits an in-progress turn. */\n\tisSplitTurn: boolean;\n}\n\n/** Find the compaction cut point that keeps approximately the requested recent-token budget. */\nexport function findCutPoint(\n\tentries: SessionTreeEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = cutPoints[0];\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst messageTokens = estimateTokens(entry.message as AgentMessage);\n\t\taccumulatedTokens += messageTokens;\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\tbreak;\n\t\t}\n\t\tcutIndex--;\n\t}\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n/** Generate or update a conversation summary for compaction. */\nexport async function generateSummary(\n\tcurrentMessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n\tpreviousSummary?: string,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<string, CompactionError>> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.8 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t);\n\tlet basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;\n\tif (customInstructions) {\n\t\tbasePrompt = `${basePrompt}\\n\\nAdditional focus: ${customInstructions}`;\n\t}\n\tconst llmMessages = convertToLlm(currentMessages);\n\tconst conversationText = serializeConversationBounded(llmMessages, model, maxTokens);\n\tlet promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n`;\n\tif (previousSummary) {\n\t\tpromptText += `<previous-summary>\\n${previousSummary}\\n</previous-summary>\\n\\n`;\n\t}\n\tpromptText += basePrompt;\n\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst completionOptions =\n\t\tmodel.reasoning && thinkingLevel && thinkingLevel !== \"off\"\n\t\t\t? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }\n\t\t\t: { maxTokens, signal, apiKey, headers };\n\n\tconst response = await retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteSimple(\n\t\t\t\tmodel,\n\t\t\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t\t\tcompletionOptions,\n\t\t\t),\n\t\tretryOptions?.retry,\n\t\tsignal,\n\t\tretryOptions?.callbacks,\n\t);\n\tif (response.stopReason === \"aborted\") {\n\t\treturn err(new CompactionError(\"aborted\", response.errorMessage || \"Summarization aborted\"));\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn err(\n\t\t\tnew CompactionError(\n\t\t\t\t\"summarization_failed\",\n\t\t\t\t`Summarization failed: ${response.errorMessage || \"Unknown error\"}`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn ok(textContent);\n}\n\n/** Prepared inputs for a compaction run. */\nexport interface CompactionPreparation {\n\t/** Entry id where retained history starts. */\n\tfirstKeptEntryId: string;\n\t/** Messages summarized into the history summary. */\n\tmessagesToSummarize: AgentMessage[];\n\t/** Prefix messages summarized separately when compaction splits a turn. */\n\tturnPrefixMessages: AgentMessage[];\n\t/** Whether compaction splits a turn. */\n\tisSplitTurn: boolean;\n\t/** Estimated context tokens before compaction. */\n\ttokensBefore: number;\n\t/** Previous compaction summary used for iterative updates. */\n\tpreviousSummary?: string;\n\t/** File operations extracted from summarized history. */\n\tfileOps: FileOperations;\n\t/** Settings used to prepare compaction. */\n\tsettings: CompactionSettings;\n}\n\n/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */\nexport function prepareCompaction(\n\tpathEntries: SessionTreeEntry[],\n\tsettings: CompactionSettings,\n): Result<CompactionPreparation | undefined, CompactionError> {\n\tif (pathEntries.length === 0 || pathEntries[pathEntries.length - 1].type === \"compaction\") {\n\t\treturn ok(undefined);\n\t}\n\n\tlet prevCompactionIndex = -1;\n\tfor (let i = pathEntries.length - 1; i >= 0; i--) {\n\t\tif (pathEntries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet previousSummary: string | undefined;\n\tlet boundaryStart = 0;\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;\n\t\tpreviousSummary = prevCompaction.summary;\n\t\tconst firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);\n\t\tboundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;\n\t}\n\tconst boundaryEnd = pathEntries.length;\n\n\tconst tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;\n\n\tconst cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\tconst dropsHistoryEntries = cutPoint.firstKeptEntryIndex > boundaryStart;\n\tconst summarizesSplitTurnPrefix = cutPoint.isSplitTurn && cutPoint.firstKeptEntryIndex > cutPoint.turnStartIndex;\n\t// Invariant: normal compactions that drop at least one entry, or summarize a\n\t// split-turn prefix, are unchanged. Only true no-ops are skipped.\n\tif (!dropsHistoryEntries && !summarizesSplitTurnPrefix) {\n\t\treturn ok(undefined);\n\t}\n\n\tconst firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];\n\tif (!firstKeptEntry?.id) {\n\t\treturn err(new CompactionError(\"invalid_session\", \"First kept entry has no UUID - session may need migration\"));\n\t}\n\tconst firstKeptEntryId = firstKeptEntry.id;\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\tconst messagesToSummarize: AgentMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\tif (msg) messagesToSummarize.push(msg);\n\t}\n\tconst turnPrefixMessages: AgentMessage[] = [];\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {\n\t\t\tconst msg = getMessageFromEntryForCompaction(pathEntries[i]);\n\t\t\tif (msg) turnPrefixMessages.push(msg);\n\t\t}\n\t}\n\tconst fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);\n\tif (cutPoint.isSplitTurn) {\n\t\tfor (const msg of turnPrefixMessages) {\n\t\t\textractFileOpsFromMessage(msg, fileOps);\n\t\t}\n\t}\n\n\treturn ok({\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn: cutPoint.isSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t});\n}\n\nexport { serializeConversation } from \"./utils.ts\";\n\n/**\n * Rough chars-per-token estimate for bounding summarization input.\n * Conservative on purpose: serialized code/JSON runs below 4 chars/token.\n */\nconst SUMMARY_CHARS_PER_TOKEN = 3.5;\n/**\n * Headroom subtracted on top of the output reserve: the summarization system\n * prompt, the <conversation>/prompt wrapper, and tokenizer drift between the\n * chars-per-token estimate and the real tokenizer.\n */\nconst SUMMARY_INPUT_SAFETY_TOKENS = 2048;\n\nfunction summaryElisionMessage(omittedCount: number): Message {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: `[... ${omittedCount} earlier message(s) omitted to fit the summarization context window ...]`,\n\t\ttimestamp: Date.now(),\n\t} as Message;\n}\n\n/**\n * Serialize a conversation for summarization, bounded to the model's context\n * window. When the full serialization would exceed the window, middle\n * messages are dropped (keeping the goal-bearing head and the recent-work\n * tail) with an explicit elision marker; oversized single messages fall back\n * to hard text truncation keeping both ends.\n *\n * Without this bound, a session that already overflows the model cannot be\n * compacted: the summarization request itself exceeds the window, the\n * provider rejects it (e.g. Codex context_length_exceeded), and overflow\n * recovery is stuck permanently.\n */\nexport function serializeConversationBounded(messages: Message[], model: Model<any>, maxOutputTokens: number): string {\n\tconst serialized = serializeConversation(messages);\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return serialized;\n\tconst budgetTokens = contextWindow - maxOutputTokens - SUMMARY_INPUT_SAFETY_TOKENS;\n\tconst budgetChars = Math.floor(Math.max(0, budgetTokens) * SUMMARY_CHARS_PER_TOKEN);\n\tif (serialized.length <= budgetChars) return serialized;\n\t// Window too small to bound meaningfully; leave it to the provider error path.\n\tif (budgetChars < 4096) return serialized;\n\n\t// Halve the kept head/tail spans until the elided conversation fits.\n\tfor (let keep = Math.floor(messages.length / 2); keep >= 1; keep = Math.floor(keep / 2)) {\n\t\tconst head = messages.slice(0, keep);\n\t\tconst tail = messages.slice(messages.length - keep);\n\t\tconst omitted = messages.length - head.length - tail.length;\n\t\tif (omitted <= 0) continue;\n\t\tconst bounded = serializeConversation([...head, summaryElisionMessage(omitted), ...tail]);\n\t\tif (bounded.length <= budgetChars) return bounded;\n\t}\n\n\t// Single oversized message(s): truncate the text itself, keeping both ends.\n\tconst headChars = Math.floor(budgetChars / 2);\n\tconst tailChars = Math.max(0, budgetChars - headChars - 128);\n\treturn `${serialized.slice(0, headChars)}\\n[... middle omitted to fit the summarization context window ...]\\n${serialized.slice(serialized.length - tailChars)}`;\n}\n\n/** Generate compaction summary data from prepared session history. */\nexport async function compact(\n\tpreparation: CompactionPreparation,\n\tmodel: Model<any>,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tcustomInstructions?: string,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<CompactionResult, CompactionError>> {\n\tconst {\n\t\tfirstKeptEntryId,\n\t\tmessagesToSummarize,\n\t\tturnPrefixMessages,\n\t\tisSplitTurn,\n\t\ttokensBefore,\n\t\tpreviousSummary,\n\t\tfileOps,\n\t\tsettings,\n\t} = preparation;\n\n\tif (!firstKeptEntryId) {\n\t\treturn err(new CompactionError(\"invalid_session\", \"First kept entry has no UUID - session may need migration\"));\n\t}\n\n\tlet summary: string;\n\n\tif (isSplitTurn && turnPrefixMessages.length > 0) {\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\tmessagesToSummarize.length > 0\n\t\t\t\t? generateSummary(\n\t\t\t\t\t\tmessagesToSummarize,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tsettings.reserveTokens,\n\t\t\t\t\t\tapiKey,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t\tcustomInstructions,\n\t\t\t\t\t\tpreviousSummary,\n\t\t\t\t\t\tthinkingLevel,\n\t\t\t\t\t\tretryOptions,\n\t\t\t\t\t)\n\t\t\t\t: Promise.resolve(ok<string, CompactionError>(\"No prior history.\")),\n\t\t\tgenerateTurnPrefixSummary(\n\t\t\t\tturnPrefixMessages,\n\t\t\t\tmodel,\n\t\t\t\tsettings.reserveTokens,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tsignal,\n\t\t\t\tthinkingLevel,\n\t\t\t\tretryOptions,\n\t\t\t),\n\t\t]);\n\t\tif (!historyResult.ok) return err(historyResult.error);\n\t\tif (!turnPrefixResult.ok) return err(turnPrefixResult.error);\n\t\tsummary = `${historyResult.value}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixResult.value}`;\n\t} else {\n\t\tconst summaryResult = await generateSummary(\n\t\t\tmessagesToSummarize,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\theaders,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t\tpreviousSummary,\n\t\t\tthinkingLevel,\n\t\t\tretryOptions,\n\t\t);\n\t\tif (!summaryResult.ok) return err(summaryResult.error);\n\t\tsummary = summaryResult.value;\n\t}\n\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\treturn ok({\n\t\tsummary,\n\t\tfirstKeptEntryId,\n\t\ttokensBefore,\n\t\tdetails: { readFiles, modifiedFiles } as CompactionDetails,\n\t});\n}\nasync function generateTurnPrefixSummary(\n\tmessages: AgentMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\theaders?: Record<string, string>,\n\tsignal?: AbortSignal,\n\tthinkingLevel?: ThinkingLevel,\n\tretryOptions?: SummarizationRetryOptions,\n): Promise<Result<string, CompactionError>> {\n\tconst maxTokens = Math.min(\n\t\tMath.floor(0.5 * reserveTokens),\n\t\tmodel.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,\n\t);\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversationBounded(llmMessages, model, maxTokens);\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteSimple(\n\t\t\t\tmodel,\n\t\t\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t\t\tmodel.reasoning && thinkingLevel && thinkingLevel !== \"off\"\n\t\t\t\t\t? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }\n\t\t\t\t\t: { maxTokens, signal, apiKey, headers },\n\t\t\t),\n\t\tretryOptions?.retry,\n\t\tsignal,\n\t\tretryOptions?.callbacks,\n\t);\n\tif (response.stopReason === \"aborted\") {\n\t\treturn err(new CompactionError(\"aborted\", response.errorMessage || \"Turn prefix summarization aborted\"));\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn err(\n\t\t\tnew CompactionError(\n\t\t\t\t\"summarization_failed\",\n\t\t\t\t`Turn prefix summarization failed: ${response.errorMessage || \"Unknown error\"}`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn ok(\n\t\tresponse.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\\n\"),\n\t);\n}\n"]}
@@ -225,22 +225,11 @@ function findValidCutPoints(entries, startIndex, endIndex) {
225
225
  case "assistant":
226
226
  cutPoints.push(i);
227
227
  break;
228
- case "toolResult":
229
228
  default:
230
229
  break;
231
230
  }
232
231
  break;
233
232
  }
234
- case "thinking_level_change":
235
- case "model_change":
236
- case "active_tools_change":
237
- case "compaction":
238
- case "branch_summary":
239
- case "custom":
240
- case "custom_message":
241
- case "label":
242
- case "session_info":
243
- case "leaf":
244
233
  default:
245
234
  break;
246
235
  }