@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,60 @@
1
+ import { sleep } from "../util/sleep.js";
2
+ /**
3
+ * Fixed best-effort settle after firing ESC, mirroring `send`'s post-submit
4
+ * window (`io/send.ts` / `io/baseline.ts`, ~the time for the input box to
5
+ * clear, typically ≤250ms). claude does not tear down the `"esc to interrupt"`
6
+ * affordance instantaneously; without this beat an *immediate* following
7
+ * `state()` can read the mid-interrupt frame and still report `working`.
8
+ *
9
+ * This is a FIXED delay, not a poll-until-not-working loop — "did the interrupt
10
+ * take" retry/backoff is the consumer's job. It never fails the interrupt.
11
+ */
12
+ const INTERRUPT_SETTLE_MS = 250;
13
+ /**
14
+ * Fire a single `Escape` at the pane — claude's own documented interrupt key
15
+ * (the classifier detects `working` by the literal `"esc to interrupt"`
16
+ * affordance, `agents/claude.ts`). ESC is already in `SendPayload`'s key union
17
+ * (`backends/types.ts`) and `sendKey` already sends it, so there is no backend
18
+ * change.
19
+ *
20
+ * ESC is sent **unconditionally** — no state-check guard. A guard would bake
21
+ * policy into the substrate and open a TOCTOU race (state read, then ESC, with
22
+ * the agent free to change state in between). ESC on an idle claude is harmless
23
+ * (it clears the input box). Gating on `state()` is the consumer's call. This
24
+ * is a mechanism, not a policy
25
+ * (`brain/decisions/0013-mechanism-not-policy-substrate-boundary.md`).
26
+ *
27
+ * That consumer-side gate is also not atomic with the ESC: a turn can finish
28
+ * between a `state()===working` read and the ESC landing — most easily across
29
+ * separate CLI processes (a short turn completes in the gap), so the ESC hits
30
+ * an already-idle agent. That is a harmless no-op, not a failure; a consumer
31
+ * that needs the interrupt to catch a turn should read `state()` and call this
32
+ * in one tight in-process sequence, not trust a stale prior-process reading.
33
+ *
34
+ * Blocks on **write delivery** plus a brief fixed settle ({@link
35
+ * INTERRUPT_SETTLE_MS}); it guarantees ESC was delivered, NOT that an in-flight
36
+ * abort has fully completed. This verb does exactly one named action — stop the
37
+ * turn — and nothing more (`brain/decisions/0013`, "a primitive does exactly the
38
+ * keystroke it names").
39
+ *
40
+ * **After interrupt(), claude does NOT return to a clean idle prompt.** It
41
+ * restores the interrupted message back into the composer, and the classifier
42
+ * reads that frame as `unknown` (never `idle`, never `working`). Two
43
+ * consequences the consumer must know:
44
+ * - `wait()` after interrupt() resolves `{ kind: "aborted" }` immediately (the
45
+ * handle records the interrupt authoritatively) — it does NOT hang waiting for
46
+ * an idle that won't come.
47
+ * - Do **not** naively `send()` a replacement after interrupt(): `send`
48
+ * pastes into the *non-empty* composer (the restored message), so the
49
+ * submission is the two texts concatenated. For a clean "interrupt and
50
+ * replace" the composer must first be cleared to empty. claude's only
51
+ * substrate-reachable composer clear is repeated ESC (its "Esc again to
52
+ * clear" ladder), so the recipe is consumer-composed and claude-specific —
53
+ * see the README "Interrupting a working agent" note. It is deliberately
54
+ * NOT bundled into this agent-agnostic verb.
55
+ */
56
+ export async function interruptOnce(backend, ref) {
57
+ await backend.send(ref, { kind: "key", key: "Escape" });
58
+ await sleep(INTERRUPT_SETTLE_MS);
59
+ }
60
+ //# sourceMappingURL=interrupt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt.js","sourceRoot":"","sources":["../../src/io/interrupt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,GAAe;IACnE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxD,MAAM,KAAK,CAAC,mBAAmB,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Backend, SendPayload } from "../backends/types.js";
2
+ /** The keystroke a `key` payload can carry (the agent picks which one). */
3
+ type KeyName = Extract<SendPayload, {
4
+ kind: "key";
5
+ }>["key"];
6
+ /**
7
+ * Answer an agent prompt: fire the single keystroke that selects the choice,
8
+ * then **self-confirm** the prompt cleared before returning — the analog of
9
+ * {@link import('./send.js').sendOnce} anchoring its own user record.
10
+ *
11
+ * Without the confirm, the call returns while the menu is still painted (the
12
+ * agent repaints prompt→working only after processing the key), so a following
13
+ * `wait()` latches the STALE prompt and reports `awaiting` again. The reliable
14
+ * signal is SEMANTIC — `stillPrompted()` (the caller's belief check) returning
15
+ * false — NOT a raw pane diff: a cursor blink between two captures fires a
16
+ * spurious change before the key takes effect (the race this exists to close).
17
+ *
18
+ * The mechanism owns *delivery + settle*; it stays agnostic of (a) which key
19
+ * means what — the agent maps choice→key and hands `key` in — and (b) how
20
+ * "still prompted" is decided — the caller injects `stillPrompted` (the
21
+ * Observer's belief). Bounded + best-effort: if the prompt never clears (a
22
+ * second prompt stacked, or the key was dropped) it returns anyway and the
23
+ * caller's next `wait()` re-reads. It does exactly the one keystroke it names
24
+ * (`brain/decisions/0013` — a primitive does exactly the keystroke it names).
25
+ */
26
+ export declare function respondOnce(backend: Backend, ref: Parameters<Backend["send"]>[0], key: KeyName, stillPrompted: () => Promise<boolean>): Promise<void>;
27
+ export {};
28
+ //# sourceMappingURL=respond.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"respond.d.ts","sourceRoot":"","sources":["../../src/io/respond.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGjE,2EAA2E;AAC3E,KAAK,OAAO,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC,KAAK,CAAC,CAAC;AAM5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EACnC,GAAG,EAAE,OAAO,EACZ,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,33 @@
1
+ import { sleep } from "../util/sleep.js";
2
+ /** Confirm-window: poll up to ~5s for the answered prompt to clear. */
3
+ const CONFIRM_POLLS = 20;
4
+ const CONFIRM_POLL_MS = 250;
5
+ /**
6
+ * Answer an agent prompt: fire the single keystroke that selects the choice,
7
+ * then **self-confirm** the prompt cleared before returning — the analog of
8
+ * {@link import('./send.js').sendOnce} anchoring its own user record.
9
+ *
10
+ * Without the confirm, the call returns while the menu is still painted (the
11
+ * agent repaints prompt→working only after processing the key), so a following
12
+ * `wait()` latches the STALE prompt and reports `awaiting` again. The reliable
13
+ * signal is SEMANTIC — `stillPrompted()` (the caller's belief check) returning
14
+ * false — NOT a raw pane diff: a cursor blink between two captures fires a
15
+ * spurious change before the key takes effect (the race this exists to close).
16
+ *
17
+ * The mechanism owns *delivery + settle*; it stays agnostic of (a) which key
18
+ * means what — the agent maps choice→key and hands `key` in — and (b) how
19
+ * "still prompted" is decided — the caller injects `stillPrompted` (the
20
+ * Observer's belief). Bounded + best-effort: if the prompt never clears (a
21
+ * second prompt stacked, or the key was dropped) it returns anyway and the
22
+ * caller's next `wait()` re-reads. It does exactly the one keystroke it names
23
+ * (`brain/decisions/0013` — a primitive does exactly the keystroke it names).
24
+ */
25
+ export async function respondOnce(backend, ref, key, stillPrompted) {
26
+ await backend.send(ref, { kind: "key", key });
27
+ for (let i = 0; i < CONFIRM_POLLS; i++) {
28
+ if (!(await stillPrompted()))
29
+ return;
30
+ await sleep(CONFIRM_POLL_MS);
31
+ }
32
+ }
33
+ //# sourceMappingURL=respond.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"respond.js","sourceRoot":"","sources":["../../src/io/respond.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAKzC,uEAAuE;AACvE,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,GAAmC,EACnC,GAAY,EACZ,aAAqC;IAErC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,MAAM,aAAa,EAAE,CAAC;YAAE,OAAO;QACrC,MAAM,KAAK,CAAC,eAAe,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { AgentDef } from "../agents/types.js";
2
+ import type { Backend, SessionRef } from "../backends/types.js";
3
+ /**
4
+ * Deliver `text` as one logical user turn: a `paste` of the body, then a
5
+ * separate `Enter`. The two backend calls happen sequentially — there is
6
+ * no path by which the paste body can self-submit.
7
+ *
8
+ * Blocks on **write delivery** (and a short post-submit settle, below), not on
9
+ * the agent's response. Callers who want to know when the agent is done should
10
+ * call `wait()` next.
11
+ *
12
+ * **Post-submit baseline.** After delivery, `send` records a fingerprint of
13
+ * the post-submit pane (see {@link captureSendBaseline}) under a session-scoped
14
+ * key. This lets a *stateless* `wait()` — the CLI reattaches in a fresh process
15
+ * — detect a turn that completed before its first poll, instead of hanging to
16
+ * `ReplTimeout` (bug 8a500a52). It adds a brief settle (~the time for the input
17
+ * box to clear, typically ≤250ms) to `send`; it is best-effort and never fails
18
+ * the send.
19
+ *
20
+ * @remarks
21
+ * The two-call sequence (paste-then-Enter) is load-bearing per
22
+ * `docs/decisions/0001-tmux-paste-mechanism.md` — bracketed paste lets the
23
+ * receiver distinguish typed `\n`
24
+ * (submit) from pasted `\n` (literal newline). Folding submission into the
25
+ * paste body would re-introduce the per-line-submit failure mode.
26
+ */
27
+ export declare function sendOnce(backend: Backend, agent: AgentDef, ref: SessionRef, text: string): Promise<void>;
28
+ /**
29
+ * Re-fire the submit key (Enter) alone — the recovery for a **lost submit**: the
30
+ * paste reached the composer but the Enter keystroke didn't register, so the turn
31
+ * sits there un-submitted and no user record is ever written.
32
+ *
33
+ * Critically it does NOT re-paste the body. A re-`send()` would paste the text a
34
+ * second time and submit `texttext`; pressing Enter only submits whatever the
35
+ * composer already holds, so the recovery can never duplicate content. On an
36
+ * already-empty composer (a genuine non-delivery, not a lost Enter) it is a
37
+ * harmless no-op — the agent ignores an empty submit.
38
+ *
39
+ * Mechanism, not policy: this owns only the keystroke. The caller decides *when*
40
+ * to use it (its own user record never appeared AND the message wasn't queued)
41
+ * and re-checks delivery afterward — this does not poll or confirm.
42
+ */
43
+ export declare function submitOnce(backend: Backend, ref: SessionRef): Promise<void>;
44
+ //# sourceMappingURL=send.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/io/send.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIhE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjF"}
@@ -0,0 +1,66 @@
1
+ import { CLASSIFIER_CAPTURE } from "../session/constants.js";
2
+ import { captureSendBaseline, writeSendBaseline } from "./baseline.js";
3
+ /**
4
+ * Deliver `text` as one logical user turn: a `paste` of the body, then a
5
+ * separate `Enter`. The two backend calls happen sequentially — there is
6
+ * no path by which the paste body can self-submit.
7
+ *
8
+ * Blocks on **write delivery** (and a short post-submit settle, below), not on
9
+ * the agent's response. Callers who want to know when the agent is done should
10
+ * call `wait()` next.
11
+ *
12
+ * **Post-submit baseline.** After delivery, `send` records a fingerprint of
13
+ * the post-submit pane (see {@link captureSendBaseline}) under a session-scoped
14
+ * key. This lets a *stateless* `wait()` — the CLI reattaches in a fresh process
15
+ * — detect a turn that completed before its first poll, instead of hanging to
16
+ * `ReplTimeout` (bug 8a500a52). It adds a brief settle (~the time for the input
17
+ * box to clear, typically ≤250ms) to `send`; it is best-effort and never fails
18
+ * the send.
19
+ *
20
+ * @remarks
21
+ * The two-call sequence (paste-then-Enter) is load-bearing per
22
+ * `docs/decisions/0001-tmux-paste-mechanism.md` — bracketed paste lets the
23
+ * receiver distinguish typed `\n`
24
+ * (submit) from pasted `\n` (literal newline). Folding submission into the
25
+ * paste body would re-introduce the per-line-submit failure mode.
26
+ */
27
+ export async function sendOnce(backend, agent, ref, text) {
28
+ // Pre-send pane: the baseline poll uses it to skip the previous idle and
29
+ // capture the *post-submit* frame instead. Best-effort — if it fails, the
30
+ // baseline is simply not recorded and wait falls back to working-arm.
31
+ let pre;
32
+ try {
33
+ pre = await backend.capture(ref, CLASSIFIER_CAPTURE);
34
+ }
35
+ catch {
36
+ pre = undefined;
37
+ }
38
+ await backend.send(ref, { kind: "paste", text });
39
+ await backend.send(ref, { kind: "key", key: "Enter" });
40
+ // Always (over)write — this turn's baseline replaces any prior turn's, and
41
+ // an empty value clears it when we couldn't establish a fresh one (e.g. the
42
+ // pre-send capture failed). Otherwise a later wait could arm on a *stale*
43
+ // fingerprint and return the previous turn's idle. Empty == "no baseline",
44
+ // so wait falls back to arming on an observed working frame.
45
+ const fingerprint = await captureSendBaseline(backend, agent, ref, pre);
46
+ await writeSendBaseline(backend, ref, fingerprint ?? "");
47
+ }
48
+ /**
49
+ * Re-fire the submit key (Enter) alone — the recovery for a **lost submit**: the
50
+ * paste reached the composer but the Enter keystroke didn't register, so the turn
51
+ * sits there un-submitted and no user record is ever written.
52
+ *
53
+ * Critically it does NOT re-paste the body. A re-`send()` would paste the text a
54
+ * second time and submit `texttext`; pressing Enter only submits whatever the
55
+ * composer already holds, so the recovery can never duplicate content. On an
56
+ * already-empty composer (a genuine non-delivery, not a lost Enter) it is a
57
+ * harmless no-op — the agent ignores an empty submit.
58
+ *
59
+ * Mechanism, not policy: this owns only the keystroke. The caller decides *when*
60
+ * to use it (its own user record never appeared AND the message wasn't queued)
61
+ * and re-checks delivery afterward — this does not poll or confirm.
62
+ */
63
+ export async function submitOnce(backend, ref) {
64
+ await backend.send(ref, { kind: "key", key: "Enter" });
65
+ }
66
+ //# sourceMappingURL=send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/io/send.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,OAAgB,EAChB,KAAe,EACf,GAAe,EACf,IAAY;IAEZ,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,IAAI,GAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAEvD,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,6DAA6D;IAC7D,MAAM,WAAW,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IACxE,MAAM,iBAAiB,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAgB,EAAE,GAAe;IAChE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Backend, SessionRef } from "../backends/types.js";
2
+ /**
3
+ * "Pane unchanged for windowMs" probe. Captures the bottom-N pane region at
4
+ * `pollMs` intervals; returns `{ stable: true, text }` once the captured
5
+ * text has not changed across an entire `windowMs` window, or
6
+ * `{ stable: false, reason: "timeout" }` once `timeoutMs` elapses.
7
+ *
8
+ * Shared by `session/boot.ts` (post-dialog advancement check) and `io/wait.ts`
9
+ * (idle stabilization).
10
+ */
11
+ export interface StabilizeResult {
12
+ stable: boolean;
13
+ text: string;
14
+ reason?: "timeout";
15
+ }
16
+ export declare function stabilize(backend: Backend, ref: SessionRef, opts: {
17
+ /** Bottom-N lines to capture. */
18
+ lines: number;
19
+ /** How long the pane must stay identical to declare stable. */
20
+ windowMs: number;
21
+ /** Capture interval. */
22
+ pollMs: number;
23
+ /** Overall budget; returns `{ stable: false, reason: "timeout" }` past it. */
24
+ timeoutMs: number;
25
+ /** Capture with ANSI styling on (so the returned `text` feeds idle checks). */
26
+ ansi?: boolean;
27
+ }): Promise<StabilizeResult>;
28
+ //# sourceMappingURL=stabilize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stabilize.d.ts","sourceRoot":"","sources":["../../src/io/stabilize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGhE;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE;IACJ,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GACA,OAAO,CAAC,eAAe,CAAC,CAkB1B"}
@@ -0,0 +1,20 @@
1
+ import { sleep } from "../util/sleep.js";
2
+ export async function stabilize(backend, ref, opts) {
3
+ const start = Date.now();
4
+ const capOpts = { lines: opts.lines, ...(opts.ansi === true ? { ansi: true } : {}) };
5
+ let lastText = await backend.capture(ref, capOpts);
6
+ let unchangedSince = Date.now();
7
+ while (Date.now() - start < opts.timeoutMs) {
8
+ if (Date.now() - unchangedSince >= opts.windowMs) {
9
+ return { stable: true, text: lastText };
10
+ }
11
+ await sleep(opts.pollMs);
12
+ const now = await backend.capture(ref, capOpts);
13
+ if (now !== lastText) {
14
+ lastText = now;
15
+ unchangedSince = Date.now();
16
+ }
17
+ }
18
+ return { stable: false, text: lastText, reason: "timeout" };
19
+ }
20
+ //# sourceMappingURL=stabilize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stabilize.js","sourceRoot":"","sources":["../../src/io/stabilize.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAiBzC,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAgB,EAChB,GAAe,EACf,IAWC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACrF,IAAI,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACnD,IAAI,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEhC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC1C,CAAC;QACD,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAChD,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,QAAQ,GAAG,GAAG,CAAC;YACf,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { Backend, SessionRef } from "../backends/types.js";
2
+ import type { Belief } from "../observe/observer.js";
3
+ import type { ReadyOpts, TurnOutcome } from "../types.js";
4
+ import type { stabilize as defaultStabilize } from "./stabilize.js";
5
+ interface WaitDeps {
6
+ stabilize: typeof defaultStabilize;
7
+ }
8
+ /**
9
+ * Reads the fused {@link Belief} for one poll, plus the raw pane text the
10
+ * fingerprint/arming need. Injected by the handle so `wait()` shares the handle's
11
+ * **incremental** observer (bounded per-poll reads) and never re-reads files or
12
+ * touches the agent itself.
13
+ */
14
+ export type BeliefReader = () => Promise<{
15
+ belief: Belief;
16
+ paneText: string;
17
+ }>;
18
+ /**
19
+ * Wait until the turn reaches a terminal {@link TurnOutcome} — the **compound
20
+ * owner** of "the turn stopped, and why." It composes two atomic sub-owners and
21
+ * re-derives neither's internals:
22
+ * - the **observe** sub-owner (the injected {@link BeliefReader}: hooks +
23
+ * transcript + pane) yields `completed` / `awaiting` / `aborted`;
24
+ * - the **policy** sub-owner — the CONSUMER's patience ({@link ReadyOpts}) —
25
+ * yields `budget-exceeded`: `reason:"max"` (wall-clock `maxMs`) vs `"idle"`
26
+ * (no progress for `idleMs`). The library owns NO patience: with neither
27
+ * bound supplied, `wait()` blocks until a terminal belief (it never invents a
28
+ * deadline; "time is the policy's").
29
+ *
30
+ * **Completion is hook-first, flush-safe.** The `stop` hook edge is the reliable
31
+ * "turn ended" trigger; but the edge fires ~100ms before the transcript flushes
32
+ * the reply, so `completed` is only declared once the pane has *also* settled to
33
+ * a stable idle box — which trails the flush — guaranteeing a following
34
+ * `messagesSince(cursor)` is race-free. With hooks off there is no edge, so a
35
+ * pane-idle that has *armed* (left idle, or diverged from the post-submit
36
+ * baseline) is the completion signal instead — the stateless-CLI fast-turn path
37
+ * (bug 8a500a52). Either way, the previous turn's lingering idle never counts:
38
+ * `completed` requires a `stop` edge newer than this wait, or a fresh arm.
39
+ *
40
+ * `dialog`/`permission-prompt`/`aborted` are actionable and return immediately
41
+ * (no settle). Never throws on timeout: a budget overrun is a returned
42
+ * `budget-exceeded`, not an exception. (A capture failure inside the reader DOES
43
+ * propagate — a gone session is terminal, not a budget matter.)
44
+ */
45
+ export declare function waitForOutcome(backend: Backend, ref: SessionRef, opts: ReadyOpts, deps: WaitDeps, readBelief: BeliefReader): Promise<TurnOutcome>;
46
+ export {};
47
+ //# sourceMappingURL=wait.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../../src/io/wait.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1D,OAAO,KAAK,EAAmB,SAAS,IAAI,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAQrF,UAAU,QAAQ;IAChB,SAAS,EAAE,OAAO,gBAAgB,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,SAAS,EAGf,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,YAAY,GACvB,OAAO,CAAC,WAAW,CAAC,CAkFtB"}
@@ -0,0 +1,117 @@
1
+ import { CLASSIFIER_BOTTOM_N } from "../session/constants.js";
2
+ import { sleep } from "../util/sleep.js";
3
+ import { paneFingerprint, readSendBaseline } from "./baseline.js";
4
+ /** How long the idle box must hold steady before "completed" (guards mid-stream returns). */
5
+ const IDLE_STABLE_WINDOW_MS = 250;
6
+ /** Polling cadence while waiting. */
7
+ const POLL_MS = 150;
8
+ /**
9
+ * Wait until the turn reaches a terminal {@link TurnOutcome} — the **compound
10
+ * owner** of "the turn stopped, and why." It composes two atomic sub-owners and
11
+ * re-derives neither's internals:
12
+ * - the **observe** sub-owner (the injected {@link BeliefReader}: hooks +
13
+ * transcript + pane) yields `completed` / `awaiting` / `aborted`;
14
+ * - the **policy** sub-owner — the CONSUMER's patience ({@link ReadyOpts}) —
15
+ * yields `budget-exceeded`: `reason:"max"` (wall-clock `maxMs`) vs `"idle"`
16
+ * (no progress for `idleMs`). The library owns NO patience: with neither
17
+ * bound supplied, `wait()` blocks until a terminal belief (it never invents a
18
+ * deadline; "time is the policy's").
19
+ *
20
+ * **Completion is hook-first, flush-safe.** The `stop` hook edge is the reliable
21
+ * "turn ended" trigger; but the edge fires ~100ms before the transcript flushes
22
+ * the reply, so `completed` is only declared once the pane has *also* settled to
23
+ * a stable idle box — which trails the flush — guaranteeing a following
24
+ * `messagesSince(cursor)` is race-free. With hooks off there is no edge, so a
25
+ * pane-idle that has *armed* (left idle, or diverged from the post-submit
26
+ * baseline) is the completion signal instead — the stateless-CLI fast-turn path
27
+ * (bug 8a500a52). Either way, the previous turn's lingering idle never counts:
28
+ * `completed` requires a `stop` edge newer than this wait, or a fresh arm.
29
+ *
30
+ * `dialog`/`permission-prompt`/`aborted` are actionable and return immediately
31
+ * (no settle). Never throws on timeout: a budget overrun is a returned
32
+ * `budget-exceeded`, not an exception. (A capture failure inside the reader DOES
33
+ * propagate — a gone session is terminal, not a budget matter.)
34
+ */
35
+ export async function waitForOutcome(backend, ref, opts,
36
+ // Both required — the handle (and tests) always inject them. A throwing default
37
+ // would just move a programmer error from compile time to runtime.
38
+ deps, readBelief) {
39
+ // The CONSUMER's patience — both optional, NO library default. `timeoutMs` is
40
+ // a deprecated alias for `maxMs`. With neither set, `wait()` owns no deadline
41
+ // and blocks until a terminal belief ("time is the policy's").
42
+ const maxMs = opts.maxMs ?? opts.timeoutMs;
43
+ const idleMs = opts.idleMs;
44
+ const start = Date.now();
45
+ // "armed" = evidence this turn ran (pane left idle, or diverged from the
46
+ // post-submit baseline) — needed for the hooks-off path; the hook `stop` edge
47
+ // makes it moot when hooks are healthy.
48
+ let armed = false;
49
+ let lastProgressAt = start;
50
+ let lastSignature = "";
51
+ const baseline = await readSendBaseline(backend, ref);
52
+ while (true) {
53
+ const now = Date.now();
54
+ // ── observe sub-owner: the one belief (incremental, via the handle) ──────
55
+ const { belief, paneText } = await readBelief();
56
+ // ── terminal verdicts the observe sub-owner already settles ─────────────
57
+ if (belief.state === "dialog")
58
+ return { kind: "awaiting", on: "dialog" };
59
+ if (belief.state === "permission-prompt")
60
+ return { kind: "awaiting", on: "permission-prompt" };
61
+ if (belief.interrupted)
62
+ return { kind: "aborted" };
63
+ // ── progress heartbeat (drives stuck detection) ─────────────────────────
64
+ const signature = `${belief.lastActivityAt ?? 0}|${belief.transcriptCount}|${paneFingerprint(paneText)}`;
65
+ if (signature !== lastSignature) {
66
+ lastSignature = signature;
67
+ lastProgressAt = now;
68
+ }
69
+ // ── completion: a hook done-edge OR an armed pane-idle, confirmed settled ─
70
+ // KNOWN LIMITATION (F42, single-host assumption): `lastStopAt` is the hook's
71
+ // own clock (`date +%s.%N` on the SESSION's host); `start` is the consumer's
72
+ // `Date.now()`. They agree only on one host / one clock. Under cross-host
73
+ // clock skew (a distributed deployment driving a remote agent), a real `stop`
74
+ // can read as before/after this wait → completion mis-fires; the pane-idle arm
75
+ // path still recovers it, but the reliable hook trigger is degraded. The fix
76
+ // is to baseline the stop-edge ORDER/count at wait-start instead of comparing
77
+ // wall-clocks (the S9 pattern); deferred until a distributed consumer needs it.
78
+ const hookDone = belief.lastStopAt !== undefined && belief.lastStopAt >= start;
79
+ if (belief.state !== "idle")
80
+ armed = true;
81
+ else if (baseline !== undefined && paneFingerprint(paneText) !== baseline)
82
+ armed = true;
83
+ if (belief.state === "idle" && (hookDone || armed)) {
84
+ // The stabilize debounce is a transport detail (legitimately the library's,
85
+ // per the read/write-split RFC); cap it by any remaining wall-clock budget.
86
+ const remaining = maxMs === undefined ? IDLE_STABLE_WINDOW_MS * 4 : Math.max(0, maxMs - (now - start));
87
+ const r = await deps.stabilize(backend, ref, {
88
+ lines: CLASSIFIER_BOTTOM_N,
89
+ windowMs: IDLE_STABLE_WINDOW_MS,
90
+ pollMs: POLL_MS,
91
+ timeoutMs: Math.min(remaining, IDLE_STABLE_WINDOW_MS * 4),
92
+ ansi: true,
93
+ });
94
+ // Re-confirm via the belief (the one owner), not raw pane rules.
95
+ if (r.stable && (await readBelief()).belief.state === "idle")
96
+ return { kind: "completed" };
97
+ }
98
+ // ── policy sub-owner: the CONSUMER's patience (the library owns none) ─────
99
+ // Wall-clock cap.
100
+ if (maxMs !== undefined && now - start > maxMs)
101
+ return { kind: "budget-exceeded", reason: "max" };
102
+ // No-progress cap. Gated on `state==="unknown" && !toolInFlight` so it means
103
+ // "stuck too long," never "still working too long": a `working` pane (the live
104
+ // `esc to interrupt` spinner) or a tool in flight is never counted as idle,
105
+ // and the heartbeat keys on the pane fingerprint so a still-animating spinner
106
+ // keeps resetting it even when a frame classifies `unknown`. The THRESHOLD is
107
+ // the consumer's (`idleMs`); the library only distinguishes stuck-from-working.
108
+ if (idleMs !== undefined &&
109
+ belief.state === "unknown" &&
110
+ !belief.toolInFlight &&
111
+ now - lastProgressAt > idleMs) {
112
+ return { kind: "budget-exceeded", reason: "idle" };
113
+ }
114
+ await sleep(POLL_MS);
115
+ }
116
+ }
117
+ //# sourceMappingURL=wait.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait.js","sourceRoot":"","sources":["../../src/io/wait.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * An incremental line reader for an **append-only** file (a hook rendezvous or a
3
+ * transcript). Each {@link poll} reads only the bytes appended since the last
4
+ * call and returns the new *complete* lines — so a long-lived session's
5
+ * `progress()`/`wait()` re-parse O(delta) per poll instead of O(file) (the
6
+ * unbounded-growth trap, F39). A trailing partial line (mid-flush write) is held
7
+ * back and completed on the next poll.
8
+ *
9
+ * Correct ONLY for append-only files: it trusts that bytes before its offset
10
+ * never change. If the file **shrinks** (truncation / rotation / an overwrite to
11
+ * a shorter body) it resets and re-reads from the start, signalling `reset` so
12
+ * the caller can discard whatever it accumulated. A claude transcript/rendezvous
13
+ * is append-only by construction (compaction summarizes the context window, it
14
+ * never rewrites the log), so the reset path is purely defensive.
15
+ */
16
+ export declare class TailReader {
17
+ #private;
18
+ /**
19
+ * The new complete lines since the last poll. `reset: true` means the file
20
+ * shrank and `lines` is the WHOLE file again — the caller must drop its
21
+ * accumulated state first. Absent/unreadable file → no change (`[]`).
22
+ */
23
+ poll(path: string): {
24
+ reset: boolean;
25
+ lines: string[];
26
+ };
27
+ }
28
+ //# sourceMappingURL=incremental.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental.d.ts","sourceRoot":"","sources":["../../src/observe/incremental.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAU;;IAIrB;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE;CA6BxD"}
@@ -0,0 +1,57 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
+ /**
3
+ * An incremental line reader for an **append-only** file (a hook rendezvous or a
4
+ * transcript). Each {@link poll} reads only the bytes appended since the last
5
+ * call and returns the new *complete* lines — so a long-lived session's
6
+ * `progress()`/`wait()` re-parse O(delta) per poll instead of O(file) (the
7
+ * unbounded-growth trap, F39). A trailing partial line (mid-flush write) is held
8
+ * back and completed on the next poll.
9
+ *
10
+ * Correct ONLY for append-only files: it trusts that bytes before its offset
11
+ * never change. If the file **shrinks** (truncation / rotation / an overwrite to
12
+ * a shorter body) it resets and re-reads from the start, signalling `reset` so
13
+ * the caller can discard whatever it accumulated. A claude transcript/rendezvous
14
+ * is append-only by construction (compaction summarizes the context window, it
15
+ * never rewrites the log), so the reset path is purely defensive.
16
+ */
17
+ export class TailReader {
18
+ #offset = 0;
19
+ #partial = "";
20
+ /**
21
+ * The new complete lines since the last poll. `reset: true` means the file
22
+ * shrank and `lines` is the WHOLE file again — the caller must drop its
23
+ * accumulated state first. Absent/unreadable file → no change (`[]`).
24
+ */
25
+ poll(path) {
26
+ let size;
27
+ try {
28
+ size = statSync(path).size;
29
+ }
30
+ catch {
31
+ return { reset: false, lines: [] }; // absent — degrade, never throw
32
+ }
33
+ let reset = false;
34
+ if (size < this.#offset) {
35
+ this.#offset = 0;
36
+ this.#partial = "";
37
+ reset = true;
38
+ }
39
+ if (size <= this.#offset)
40
+ return { reset, lines: [] };
41
+ const fd = openSync(path, "r");
42
+ try {
43
+ const len = size - this.#offset;
44
+ const buf = Buffer.allocUnsafe(len);
45
+ const n = readSync(fd, buf, 0, len, this.#offset);
46
+ this.#offset += n;
47
+ const chunk = this.#partial + buf.subarray(0, n).toString("utf8");
48
+ const lines = chunk.split("\n");
49
+ this.#partial = lines.pop() ?? ""; // last element = an incomplete line → hold
50
+ return { reset, lines };
51
+ }
52
+ finally {
53
+ closeSync(fd);
54
+ }
55
+ }
56
+ }
57
+ //# sourceMappingURL=incremental.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental.js","sourceRoot":"","sources":["../../src/observe/incremental.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAElE;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,UAAU;IACrB,OAAO,GAAG,CAAC,CAAC;IACZ,QAAQ,GAAG,EAAE,CAAC;IAEd;;;;OAIG;IACH,IAAI,CAAC,IAAY;QACf,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,gCAAgC;QACtE,CAAC;QACD,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,IAAI,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;YACjB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAEtD,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;YAClB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAClE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,2CAA2C;YAC9E,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC1B,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,EAAE,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;CACF"}