@sapiom/harness 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +105 -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-SYpugKQ1.css +32 -0
  171. package/dist/web/assets/index-vYYd44hZ.js +446 -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,130 @@
1
+ /**
2
+ * Rolling session summary — the thing that makes rehydration good rather than
3
+ * merely possible.
4
+ *
5
+ * A resume brief built from the last N turns alone tells a fresh session what
6
+ * just happened; it cannot tell it what the session was *for*. So, opt-in, we
7
+ * periodically fold the whole record down to a ≤500-word summary and cache it
8
+ * at `<generated>/<harnessSessionId>/summary.md`. The brief
9
+ * (core/resume-brief.ts) picks it up when it's there and degrades cleanly to
10
+ * last-N-turns when it isn't — which is the default, since this is
11
+ * setting-gated (`HarnessSettings.rollingSummary`).
12
+ *
13
+ * NEVER BLOCKS A TURN. Everything here is fire-and-forget off the ingest path:
14
+ * `noteEvent` returns synchronously, the fold runs as a headless one-shot
15
+ * `TaskManager` job (a separate process, a cheap model, `--max-turns 1` — the
16
+ * bounded shape `HarnessAdapter.launchTask` exists for), and every failure
17
+ * mode short-circuits to "no summary". A user's session must never be slower,
18
+ * or fail, because a summary didn't happen.
19
+ *
20
+ * The task does not write the file itself. It is asked for the summary text
21
+ * and nothing else; we write `summary.md` from its `resultText` when it
22
+ * completes. Two reasons: the target lives under `<generated>/`, outside the
23
+ * session's cwd — which codex's `workspace-write` sandbox forbids and
24
+ * claude-code's `acceptEdits` shouldn't be asked to reach — and a task that
25
+ * writes its own output can half-write it, where a task that returns text
26
+ * either produces a whole summary or none.
27
+ *
28
+ * WHERE IT DOESN'T WORK: `launchTask` is optional on the adapter contract and
29
+ * codex has no non-interactive mode wired for it, so a codex session simply
30
+ * never produces a summary and its briefs are last-N-turns. That is a real
31
+ * gap, stated rather than papered over; the brief is honest either way.
32
+ */
33
+ import type { AnalyticsEvent, BackgroundTask, HarnessKind, SessionRecord } from "../shared/types.js";
34
+ /** Filename under `<generated>/<harnessSessionId>/`. */
35
+ export declare const ROLLING_SUMMARY_FILE = "summary.md";
36
+ /** Completed turns that must accumulate before a re-fold is worth its cost. */
37
+ export declare const ROLLING_SUMMARY_TURN_INTERVAL = 10;
38
+ /** The contract stated in the prompt, and the clamp applied to what comes back. */
39
+ export declare const ROLLING_SUMMARY_MAX_WORDS = 500;
40
+ /**
41
+ * Model alias for the fold. A summary of text we already hold is the cheapest
42
+ * possible LLM job — pinning a small model here keeps an opt-in background
43
+ * feature from quietly costing what the user's own session does.
44
+ */
45
+ export declare const ROLLING_SUMMARY_MODEL = "haiku";
46
+ /** Hard turn cap: one turn in, one summary out. */
47
+ export declare const ROLLING_SUMMARY_MAX_TURNS = 1;
48
+ /** Macro id the task is registered under — also TaskManager's dedupe key, so
49
+ * a session can never have two folds in flight at once. */
50
+ export declare const ROLLING_SUMMARY_MACRO_ID = "rolling-summary";
51
+ /**
52
+ * Token budget for the material handed to the fold. Larger than a brief's
53
+ * (this is throwaway input to a cheap model, not a system prompt competing
54
+ * with the user's own context) but still bounded, so a marathon session can't
55
+ * turn one fold into a huge request.
56
+ */
57
+ export declare const ROLLING_SUMMARY_INPUT_MAX_TOKENS = 20000;
58
+ /** Absolute path of a session's cached summary. */
59
+ export declare function rollingSummaryPath(generatedRoot: string, harnessSessionId: string): string;
60
+ /**
61
+ * The cached summary for a session, or null when there is none — never throws.
62
+ * A brief that fails to find a summary is a brief without one, not an error.
63
+ */
64
+ export declare function readRollingSummary(generatedRoot: string, harnessSessionId: string): Promise<string | null>;
65
+ /** Trim `text` to at most `maxWords` words, marking the cut. */
66
+ export declare function clampToWords(text: string, maxWords?: number): string;
67
+ /**
68
+ * The one-shot prompt handed to `launchTask`. The material is the record
69
+ * rendered by {@link buildResumeBrief} at a larger budget — deliberately the
70
+ * same renderer the brief uses, so the fold reads exactly the shape a future
71
+ * brief will present, and there is one place where "what a record looks like
72
+ * as text" is defined.
73
+ *
74
+ * `previous` is folded back in rather than discarded: the record itself is
75
+ * capped at the input budget, so on a long session the earliest turns are
76
+ * already gone from the material, and the last summary is the only surviving
77
+ * account of them.
78
+ */
79
+ export declare function buildRollingSummaryPrompt(record: SessionRecord, previous: string | null): string;
80
+ /** What the summarizer needs from the outside world. All injectable, so the
81
+ * unit tests drive it without a filesystem or a spawned process. */
82
+ export interface RollingSummarizerDeps {
83
+ /** Root the per-session generated dirs live under. */
84
+ generatedRoot: string;
85
+ /** The `HarnessSettings.rollingSummary` gate, re-read per fold so toggling
86
+ * the setting takes effect without a restart. Never throws. */
87
+ enabled: () => Promise<boolean>;
88
+ /** Reads the reconstructed record for a session (SessionRecordReader.read). */
89
+ readRecord: (harnessSessionId: string) => Promise<SessionRecord | null>;
90
+ /** The live session, for the harness kind and cwd the task runs as. Returns
91
+ * undefined once the session is gone from the registry. */
92
+ getSession: (harnessSessionId: string) => {
93
+ harness: HarnessKind;
94
+ cwd: string;
95
+ } | undefined;
96
+ /** TaskManager.run, narrowed to what this needs. */
97
+ runTask: (req: {
98
+ macroId: string;
99
+ label: string;
100
+ harnessSessionId: string;
101
+ harness: HarnessKind;
102
+ cwd: string;
103
+ prompt: string;
104
+ model: string;
105
+ maxTurns: number;
106
+ }) => Promise<BackgroundTask>;
107
+ /** Completed turns between folds. Default {@link ROLLING_SUMMARY_TURN_INTERVAL}. */
108
+ turnInterval?: number;
109
+ /** Diagnostics sink. Defaults to console.error — a fold failing is worth a
110
+ * log line and nothing more. */
111
+ onError?: (err: unknown) => void;
112
+ }
113
+ export interface RollingSummarizer {
114
+ /**
115
+ * Feed one normalized analytics event. Synchronous and total: it counts
116
+ * `turn.completed`, triggers a fold on the interval and on `session.end`,
117
+ * and ignores everything else. Any work it starts is detached.
118
+ */
119
+ noteEvent(event: AnalyticsEvent): void;
120
+ /**
121
+ * Feed a task status change (TaskManager.onStatusChange). A completed fold
122
+ * task's `resultText` is what becomes `summary.md`.
123
+ */
124
+ noteTaskStatus(task: BackgroundTask): void;
125
+ /** Resolves when no fold this summarizer started is still in flight. For
126
+ * tests and for shutdown; production callers never need to await it. */
127
+ idle(): Promise<void>;
128
+ }
129
+ export declare function createRollingSummarizer(deps: RollingSummarizerDeps): RollingSummarizer;
130
+ //# sourceMappingURL=rolling-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rolling-summary.d.ts","sourceRoot":"","sources":["../../src/core/rolling-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGrG,wDAAwD;AACxD,eAAO,MAAM,oBAAoB,eAAe,CAAC;AACjD,+EAA+E;AAC/E,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,mFAAmF;AACnF,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,UAAU,CAAC;AAC7C,mDAAmD;AACnD,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAC3C;4DAC4D;AAC5D,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAE1F;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAkC,GAAG,MAAM,CAI/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CA+BhG;AAED;qEACqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,sDAAsD;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB;oEACgE;IAChE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,+EAA+E;IAC/E,UAAU,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACxE;gEAC4D;IAC5D,UAAU,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC5F,oDAAoD;IACpD,OAAO,EAAE,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,gBAAgB,EAAE,MAAM,CAAC;QACzB,OAAO,EAAE,WAAW,CAAC;QACrB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;qCACiC;IACjC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC3C;6EACyE;IACzE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAgGtF"}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Rolling session summary — the thing that makes rehydration good rather than
3
+ * merely possible.
4
+ *
5
+ * A resume brief built from the last N turns alone tells a fresh session what
6
+ * just happened; it cannot tell it what the session was *for*. So, opt-in, we
7
+ * periodically fold the whole record down to a ≤500-word summary and cache it
8
+ * at `<generated>/<harnessSessionId>/summary.md`. The brief
9
+ * (core/resume-brief.ts) picks it up when it's there and degrades cleanly to
10
+ * last-N-turns when it isn't — which is the default, since this is
11
+ * setting-gated (`HarnessSettings.rollingSummary`).
12
+ *
13
+ * NEVER BLOCKS A TURN. Everything here is fire-and-forget off the ingest path:
14
+ * `noteEvent` returns synchronously, the fold runs as a headless one-shot
15
+ * `TaskManager` job (a separate process, a cheap model, `--max-turns 1` — the
16
+ * bounded shape `HarnessAdapter.launchTask` exists for), and every failure
17
+ * mode short-circuits to "no summary". A user's session must never be slower,
18
+ * or fail, because a summary didn't happen.
19
+ *
20
+ * The task does not write the file itself. It is asked for the summary text
21
+ * and nothing else; we write `summary.md` from its `resultText` when it
22
+ * completes. Two reasons: the target lives under `<generated>/`, outside the
23
+ * session's cwd — which codex's `workspace-write` sandbox forbids and
24
+ * claude-code's `acceptEdits` shouldn't be asked to reach — and a task that
25
+ * writes its own output can half-write it, where a task that returns text
26
+ * either produces a whole summary or none.
27
+ *
28
+ * WHERE IT DOESN'T WORK: `launchTask` is optional on the adapter contract and
29
+ * codex has no non-interactive mode wired for it, so a codex session simply
30
+ * never produces a summary and its briefs are last-N-turns. That is a real
31
+ * gap, stated rather than papered over; the brief is honest either way.
32
+ */
33
+ import * as fs from "node:fs/promises";
34
+ import * as path from "node:path";
35
+ import { buildResumeBrief } from "./resume-brief.js";
36
+ /** Filename under `<generated>/<harnessSessionId>/`. */
37
+ export const ROLLING_SUMMARY_FILE = "summary.md";
38
+ /** Completed turns that must accumulate before a re-fold is worth its cost. */
39
+ export const ROLLING_SUMMARY_TURN_INTERVAL = 10;
40
+ /** The contract stated in the prompt, and the clamp applied to what comes back. */
41
+ export const ROLLING_SUMMARY_MAX_WORDS = 500;
42
+ /**
43
+ * Model alias for the fold. A summary of text we already hold is the cheapest
44
+ * possible LLM job — pinning a small model here keeps an opt-in background
45
+ * feature from quietly costing what the user's own session does.
46
+ */
47
+ export const ROLLING_SUMMARY_MODEL = "haiku";
48
+ /** Hard turn cap: one turn in, one summary out. */
49
+ export const ROLLING_SUMMARY_MAX_TURNS = 1;
50
+ /** Macro id the task is registered under — also TaskManager's dedupe key, so
51
+ * a session can never have two folds in flight at once. */
52
+ export const ROLLING_SUMMARY_MACRO_ID = "rolling-summary";
53
+ /**
54
+ * Token budget for the material handed to the fold. Larger than a brief's
55
+ * (this is throwaway input to a cheap model, not a system prompt competing
56
+ * with the user's own context) but still bounded, so a marathon session can't
57
+ * turn one fold into a huge request.
58
+ */
59
+ export const ROLLING_SUMMARY_INPUT_MAX_TOKENS = 20000;
60
+ /** Absolute path of a session's cached summary. */
61
+ export function rollingSummaryPath(generatedRoot, harnessSessionId) {
62
+ return path.join(generatedRoot, harnessSessionId, ROLLING_SUMMARY_FILE);
63
+ }
64
+ /**
65
+ * The cached summary for a session, or null when there is none — never throws.
66
+ * A brief that fails to find a summary is a brief without one, not an error.
67
+ */
68
+ export async function readRollingSummary(generatedRoot, harnessSessionId) {
69
+ try {
70
+ const text = await fs.readFile(rollingSummaryPath(generatedRoot, harnessSessionId), "utf8");
71
+ const trimmed = text.trim();
72
+ return trimmed.length > 0 ? trimmed : null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** Trim `text` to at most `maxWords` words, marking the cut. */
79
+ export function clampToWords(text, maxWords = ROLLING_SUMMARY_MAX_WORDS) {
80
+ const words = text.trim().split(/\s+/);
81
+ if (words.length <= maxWords)
82
+ return text.trim();
83
+ return `${words.slice(0, maxWords).join(" ")} …[summary truncated]`;
84
+ }
85
+ /**
86
+ * The one-shot prompt handed to `launchTask`. The material is the record
87
+ * rendered by {@link buildResumeBrief} at a larger budget — deliberately the
88
+ * same renderer the brief uses, so the fold reads exactly the shape a future
89
+ * brief will present, and there is one place where "what a record looks like
90
+ * as text" is defined.
91
+ *
92
+ * `previous` is folded back in rather than discarded: the record itself is
93
+ * capped at the input budget, so on a long session the earliest turns are
94
+ * already gone from the material, and the last summary is the only surviving
95
+ * account of them.
96
+ */
97
+ export function buildRollingSummaryPrompt(record, previous) {
98
+ const material = buildResumeBrief(record, {
99
+ summary: previous,
100
+ maxTokens: ROLLING_SUMMARY_INPUT_MAX_TOKENS,
101
+ maxTurns: Number.MAX_SAFE_INTEGER,
102
+ });
103
+ return [
104
+ `Summarize the coding session described below in at most ${ROLLING_SUMMARY_MAX_WORDS} words.`,
105
+ "",
106
+ "The summary is read by a FRESH agent session that has none of this context,",
107
+ "so write for someone picking the work up cold. Cover, in this order and only",
108
+ "where the material actually supports it:",
109
+ "",
110
+ "1. What the session was trying to accomplish.",
111
+ "2. What was decided, and why — decisions are the part a transcript replay",
112
+ " cannot reconstruct.",
113
+ "3. What was actually changed (files, behaviour).",
114
+ "4. What is unfinished, blocked, or known-broken.",
115
+ "",
116
+ "Rules:",
117
+ "- Output ONLY the summary as markdown prose. No preamble, no sign-off, no",
118
+ " code fences around the whole thing.",
119
+ "- Do not invent anything the material does not state. If something is",
120
+ " unclear or was never recorded, say so or leave it out — a confident",
121
+ " summary of a gap is the single worst outcome here.",
122
+ "- Do not use any tools. Everything you need is in this prompt.",
123
+ "",
124
+ "--- SESSION MATERIAL ---",
125
+ "",
126
+ material,
127
+ ].join("\n");
128
+ }
129
+ export function createRollingSummarizer(deps) {
130
+ const turnInterval = deps.turnInterval ?? ROLLING_SUMMARY_TURN_INTERVAL;
131
+ const onError = deps.onError ?? ((err) => console.error("[harness] rolling summary failed:", err));
132
+ /** Completed turns seen for a session since its last fold was *started*. */
133
+ const turnsSinceFold = new Map();
134
+ /** Fold task id → the session it summarizes, so a completion can be routed
135
+ * back. Only ids this summarizer started are ever in here, which is what
136
+ * keeps it from writing summary.md for some other background task. */
137
+ const foldTargets = new Map();
138
+ /** In-flight promises, awaited by `idle()`. */
139
+ const pending = new Set();
140
+ const track = (work) => {
141
+ const tracked = work.catch(onError).finally(() => pending.delete(tracked));
142
+ pending.add(tracked);
143
+ };
144
+ async function fold(harnessSessionId) {
145
+ if (!(await deps.enabled()))
146
+ return;
147
+ const session = deps.getSession(harnessSessionId);
148
+ // Gone from the registry (swept, or a task's own id leaking through the
149
+ // ingest path) — there is nothing to run the fold as.
150
+ if (!session)
151
+ return;
152
+ const record = await deps.readRecord(harnessSessionId);
153
+ if (!record || record.turns.length === 0)
154
+ return;
155
+ const previous = await readRollingSummary(deps.generatedRoot, harnessSessionId);
156
+ const task = await deps.runTask({
157
+ macroId: ROLLING_SUMMARY_MACRO_ID,
158
+ label: "Session summary",
159
+ harnessSessionId,
160
+ harness: session.harness,
161
+ cwd: session.cwd,
162
+ prompt: buildRollingSummaryPrompt(record, previous),
163
+ model: ROLLING_SUMMARY_MODEL,
164
+ maxTurns: ROLLING_SUMMARY_MAX_TURNS,
165
+ });
166
+ foldTargets.set(task.id, harnessSessionId);
167
+ }
168
+ async function writeSummary(harnessSessionId, text) {
169
+ const clamped = clampToWords(text.trim());
170
+ if (clamped.length === 0)
171
+ return;
172
+ const filePath = rollingSummaryPath(deps.generatedRoot, harnessSessionId);
173
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
174
+ await fs.writeFile(filePath, `${clamped}\n`, "utf8");
175
+ }
176
+ return {
177
+ noteEvent(event) {
178
+ const sessionId = event.harnessSessionId;
179
+ if (event.type === "turn.completed") {
180
+ const count = (turnsSinceFold.get(sessionId) ?? 0) + 1;
181
+ if (count < turnInterval) {
182
+ turnsSinceFold.set(sessionId, count);
183
+ return;
184
+ }
185
+ // Reset BEFORE the fold rather than after it completes: the counter
186
+ // paces how often folds are *started*, and resetting on completion
187
+ // would let a slow fold queue up a second one the moment it lands.
188
+ turnsSinceFold.set(sessionId, 0);
189
+ track(fold(sessionId));
190
+ return;
191
+ }
192
+ if (event.type === "session.end") {
193
+ // Always fold at session end when anything at all happened since the
194
+ // last one — this is the summary a future rehydration will actually
195
+ // read, so it must not be `turnInterval - 1` turns stale.
196
+ const pendingTurns = turnsSinceFold.get(sessionId) ?? 0;
197
+ turnsSinceFold.delete(sessionId);
198
+ if (pendingTurns > 0)
199
+ track(fold(sessionId));
200
+ }
201
+ },
202
+ noteTaskStatus(task) {
203
+ const harnessSessionId = foldTargets.get(task.id);
204
+ if (harnessSessionId === undefined)
205
+ return;
206
+ if (task.status === "running")
207
+ return;
208
+ foldTargets.delete(task.id);
209
+ if (task.status === "failed" || !task.resultText) {
210
+ // A failed fold leaves the previous summary in place — a stale summary
211
+ // is strictly better than none, and the brief labels its own age via
212
+ // the record's own timestamps either way.
213
+ if (task.status === "failed")
214
+ onError(new Error(task.errorTail ?? "rolling summary task failed"));
215
+ return;
216
+ }
217
+ track(writeSummary(harnessSessionId, task.resultText));
218
+ },
219
+ async idle() {
220
+ // Re-read the set each pass: writing a summary is itself tracked work
221
+ // started by an earlier tracked promise.
222
+ while (pending.size > 0)
223
+ await Promise.all([...pending]);
224
+ },
225
+ };
226
+ }
227
+ //# sourceMappingURL=rolling-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rolling-summary.js","sourceRoot":"","sources":["../../src/core/rolling-summary.ts"],"names":[],"mappings":"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"}
@@ -35,7 +35,7 @@ export type SessionActivityListener = (harnessSessionId: string) => void;
35
35
  * whichever shape is handed in; existing sync test doubles keep working
36
36
  * unchanged (`await` on a plain value just resolves immediately).
37
37
  */
38
- export type LaunchOptsBuilder = (harnessSessionId: string, req: Pick<CreateSessionRequest, "cwd" | "harness" | "profile">) => Omit<LaunchOpts, "harnessSessionId" | "cwd"> | Promise<Omit<LaunchOpts, "harnessSessionId" | "cwd">>;
38
+ export type LaunchOptsBuilder = (harnessSessionId: string, req: Pick<CreateSessionRequest, "cwd" | "harness" | "profile" | "rehydrateFrom">) => Omit<LaunchOpts, "harnessSessionId" | "cwd"> | Promise<Omit<LaunchOpts, "harnessSessionId" | "cwd">>;
39
39
  export interface SessionManagerOptions {
40
40
  adapters: Partial<Record<HarnessKind, HarnessAdapter>>;
41
41
  /** Base URL the harness server is reachable at, e.g. http://127.0.0.1:4100. */
@@ -125,8 +125,16 @@ export declare class SessionManager {
125
125
  create(req: CreateSessionRequest): Promise<HarnessSession>;
126
126
  /**
127
127
  * Registers a purely historical (never-launched-by-this-harness) session so
128
- * it can subsequently be resumed via `resume()`. Used by the server layer
129
- * when a user picks a past session out of transcript-scanned history.
128
+ * it can subsequently be resumed via `resume()`. Called by
129
+ * `POST /api/sessions/adopt` when a user opens a transcript-only history row
130
+ * whose transcript the agent genuinely still holds — that row then resumes
131
+ * for real instead of silently starting a fresh session in the directory,
132
+ * which is what happened for as long as nothing called this.
133
+ *
134
+ * The record is born `exited` with `createdAt === lastActiveAt` (nothing has
135
+ * run under our management yet). `resume()` is what gives it a pty, and the
136
+ * adopt route pre-verifies resumability before registering, so this never
137
+ * mints a record that can't be resumed.
130
138
  */
131
139
  registerHistorical(input: {
132
140
  agentSessionId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
11
11
  import { createRequire } from "node:module";
12
12
  import { ENV, HARNESS_PATHS, } from "../shared/types.js";
13
13
  import { expandHome } from "./paths.js";
14
+ import { resolveSpawnTarget } from "./spawn-target.js";
14
15
  import { AdapterNotFoundError, ExternalHarnessError, SessionAlreadyLiveError, SessionNotReadyError, SessionNotResumeableError, UnknownSessionError, } from "./errors.js";
15
16
  import { listHarnessAdapters } from "./adapters/registry.js";
16
17
  export { AdapterNotFoundError, ExternalHarnessError, SessionAlreadyLiveError, SessionNotReadyError, SessionNotResumeableError, UnknownSessionError, } from "./errors.js";
@@ -245,6 +246,11 @@ export class SessionManager {
245
246
  lastActiveAt: this.now(),
246
247
  exitCode: null,
247
248
  boundWorkflowPath: null,
249
+ // What the builder actually managed to assemble, not what the caller
250
+ // asked for: `req.rehydrateFrom` naming a session our event log holds
251
+ // nothing for yields no brief, and this stays null so nothing downstream
252
+ // presents an empty-handed fresh session as a continuation.
253
+ rehydratedFrom: opts.rehydratedFrom ?? null,
248
254
  ready: false,
249
255
  };
250
256
  this.sessions.set(id, session);
@@ -271,8 +277,16 @@ export class SessionManager {
271
277
  }
272
278
  /**
273
279
  * Registers a purely historical (never-launched-by-this-harness) session so
274
- * it can subsequently be resumed via `resume()`. Used by the server layer
275
- * when a user picks a past session out of transcript-scanned history.
280
+ * it can subsequently be resumed via `resume()`. Called by
281
+ * `POST /api/sessions/adopt` when a user opens a transcript-only history row
282
+ * whose transcript the agent genuinely still holds — that row then resumes
283
+ * for real instead of silently starting a fresh session in the directory,
284
+ * which is what happened for as long as nothing called this.
285
+ *
286
+ * The record is born `exited` with `createdAt === lastActiveAt` (nothing has
287
+ * run under our management yet). `resume()` is what gives it a pty, and the
288
+ * adopt route pre-verifies resumability before registering, so this never
289
+ * mints a record that can't be resumed.
276
290
  */
277
291
  registerHistorical(input) {
278
292
  const id = this.generateId();
@@ -304,12 +318,32 @@ export class SessionManager {
304
318
  throw new SessionAlreadyLiveError(id);
305
319
  }
306
320
  const adapter = this.getAdapter(session.harness);
321
+ // Pre-flight against the agent's OWN store before touching the record.
322
+ // Holding an agentSessionId only means our SessionStart hook fired once;
323
+ // an agent that never received a prompt writes no transcript, so
324
+ // `--resume` would exit 1 with "No conversation found with session ID"
325
+ // and leave the user on a dead pane offering Resume all over again.
326
+ // Failing here instead keeps the record exactly as it was — unspawned,
327
+ // and (see below) with its real lastActiveAt intact.
328
+ if (!(await adapter.canResume(session.agentSessionId, session.cwd))) {
329
+ const label = listHarnessAdapters().find((a) => a.id === session.harness)?.label ?? session.harness;
330
+ throw new SessionNotResumeableError(id, `${label} no longer has the conversation for this session (${session.agentSessionId}) in ${session.cwd}. ` +
331
+ `Sessions that ended before their first prompt are never written to the agent's history, so there is nothing to resume — start a new session in this directory instead.`);
332
+ }
307
333
  const opts = {
308
334
  harnessSessionId: id,
309
335
  cwd: session.cwd,
310
336
  ...(await this.buildLaunchOpts(id, session)),
311
337
  };
312
338
  const spec = adapter.resume(session.agentSessionId, opts);
339
+ // Kept so the failure path below can put it back: `lastActiveAt` is
340
+ // stamped here only to keep sweepDeadSessions() from reaping this record
341
+ // during the pre-pty window (it reaps non-exited records with no pty once
342
+ // they're older than the grace period). If the resume never produces a
343
+ // pty, that stamp is not activity and must not survive — otherwise a
344
+ // session idle since last night reports "Ran for 6h 25m" purely because
345
+ // someone clicked Resume.
346
+ const lastActiveBeforeResume = session.lastActiveAt;
313
347
  session.status = "starting";
314
348
  session.exitCode = null;
315
349
  session.lastActiveAt = this.now();
@@ -332,8 +366,12 @@ export class SessionManager {
332
366
  catch (err) {
333
367
  // Same reconciliation as create(): the record just went back to
334
368
  // "starting" and was persisted — a failure before the new pty is live
335
- // must not leave it stranded there with nothing behind it.
336
- await this.transitionExited(session, null);
369
+ // must not leave it stranded there with nothing behind it. Roll the
370
+ // pre-pty `lastActiveAt` stamp back at the same time: no pty ever ran,
371
+ // so the session's last real activity is still where it was, and the
372
+ // dead pane's "Ran for" stays truthful.
373
+ session.lastActiveAt = lastActiveBeforeResume;
374
+ await this.transitionExited(session, null, { stampLastActive: false });
337
375
  throw err;
338
376
  }
339
377
  return session;
@@ -701,11 +739,18 @@ export class SessionManager {
701
739
  env[ENV.sessionId] = session.id;
702
740
  if (this.collectorUrl)
703
741
  env[ENV.collectorUrl] = this.collectorUrl;
742
+ // On Windows a bare command name (or a .cmd shim, which is what npm installs
743
+ // for `claude`) cannot be spawned: node-pty uses CreateProcess, which does no
744
+ // PATHEXT resolution and can't execute a .cmd. resolveSpawnTarget RESOLVES the
745
+ // shim to its real target — deliberately NOT via cmd.exe, which would expose
746
+ // these arguments to shell parsing (command injection; see that module).
747
+ // No-op on POSIX.
748
+ const target = resolveSpawnTarget(spec.command, spec.args);
704
749
  // A throw here — spawnFn itself, or loadDefaultSpawn() above (a broken
705
750
  // node-pty prebuild surfaces there, not at import time) — propagates to
706
751
  // create()/resume(), which own reconciling the session record to
707
752
  // "exited" for every pre-pty failure, not just this one.
708
- const pty = spawnFn(spec.command, spec.args, {
753
+ const pty = spawnFn(target.command, target.args, {
709
754
  name: "xterm-256color",
710
755
  cols: DEFAULT_COLS,
711
756
  rows: DEFAULT_ROWS,
@@ -765,10 +810,14 @@ export class SessionManager {
765
810
  * the registry durably updated before rethrowing (create/resume) can
766
811
  * await it; event-driven callers fire-and-forget it like any other write.
767
812
  */
768
- transitionExited(session, exitCode) {
813
+ transitionExited(session, exitCode, { stampLastActive = true } = {}) {
769
814
  session.status = "exited";
770
815
  session.exitCode = exitCode;
771
- session.lastActiveAt = this.now();
816
+ // `stampLastActive: false` is for reconciling a resume that never got a
817
+ // pty: "we noticed it's dead" is not activity, and stamping it there is
818
+ // what made an untouched session's duration grow on every failed Resume.
819
+ if (stampLastActive)
820
+ session.lastActiveAt = this.now();
772
821
  const persisted = this.persist();
773
822
  this.emitStatus(session);
774
823
  return persisted;