@sapiom/harness 0.1.5 → 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.
- package/CHANGELOG.md +151 -0
- package/dist/cli/args.d.ts +16 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +80 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.js +30 -66
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/consent.js +1 -1
- package/dist/cli/settings.d.ts.map +1 -1
- package/dist/cli/settings.js +33 -1
- package/dist/cli/settings.js.map +1 -1
- package/dist/core/adapters/adapter.d.ts +2 -2
- package/dist/core/adapters/claude-code.d.ts +60 -2
- package/dist/core/adapters/claude-code.d.ts.map +1 -1
- package/dist/core/adapters/claude-code.js +119 -32
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/adapters/codex.d.ts +21 -2
- package/dist/core/adapters/codex.d.ts.map +1 -1
- package/dist/core/adapters/codex.js +53 -2
- package/dist/core/adapters/codex.js.map +1 -1
- package/dist/core/adapters/detect.d.ts +9 -0
- package/dist/core/adapters/detect.d.ts.map +1 -1
- package/dist/core/adapters/detect.js +9 -0
- package/dist/core/adapters/detect.js.map +1 -1
- package/dist/core/api-key-provider.d.ts +79 -0
- package/dist/core/api-key-provider.d.ts.map +1 -0
- package/dist/core/api-key-provider.js +91 -0
- package/dist/core/api-key-provider.js.map +1 -0
- package/dist/core/canvas-body.d.ts +28 -28
- package/dist/core/canvas-body.d.ts.map +1 -1
- package/dist/core/canvas-body.js +117 -55
- package/dist/core/canvas-body.js.map +1 -1
- package/dist/core/canvas-derive.d.ts +43 -0
- package/dist/core/canvas-derive.d.ts.map +1 -0
- package/dist/core/canvas-derive.js +95 -0
- package/dist/core/canvas-derive.js.map +1 -0
- package/dist/core/canvas-enrichment.d.ts +52 -127
- package/dist/core/canvas-enrichment.d.ts.map +1 -1
- package/dist/core/canvas-enrichment.js +17 -178
- package/dist/core/canvas-enrichment.js.map +1 -1
- package/dist/core/canvas-graph.d.ts +40 -1
- package/dist/core/canvas-graph.d.ts.map +1 -1
- package/dist/core/canvas-graph.js +71 -9
- package/dist/core/canvas-graph.js.map +1 -1
- package/dist/core/canvas-interconnections.d.ts +26 -8
- package/dist/core/canvas-interconnections.d.ts.map +1 -1
- package/dist/core/canvas-interconnections.js +132 -24
- package/dist/core/canvas-interconnections.js.map +1 -1
- package/dist/core/canvas-manifest-check.d.ts.map +1 -1
- package/dist/core/canvas-manifest-check.js +15 -2
- package/dist/core/canvas-manifest-check.js.map +1 -1
- package/dist/core/canvas-render.d.ts +8 -15
- package/dist/core/canvas-render.d.ts.map +1 -1
- package/dist/core/canvas-render.js +29 -41
- package/dist/core/canvas-render.js.map +1 -1
- package/dist/core/canvas-run-state.d.ts +50 -5
- package/dist/core/canvas-run-state.d.ts.map +1 -1
- package/dist/core/canvas-run-state.js +176 -11
- package/dist/core/canvas-run-state.js.map +1 -1
- package/dist/core/canvas-svg.d.ts +9 -4
- package/dist/core/canvas-svg.d.ts.map +1 -1
- package/dist/core/canvas-svg.js +84 -13
- package/dist/core/canvas-svg.js.map +1 -1
- package/dist/core/canvas-template.d.ts +9 -10
- package/dist/core/canvas-template.d.ts.map +1 -1
- package/dist/core/canvas-template.js +57 -20
- package/dist/core/canvas-template.js.map +1 -1
- package/dist/core/canvas-watcher.d.ts +22 -1
- package/dist/core/canvas-watcher.d.ts.map +1 -1
- package/dist/core/canvas-watcher.js +111 -30
- package/dist/core/canvas-watcher.js.map +1 -1
- package/dist/core/collector/normalizer.d.ts +8 -0
- package/dist/core/collector/normalizer.d.ts.map +1 -1
- package/dist/core/collector/normalizer.js +8 -0
- package/dist/core/collector/normalizer.js.map +1 -1
- package/dist/core/collector/store.d.ts +75 -1
- package/dist/core/collector/store.d.ts.map +1 -1
- package/dist/core/collector/store.js +334 -2
- package/dist/core/collector/store.js.map +1 -1
- package/dist/core/definition-slug-resolver.d.ts +13 -0
- package/dist/core/definition-slug-resolver.d.ts.map +1 -1
- package/dist/core/definition-slug-resolver.js +29 -0
- package/dist/core/definition-slug-resolver.js.map +1 -1
- package/dist/core/errors.d.ts +10 -4
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +11 -5
- package/dist/core/errors.js.map +1 -1
- package/dist/core/example-seed.d.ts +8 -0
- package/dist/core/example-seed.d.ts.map +1 -1
- package/dist/core/example-seed.js +47 -10
- package/dist/core/example-seed.js.map +1 -1
- package/dist/core/inject/system-prompt.d.ts +8 -0
- package/dist/core/inject/system-prompt.d.ts.map +1 -1
- package/dist/core/inject/system-prompt.js +3 -1
- package/dist/core/inject/system-prompt.js.map +1 -1
- package/dist/core/macros.d.ts.map +1 -1
- package/dist/core/macros.js +5 -6
- package/dist/core/macros.js.map +1 -1
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +1 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/record-archive.d.ts +187 -0
- package/dist/core/record-archive.d.ts.map +1 -0
- package/dist/core/record-archive.js +465 -0
- package/dist/core/record-archive.js.map +1 -0
- package/dist/core/rehydration.d.ts +72 -0
- package/dist/core/rehydration.d.ts.map +1 -0
- package/dist/core/rehydration.js +72 -0
- package/dist/core/rehydration.js.map +1 -0
- package/dist/core/render-local-run.d.ts +82 -0
- package/dist/core/render-local-run.d.ts.map +1 -0
- package/dist/core/render-local-run.js +105 -0
- package/dist/core/render-local-run.js.map +1 -0
- package/dist/core/render-log-slice.d.ts +30 -0
- package/dist/core/render-log-slice.d.ts.map +1 -0
- package/dist/core/render-log-slice.js +49 -0
- package/dist/core/render-log-slice.js.map +1 -0
- package/dist/core/render-run-state.d.ts.map +1 -1
- package/dist/core/render-run-state.js +30 -49
- package/dist/core/render-run-state.js.map +1 -1
- package/dist/core/resume-brief.d.ts +130 -0
- package/dist/core/resume-brief.d.ts.map +1 -0
- package/dist/core/resume-brief.js +404 -0
- package/dist/core/resume-brief.js.map +1 -0
- package/dist/core/rolling-summary.d.ts +130 -0
- package/dist/core/rolling-summary.d.ts.map +1 -0
- package/dist/core/rolling-summary.js +227 -0
- package/dist/core/rolling-summary.js.map +1 -0
- package/dist/core/run-local-bootstrap.d.ts +93 -0
- package/dist/core/run-local-bootstrap.d.ts.map +1 -0
- package/dist/core/run-local-bootstrap.js +155 -0
- package/dist/core/run-local-bootstrap.js.map +1 -0
- package/dist/core/run-state.d.ts +16 -1
- package/dist/core/run-state.d.ts.map +1 -1
- package/dist/core/run-state.js +75 -25
- package/dist/core/run-state.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +56 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-record.d.ts +165 -0
- package/dist/core/session-record.d.ts.map +1 -0
- package/dist/core/session-record.js +470 -0
- package/dist/core/session-record.js.map +1 -0
- package/dist/core/spawn-target.d.ts +12 -0
- package/dist/core/spawn-target.d.ts.map +1 -0
- package/dist/core/spawn-target.js +187 -0
- package/dist/core/spawn-target.js.map +1 -0
- package/dist/core/stub-feedback.d.ts +52 -0
- package/dist/core/stub-feedback.d.ts.map +1 -0
- package/dist/core/stub-feedback.js +33 -0
- package/dist/core/stub-feedback.js.map +1 -0
- package/dist/core/task-manager.d.ts.map +1 -1
- package/dist/core/task-manager.js +9 -1
- package/dist/core/task-manager.js.map +1 -1
- package/dist/core/template-catalog.d.ts +48 -0
- package/dist/core/template-catalog.d.ts.map +1 -0
- package/dist/core/template-catalog.js +338 -0
- package/dist/core/template-catalog.js.map +1 -0
- package/dist/core/workflow-registry.d.ts.map +1 -1
- package/dist/core/workflow-registry.js +2 -2
- package/dist/core/workflow-registry.js.map +1 -1
- package/dist/core/workspace-watcher.d.ts.map +1 -1
- package/dist/core/workspace-watcher.js +4 -3
- package/dist/core/workspace-watcher.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/profiles/default.d.ts.map +1 -1
- package/dist/profiles/default.js +9 -14
- package/dist/profiles/default.js.map +1 -1
- package/dist/server/actions.d.ts +139 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/actions.js +430 -0
- package/dist/server/actions.js.map +1 -0
- package/dist/server/auth-routes.d.ts +92 -0
- package/dist/server/auth-routes.d.ts.map +1 -0
- package/dist/server/auth-routes.js +151 -0
- package/dist/server/auth-routes.js.map +1 -0
- package/dist/server/canvas.d.ts.map +1 -1
- package/dist/server/canvas.js +7 -7
- package/dist/server/canvas.js.map +1 -1
- package/dist/server/fs.d.ts.map +1 -1
- package/dist/server/fs.js +17 -1
- package/dist/server/fs.js.map +1 -1
- package/dist/server/index.d.ts +12 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/ingest.d.ts +11 -0
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +1 -0
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/macros.d.ts +4 -6
- package/dist/server/macros.d.ts.map +1 -1
- package/dist/server/macros.js.map +1 -1
- package/dist/server/rest.d.ts +18 -6
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +185 -50
- package/dist/server/rest.js.map +1 -1
- package/dist/server/runs.d.ts +16 -15
- package/dist/server/runs.d.ts.map +1 -1
- package/dist/server/runs.js +15 -80
- package/dist/server/runs.js.map +1 -1
- package/dist/server/static.d.ts +15 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +54 -4
- package/dist/server/static.js.map +1 -1
- package/dist/server/templates.d.ts +24 -0
- package/dist/server/templates.d.ts.map +1 -0
- package/dist/server/templates.js +54 -0
- package/dist/server/templates.js.map +1 -0
- package/dist/shared/types.d.ts +558 -66
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +19 -11
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-DC65cskq.js +446 -0
- package/dist/web/assets/index-xtD-3FFa.css +32 -0
- package/dist/web/canvas/sess-boot/index.html +305 -0
- package/dist/web/index.html +4 -9
- package/package.json +8 -4
- package/dist/core/canvas-enrich.d.ts +0 -115
- package/dist/core/canvas-enrich.d.ts.map +0 -1
- package/dist/core/canvas-enrich.js +0 -186
- package/dist/core/canvas-enrich.js.map +0 -1
- package/dist/core/run-spend.d.ts +0 -55
- package/dist/core/run-spend.d.ts.map +0 -1
- package/dist/core/run-spend.js +0 -109
- package/dist/core/run-spend.js.map +0 -1
- package/dist/core/run-transactions.d.ts +0 -60
- package/dist/core/run-transactions.d.ts.map +0 -1
- package/dist/core/run-transactions.js +0 -129
- package/dist/core/run-transactions.js.map +0 -1
- package/dist/profiles/canvas-guidelines.d.ts +0 -20
- package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
- package/dist/profiles/canvas-guidelines.js +0 -48
- package/dist/profiles/canvas-guidelines.js.map +0 -1
- package/dist/server/skills.d.ts +0 -51
- package/dist/server/skills.d.ts.map +0 -1
- package/dist/server/skills.js +0 -312
- package/dist/server/skills.js.map +0 -1
- package/dist/web/assets/index-DM84J83Y.js +0 -234
- package/dist/web/assets/index-DemtWW4L.css +0 -32
|
@@ -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"}
|