@sapiom/harness 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/cli/args.d.ts +16 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +80 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/bin.js +12 -65
  7. package/dist/cli/bin.js.map +1 -1
  8. package/dist/cli/settings.d.ts.map +1 -1
  9. package/dist/cli/settings.js +33 -1
  10. package/dist/cli/settings.js.map +1 -1
  11. package/dist/core/adapters/claude-code.d.ts +60 -2
  12. package/dist/core/adapters/claude-code.d.ts.map +1 -1
  13. package/dist/core/adapters/claude-code.js +119 -32
  14. package/dist/core/adapters/claude-code.js.map +1 -1
  15. package/dist/core/adapters/codex.d.ts +21 -2
  16. package/dist/core/adapters/codex.d.ts.map +1 -1
  17. package/dist/core/adapters/codex.js +53 -2
  18. package/dist/core/adapters/codex.js.map +1 -1
  19. package/dist/core/adapters/detect.d.ts +9 -0
  20. package/dist/core/adapters/detect.d.ts.map +1 -1
  21. package/dist/core/adapters/detect.js +9 -0
  22. package/dist/core/adapters/detect.js.map +1 -1
  23. package/dist/core/canvas-body.d.ts +28 -28
  24. package/dist/core/canvas-body.d.ts.map +1 -1
  25. package/dist/core/canvas-body.js +117 -55
  26. package/dist/core/canvas-body.js.map +1 -1
  27. package/dist/core/canvas-derive.d.ts +43 -0
  28. package/dist/core/canvas-derive.d.ts.map +1 -0
  29. package/dist/core/canvas-derive.js +95 -0
  30. package/dist/core/canvas-derive.js.map +1 -0
  31. package/dist/core/canvas-enrichment.d.ts +52 -127
  32. package/dist/core/canvas-enrichment.d.ts.map +1 -1
  33. package/dist/core/canvas-enrichment.js +17 -178
  34. package/dist/core/canvas-enrichment.js.map +1 -1
  35. package/dist/core/canvas-graph.d.ts +40 -1
  36. package/dist/core/canvas-graph.d.ts.map +1 -1
  37. package/dist/core/canvas-graph.js +71 -9
  38. package/dist/core/canvas-graph.js.map +1 -1
  39. package/dist/core/canvas-interconnections.d.ts +26 -8
  40. package/dist/core/canvas-interconnections.d.ts.map +1 -1
  41. package/dist/core/canvas-interconnections.js +132 -24
  42. package/dist/core/canvas-interconnections.js.map +1 -1
  43. package/dist/core/canvas-manifest-check.d.ts.map +1 -1
  44. package/dist/core/canvas-manifest-check.js +15 -2
  45. package/dist/core/canvas-manifest-check.js.map +1 -1
  46. package/dist/core/canvas-render.d.ts +8 -15
  47. package/dist/core/canvas-render.d.ts.map +1 -1
  48. package/dist/core/canvas-render.js +29 -41
  49. package/dist/core/canvas-render.js.map +1 -1
  50. package/dist/core/canvas-run-state.d.ts +41 -0
  51. package/dist/core/canvas-run-state.d.ts.map +1 -1
  52. package/dist/core/canvas-run-state.js +136 -0
  53. package/dist/core/canvas-run-state.js.map +1 -1
  54. package/dist/core/canvas-svg.d.ts +9 -4
  55. package/dist/core/canvas-svg.d.ts.map +1 -1
  56. package/dist/core/canvas-svg.js +84 -13
  57. package/dist/core/canvas-svg.js.map +1 -1
  58. package/dist/core/canvas-template.d.ts +9 -10
  59. package/dist/core/canvas-template.d.ts.map +1 -1
  60. package/dist/core/canvas-template.js +57 -20
  61. package/dist/core/canvas-template.js.map +1 -1
  62. package/dist/core/canvas-watcher.d.ts +22 -1
  63. package/dist/core/canvas-watcher.d.ts.map +1 -1
  64. package/dist/core/canvas-watcher.js +111 -30
  65. package/dist/core/canvas-watcher.js.map +1 -1
  66. package/dist/core/collector/normalizer.d.ts +8 -0
  67. package/dist/core/collector/normalizer.d.ts.map +1 -1
  68. package/dist/core/collector/normalizer.js +8 -0
  69. package/dist/core/collector/normalizer.js.map +1 -1
  70. package/dist/core/collector/store.d.ts +75 -1
  71. package/dist/core/collector/store.d.ts.map +1 -1
  72. package/dist/core/collector/store.js +334 -2
  73. package/dist/core/collector/store.js.map +1 -1
  74. package/dist/core/errors.d.ts +10 -4
  75. package/dist/core/errors.d.ts.map +1 -1
  76. package/dist/core/errors.js +11 -5
  77. package/dist/core/errors.js.map +1 -1
  78. package/dist/core/example-seed.d.ts +8 -0
  79. package/dist/core/example-seed.d.ts.map +1 -1
  80. package/dist/core/example-seed.js +47 -10
  81. package/dist/core/example-seed.js.map +1 -1
  82. package/dist/core/inject/system-prompt.d.ts +8 -0
  83. package/dist/core/inject/system-prompt.d.ts.map +1 -1
  84. package/dist/core/inject/system-prompt.js +3 -1
  85. package/dist/core/inject/system-prompt.js.map +1 -1
  86. package/dist/core/macros.d.ts.map +1 -1
  87. package/dist/core/macros.js +5 -6
  88. package/dist/core/macros.js.map +1 -1
  89. package/dist/core/paths.d.ts +1 -0
  90. package/dist/core/paths.d.ts.map +1 -1
  91. package/dist/core/paths.js +1 -0
  92. package/dist/core/paths.js.map +1 -1
  93. package/dist/core/record-archive.d.ts +187 -0
  94. package/dist/core/record-archive.d.ts.map +1 -0
  95. package/dist/core/record-archive.js +465 -0
  96. package/dist/core/record-archive.js.map +1 -0
  97. package/dist/core/rehydration.d.ts +72 -0
  98. package/dist/core/rehydration.d.ts.map +1 -0
  99. package/dist/core/rehydration.js +72 -0
  100. package/dist/core/rehydration.js.map +1 -0
  101. package/dist/core/resume-brief.d.ts +130 -0
  102. package/dist/core/resume-brief.d.ts.map +1 -0
  103. package/dist/core/resume-brief.js +404 -0
  104. package/dist/core/resume-brief.js.map +1 -0
  105. package/dist/core/rolling-summary.d.ts +130 -0
  106. package/dist/core/rolling-summary.d.ts.map +1 -0
  107. package/dist/core/rolling-summary.js +227 -0
  108. package/dist/core/rolling-summary.js.map +1 -0
  109. package/dist/core/session-manager.d.ts +11 -3
  110. package/dist/core/session-manager.d.ts.map +1 -1
  111. package/dist/core/session-manager.js +56 -7
  112. package/dist/core/session-manager.js.map +1 -1
  113. package/dist/core/session-record.d.ts +165 -0
  114. package/dist/core/session-record.d.ts.map +1 -0
  115. package/dist/core/session-record.js +470 -0
  116. package/dist/core/session-record.js.map +1 -0
  117. package/dist/core/spawn-target.d.ts +12 -0
  118. package/dist/core/spawn-target.d.ts.map +1 -0
  119. package/dist/core/spawn-target.js +187 -0
  120. package/dist/core/spawn-target.js.map +1 -0
  121. package/dist/core/task-manager.d.ts.map +1 -1
  122. package/dist/core/task-manager.js +9 -1
  123. package/dist/core/task-manager.js.map +1 -1
  124. package/dist/core/template-catalog.d.ts +48 -0
  125. package/dist/core/template-catalog.d.ts.map +1 -0
  126. package/dist/core/template-catalog.js +338 -0
  127. package/dist/core/template-catalog.js.map +1 -0
  128. package/dist/core/workflow-registry.d.ts.map +1 -1
  129. package/dist/core/workflow-registry.js +2 -2
  130. package/dist/core/workflow-registry.js.map +1 -1
  131. package/dist/core/workspace-watcher.d.ts.map +1 -1
  132. package/dist/core/workspace-watcher.js +4 -3
  133. package/dist/core/workspace-watcher.js.map +1 -1
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.js +21 -0
  137. package/dist/index.js.map +1 -1
  138. package/dist/profiles/default.d.ts.map +1 -1
  139. package/dist/profiles/default.js +9 -14
  140. package/dist/profiles/default.js.map +1 -1
  141. package/dist/server/canvas.d.ts.map +1 -1
  142. package/dist/server/canvas.js +7 -7
  143. package/dist/server/canvas.js.map +1 -1
  144. package/dist/server/fs.d.ts.map +1 -1
  145. package/dist/server/fs.js +17 -1
  146. package/dist/server/fs.js.map +1 -1
  147. package/dist/server/index.d.ts +12 -3
  148. package/dist/server/index.d.ts.map +1 -1
  149. package/dist/server/index.js +321 -50
  150. package/dist/server/index.js.map +1 -1
  151. package/dist/server/ingest.d.ts +11 -0
  152. package/dist/server/ingest.d.ts.map +1 -1
  153. package/dist/server/ingest.js +1 -0
  154. package/dist/server/ingest.js.map +1 -1
  155. package/dist/server/macros.d.ts +4 -6
  156. package/dist/server/macros.d.ts.map +1 -1
  157. package/dist/server/macros.js.map +1 -1
  158. package/dist/server/rest.d.ts +18 -6
  159. package/dist/server/rest.d.ts.map +1 -1
  160. package/dist/server/rest.js +185 -48
  161. package/dist/server/rest.js.map +1 -1
  162. package/dist/server/templates.d.ts +24 -0
  163. package/dist/server/templates.d.ts.map +1 -0
  164. package/dist/server/templates.js +54 -0
  165. package/dist/server/templates.js.map +1 -0
  166. package/dist/shared/types.d.ts +477 -29
  167. package/dist/shared/types.d.ts.map +1 -1
  168. package/dist/shared/types.js +19 -11
  169. package/dist/shared/types.js.map +1 -1
  170. package/dist/web/assets/index-DC65cskq.js +446 -0
  171. package/dist/web/assets/index-xtD-3FFa.css +32 -0
  172. package/dist/web/index.html +2 -2
  173. package/package.json +5 -4
  174. package/dist/core/canvas-enrich.d.ts +0 -115
  175. package/dist/core/canvas-enrich.d.ts.map +0 -1
  176. package/dist/core/canvas-enrich.js +0 -186
  177. package/dist/core/canvas-enrich.js.map +0 -1
  178. package/dist/profiles/canvas-guidelines.d.ts +0 -20
  179. package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
  180. package/dist/profiles/canvas-guidelines.js +0 -48
  181. package/dist/profiles/canvas-guidelines.js.map +0 -1
  182. package/dist/web/assets/index-DCtyI4VW.css +0 -32
  183. package/dist/web/assets/index-WSw5RvYR.js +0 -445
  184. package/dist/web/welcome-hero-dark.png +0 -0
  185. package/dist/web/welcome-hero-light.png +0 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Durable session records — what makes session history outlive the raw event
3
+ * log it was rebuilt from.
4
+ *
5
+ * THE PROBLEM. `events.ndjson` is an analytics sink with an analytics sink's
6
+ * retention: 50 MB / 30 days, truncated oldest-first (core/collector/
7
+ * store-retention.ts). Once session history is *built on* that file
8
+ * (core/session-record.ts), the sweep silently deletes history — a session a
9
+ * user can read today is gone next month, with no way to tell "never existed"
10
+ * from "swept". A record the user can open is a product surface; a byte cap on
11
+ * an append-only debug log is not the right lifetime for it.
12
+ *
13
+ * THE FIX. At session end we fold the record once and write a compacted copy to
14
+ * `~/.sapiom/harness/records/<harnessSessionId>.json`. It is bounded (see the
15
+ * caps below), it is one file per conversation, and it survives every sweep of
16
+ * the ndjson because it isn't in it. The ndjson goes back to being what it is:
17
+ * the raw analytics sink.
18
+ *
19
+ * WHY NOT UNDER `<generated>/<sessionId>/`, which the ticket suggested and
20
+ * which already has a retention sweep: that directory is deleted the moment a
21
+ * session's pty exits (server/index.ts's "exited" handler calls
22
+ * removeGeneratedSessionDir — every file in it is regenerated on the next
23
+ * launch), and whatever survives that is swept 7 days after it goes stale.
24
+ * Both lifetimes are *shorter* than the 30 days of events the archive exists to
25
+ * outlive, so a record written there would die sooner than the log it was meant
26
+ * to survive. Records get their own root and their own policy instead.
27
+ *
28
+ * COMPACTION is lossy, on purpose, and says so. Tool inputs and tool results
29
+ * are the bulk of the bytes (`tool.call` was ~80% of observed event volume) and
30
+ * the least useful part of a record read weeks later, so they are clipped hard;
31
+ * prompts and assistant text — the conversation — are clipped only at the
32
+ * collector's own field cap, so in practice they are kept whole. Every loss
33
+ * lands in `SessionRecord.limitations` (`compacted-archive`,
34
+ * `dropped-early-turns`) and the UI renders it as prose, per this epic's rule
35
+ * that a reconstruction never gets to imply it is complete.
36
+ *
37
+ * SIZE CEILING, stated exactly: a record is compacted to at most
38
+ * {@link RECORD_MAX_BYTES} by clipping fields and then dropping whole turns
39
+ * oldest-first — but never below one turn. A single turn with hundreds of tool
40
+ * calls can therefore exceed the per-record cap; the store's total-bytes cap
41
+ * ({@link RECORDS_MAX_TOTAL_BYTES}, enforced against real file sizes) is the
42
+ * hard bound, and one outsized record just crowds out older ones sooner.
43
+ */
44
+ import * as fs from "node:fs/promises";
45
+ import * as path from "node:path";
46
+ import { PAYLOAD_TRUNCATION_MARKER } from "./collector/normalizer.js";
47
+ import { childPath } from "./path-safety.js";
48
+ /** Tool input, clipped hard: a 40 KB Edit payload is not what makes a
49
+ * months-old record worth keeping, and it is most of its bytes. */
50
+ export const RECORDS_MAX_TOOL_INPUT_CHARS = 512;
51
+ /** Tool result, same reasoning. The collector already caps these at 16 KB. */
52
+ export const RECORDS_MAX_TOOL_RESPONSE_CHARS = 512;
53
+ /** Prompts and assistant text — the conversation itself. Set at the
54
+ * collector's own field cap (normalizer.ts's MAX_FIELD_LENGTH), so this
55
+ * clips nothing the log didn't already clip. Kept as our own constant
56
+ * because the archive's contract is "the conversation survives whole". */
57
+ export const RECORDS_MAX_TEXT_CHARS = 4000;
58
+ /** Per-record byte ceiling — see the module header for the one case that
59
+ * legitimately exceeds it. ~64 KB holds a long session comfortably once tool
60
+ * payloads are clipped. */
61
+ export const RECORD_MAX_BYTES = 64 * 1024;
62
+ /** Whole-store byte ceiling. A third of the raw sink's 50 MB, for two orders
63
+ * of magnitude more sessions: at 64 KB worst-case per record it guarantees
64
+ * 256 archived conversations, and at realistic sizes holds thousands. */
65
+ export const RECORDS_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
66
+ /** Age ceiling. Deliberately far longer than the events' 30 days — the whole
67
+ * point of the archive is to outlive them — while still bounded, so an
68
+ * install that runs for years doesn't accumulate forever. */
69
+ export const RECORDS_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1000;
70
+ // ---------------------------------------------------------------------------
71
+ // Compaction — pure
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Clip a nullable string to `maxChars`, wearing the same marker the collector
75
+ * uses — so an archive-clipped tool result is recognized as truncated by
76
+ * exactly the test the collector's own truncation is (see session-record.ts).
77
+ *
78
+ * An existing marker is UNWRAPPED first and its count folded into the new one.
79
+ * Two things fall out of that, both load-bearing: the collector's 16 KB tool
80
+ * results (which already end in a marker, and are the bytes worth taking) get
81
+ * clipped rather than waved through, and clipping is idempotent — a second pass
82
+ * finds a body already at the cap and returns it untouched, instead of eating
83
+ * the marker and shaving another 23 characters off the text every time a
84
+ * record is re-archived.
85
+ */
86
+ function clip(value, maxChars) {
87
+ if (value === null)
88
+ return null;
89
+ const marker = PAYLOAD_TRUNCATION_MARKER.exec(value);
90
+ const body = marker ? value.slice(0, value.length - marker[0].length) : value;
91
+ if (body.length <= maxChars)
92
+ return value;
93
+ const omitted = body.length - maxChars + (marker ? Number(marker[1]) : 0);
94
+ return `${body.slice(0, maxChars)}…[truncated ${omitted} chars]`;
95
+ }
96
+ /**
97
+ * The bounded, archivable form of a record: fields clipped, oldest turns
98
+ * dropped if it still doesn't fit, and every loss declared in `limitations`.
99
+ *
100
+ * Pure and idempotent — compacting an already-compacted record clips nothing
101
+ * further and drops nothing further, which matters because a re-archive
102
+ * (a session that ends twice, a backfill) must not shrink a record a little
103
+ * more each time.
104
+ *
105
+ * `turnCount` and `eventCount` deliberately keep their pre-compaction values:
106
+ * they describe the conversation that happened, not the excerpt that survived.
107
+ * A history row's turn count therefore doesn't change when its record is
108
+ * archived, and `dropped-early-turns` is what tells the reader that `turns`
109
+ * holds fewer than `turnCount` of them.
110
+ */
111
+ export function compactSessionRecord(record, options = {}) {
112
+ const maxBytes = options.maxBytes ?? RECORD_MAX_BYTES;
113
+ const maxToolInputChars = options.maxToolInputChars ?? RECORDS_MAX_TOOL_INPUT_CHARS;
114
+ const maxToolResponseChars = options.maxToolResponseChars ?? RECORDS_MAX_TOOL_RESPONSE_CHARS;
115
+ const maxTextChars = options.maxTextChars ?? RECORDS_MAX_TEXT_CHARS;
116
+ const archivedAt = options.archivedAt ?? new Date().toISOString();
117
+ const clipped = record.turns.map((turn) => ({
118
+ ...turn,
119
+ prompt: clip(turn.prompt, maxTextChars),
120
+ assistantText: clip(turn.assistantText, maxTextChars),
121
+ toolCalls: turn.toolCalls.map((call) => {
122
+ const responseSummary = clip(call.responseSummary, maxToolResponseChars);
123
+ return {
124
+ ...call,
125
+ input: clip(call.input, maxToolInputChars),
126
+ responseSummary,
127
+ // Recomputed rather than carried: a response this pass shortened is
128
+ // truncated whether or not the collector had already truncated it.
129
+ responseTruncated: call.responseTruncated || responseSummary !== call.responseSummary,
130
+ };
131
+ }),
132
+ }));
133
+ // Cumulative-bytes pass rather than re-serializing the record per dropped
134
+ // turn (the same shape store-retention.ts's size cap uses, and for the same
135
+ // reason: a marathon session shouldn't cost O(n²) to archive).
136
+ const turnBytes = clipped.map((turn) => Buffer.byteLength(JSON.stringify(turn), "utf8") + 1);
137
+ // Measured against the record this function will RETURN, at its largest: the
138
+ // stamp is set and both archive limitation codes may be added. Measuring the
139
+ // (smaller) input's header instead would let the result overshoot the cap by
140
+ // their bytes — a cap that is only nearly a cap isn't one.
141
+ const headerBytes = Buffer.byteLength(JSON.stringify({
142
+ ...record,
143
+ turns: [],
144
+ archivedAt,
145
+ limitations: [...new Set([...record.limitations, "compacted-archive", "dropped-early-turns"])],
146
+ }), "utf8");
147
+ let total = turnBytes.reduce((sum, n) => sum + n, headerBytes);
148
+ let dropCount = 0;
149
+ // Never drop the last turn: an empty archive is strictly worse than one that
150
+ // overshoots the cap, and the store's total-bytes cap still bounds the whole.
151
+ while (dropCount < clipped.length - 1 && total > maxBytes) {
152
+ total -= turnBytes[dropCount];
153
+ dropCount += 1;
154
+ }
155
+ const turns = dropCount > 0 ? clipped.slice(dropCount) : clipped;
156
+ const limitations = new Set(record.limitations);
157
+ // Judged on the turns that SURVIVED: a clip inside a turn that was dropped
158
+ // wholesale is already covered by `dropped-early-turns`, and claiming
159
+ // "content was shortened" for text nobody can look for reads as a second,
160
+ // separate loss.
161
+ if (turns.some((turn, index) => turnChangedByCompaction(record.turns[dropCount + index], turn))) {
162
+ limitations.add("compacted-archive");
163
+ }
164
+ // `truncated-tool-output` can newly become true here: a result the collector
165
+ // kept whole may have been clipped by this pass.
166
+ if (turns.some((turn) => turn.toolCalls.some((call) => call.responseTruncated))) {
167
+ limitations.add("truncated-tool-output");
168
+ }
169
+ if (dropCount > 0)
170
+ limitations.add("dropped-early-turns");
171
+ return { ...record, turns, limitations: [...limitations], archivedAt };
172
+ }
173
+ function turnChangedByCompaction(before, after) {
174
+ if (before.prompt !== after.prompt)
175
+ return true;
176
+ if (before.assistantText !== after.assistantText)
177
+ return true;
178
+ return before.toolCalls.some((call, index) => call.input !== after.toolCalls[index].input ||
179
+ call.responseSummary !== after.toolCalls[index].responseSummary);
180
+ }
181
+ // ---------------------------------------------------------------------------
182
+ // Store
183
+ // ---------------------------------------------------------------------------
184
+ /** Every id a record should be findable by. A conversation is addressed by its
185
+ * primary session id, by any session that resumed it, and by the agent's own
186
+ * session id (transcript-sourced history rows only have the latter). */
187
+ function keysFor(record) {
188
+ const keys = [record.harnessSessionId, ...record.mergedSessionIds];
189
+ if (record.agentSessionId)
190
+ keys.push(record.agentSessionId);
191
+ return [...new Set(keys.filter((key) => key.length > 0))];
192
+ }
193
+ const EMPTY_INDEX = { byKey: new Map(), records: [], dirMtimeMs: -1 };
194
+ function parseRecord(text) {
195
+ let parsed;
196
+ try {
197
+ parsed = JSON.parse(text);
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ if (typeof parsed !== "object" || parsed === null)
203
+ return null;
204
+ const candidate = parsed;
205
+ // A file without these can't be placed or rendered, so it isn't a record as
206
+ // far as the archive is concerned — treated as absent, never thrown on.
207
+ if (typeof candidate.harnessSessionId !== "string")
208
+ return null;
209
+ if (!Array.isArray(candidate.turns) || !Array.isArray(candidate.mergedSessionIds))
210
+ return null;
211
+ if (typeof candidate.archivedAt !== "string")
212
+ return null;
213
+ return candidate;
214
+ }
215
+ export function createRecordArchive(options) {
216
+ const root = options.root;
217
+ const maxRecordBytes = options.maxRecordBytes ?? RECORD_MAX_BYTES;
218
+ const maxTotalBytes = options.maxTotalBytes ?? RECORDS_MAX_TOTAL_BYTES;
219
+ const maxAgeMs = options.maxAgeMs ?? RECORDS_MAX_AGE_MS;
220
+ const now = options.now ?? (() => Date.now());
221
+ const onError = options.onError ?? ((err) => console.error("[harness] session record archive:", err));
222
+ /**
223
+ * Cached directory index. Invalidated by the root's mtime, which changes when
224
+ * a file is created or removed — so a second harness boot writing to the same
225
+ * root is picked up. (Coarse-mtime filesystems can hide a foreign write made
226
+ * within the same second as our scan; it self-heals on the next one. Our own
227
+ * writes update the cache directly and never depend on that.)
228
+ */
229
+ let cache = null;
230
+ /**
231
+ * Serializes the two mutating operations. Both archive triggers
232
+ * (server/index.ts: the `session.end` event and the session's exit) can fire
233
+ * for one session within the same millisecond, and a sweep can land on top —
234
+ * so without this, two writes race for the same temp path and one of them
235
+ * renames a file the other already moved. Reads stay outside the queue: they
236
+ * only ever open a whole file that a rename put there atomically.
237
+ */
238
+ let queue = Promise.resolve();
239
+ function enqueue(run) {
240
+ const next = queue.catch(() => { }).then(run);
241
+ queue = next.catch(() => { });
242
+ return next;
243
+ }
244
+ /** Distinguishes temp files written in the same millisecond by the same pid. */
245
+ let tmpCounter = 0;
246
+ function fileFor(harnessSessionId) {
247
+ return childPath(root, `${harnessSessionId}.json`);
248
+ }
249
+ async function buildIndex(dirMtimeMs) {
250
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => null);
251
+ if (!entries)
252
+ return { ...EMPTY_INDEX, dirMtimeMs };
253
+ const byKey = new Map();
254
+ const records = [];
255
+ for (const entry of entries) {
256
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
257
+ continue;
258
+ const file = childPath(root, entry.name);
259
+ if (!file)
260
+ continue;
261
+ const text = await fs.readFile(file, "utf8").catch(() => null);
262
+ if (text === null)
263
+ continue;
264
+ const record = parseRecord(text);
265
+ if (!record)
266
+ continue;
267
+ const summary = {
268
+ harnessSessionId: record.harnessSessionId,
269
+ keys: keysFor(record),
270
+ turnCount: record.turnCount,
271
+ archivedAt: record.archivedAt,
272
+ bytes: Buffer.byteLength(text, "utf8"),
273
+ file,
274
+ };
275
+ records.push(summary);
276
+ for (const key of summary.keys) {
277
+ // First writer wins per key, and records are visited in readdir order,
278
+ // so a stray duplicate (two archives claiming one agent session) can't
279
+ // make the lookup depend on which one was read last: the primary file
280
+ // named after the requested id is still found by the direct hit in
281
+ // read(), which never consults this map.
282
+ if (!byKey.has(key))
283
+ byKey.set(key, summary);
284
+ }
285
+ }
286
+ records.sort((a, b) => (a.archivedAt < b.archivedAt ? 1 : a.archivedAt > b.archivedAt ? -1 : 0));
287
+ return { byKey, records, dirMtimeMs };
288
+ }
289
+ async function index() {
290
+ const stat = await fs.stat(root).catch(() => null);
291
+ if (!stat) {
292
+ cache = null;
293
+ return EMPTY_INDEX;
294
+ }
295
+ if (cache && cache.dirMtimeMs === stat.mtimeMs)
296
+ return cache;
297
+ const built = await buildIndex(stat.mtimeMs);
298
+ cache = built;
299
+ return built;
300
+ }
301
+ async function readFile(file) {
302
+ const text = await fs.readFile(file, "utf8").catch(() => null);
303
+ return text === null ? null : parseRecord(text);
304
+ }
305
+ async function writeRecord(record) {
306
+ // Nothing to preserve: a session that recorded no turn at all reads the
307
+ // same after the sweep as before it ("no recorded events"), and writing a
308
+ // turn-less file would spend the store's budget saying so.
309
+ if (record.turns.length === 0)
310
+ return null;
311
+ const file = fileFor(record.harnessSessionId);
312
+ if (!file) {
313
+ onError(new Error(`refusing to archive a record with an unsafe session id "${record.harnessSessionId}"`));
314
+ return null;
315
+ }
316
+ const compacted = compactSessionRecord(record, {
317
+ maxBytes: maxRecordBytes,
318
+ archivedAt: new Date(now()).toISOString(),
319
+ });
320
+ const body = `${JSON.stringify(compacted)}\n`;
321
+ // Atomic replace (temp + rename in the same directory), so a crash
322
+ // mid-write can never leave a half-written record where a whole one was.
323
+ tmpCounter += 1;
324
+ const tmpFile = path.join(root, `.record-tmp-${process.pid}-${now()}-${tmpCounter}.json`);
325
+ try {
326
+ await fs.mkdir(root, { recursive: true });
327
+ await fs.writeFile(tmpFile, body, "utf8");
328
+ await fs.rename(tmpFile, file);
329
+ }
330
+ catch (err) {
331
+ await fs.unlink(tmpFile).catch(() => undefined);
332
+ onError(err);
333
+ return null;
334
+ }
335
+ // Keep the cache correct without re-reading every file: upsert this
336
+ // summary and adopt the root's new mtime as the one we're current with.
337
+ const summary = {
338
+ harnessSessionId: compacted.harnessSessionId,
339
+ keys: keysFor(compacted),
340
+ turnCount: compacted.turnCount,
341
+ archivedAt: compacted.archivedAt,
342
+ bytes: Buffer.byteLength(body, "utf8"),
343
+ file,
344
+ };
345
+ if (cache) {
346
+ const records = cache.records.filter((entry) => entry.file !== file);
347
+ records.unshift(summary);
348
+ const byKey = new Map(cache.byKey);
349
+ for (const key of summary.keys)
350
+ byKey.set(key, summary);
351
+ const stat = await fs.stat(root).catch(() => null);
352
+ cache = { byKey, records, dirMtimeMs: stat?.mtimeMs ?? -1 };
353
+ }
354
+ return compacted;
355
+ }
356
+ async function sweepRecords() {
357
+ const { records } = await index();
358
+ if (records.length === 0)
359
+ return [];
360
+ // Oldest first — both caps evict in that order.
361
+ const oldestFirst = [...records].reverse();
362
+ const cutoffMs = now() - maxAgeMs;
363
+ let totalBytes = oldestFirst.reduce((sum, entry) => sum + entry.bytes, 0);
364
+ const doomed = [];
365
+ for (const entry of oldestFirst) {
366
+ const archivedMs = Date.parse(entry.archivedAt);
367
+ const expired = !Number.isNaN(archivedMs) && archivedMs < cutoffMs;
368
+ if (!expired && totalBytes <= maxTotalBytes)
369
+ break;
370
+ doomed.push(entry);
371
+ totalBytes -= entry.bytes;
372
+ }
373
+ const removed = [];
374
+ for (const entry of doomed) {
375
+ try {
376
+ await fs.unlink(entry.file);
377
+ removed.push(entry.harnessSessionId);
378
+ }
379
+ catch {
380
+ // Leave it for the next sweep rather than aborting the rest.
381
+ }
382
+ }
383
+ if (removed.length > 0)
384
+ cache = null;
385
+ return removed;
386
+ }
387
+ return {
388
+ write(record) {
389
+ return enqueue(() => writeRecord(record));
390
+ },
391
+ async read(id) {
392
+ // Direct hit first — the overwhelmingly common case (a row addressed by
393
+ // the id its record is filed under) costs one file read and never builds
394
+ // the directory index.
395
+ const direct = fileFor(id);
396
+ if (direct) {
397
+ const record = await readFile(direct);
398
+ if (record)
399
+ return record;
400
+ }
401
+ const summary = (await index()).byKey.get(id);
402
+ if (!summary)
403
+ return null;
404
+ return readFile(summary.file);
405
+ },
406
+ async has(id) {
407
+ const direct = fileFor(id);
408
+ if (direct) {
409
+ const stat = await fs.stat(direct).catch(() => null);
410
+ if (stat?.isFile())
411
+ return true;
412
+ }
413
+ return (await index()).byKey.has(id);
414
+ },
415
+ async list() {
416
+ return [...(await index()).records];
417
+ },
418
+ sweep() {
419
+ return enqueue(sweepRecords);
420
+ },
421
+ };
422
+ }
423
+ /** Default ceiling for one backfill pass. High enough to cover a typical
424
+ * install's whole history on the first boot after this shipped, low enough
425
+ * that a pathological log doesn't turn boot into a write storm. Whatever is
426
+ * left is archived by the next boot's pass. */
427
+ export const RECORDS_BACKFILL_MAX = 200;
428
+ /**
429
+ * Archive conversations the event log still holds but the archive doesn't.
430
+ *
431
+ * Two things need this, and neither is covered by archiving at session end:
432
+ * sessions that ended without one (the harness force-killed, the machine
433
+ * rebooted), and every session that ended *before* this feature existed — whose
434
+ * history would otherwise disappear at its 30-day mark with the archive looking
435
+ * on. Idempotent: a conversation already archived is skipped, so the steady
436
+ * state after the first pass is "nothing to do".
437
+ *
438
+ * Never throws. Returns the ids it archived.
439
+ */
440
+ export async function backfillSessionRecords(options) {
441
+ const maxRecords = options.maxRecords ?? RECORDS_BACKFILL_MAX;
442
+ const ids = await options.conversationIds();
443
+ const archived = [];
444
+ let remaining = 0;
445
+ for (const id of ids) {
446
+ if (options.isLiveSession?.(id))
447
+ continue;
448
+ if (await options.archive.has(id))
449
+ continue;
450
+ if (archived.length >= maxRecords) {
451
+ remaining += 1;
452
+ continue;
453
+ }
454
+ const record = await options.readFromEvents(id);
455
+ if (!record)
456
+ continue;
457
+ const written = await options.archive.write(record);
458
+ if (written)
459
+ archived.push(id);
460
+ }
461
+ if (remaining > 0)
462
+ options.onCapped?.(remaining);
463
+ return archived;
464
+ }
465
+ //# sourceMappingURL=record-archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-archive.js","sourceRoot":"","sources":["../../src/core/record-archive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Portable continue, wiring layer: turns a `rehydrateFrom` id into brief text,
3
+ * and decides which of the two delivery channels carries it.
4
+ *
5
+ * The brief itself is pure (core/resume-brief.ts); this is the part that has to
6
+ * touch the record reader, the summary cache, and the session registry. Kept
7
+ * out of server/index.ts so the resolution order — and the honest "there was
8
+ * nothing recorded for that id" answer — is testable without standing up a
9
+ * server.
10
+ *
11
+ * THE TWO CHANNELS, and why there are two:
12
+ *
13
+ * - `launch-flag` — the brief is composed into the generated system-prompt
14
+ * file, which claude-code reads via `--append-system-prompt` and codex via
15
+ * `developer_instructions` (see core/adapters/codex.ts's header for why it
16
+ * uses that rather than `model_instructions_file`). Both shipped harnesses
17
+ * use this: no adapter change, no injected keystrokes, and the brief is in
18
+ * place before the agent's first token.
19
+ * - `post-ready-injection` — the universal fallback for a harness with no
20
+ * prompt flag at all: wait for the session to report `ready`, then send the
21
+ * brief through the ordinary input path. It is gated on readiness because
22
+ * writing into a TUI that is sitting on a "trust this directory?" prompt
23
+ * feeds the brief to the prompt instead of the agent.
24
+ *
25
+ * No adapter shipped today needs the second channel — which is exactly why it
26
+ * is declared per-adapter rather than inferred: a future adapter that cannot
27
+ * take a prompt flag says so in one line and the context still gets across,
28
+ * instead of being silently dropped.
29
+ */
30
+ import type { HarnessAdapter, SessionRecord, SystemPromptDelivery } from "../shared/types.js";
31
+ import { type ResumeBriefWorkflow } from "./resume-brief.js";
32
+ /**
33
+ * How a harness receives a rehydration brief. Absent on the adapter means the
34
+ * fallback, not the flag: an adapter that never wired `systemPromptFile` and
35
+ * never declared anything would otherwise have its brief written to a file
36
+ * nothing reads. Failing toward "delivered noisily" beats failing toward
37
+ * "silently dropped" for a feature whose entire purpose is that the context
38
+ * arrives.
39
+ */
40
+ export declare function systemPromptDeliveryFor(adapter: HarnessAdapter | undefined): SystemPromptDelivery;
41
+ /** Context only the caller can resolve — the record carries no title, no git
42
+ * branch, and no workflow binding (those live in the session registry and the
43
+ * workflow registry respectively). */
44
+ export interface RehydrationContext {
45
+ title?: string | null;
46
+ gitBranch?: string | null;
47
+ workflow?: ResumeBriefWorkflow | null;
48
+ }
49
+ export interface RehydrationDeps {
50
+ /** `SessionRecordReader.read` — resolves a harnessSessionId or an agent
51
+ * session id to the folded record. */
52
+ readRecord: (id: string) => Promise<SessionRecord | null>;
53
+ /** The cached rolling summary for one harness session, or null. */
54
+ readSummary: (harnessSessionId: string) => Promise<string | null>;
55
+ /** Registry-only context for the record. Optional; the brief renders
56
+ * without it, just less specifically. May be async — the git branch, for
57
+ * one, is only knowable from an adapter history scan. */
58
+ resolveContext?: (record: SessionRecord) => RehydrationContext | undefined | Promise<RehydrationContext | undefined>;
59
+ /** Token ceiling override, forwarded to `buildResumeBrief`. */
60
+ maxTokens?: number;
61
+ }
62
+ /**
63
+ * The brief for `rehydrateFrom`, or null when our event log holds nothing for
64
+ * it — the honest answer, and the one that lets a caller say "this continue
65
+ * carried no context" rather than opening a fresh session dressed up as a
66
+ * continuation.
67
+ *
68
+ * Never throws: a rehydration that can't be assembled must degrade to a plain
69
+ * new session, never fail the session-create it is decorating.
70
+ */
71
+ export declare function buildRehydrationBrief(rehydrateFrom: string, deps: RehydrationDeps): Promise<string | null>;
72
+ //# sourceMappingURL=rehydration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rehydration.d.ts","sourceRoot":"","sources":["../../src/core/rehydration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAoB,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE/E;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,GAAG,oBAAoB,CAEjG;AAED;;uCAEuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC1D,mEAAmE;IACnE,WAAW,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClE;;8DAE0D;IAC1D,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,aAAa,KAClB,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC9E,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBxB"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Portable continue, wiring layer: turns a `rehydrateFrom` id into brief text,
3
+ * and decides which of the two delivery channels carries it.
4
+ *
5
+ * The brief itself is pure (core/resume-brief.ts); this is the part that has to
6
+ * touch the record reader, the summary cache, and the session registry. Kept
7
+ * out of server/index.ts so the resolution order — and the honest "there was
8
+ * nothing recorded for that id" answer — is testable without standing up a
9
+ * server.
10
+ *
11
+ * THE TWO CHANNELS, and why there are two:
12
+ *
13
+ * - `launch-flag` — the brief is composed into the generated system-prompt
14
+ * file, which claude-code reads via `--append-system-prompt` and codex via
15
+ * `developer_instructions` (see core/adapters/codex.ts's header for why it
16
+ * uses that rather than `model_instructions_file`). Both shipped harnesses
17
+ * use this: no adapter change, no injected keystrokes, and the brief is in
18
+ * place before the agent's first token.
19
+ * - `post-ready-injection` — the universal fallback for a harness with no
20
+ * prompt flag at all: wait for the session to report `ready`, then send the
21
+ * brief through the ordinary input path. It is gated on readiness because
22
+ * writing into a TUI that is sitting on a "trust this directory?" prompt
23
+ * feeds the brief to the prompt instead of the agent.
24
+ *
25
+ * No adapter shipped today needs the second channel — which is exactly why it
26
+ * is declared per-adapter rather than inferred: a future adapter that cannot
27
+ * take a prompt flag says so in one line and the context still gets across,
28
+ * instead of being silently dropped.
29
+ */
30
+ import { buildResumeBrief } from "./resume-brief.js";
31
+ /**
32
+ * How a harness receives a rehydration brief. Absent on the adapter means the
33
+ * fallback, not the flag: an adapter that never wired `systemPromptFile` and
34
+ * never declared anything would otherwise have its brief written to a file
35
+ * nothing reads. Failing toward "delivered noisily" beats failing toward
36
+ * "silently dropped" for a feature whose entire purpose is that the context
37
+ * arrives.
38
+ */
39
+ export function systemPromptDeliveryFor(adapter) {
40
+ return adapter?.systemPromptDelivery ?? "post-ready-injection";
41
+ }
42
+ /**
43
+ * The brief for `rehydrateFrom`, or null when our event log holds nothing for
44
+ * it — the honest answer, and the one that lets a caller say "this continue
45
+ * carried no context" rather than opening a fresh session dressed up as a
46
+ * continuation.
47
+ *
48
+ * Never throws: a rehydration that can't be assembled must degrade to a plain
49
+ * new session, never fail the session-create it is decorating.
50
+ */
51
+ export async function buildRehydrationBrief(rehydrateFrom, deps) {
52
+ const record = await deps.readRecord(rehydrateFrom).catch(() => null);
53
+ if (!record)
54
+ return null;
55
+ // A conversation can span several harness sessions (a resume, or an adopt),
56
+ // each with its own summary file. Newest-last is the fold that saw the most,
57
+ // so walk backwards and take the first one that exists rather than merging
58
+ // several partial accounts of the same work.
59
+ let summary = null;
60
+ for (const harnessSessionId of [...record.mergedSessionIds].reverse()) {
61
+ summary = await deps.readSummary(harnessSessionId).catch(() => null);
62
+ if (summary)
63
+ break;
64
+ }
65
+ const context = (await deps.resolveContext?.(record)) ?? {};
66
+ return buildResumeBrief(record, {
67
+ ...context,
68
+ summary,
69
+ ...(deps.maxTokens !== undefined ? { maxTokens: deps.maxTokens } : {}),
70
+ });
71
+ }
72
+ //# sourceMappingURL=rehydration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rehydration.js","sourceRoot":"","sources":["../../src/core/rehydration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAOH,OAAO,EAAE,gBAAgB,EAA4B,MAAM,mBAAmB,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAmC;IACzE,OAAO,OAAO,EAAE,oBAAoB,IAAI,sBAAsB,CAAC;AACjE,CAAC;AA2BD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,aAAqB,EACrB,IAAqB;IAErB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACtE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,6CAA6C;IAC7C,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,KAAK,MAAM,gBAAgB,IAAI,CAAC,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QACtE,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACrE,IAAI,OAAO;YAAE,MAAM;IACrB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,OAAO,gBAAgB,CAAC,MAAM,EAAE;QAC9B,GAAG,OAAO;QACV,OAAO;QACP,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvE,CAAC,CAAC;AACL,CAAC"}