@wastedcode/claudemux 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 (249) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/LICENSE +21 -0
  3. package/README.md +493 -0
  4. package/bin/claudemux +6 -0
  5. package/dist/agents/claude.d.ts +3 -0
  6. package/dist/agents/claude.d.ts.map +1 -0
  7. package/dist/agents/claude.js +585 -0
  8. package/dist/agents/claude.js.map +1 -0
  9. package/dist/agents/index.d.ts +2 -0
  10. package/dist/agents/index.d.ts.map +1 -0
  11. package/dist/agents/index.js +2 -0
  12. package/dist/agents/index.js.map +1 -0
  13. package/dist/agents/types.d.ts +252 -0
  14. package/dist/agents/types.d.ts.map +1 -0
  15. package/dist/agents/types.js +2 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/backends/tmux/capture.d.ts +25 -0
  18. package/dist/backends/tmux/capture.d.ts.map +1 -0
  19. package/dist/backends/tmux/capture.js +35 -0
  20. package/dist/backends/tmux/capture.js.map +1 -0
  21. package/dist/backends/tmux/exec.d.ts +105 -0
  22. package/dist/backends/tmux/exec.d.ts.map +1 -0
  23. package/dist/backends/tmux/exec.js +226 -0
  24. package/dist/backends/tmux/exec.js.map +1 -0
  25. package/dist/backends/tmux/index.d.ts +22 -0
  26. package/dist/backends/tmux/index.d.ts.map +1 -0
  27. package/dist/backends/tmux/index.js +108 -0
  28. package/dist/backends/tmux/index.js.map +1 -0
  29. package/dist/backends/tmux/keys.d.ts +38 -0
  30. package/dist/backends/tmux/keys.d.ts.map +1 -0
  31. package/dist/backends/tmux/keys.js +63 -0
  32. package/dist/backends/tmux/keys.js.map +1 -0
  33. package/dist/backends/tmux/options.d.ts +24 -0
  34. package/dist/backends/tmux/options.d.ts.map +1 -0
  35. package/dist/backends/tmux/options.js +84 -0
  36. package/dist/backends/tmux/options.js.map +1 -0
  37. package/dist/backends/tmux/sessions.d.ts +70 -0
  38. package/dist/backends/tmux/sessions.d.ts.map +1 -0
  39. package/dist/backends/tmux/sessions.js +156 -0
  40. package/dist/backends/tmux/sessions.js.map +1 -0
  41. package/dist/backends/tmux/socket.d.ts +26 -0
  42. package/dist/backends/tmux/socket.d.ts.map +1 -0
  43. package/dist/backends/tmux/socket.js +31 -0
  44. package/dist/backends/tmux/socket.js.map +1 -0
  45. package/dist/backends/types.d.ts +110 -0
  46. package/dist/backends/types.d.ts.map +1 -0
  47. package/dist/backends/types.js +24 -0
  48. package/dist/backends/types.js.map +1 -0
  49. package/dist/cli/ask.d.ts +11 -0
  50. package/dist/cli/ask.d.ts.map +1 -0
  51. package/dist/cli/ask.js +17 -0
  52. package/dist/cli/ask.js.map +1 -0
  53. package/dist/cli/capture.d.ts +8 -0
  54. package/dist/cli/capture.d.ts.map +1 -0
  55. package/dist/cli/capture.js +15 -0
  56. package/dist/cli/capture.js.map +1 -0
  57. package/dist/cli/context.d.ts +71 -0
  58. package/dist/cli/context.d.ts.map +1 -0
  59. package/dist/cli/context.js +82 -0
  60. package/dist/cli/context.js.map +1 -0
  61. package/dist/cli/exists.d.ts +7 -0
  62. package/dist/cli/exists.d.ts.map +1 -0
  63. package/dist/cli/exists.js +16 -0
  64. package/dist/cli/exists.js.map +1 -0
  65. package/dist/cli/interrupt.d.ts +10 -0
  66. package/dist/cli/interrupt.d.ts.map +1 -0
  67. package/dist/cli/interrupt.js +13 -0
  68. package/dist/cli/interrupt.js.map +1 -0
  69. package/dist/cli/kill.d.ts +7 -0
  70. package/dist/cli/kill.d.ts.map +1 -0
  71. package/dist/cli/kill.js +14 -0
  72. package/dist/cli/kill.js.map +1 -0
  73. package/dist/cli/list.d.ts +10 -0
  74. package/dist/cli/list.d.ts.map +1 -0
  75. package/dist/cli/list.js +19 -0
  76. package/dist/cli/list.js.map +1 -0
  77. package/dist/cli/main.d.ts +13 -0
  78. package/dist/cli/main.d.ts.map +1 -0
  79. package/dist/cli/main.js +143 -0
  80. package/dist/cli/main.js.map +1 -0
  81. package/dist/cli/messages.d.ts +9 -0
  82. package/dist/cli/messages.d.ts.map +1 -0
  83. package/dist/cli/messages.js +13 -0
  84. package/dist/cli/messages.js.map +1 -0
  85. package/dist/cli/respond.d.ts +10 -0
  86. package/dist/cli/respond.d.ts.map +1 -0
  87. package/dist/cli/respond.js +23 -0
  88. package/dist/cli/respond.js.map +1 -0
  89. package/dist/cli/resume.d.ts +12 -0
  90. package/dist/cli/resume.d.ts.map +1 -0
  91. package/dist/cli/resume.js +21 -0
  92. package/dist/cli/resume.js.map +1 -0
  93. package/dist/cli/send.d.ts +9 -0
  94. package/dist/cli/send.d.ts.map +1 -0
  95. package/dist/cli/send.js +16 -0
  96. package/dist/cli/send.js.map +1 -0
  97. package/dist/cli/spawn.d.ts +14 -0
  98. package/dist/cli/spawn.d.ts.map +1 -0
  99. package/dist/cli/spawn.js +21 -0
  100. package/dist/cli/spawn.js.map +1 -0
  101. package/dist/cli/state.d.ts +7 -0
  102. package/dist/cli/state.d.ts.map +1 -0
  103. package/dist/cli/state.js +11 -0
  104. package/dist/cli/state.js.map +1 -0
  105. package/dist/cli/turn-complete.d.ts +8 -0
  106. package/dist/cli/turn-complete.d.ts.map +1 -0
  107. package/dist/cli/turn-complete.js +14 -0
  108. package/dist/cli/turn-complete.js.map +1 -0
  109. package/dist/cli/wait.d.ts +13 -0
  110. package/dist/cli/wait.d.ts.map +1 -0
  111. package/dist/cli/wait.js +17 -0
  112. package/dist/cli/wait.js.map +1 -0
  113. package/dist/compose.d.ts +81 -0
  114. package/dist/compose.d.ts.map +1 -0
  115. package/dist/compose.js +64 -0
  116. package/dist/compose.js.map +1 -0
  117. package/dist/errors.d.ts +250 -0
  118. package/dist/errors.d.ts.map +1 -0
  119. package/dist/errors.js +300 -0
  120. package/dist/errors.js.map +1 -0
  121. package/dist/index.d.ts +22 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +17 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/io/baseline.d.ts +53 -0
  126. package/dist/io/baseline.d.ts.map +1 -0
  127. package/dist/io/baseline.js +97 -0
  128. package/dist/io/baseline.js.map +1 -0
  129. package/dist/io/capture.d.ts +15 -0
  130. package/dist/io/capture.d.ts.map +1 -0
  131. package/dist/io/capture.js +13 -0
  132. package/dist/io/capture.js.map +1 -0
  133. package/dist/io/interrupt.d.ts +46 -0
  134. package/dist/io/interrupt.d.ts.map +1 -0
  135. package/dist/io/interrupt.js +60 -0
  136. package/dist/io/interrupt.js.map +1 -0
  137. package/dist/io/respond.d.ts +28 -0
  138. package/dist/io/respond.d.ts.map +1 -0
  139. package/dist/io/respond.js +33 -0
  140. package/dist/io/respond.js.map +1 -0
  141. package/dist/io/send.d.ts +44 -0
  142. package/dist/io/send.d.ts.map +1 -0
  143. package/dist/io/send.js +66 -0
  144. package/dist/io/send.js.map +1 -0
  145. package/dist/io/stabilize.d.ts +28 -0
  146. package/dist/io/stabilize.d.ts.map +1 -0
  147. package/dist/io/stabilize.js +20 -0
  148. package/dist/io/stabilize.js.map +1 -0
  149. package/dist/io/wait.d.ts +47 -0
  150. package/dist/io/wait.d.ts.map +1 -0
  151. package/dist/io/wait.js +117 -0
  152. package/dist/io/wait.js.map +1 -0
  153. package/dist/observe/incremental.d.ts +28 -0
  154. package/dist/observe/incremental.d.ts.map +1 -0
  155. package/dist/observe/incremental.js +57 -0
  156. package/dist/observe/incremental.js.map +1 -0
  157. package/dist/observe/observer.d.ts +86 -0
  158. package/dist/observe/observer.d.ts.map +1 -0
  159. package/dist/observe/observer.js +167 -0
  160. package/dist/observe/observer.js.map +1 -0
  161. package/dist/observe/session-observer.d.ts +49 -0
  162. package/dist/observe/session-observer.d.ts.map +1 -0
  163. package/dist/observe/session-observer.js +123 -0
  164. package/dist/observe/session-observer.js.map +1 -0
  165. package/dist/session/adopt.d.ts +52 -0
  166. package/dist/session/adopt.d.ts.map +1 -0
  167. package/dist/session/adopt.js +57 -0
  168. package/dist/session/adopt.js.map +1 -0
  169. package/dist/session/boot.d.ts +66 -0
  170. package/dist/session/boot.d.ts.map +1 -0
  171. package/dist/session/boot.js +216 -0
  172. package/dist/session/boot.js.map +1 -0
  173. package/dist/session/constants.d.ts +57 -0
  174. package/dist/session/constants.d.ts.map +1 -0
  175. package/dist/session/constants.js +54 -0
  176. package/dist/session/constants.js.map +1 -0
  177. package/dist/session/create.d.ts +88 -0
  178. package/dist/session/create.d.ts.map +1 -0
  179. package/dist/session/create.js +66 -0
  180. package/dist/session/create.js.map +1 -0
  181. package/dist/session/default-backend.d.ts +27 -0
  182. package/dist/session/default-backend.d.ts.map +1 -0
  183. package/dist/session/default-backend.js +58 -0
  184. package/dist/session/default-backend.js.map +1 -0
  185. package/dist/session/handle.d.ts +63 -0
  186. package/dist/session/handle.d.ts.map +1 -0
  187. package/dist/session/handle.js +284 -0
  188. package/dist/session/handle.js.map +1 -0
  189. package/dist/session/hooks.d.ts +37 -0
  190. package/dist/session/hooks.d.ts.map +1 -0
  191. package/dist/session/hooks.js +42 -0
  192. package/dist/session/hooks.js.map +1 -0
  193. package/dist/session/mutex.d.ts +15 -0
  194. package/dist/session/mutex.d.ts.map +1 -0
  195. package/dist/session/mutex.js +29 -0
  196. package/dist/session/mutex.js.map +1 -0
  197. package/dist/session/recover.d.ts +43 -0
  198. package/dist/session/recover.d.ts.map +1 -0
  199. package/dist/session/recover.js +45 -0
  200. package/dist/session/recover.js.map +1 -0
  201. package/dist/session/ref.d.ts +2 -0
  202. package/dist/session/ref.d.ts.map +1 -0
  203. package/dist/session/ref.js +5 -0
  204. package/dist/session/ref.js.map +1 -0
  205. package/dist/session/registry.d.ts +31 -0
  206. package/dist/session/registry.d.ts.map +1 -0
  207. package/dist/session/registry.js +32 -0
  208. package/dist/session/registry.js.map +1 -0
  209. package/dist/session/resolve.d.ts +30 -0
  210. package/dist/session/resolve.d.ts.map +1 -0
  211. package/dist/session/resolve.js +24 -0
  212. package/dist/session/resolve.js.map +1 -0
  213. package/dist/session/resume.d.ts +68 -0
  214. package/dist/session/resume.d.ts.map +1 -0
  215. package/dist/session/resume.js +54 -0
  216. package/dist/session/resume.js.map +1 -0
  217. package/dist/session/spawn-boot.d.ts +44 -0
  218. package/dist/session/spawn-boot.d.ts.map +1 -0
  219. package/dist/session/spawn-boot.js +87 -0
  220. package/dist/session/spawn-boot.js.map +1 -0
  221. package/dist/session/validate.d.ts +10 -0
  222. package/dist/session/validate.d.ts.map +1 -0
  223. package/dist/session/validate.js +0 -0
  224. package/dist/session/validate.js.map +1 -0
  225. package/dist/state/classifier.d.ts +29 -0
  226. package/dist/state/classifier.d.ts.map +1 -0
  227. package/dist/state/classifier.js +37 -0
  228. package/dist/state/classifier.js.map +1 -0
  229. package/dist/state/types.d.ts +32 -0
  230. package/dist/state/types.d.ts.map +1 -0
  231. package/dist/state/types.js +2 -0
  232. package/dist/state/types.js.map +1 -0
  233. package/dist/types.d.ts +401 -0
  234. package/dist/types.d.ts.map +1 -0
  235. package/dist/types.js +9 -0
  236. package/dist/types.js.map +1 -0
  237. package/dist/util/ansi.d.ts +14 -0
  238. package/dist/util/ansi.d.ts.map +1 -0
  239. package/dist/util/ansi.js +18 -0
  240. package/dist/util/ansi.js.map +1 -0
  241. package/dist/util/emitter.d.ts +17 -0
  242. package/dist/util/emitter.d.ts.map +1 -0
  243. package/dist/util/emitter.js +33 -0
  244. package/dist/util/emitter.js.map +1 -0
  245. package/dist/util/sleep.d.ts +8 -0
  246. package/dist/util/sleep.d.ts.map +1 -0
  247. package/dist/util/sleep.js +10 -0
  248. package/dist/util/sleep.js.map +1 -0
  249. package/package.json +50 -0
@@ -0,0 +1,86 @@
1
+ import type { AgentDef, HookEdge } from "../agents/types.js";
2
+ import type { Progress, State } from "../types.js";
3
+ /**
4
+ * Derive a {@link Progress} from the ordered hook edges + a transcript count.
5
+ * Pure — the testable heart of the Observer.
6
+ *
7
+ * `phase`: `stop` ⇒ `done`; else a tool in flight ⇒ `tool`; else the last edge
8
+ * being `tool-end` ⇒ `composing`; else ⇒ `prompt`; no edges ⇒ `unknown`.
9
+ * `toolInFlight`: net-open tools (`tool-start` count minus `tool-end` count).
10
+ */
11
+ export declare function deriveProgress(o: {
12
+ edges: readonly HookEdge[];
13
+ transcriptCount: number;
14
+ }): Progress;
15
+ /**
16
+ * The single fused belief about a session **right now** — the one owner of
17
+ * "what's true." Extends {@link Progress} with the pane-only facts hooks can't
18
+ * see (`interrupted`) and the edge timings `wait()` composes into a
19
+ * {@link import('../types.js').TurnOutcome} (`lastStopAt` = the turn-end trigger,
20
+ * `lastActivityAt` = a progress heartbeat). Pure: the caller supplies the
21
+ * already-classified pane (classification is the agent's job, fusion is ours).
22
+ */
23
+ export interface Belief extends Progress {
24
+ /** The pane shows an interrupted (ESC'd) turn — `wait()` maps this to `aborted`. */
25
+ readonly interrupted: boolean;
26
+ /** ms of the most recent turn-end (`stop`) edge, if any. */
27
+ readonly lastStopAt?: number;
28
+ /** ms of the most recent lifecycle edge of any kind — a liveness heartbeat. */
29
+ readonly lastActivityAt?: number;
30
+ }
31
+ /**
32
+ * Edges from the **current** session lifecycle only: everything from the latest
33
+ * `session-start` onward. The rendezvous is keyed by conversation id and *reused
34
+ * across resume*, so a crashed prior life leaves an unclosed `prompt-submit` (and
35
+ * a stale `session-start`) in the same file. Computing the belief over those
36
+ * poisons it (the false-`working`-after-resume bug). Resetting at the last
37
+ * `session-start` boundary is the one fix for that whole family. Edges are sorted
38
+ * by time, so the last `session-start` in the array is the most recent; with no
39
+ * `session-start` at all (a bare progress sequence) everything is kept.
40
+ */
41
+ export declare function currentLifeEdges(edges: readonly HookEdge[]): readonly HookEdge[];
42
+ /**
43
+ * Fuse the reliable signals + the pre-classified pane into one {@link Belief}.
44
+ * `state` precedence: pane-only modals (dialog/permission) → hook lifecycle
45
+ * (when the channel is live and a turn has happened) → pane (hooks silent/off).
46
+ * The hook-lifecycle branch carries one pane cross-check for its blind spot: a
47
+ * denied/abandoned tool leaves a dangling `tool-start` (no `tool-end`), so a
48
+ * settled idle pane overrides a stuck hook `working`. Only {@link currentLifeEdges}
49
+ * feed the belief, so a resumed session is never judged by its prior life's edges.
50
+ */
51
+ export declare function believe(o: {
52
+ edges: readonly HookEdge[];
53
+ transcriptCount: number;
54
+ /**
55
+ * The pre-classified pane. `nonEmpty` (does the captured frame carry any real,
56
+ * non-whitespace content) feeds the {@link Belief.agentChannelHealthy} drift
57
+ * canary — we only judge "all channels blind" against a pane that actually has
58
+ * content. Optional: absent ⇒ treated as empty (no drift judgment).
59
+ */
60
+ pane: {
61
+ state: State;
62
+ interrupted: boolean;
63
+ nonEmpty?: boolean;
64
+ };
65
+ /**
66
+ * Authoritative "this handle issued an interrupt not yet superseded by a send."
67
+ * An interrupt fires NO `stop` edge AND leaves the spinner's `esc to interrupt`
68
+ * frozen in scrollback (so the pane mis-classifies as `working`) — neither
69
+ * channel can tell a frozen spinner from a live one. The handle KNOWS, so this
70
+ * flag overrides both. The pane's "Interrupted" text is only a best-effort
71
+ * fallback for a *human* interrupt we didn't issue.
72
+ */
73
+ weInterrupted?: boolean;
74
+ }): Belief;
75
+ /**
76
+ * Read the hook rendezvous into ordered {@link HookEdge}s (chronological).
77
+ * Empty when hooks are off, the agent has no hook spec, or the file is absent
78
+ * — degrades, never throws. A FULL read: used by `bootSession` (a one-shot, not
79
+ * a hot path); the per-poll session path is the incremental
80
+ * {@link import('./session-observer.js').SessionObserver}.
81
+ */
82
+ export declare function readHookEdges(o: {
83
+ agent: AgentDef;
84
+ rendezvousPath: string;
85
+ }): HookEdge[];
86
+ //# sourceMappingURL=observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../../src/observe/observer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAwBnD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE;IAChC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,QAAQ,CAyCX;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,MAAO,SAAQ,QAAQ;IACtC,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ,EAAE,CAMhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE;IACzB,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACjE;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,MAAM,CA6CT;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,EAAE,CAUxF"}
@@ -0,0 +1,167 @@
1
+ import { readFileSync } from "node:fs";
2
+ /**
3
+ * The Observer — **agent-agnostic** fusion of the reliable observe signals into
4
+ * one belief. It knows nothing of any agent's transcript schema or hook
5
+ * vocabulary: it reads files by path and delegates every agent-specific bit to
6
+ * {@link AgentDef.transcript} / {@link AgentDef.hooks}, and takes the pane only
7
+ * as a pre-classified verdict. (grep-enforced: no jsonl/claude vocabulary here.)
8
+ *
9
+ * **Single owner of "what's true."** `state()` and `progress()` both defer to
10
+ * {@link believe}; `wait()` composes that belief with the patience policy. No
11
+ * caller forms its own belief from raw signals. Fusion precedence: pane-only
12
+ * modal states (dialog/permission) win (hooks can't see them) → else the hook
13
+ * lifecycle (the reliable channel) → else the pane (hooks silent/off).
14
+ */
15
+ function readLines(path) {
16
+ try {
17
+ return readFileSync(path, "utf8").split("\n");
18
+ }
19
+ catch {
20
+ return []; // absent/unreadable file → no signal (degrades, never throws)
21
+ }
22
+ }
23
+ /**
24
+ * Derive a {@link Progress} from the ordered hook edges + a transcript count.
25
+ * Pure — the testable heart of the Observer.
26
+ *
27
+ * `phase`: `stop` ⇒ `done`; else a tool in flight ⇒ `tool`; else the last edge
28
+ * being `tool-end` ⇒ `composing`; else ⇒ `prompt`; no edges ⇒ `unknown`.
29
+ * `toolInFlight`: net-open tools (`tool-start` count minus `tool-end` count).
30
+ */
31
+ export function deriveProgress(o) {
32
+ let open = 0;
33
+ for (const e of o.edges) {
34
+ if (e.event === "tool-start")
35
+ open += 1;
36
+ else if (e.event === "tool-end")
37
+ open = Math.max(0, open - 1);
38
+ }
39
+ const toolInFlight = open > 0;
40
+ const lifecycle = o.edges.filter((e) => e.event === "prompt-submit" ||
41
+ e.event === "tool-start" ||
42
+ e.event === "tool-end" ||
43
+ e.event === "stop");
44
+ const last = lifecycle[lifecycle.length - 1];
45
+ let phase;
46
+ if (last === undefined)
47
+ phase = "unknown";
48
+ else if (last.event === "stop")
49
+ phase = "done";
50
+ else if (toolInFlight)
51
+ phase = "tool";
52
+ else if (last.event === "tool-end")
53
+ phase = "composing";
54
+ else
55
+ phase = "prompt";
56
+ const hookChannelHealthy = o.edges.length > 0;
57
+ // Hook-derived state. The pane fallback (dialog / permission-prompt) is fused
58
+ // in by the state/wait consolidation; here state reflects only the reliable
59
+ // hook signal: working unless the turn has ended (or no signal at all).
60
+ const state = phase === "done" ? "idle" : phase === "unknown" ? "unknown" : "working";
61
+ // `agentChannelHealthy` is a FUSED (pane-aware) judgment — `believe()` computes
62
+ // the real value; from hooks/transcript alone there is no drift evidence, so the
63
+ // base is `true`. (deriveProgress is hook-only; the canary needs the pane.)
64
+ return {
65
+ phase,
66
+ toolInFlight,
67
+ transcriptCount: o.transcriptCount,
68
+ hookChannelHealthy,
69
+ agentChannelHealthy: true,
70
+ state,
71
+ };
72
+ }
73
+ /**
74
+ * Edges from the **current** session lifecycle only: everything from the latest
75
+ * `session-start` onward. The rendezvous is keyed by conversation id and *reused
76
+ * across resume*, so a crashed prior life leaves an unclosed `prompt-submit` (and
77
+ * a stale `session-start`) in the same file. Computing the belief over those
78
+ * poisons it (the false-`working`-after-resume bug). Resetting at the last
79
+ * `session-start` boundary is the one fix for that whole family. Edges are sorted
80
+ * by time, so the last `session-start` in the array is the most recent; with no
81
+ * `session-start` at all (a bare progress sequence) everything is kept.
82
+ */
83
+ export function currentLifeEdges(edges) {
84
+ let lastStart = -1;
85
+ for (let i = 0; i < edges.length; i++) {
86
+ if (edges[i]?.event === "session-start")
87
+ lastStart = i;
88
+ }
89
+ return lastStart < 0 ? edges : edges.slice(lastStart);
90
+ }
91
+ /**
92
+ * Fuse the reliable signals + the pre-classified pane into one {@link Belief}.
93
+ * `state` precedence: pane-only modals (dialog/permission) → hook lifecycle
94
+ * (when the channel is live and a turn has happened) → pane (hooks silent/off).
95
+ * The hook-lifecycle branch carries one pane cross-check for its blind spot: a
96
+ * denied/abandoned tool leaves a dangling `tool-start` (no `tool-end`), so a
97
+ * settled idle pane overrides a stuck hook `working`. Only {@link currentLifeEdges}
98
+ * feed the belief, so a resumed session is never judged by its prior life's edges.
99
+ */
100
+ export function believe(o) {
101
+ const edges = currentLifeEdges(o.edges);
102
+ const prog = deriveProgress({ edges, transcriptCount: o.transcriptCount });
103
+ // `weInterrupted` is authoritative; else the human-interrupt heuristic: the
104
+ // pane shows "Interrupted" AND is the post-interrupt draft (`unknown`), not a
105
+ // working/idle box (a new turn, or a resume replaying old "Interrupted" text).
106
+ const interrupted = o.weInterrupted === true || (o.pane.interrupted && o.pane.state === "unknown");
107
+ let state;
108
+ if (o.pane.state === "dialog" || o.pane.state === "permission-prompt") {
109
+ state = o.pane.state; // only the pane sees modals — they win
110
+ }
111
+ else if (interrupted) {
112
+ state = "unknown"; // aborted: hook phase is stale, pane is a draft (not idle)
113
+ }
114
+ else if (prog.hookChannelHealthy && prog.phase !== "unknown") {
115
+ // The reliable hook lifecycle (working / idle) — with ONE pane cross-check
116
+ // for its blind spot. A tool the consumer DENIES (or claude abandons) fires
117
+ // `tool-start` but never `tool-end`, so the hook phase stays `tool` → working
118
+ // forever though the turn is over. When the hooks say working but the pane
119
+ // has settled to a clean idle box, the turn actually ended — trust the pane.
120
+ // A genuinely in-flight tool never renders the idle box (it shows the
121
+ // "esc to interrupt" spinner), so this only ever fires on the dangling-tool
122
+ // case; any transient idle frame is filtered by wait()'s idle stabilization.
123
+ state = prog.state === "working" && o.pane.state === "idle" ? "idle" : prog.state;
124
+ }
125
+ else {
126
+ state = o.pane.state; // hooks silent (off, or no turn yet) → trust the pane
127
+ }
128
+ const lastStop = [...edges].reverse().find((e) => e.event === "stop");
129
+ const lastEdge = edges[edges.length - 1];
130
+ // Drift canary: against a non-empty pane, at least ONE channel must extract
131
+ // signal — a recognized pane state, a known interrupt, hook edges, or a parsed
132
+ // message. All blind at once ⇒ the agent's output format likely drifted.
133
+ const recognizedSomething = o.pane.state !== "unknown" ||
134
+ interrupted ||
135
+ prog.hookChannelHealthy ||
136
+ prog.transcriptCount > 0;
137
+ const agentChannelHealthy = o.pane.nonEmpty !== true || recognizedSomething;
138
+ return {
139
+ ...prog,
140
+ state,
141
+ interrupted,
142
+ agentChannelHealthy,
143
+ ...(lastStop === undefined ? {} : { lastStopAt: lastStop.at }),
144
+ ...(lastEdge === undefined ? {} : { lastActivityAt: lastEdge.at }),
145
+ };
146
+ }
147
+ /**
148
+ * Read the hook rendezvous into ordered {@link HookEdge}s (chronological).
149
+ * Empty when hooks are off, the agent has no hook spec, or the file is absent
150
+ * — degrades, never throws. A FULL read: used by `bootSession` (a one-shot, not
151
+ * a hot path); the per-poll session path is the incremental
152
+ * {@link import('./session-observer.js').SessionObserver}.
153
+ */
154
+ export function readHookEdges(o) {
155
+ const hooks = o.agent.hooks;
156
+ if (hooks === undefined)
157
+ return [];
158
+ const edges = [];
159
+ for (const line of readLines(o.rendezvousPath)) {
160
+ const edge = hooks.parseMarker(line);
161
+ if (edge !== null)
162
+ edges.push(edge);
163
+ }
164
+ edges.sort((a, b) => a.at - b.at);
165
+ return edges;
166
+ }
167
+ //# sourceMappingURL=observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.js","sourceRoot":"","sources":["../../src/observe/observer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvC;;;;;;;;;;;;GAYG;AAEH,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,8DAA8D;IAC3E,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,CAG9B;IACC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,KAAK,KAAK,YAAY;YAAE,IAAI,IAAI,CAAC,CAAC;aACnC,IAAI,CAAC,CAAC,KAAK,KAAK,UAAU;YAAE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC;IAE9B,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAC9B,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,KAAK,KAAK,eAAe;QAC3B,CAAC,CAAC,KAAK,KAAK,YAAY;QACxB,CAAC,CAAC,KAAK,KAAK,UAAU;QACtB,CAAC,CAAC,KAAK,KAAK,MAAM,CACrB,CAAC;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7C,IAAI,KAAwB,CAAC;IAC7B,IAAI,IAAI,KAAK,SAAS;QAAE,KAAK,GAAG,SAAS,CAAC;SACrC,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM;QAAE,KAAK,GAAG,MAAM,CAAC;SAC1C,IAAI,YAAY;QAAE,KAAK,GAAG,MAAM,CAAC;SACjC,IAAI,IAAI,CAAC,KAAK,KAAK,UAAU;QAAE,KAAK,GAAG,WAAW,CAAC;;QACnD,KAAK,GAAG,QAAQ,CAAC;IAEtB,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9C,8EAA8E;IAC9E,4EAA4E;IAC5E,wEAAwE;IACxE,MAAM,KAAK,GACT,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,gFAAgF;IAChF,iFAAiF;IACjF,4EAA4E;IAC5E,OAAO;QACL,KAAK;QACL,YAAY;QACZ,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,kBAAkB;QAClB,mBAAmB,EAAE,IAAI;QACzB,KAAK;KACN,CAAC;AACJ,CAAC;AAmBD;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA0B;IACzD,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,eAAe;YAAE,SAAS,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,CAmBvB;IACC,MAAM,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;IAC3E,4EAA4E;IAC5E,8EAA8E;IAC9E,+EAA+E;IAC/E,MAAM,WAAW,GACf,CAAC,CAAC,aAAa,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC;IACjF,IAAI,KAAY,CAAC;IACjB,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,mBAAmB,EAAE,CAAC;QACtE,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,uCAAuC;IAC/D,CAAC;SAAM,IAAI,WAAW,EAAE,CAAC;QACvB,KAAK,GAAG,SAAS,CAAC,CAAC,2DAA2D;IAChF,CAAC;SAAM,IAAI,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/D,2EAA2E;QAC3E,4EAA4E;QAC5E,8EAA8E;QAC9E,2EAA2E;QAC3E,6EAA6E;QAC7E,sEAAsE;QACtE,4EAA4E;QAC5E,6EAA6E;QAC7E,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACpF,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,sDAAsD;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;IACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,4EAA4E;IAC5E,+EAA+E;IAC/E,yEAAyE;IACzE,MAAM,mBAAmB,GACvB,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS;QAC1B,WAAW;QACX,IAAI,CAAC,kBAAkB;QACvB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;IAC3B,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,mBAAmB,CAAC;IAC5E,OAAO;QACL,GAAG,IAAI;QACP,KAAK;QACL,WAAW;QACX,mBAAmB;QACnB,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;QAC9D,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;KACnE,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,CAA8C;IAC1E,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { AgentDef } from "../agents/types.js";
2
+ import type { Message, State } from "../types.js";
3
+ import { type Belief } from "./observer.js";
4
+ /**
5
+ * The per-session stateful read core — the single owner of "what's true" with
6
+ * **bounded** reads. It holds incremental {@link TailReader}s over the hook
7
+ * rendezvous and the transcript, so each `state()`/`progress()`/`wait()` poll
8
+ * parses only the bytes appended since the last one (O(delta), not O(file)) — the
9
+ * fix for a long-lived session re-parsing its whole transcript every 150ms (F39).
10
+ *
11
+ * It accumulates the parsed edges + messages + ancestry graph across polls, and
12
+ * resolves the transcript path from the hook's own report (free — it's already
13
+ * in the edges) before falling back to the agent's locate. One per handle; every
14
+ * method that reads session state defers to it. (Boot still does a one-shot full
15
+ * read — it isn't a hot path.)
16
+ */
17
+ export declare class SessionObserver {
18
+ #private;
19
+ constructor(o: {
20
+ agent: AgentDef;
21
+ rendezvousPath?: string;
22
+ agentSessionId?: string;
23
+ });
24
+ /**
25
+ * The one fused {@link Belief}, given the caller's pre-classified pane and the
26
+ * handle's authoritative interrupt flag. Refreshes both channels first.
27
+ */
28
+ belief(pane: {
29
+ state: State;
30
+ interrupted: boolean;
31
+ nonEmpty?: boolean;
32
+ }, weInterrupted: boolean): Belief;
33
+ /**
34
+ * Is the transcript **addressable** — do we hold an `agentSessionId` to locate
35
+ * it by, or has a hook edge reported its path? This is addressability, NOT file
36
+ * existence: a fresh session with an id (transcript not flushed yet) is
37
+ * locatable and reads empty legitimately. `false` means we have NO handle on
38
+ * where the transcript lives (an adopt-miss, a non-claudemux session, or a fork
39
+ * before its first hook edge) — reads are blind, and the handle throws
40
+ * `TranscriptUnlocatable` rather than returning a deceptive empty.
41
+ */
42
+ transcriptLocatable(): boolean;
43
+ /** The accumulated messages + ancestry graph (for `messagesSince`/`turnComplete`). */
44
+ thread(): {
45
+ messages: readonly Message[];
46
+ parentOf: Map<string, string | undefined>;
47
+ };
48
+ }
49
+ //# sourceMappingURL=session-observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-observer.d.ts","sourceRoot":"","sources":["../../src/observe/session-observer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAW,MAAM,eAAe,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAe;;gBAYd,CAAC,EAAE;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE;IA0DpF;;;OAGG;IACH,MAAM,CACJ,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,EAChE,aAAa,EAAE,OAAO,GACrB,MAAM;IAWT;;;;;;;;OAQG;IACH,mBAAmB,IAAI,OAAO;IAM9B,sFAAsF;IACtF,MAAM,IAAI;QAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;KAAE;CAKtF"}
@@ -0,0 +1,123 @@
1
+ import { TailReader } from "./incremental.js";
2
+ import { believe } from "./observer.js";
3
+ /**
4
+ * The per-session stateful read core — the single owner of "what's true" with
5
+ * **bounded** reads. It holds incremental {@link TailReader}s over the hook
6
+ * rendezvous and the transcript, so each `state()`/`progress()`/`wait()` poll
7
+ * parses only the bytes appended since the last one (O(delta), not O(file)) — the
8
+ * fix for a long-lived session re-parsing its whole transcript every 150ms (F39).
9
+ *
10
+ * It accumulates the parsed edges + messages + ancestry graph across polls, and
11
+ * resolves the transcript path from the hook's own report (free — it's already
12
+ * in the edges) before falling back to the agent's locate. One per handle; every
13
+ * method that reads session state defers to it. (Boot still does a one-shot full
14
+ * read — it isn't a hot path.)
15
+ */
16
+ export class SessionObserver {
17
+ #agent;
18
+ #rendezvousPath;
19
+ #agentSessionId;
20
+ #rvReader = new TailReader();
21
+ #txReader = new TailReader();
22
+ #edges = [];
23
+ #messages = [];
24
+ #parentOf = new Map();
25
+ #txPath; // memoized once resolved
26
+ constructor(o) {
27
+ this.#agent = o.agent;
28
+ this.#rendezvousPath = o.rendezvousPath;
29
+ this.#agentSessionId = o.agentSessionId;
30
+ }
31
+ /** Incrementally fold new rendezvous lines into the cached edges. */
32
+ #refreshEdges() {
33
+ const path = this.#rendezvousPath;
34
+ const hooks = this.#agent.hooks;
35
+ if (path === undefined || hooks === undefined)
36
+ return;
37
+ const { reset, lines } = this.#rvReader.poll(path);
38
+ if (reset)
39
+ this.#edges = [];
40
+ for (const line of lines) {
41
+ const edge = hooks.parseMarker(line);
42
+ if (edge !== null)
43
+ this.#edges.push(edge);
44
+ }
45
+ }
46
+ /**
47
+ * The transcript path: the hook-reported one (from the edges we already hold —
48
+ * authoritative, no extra read) preferred over the agent's fragile locate.
49
+ * Memoized once non-undefined (it is stable for a session's lifetime).
50
+ */
51
+ #resolveTxPath() {
52
+ if (this.#txPath !== undefined)
53
+ return this.#txPath;
54
+ for (let i = this.#edges.length - 1; i >= 0; i--) {
55
+ const p = this.#edges[i]?.transcriptPath;
56
+ if (p !== undefined) {
57
+ this.#txPath = p;
58
+ return p;
59
+ }
60
+ }
61
+ const transcript = this.#agent.transcript;
62
+ if (transcript !== undefined && this.#agentSessionId !== undefined) {
63
+ this.#txPath = transcript.locate({ agentSessionId: this.#agentSessionId }) ?? undefined;
64
+ }
65
+ return this.#txPath;
66
+ }
67
+ /** Incrementally fold new transcript lines into the cached messages + graph. */
68
+ #refreshTranscript() {
69
+ const transcript = this.#agent.transcript;
70
+ const path = this.#resolveTxPath();
71
+ if (path === undefined || transcript === undefined)
72
+ return;
73
+ const { reset, lines } = this.#txReader.poll(path);
74
+ if (reset) {
75
+ this.#messages = [];
76
+ this.#parentOf = new Map();
77
+ }
78
+ for (const line of lines) {
79
+ const m = transcript.parseLine(line);
80
+ if (m !== null)
81
+ this.#messages.push(m);
82
+ const e = transcript.parseEdge?.(line);
83
+ if (e !== null && e !== undefined)
84
+ this.#parentOf.set(e.id, e.parentId);
85
+ }
86
+ }
87
+ /**
88
+ * The one fused {@link Belief}, given the caller's pre-classified pane and the
89
+ * handle's authoritative interrupt flag. Refreshes both channels first.
90
+ */
91
+ belief(pane, weInterrupted) {
92
+ this.#refreshEdges();
93
+ this.#refreshTranscript();
94
+ return believe({
95
+ edges: this.#edges,
96
+ transcriptCount: this.#messages.length,
97
+ pane,
98
+ weInterrupted,
99
+ });
100
+ }
101
+ /**
102
+ * Is the transcript **addressable** — do we hold an `agentSessionId` to locate
103
+ * it by, or has a hook edge reported its path? This is addressability, NOT file
104
+ * existence: a fresh session with an id (transcript not flushed yet) is
105
+ * locatable and reads empty legitimately. `false` means we have NO handle on
106
+ * where the transcript lives (an adopt-miss, a non-claudemux session, or a fork
107
+ * before its first hook edge) — reads are blind, and the handle throws
108
+ * `TranscriptUnlocatable` rather than returning a deceptive empty.
109
+ */
110
+ transcriptLocatable() {
111
+ if (this.#agentSessionId !== undefined)
112
+ return true;
113
+ this.#refreshEdges();
114
+ return this.#edges.some((e) => e.transcriptPath !== undefined);
115
+ }
116
+ /** The accumulated messages + ancestry graph (for `messagesSince`/`turnComplete`). */
117
+ thread() {
118
+ this.#refreshEdges(); // so the hook transcript-path is preferred on a first read
119
+ this.#refreshTranscript();
120
+ return { messages: this.#messages, parentOf: this.#parentOf };
121
+ }
122
+ }
123
+ //# sourceMappingURL=session-observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-observer.js","sourceRoot":"","sources":["../../src/observe/session-observer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { AgentDef } from "../agents/types.js";
2
+ import type { Backend } from "../backends/types.js";
3
+ import type { SessionHandle } from "../types.js";
4
+ /**
5
+ * Options for {@link adopt}. Mirrors {@link CreateOptions} minus the spawn-only
6
+ * fields (`cwd`, `extraArgs`, `env`, `bootTimeoutMs`, `trustWorkspace`) — adopt
7
+ * neither spawns nor boots.
8
+ */
9
+ export interface AdoptOptions {
10
+ /** Name of the session to re-adopt; must match the live session's name. */
11
+ name: string;
12
+ /** Namespace prefix (default: `"claudemux"`). Must match the live session's namespace. */
13
+ namespace?: string;
14
+ /**
15
+ * Agent definition controlling state/idle classification (default: claude).
16
+ * MUST be the same agent the original `create()` used — the classifier reads
17
+ * THIS agent's `rules`, not the session's. Passing the wrong agent silently
18
+ * misclassifies `state()`/`wait()`. See README §adopt.
19
+ */
20
+ agent?: AgentDef;
21
+ /** Backend the live session runs in (default: the process-wide shared default — stable socket). */
22
+ backend?: Backend;
23
+ }
24
+ /**
25
+ * Re-adopt a session that is already live but was created by another process —
26
+ * the mirror of {@link create}. Pure attach: no spawn, no boot, no dialog dismissal.
27
+ *
28
+ * After a successful adopt the consumer MUST call `state()` before driving the
29
+ * pane (covers wedged / mid-dialog). See README §adopt for the A/B/C recovery
30
+ * taxonomy and the single-writer invariant.
31
+ *
32
+ * @throws `InvalidSessionName` if `name`/`namespace` contain reserved characters
33
+ * (thrown before the exists-check).
34
+ * @throws `SessionGone` if no such session exists — incl. the whole backend
35
+ * server being down, which `exists()` reports as absence.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { adopt, SessionGone } from "claudemux";
40
+ * try {
41
+ * const session = await adopt({ name: "job" });
42
+ * await session.state(); // ALWAYS call state() before driving the pane
43
+ * } catch (err) {
44
+ * if (err instanceof SessionGone) {
45
+ * // the pane is gone — continue the conversation in a fresh one:
46
+ * // await resume({ name: "job-2", cwd, agentSessionId });
47
+ * }
48
+ * }
49
+ * ```
50
+ */
51
+ export declare function adopt(opts: AdoptOptions): Promise<SessionHandle>;
52
+ //# sourceMappingURL=adopt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adopt.d.ts","sourceRoot":"","sources":["../../src/session/adopt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,mGAAmG;IACnG,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CA0BtE"}
@@ -0,0 +1,57 @@
1
+ import { SessionGone } from "../errors.js";
2
+ import { AGENT_SESSION_ID_META_KEY } from "./constants.js";
3
+ import { attachHandle } from "./handle.js";
4
+ import { formatSessionLabel } from "./ref.js";
5
+ import { resolveSessionContext } from "./resolve.js";
6
+ /**
7
+ * Re-adopt a session that is already live but was created by another process —
8
+ * the mirror of {@link create}. Pure attach: no spawn, no boot, no dialog dismissal.
9
+ *
10
+ * After a successful adopt the consumer MUST call `state()` before driving the
11
+ * pane (covers wedged / mid-dialog). See README §adopt for the A/B/C recovery
12
+ * taxonomy and the single-writer invariant.
13
+ *
14
+ * @throws `InvalidSessionName` if `name`/`namespace` contain reserved characters
15
+ * (thrown before the exists-check).
16
+ * @throws `SessionGone` if no such session exists — incl. the whole backend
17
+ * server being down, which `exists()` reports as absence.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { adopt, SessionGone } from "claudemux";
22
+ * try {
23
+ * const session = await adopt({ name: "job" });
24
+ * await session.state(); // ALWAYS call state() before driving the pane
25
+ * } catch (err) {
26
+ * if (err instanceof SessionGone) {
27
+ * // the pane is gone — continue the conversation in a fresh one:
28
+ * // await resume({ name: "job-2", cwd, agentSessionId });
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export async function adopt(opts) {
34
+ const { ref, agent, backend } = resolveSessionContext(opts);
35
+ // Mirror of create()'s exists-check, inverted. adopt REQUIRES the session to
36
+ // be present; absence (including whole-server-down, which exists() collapses
37
+ // to false) is SessionGone — the symmetric counterpart to SessionExists.
38
+ if (!(await backend.exists(ref))) {
39
+ throw new SessionGone(formatSessionLabel(ref));
40
+ }
41
+ // Best-effort: recover the agent's conversation id from the session-meta the
42
+ // creating process cached. `undefined` on a miss (older/non-claudemux session,
43
+ // a creator that never wrote it, or a store read failure) — adopt never
44
+ // fabricates an id, it tells the truth and lets the consumer fall back to its
45
+ // own store. getSessionMeta already collapses "unreadable" to `undefined`.
46
+ const agentSessionId = await backend.getSessionMeta(ref, AGENT_SESSION_ID_META_KEY);
47
+ // Pure attach — no spawn, no boot, no dialog dismissal. The consumer MUST call
48
+ // state() after adopt to learn where the live pane stands.
49
+ return attachHandle({
50
+ backend,
51
+ agent,
52
+ namespace: ref.namespace,
53
+ name: ref.name,
54
+ ...(agentSessionId === undefined ? {} : { agentSessionId }),
55
+ });
56
+ }
57
+ //# sourceMappingURL=adopt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adopt.js","sourceRoot":"","sources":["../../src/session/adopt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAuBrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAkB;IAC5C,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAE5D,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,WAAW,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,wEAAwE;IACxE,8EAA8E;IAC9E,2EAA2E;IAC3E,MAAM,cAAc,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC;IAEpF,+EAA+E;IAC/E,2DAA2D;IAC3D,OAAO,YAAY,CAAC;QAClB,OAAO;QACP,KAAK;QACL,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;KAC5D,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { AgentDef } from "../agents/types.js";
2
+ import type { Backend, SessionRef } from "../backends/types.js";
3
+ /** Boot options threaded from `create`. */
4
+ export interface BootOptions {
5
+ /** Total boot budget (default 60s). */
6
+ timeoutMs?: number;
7
+ /**
8
+ * Opt in to auto-dismissing the agent's workspace-trust dialog. Default
9
+ * **false** — trusting a folder is an authority grant the substrate does
10
+ * not make for the caller. Without it, an untrusted-cwd trust dialog
11
+ * throws `WorkspaceUntrusted` before any keystroke is sent. See that
12
+ * error's TSDoc for the persistent/global-trust caveats.
13
+ */
14
+ trustWorkspace?: boolean;
15
+ /** The cwd being booted into — carried on `WorkspaceUntrusted` for the caller. */
16
+ cwd?: string;
17
+ /**
18
+ * The caller-chosen `agentSessionId` for this spawn, if any. Carried onto
19
+ * {@link AgentExitedDuringBoot} when the agent exits before ready, so the
20
+ * (overwhelmingly likely) collision case stays actionable. Omitted for a
21
+ * minted id — a v4 mint collides with ~zero probability, so attributing a
22
+ * minted-id boot-death to "id in use" would mislead.
23
+ */
24
+ agentSessionId?: string;
25
+ /**
26
+ * Path to the session's hook rendezvous file, when hooks were injected
27
+ * (default-on). When set, boot **gates** readiness on the agent's
28
+ * `SessionStart` hook edge appearing here: a ready-looking pane alone never
29
+ * declares ready until the edge fires. Verified against claude 2.1.162:
30
+ * `SessionStart` fires only *after* any boot dialog is dismissed, once input
31
+ * is interactive — so an edge can never signal ready while a dialog is up.
32
+ * After the edge, boot still waits for a *stable* `isReady` pane (the first
33
+ * send otherwise races the welcome/MCP render storm and is lost). Omitted
34
+ * under `create({ hooks: false })`, where the pane is the only ready signal.
35
+ */
36
+ rendezvousPath?: string;
37
+ }
38
+ /**
39
+ * Boot the session: dismiss any matching dialogs in order, then wait for ready.
40
+ *
41
+ * **Ready signal:** a hook *gate* plus a pane *settle*.
42
+ * 1. **`session-start` hook edge — the authoritative "started" gate.** With
43
+ * hooks on (the default, `opts.rendezvousPath` set), boot will not declare
44
+ * ready until this edge fires — a ready-*looking* pane is NOT trusted on
45
+ * its own (the founder's "hooks, not screen-scraping" north star). The edge
46
+ * lands only once input is interactive and post-dialog. With hooks off
47
+ * there is no edge, so the pane is the only signal.
48
+ * 2. **Stable ready box — the delivery-safety settle.** Even after "started,"
49
+ * a fresh REPL is still painting its welcome/MCP render, and the *first*
50
+ * send pasted into that render storm is silently lost (verified). So boot
51
+ * returns only once the ready box has held *stable*, guaranteeing the input
52
+ * is paintable. Dialogs are handled before either check each iteration.
53
+ *
54
+ * Throws on the documented failures.
55
+ *
56
+ * @throws `WorkspaceUntrusted` if the workspace-trust dialog fires and
57
+ * `trustWorkspace` was not set — thrown *before* any keystroke, so no
58
+ * persistent trust flag is written.
59
+ * @throws `LoginRequired` if the login-method dialog fires.
60
+ * @throws `DialogStuck` if a recognized dialog persists after its response.
61
+ * @throws `AgentExitedDuringBoot` if the agent process exits (its session is
62
+ * reaped) before becoming ready — most often an `agentSessionId` collision.
63
+ * @throws `ReplTimeout` if the total budget elapses before a stable ready.
64
+ */
65
+ export declare function bootSession(backend: Backend, agent: AgentDef, ref: SessionRef, opts?: BootOptions): Promise<void>;
66
+ //# sourceMappingURL=boot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.d.ts","sourceRoot":"","sources":["../../src/session/boot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,oBAAoB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AA6ChE,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,UAAU,EACf,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAmEf"}