@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.
- package/CHANGELOG.md +257 -0
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/bin/claudemux +6 -0
- package/dist/agents/claude.d.ts +3 -0
- package/dist/agents/claude.d.ts.map +1 -0
- package/dist/agents/claude.js +585 -0
- package/dist/agents/claude.js.map +1 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/types.d.ts +252 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/backends/tmux/capture.d.ts +25 -0
- package/dist/backends/tmux/capture.d.ts.map +1 -0
- package/dist/backends/tmux/capture.js +35 -0
- package/dist/backends/tmux/capture.js.map +1 -0
- package/dist/backends/tmux/exec.d.ts +105 -0
- package/dist/backends/tmux/exec.d.ts.map +1 -0
- package/dist/backends/tmux/exec.js +226 -0
- package/dist/backends/tmux/exec.js.map +1 -0
- package/dist/backends/tmux/index.d.ts +22 -0
- package/dist/backends/tmux/index.d.ts.map +1 -0
- package/dist/backends/tmux/index.js +108 -0
- package/dist/backends/tmux/index.js.map +1 -0
- package/dist/backends/tmux/keys.d.ts +38 -0
- package/dist/backends/tmux/keys.d.ts.map +1 -0
- package/dist/backends/tmux/keys.js +63 -0
- package/dist/backends/tmux/keys.js.map +1 -0
- package/dist/backends/tmux/options.d.ts +24 -0
- package/dist/backends/tmux/options.d.ts.map +1 -0
- package/dist/backends/tmux/options.js +84 -0
- package/dist/backends/tmux/options.js.map +1 -0
- package/dist/backends/tmux/sessions.d.ts +70 -0
- package/dist/backends/tmux/sessions.d.ts.map +1 -0
- package/dist/backends/tmux/sessions.js +156 -0
- package/dist/backends/tmux/sessions.js.map +1 -0
- package/dist/backends/tmux/socket.d.ts +26 -0
- package/dist/backends/tmux/socket.d.ts.map +1 -0
- package/dist/backends/tmux/socket.js +31 -0
- package/dist/backends/tmux/socket.js.map +1 -0
- package/dist/backends/types.d.ts +110 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/types.js +24 -0
- package/dist/backends/types.js.map +1 -0
- package/dist/cli/ask.d.ts +11 -0
- package/dist/cli/ask.d.ts.map +1 -0
- package/dist/cli/ask.js +17 -0
- package/dist/cli/ask.js.map +1 -0
- package/dist/cli/capture.d.ts +8 -0
- package/dist/cli/capture.d.ts.map +1 -0
- package/dist/cli/capture.js +15 -0
- package/dist/cli/capture.js.map +1 -0
- package/dist/cli/context.d.ts +71 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +82 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/exists.d.ts +7 -0
- package/dist/cli/exists.d.ts.map +1 -0
- package/dist/cli/exists.js +16 -0
- package/dist/cli/exists.js.map +1 -0
- package/dist/cli/interrupt.d.ts +10 -0
- package/dist/cli/interrupt.d.ts.map +1 -0
- package/dist/cli/interrupt.js +13 -0
- package/dist/cli/interrupt.js.map +1 -0
- package/dist/cli/kill.d.ts +7 -0
- package/dist/cli/kill.d.ts.map +1 -0
- package/dist/cli/kill.js +14 -0
- package/dist/cli/kill.js.map +1 -0
- package/dist/cli/list.d.ts +10 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +19 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/main.d.ts +13 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +143 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/messages.d.ts +9 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +13 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/respond.d.ts +10 -0
- package/dist/cli/respond.d.ts.map +1 -0
- package/dist/cli/respond.js +23 -0
- package/dist/cli/respond.js.map +1 -0
- package/dist/cli/resume.d.ts +12 -0
- package/dist/cli/resume.d.ts.map +1 -0
- package/dist/cli/resume.js +21 -0
- package/dist/cli/resume.js.map +1 -0
- package/dist/cli/send.d.ts +9 -0
- package/dist/cli/send.d.ts.map +1 -0
- package/dist/cli/send.js +16 -0
- package/dist/cli/send.js.map +1 -0
- package/dist/cli/spawn.d.ts +14 -0
- package/dist/cli/spawn.d.ts.map +1 -0
- package/dist/cli/spawn.js +21 -0
- package/dist/cli/spawn.js.map +1 -0
- package/dist/cli/state.d.ts +7 -0
- package/dist/cli/state.d.ts.map +1 -0
- package/dist/cli/state.js +11 -0
- package/dist/cli/state.js.map +1 -0
- package/dist/cli/turn-complete.d.ts +8 -0
- package/dist/cli/turn-complete.d.ts.map +1 -0
- package/dist/cli/turn-complete.js +14 -0
- package/dist/cli/turn-complete.js.map +1 -0
- package/dist/cli/wait.d.ts +13 -0
- package/dist/cli/wait.d.ts.map +1 -0
- package/dist/cli/wait.js +17 -0
- package/dist/cli/wait.js.map +1 -0
- package/dist/compose.d.ts +81 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +64 -0
- package/dist/compose.js.map +1 -0
- package/dist/errors.d.ts +250 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +300 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/io/baseline.d.ts +53 -0
- package/dist/io/baseline.d.ts.map +1 -0
- package/dist/io/baseline.js +97 -0
- package/dist/io/baseline.js.map +1 -0
- package/dist/io/capture.d.ts +15 -0
- package/dist/io/capture.d.ts.map +1 -0
- package/dist/io/capture.js +13 -0
- package/dist/io/capture.js.map +1 -0
- package/dist/io/interrupt.d.ts +46 -0
- package/dist/io/interrupt.d.ts.map +1 -0
- package/dist/io/interrupt.js +60 -0
- package/dist/io/interrupt.js.map +1 -0
- package/dist/io/respond.d.ts +28 -0
- package/dist/io/respond.d.ts.map +1 -0
- package/dist/io/respond.js +33 -0
- package/dist/io/respond.js.map +1 -0
- package/dist/io/send.d.ts +44 -0
- package/dist/io/send.d.ts.map +1 -0
- package/dist/io/send.js +66 -0
- package/dist/io/send.js.map +1 -0
- package/dist/io/stabilize.d.ts +28 -0
- package/dist/io/stabilize.d.ts.map +1 -0
- package/dist/io/stabilize.js +20 -0
- package/dist/io/stabilize.js.map +1 -0
- package/dist/io/wait.d.ts +47 -0
- package/dist/io/wait.d.ts.map +1 -0
- package/dist/io/wait.js +117 -0
- package/dist/io/wait.js.map +1 -0
- package/dist/observe/incremental.d.ts +28 -0
- package/dist/observe/incremental.d.ts.map +1 -0
- package/dist/observe/incremental.js +57 -0
- package/dist/observe/incremental.js.map +1 -0
- package/dist/observe/observer.d.ts +86 -0
- package/dist/observe/observer.d.ts.map +1 -0
- package/dist/observe/observer.js +167 -0
- package/dist/observe/observer.js.map +1 -0
- package/dist/observe/session-observer.d.ts +49 -0
- package/dist/observe/session-observer.d.ts.map +1 -0
- package/dist/observe/session-observer.js +123 -0
- package/dist/observe/session-observer.js.map +1 -0
- package/dist/session/adopt.d.ts +52 -0
- package/dist/session/adopt.d.ts.map +1 -0
- package/dist/session/adopt.js +57 -0
- package/dist/session/adopt.js.map +1 -0
- package/dist/session/boot.d.ts +66 -0
- package/dist/session/boot.d.ts.map +1 -0
- package/dist/session/boot.js +216 -0
- package/dist/session/boot.js.map +1 -0
- package/dist/session/constants.d.ts +57 -0
- package/dist/session/constants.d.ts.map +1 -0
- package/dist/session/constants.js +54 -0
- package/dist/session/constants.js.map +1 -0
- package/dist/session/create.d.ts +88 -0
- package/dist/session/create.d.ts.map +1 -0
- package/dist/session/create.js +66 -0
- package/dist/session/create.js.map +1 -0
- package/dist/session/default-backend.d.ts +27 -0
- package/dist/session/default-backend.d.ts.map +1 -0
- package/dist/session/default-backend.js +58 -0
- package/dist/session/default-backend.js.map +1 -0
- package/dist/session/handle.d.ts +63 -0
- package/dist/session/handle.d.ts.map +1 -0
- package/dist/session/handle.js +284 -0
- package/dist/session/handle.js.map +1 -0
- package/dist/session/hooks.d.ts +37 -0
- package/dist/session/hooks.d.ts.map +1 -0
- package/dist/session/hooks.js +42 -0
- package/dist/session/hooks.js.map +1 -0
- package/dist/session/mutex.d.ts +15 -0
- package/dist/session/mutex.d.ts.map +1 -0
- package/dist/session/mutex.js +29 -0
- package/dist/session/mutex.js.map +1 -0
- package/dist/session/recover.d.ts +43 -0
- package/dist/session/recover.d.ts.map +1 -0
- package/dist/session/recover.js +45 -0
- package/dist/session/recover.js.map +1 -0
- package/dist/session/ref.d.ts +2 -0
- package/dist/session/ref.d.ts.map +1 -0
- package/dist/session/ref.js +5 -0
- package/dist/session/ref.js.map +1 -0
- package/dist/session/registry.d.ts +31 -0
- package/dist/session/registry.d.ts.map +1 -0
- package/dist/session/registry.js +32 -0
- package/dist/session/registry.js.map +1 -0
- package/dist/session/resolve.d.ts +30 -0
- package/dist/session/resolve.d.ts.map +1 -0
- package/dist/session/resolve.js +24 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/session/resume.d.ts +68 -0
- package/dist/session/resume.d.ts.map +1 -0
- package/dist/session/resume.js +54 -0
- package/dist/session/resume.js.map +1 -0
- package/dist/session/spawn-boot.d.ts +44 -0
- package/dist/session/spawn-boot.d.ts.map +1 -0
- package/dist/session/spawn-boot.js +87 -0
- package/dist/session/spawn-boot.js.map +1 -0
- package/dist/session/validate.d.ts +10 -0
- package/dist/session/validate.d.ts.map +1 -0
- package/dist/session/validate.js +0 -0
- package/dist/session/validate.js.map +1 -0
- package/dist/state/classifier.d.ts +29 -0
- package/dist/state/classifier.d.ts.map +1 -0
- package/dist/state/classifier.js +37 -0
- package/dist/state/classifier.js.map +1 -0
- package/dist/state/types.d.ts +32 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +2 -0
- package/dist/state/types.js.map +1 -0
- package/dist/types.d.ts +401 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util/ansi.d.ts +14 -0
- package/dist/util/ansi.d.ts.map +1 -0
- package/dist/util/ansi.js +18 -0
- package/dist/util/ansi.js.map +1 -0
- package/dist/util/emitter.d.ts +17 -0
- package/dist/util/emitter.d.ts.map +1 -0
- package/dist/util/emitter.js +33 -0
- package/dist/util/emitter.js.map +1 -0
- package/dist/util/sleep.d.ts +8 -0
- package/dist/util/sleep.d.ts.map +1 -0
- package/dist/util/sleep.js +10 -0
- package/dist/util/sleep.js.map +1 -0
- 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"}
|
package/dist/io/send.js
ADDED
|
@@ -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"}
|
package/dist/io/wait.js
ADDED
|
@@ -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"}
|