@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,63 @@
|
|
|
1
|
+
import type { AgentDef } from "../agents/types.js";
|
|
2
|
+
import type { Backend } from "../backends/types.js";
|
|
3
|
+
import type { SessionHandle } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The {@link Cursor} {@link SessionHandle.send} returns when it could **not
|
|
6
|
+
* confirm delivery** — no user record for the sent text appeared (a lost Enter,
|
|
7
|
+
* a boot-race drop). It is a detectable sentinel, NOT a positional count: a count
|
|
8
|
+
* cursor persisted and reused later would silently slice the WHOLE transcript
|
|
9
|
+
* ("everything since index 0"). `messagesSince`/`turnComplete` on this sentinel
|
|
10
|
+
* read empty/false, so an unconfirmed turn surfaces as "re-send me", never a
|
|
11
|
+
* flood. Consumers can compare `cursor === DELIVERY_UNCONFIRMED`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DELIVERY_UNCONFIRMED = "delivery-unconfirmed";
|
|
14
|
+
/**
|
|
15
|
+
* The {@link Cursor} {@link SessionHandle.send} returns when the message was sent
|
|
16
|
+
* into a **busy** session and the agent **queued** it — accepted, and it will run
|
|
17
|
+
* after the in-flight turn finishes (claude shows "Press up to edit queued
|
|
18
|
+
* messages"). Distinct from {@link DELIVERY_UNCONFIRMED} on purpose: the message
|
|
19
|
+
* is NOT lost, so a consumer must **not** re-send (that would double-run). Its
|
|
20
|
+
* user record does not exist yet (the queued turn hasn't started), so like
|
|
21
|
+
* `DELIVERY_UNCONFIRMED` it resolves empty in `messagesSince`/`turnComplete` —
|
|
22
|
+
* the consumer `wait()`s for the current turn, lets the queued turn run, then
|
|
23
|
+
* reads with a fresh cursor. Compare `cursor === DELIVERED_QUEUED`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DELIVERED_QUEUED = "delivered-queued";
|
|
26
|
+
interface HandleDeps {
|
|
27
|
+
backend: Backend;
|
|
28
|
+
agent: AgentDef;
|
|
29
|
+
namespace: string;
|
|
30
|
+
name: string;
|
|
31
|
+
/**
|
|
32
|
+
* The agent's conversation id, when known — surfaced as
|
|
33
|
+
* {@link SessionHandle.agentSessionId}. `undefined` for sessions with no
|
|
34
|
+
* recoverable id (older/non-claudemux, a cache-miss at adopt, or a bare
|
|
35
|
+
* `--resume`); never fabricated.
|
|
36
|
+
*/
|
|
37
|
+
agentSessionId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The hook rendezvous file injected at spawn (for the observe channel). When
|
|
40
|
+
* absent (e.g. {@link attachHandle}/adopt), the handle re-derives it from
|
|
41
|
+
* {@link agentSessionId} — correct for the common (non-resume) case.
|
|
42
|
+
*/
|
|
43
|
+
rendezvousPath?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build a {@link SessionHandle} backed by a per-handle mutex. Every public
|
|
47
|
+
* method that mutates or reads pane state goes through the mutex so
|
|
48
|
+
* concurrent consumer calls cannot interleave bytes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function makeHandle(deps: HandleDeps): SessionHandle;
|
|
51
|
+
/**
|
|
52
|
+
* Build a {@link SessionHandle} that points at an existing session, without
|
|
53
|
+
* spawning or booting — the "attach to a live session" seam.
|
|
54
|
+
*
|
|
55
|
+
* Two public consumers: the stateless CLI reattaches through this on every
|
|
56
|
+
* invocation, and the public `adopt()` primitive is built directly on it —
|
|
57
|
+
* `adopt()` asserts the session EXISTS, then calls `attachHandle` (the
|
|
58
|
+
* exists-asserting mirror of `create()`'s SessionExists guard). `create()`
|
|
59
|
+
* remains the only path that *boots*; `attachHandle` is pure attach.
|
|
60
|
+
*/
|
|
61
|
+
export declare function attachHandle(deps: HandleDeps): SessionHandle;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../src/session/handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAc,MAAM,sBAAsB,CAAC;AAUhE,OAAO,KAAK,EAMV,aAAa,EAEd,MAAM,aAAa,CAAC;AAOrB;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,UAAU,UAAU;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAoJ1D;AA8GD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAE5D"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { PromptResponseUnsupported, TranscriptUnlocatable } from "../errors.js";
|
|
2
|
+
import { interruptOnce } from "../io/interrupt.js";
|
|
3
|
+
import { respondOnce } from "../io/respond.js";
|
|
4
|
+
import { sendOnce, submitOnce } from "../io/send.js";
|
|
5
|
+
import { stabilize } from "../io/stabilize.js";
|
|
6
|
+
import { waitForOutcome } from "../io/wait.js";
|
|
7
|
+
import { SessionObserver } from "../observe/session-observer.js";
|
|
8
|
+
import { classify } from "../state/classifier.js";
|
|
9
|
+
import { stripSgr } from "../util/ansi.js";
|
|
10
|
+
import { sleep } from "../util/sleep.js";
|
|
11
|
+
import { CLASSIFIER_CAPTURE } from "./constants.js";
|
|
12
|
+
import { rendezvousPathFor } from "./hooks.js";
|
|
13
|
+
import { Mutex } from "./mutex.js";
|
|
14
|
+
/**
|
|
15
|
+
* The {@link Cursor} {@link SessionHandle.send} returns when it could **not
|
|
16
|
+
* confirm delivery** — no user record for the sent text appeared (a lost Enter,
|
|
17
|
+
* a boot-race drop). It is a detectable sentinel, NOT a positional count: a count
|
|
18
|
+
* cursor persisted and reused later would silently slice the WHOLE transcript
|
|
19
|
+
* ("everything since index 0"). `messagesSince`/`turnComplete` on this sentinel
|
|
20
|
+
* read empty/false, so an unconfirmed turn surfaces as "re-send me", never a
|
|
21
|
+
* flood. Consumers can compare `cursor === DELIVERY_UNCONFIRMED`.
|
|
22
|
+
*/
|
|
23
|
+
export const DELIVERY_UNCONFIRMED = "delivery-unconfirmed";
|
|
24
|
+
/**
|
|
25
|
+
* The {@link Cursor} {@link SessionHandle.send} returns when the message was sent
|
|
26
|
+
* into a **busy** session and the agent **queued** it — accepted, and it will run
|
|
27
|
+
* after the in-flight turn finishes (claude shows "Press up to edit queued
|
|
28
|
+
* messages"). Distinct from {@link DELIVERY_UNCONFIRMED} on purpose: the message
|
|
29
|
+
* is NOT lost, so a consumer must **not** re-send (that would double-run). Its
|
|
30
|
+
* user record does not exist yet (the queued turn hasn't started), so like
|
|
31
|
+
* `DELIVERY_UNCONFIRMED` it resolves empty in `messagesSince`/`turnComplete` —
|
|
32
|
+
* the consumer `wait()`s for the current turn, lets the queued turn run, then
|
|
33
|
+
* reads with a fresh cursor. Compare `cursor === DELIVERED_QUEUED`.
|
|
34
|
+
*/
|
|
35
|
+
export const DELIVERED_QUEUED = "delivered-queued";
|
|
36
|
+
/**
|
|
37
|
+
* Build a {@link SessionHandle} backed by a per-handle mutex. Every public
|
|
38
|
+
* method that mutates or reads pane state goes through the mutex so
|
|
39
|
+
* concurrent consumer calls cannot interleave bytes.
|
|
40
|
+
*/
|
|
41
|
+
export function makeHandle(deps) {
|
|
42
|
+
const ref = { namespace: deps.namespace, name: deps.name };
|
|
43
|
+
const mutex = new Mutex();
|
|
44
|
+
// Authoritative session-interaction state: this handle issued an `interrupt()`
|
|
45
|
+
// not yet superseded by a `send()`. An interrupt fires no `stop` edge and
|
|
46
|
+
// leaves the spinner's "esc to interrupt" frozen in scrollback, so neither
|
|
47
|
+
// the hook nor the pane can tell aborted from working — but the handle knows.
|
|
48
|
+
let interruptPending = false;
|
|
49
|
+
// The per-handle stateful read core: incremental (bounded) reads of the hook
|
|
50
|
+
// rendezvous + transcript. state()/progress()/wait()/messagesSince all defer to
|
|
51
|
+
// it — one owner of "what's true", parsing only newly-appended bytes per poll.
|
|
52
|
+
const rv = rendezvousPath(deps);
|
|
53
|
+
const observer = new SessionObserver({
|
|
54
|
+
agent: deps.agent,
|
|
55
|
+
...(rv === undefined ? {} : { rendezvousPath: rv }),
|
|
56
|
+
...(deps.agentSessionId === undefined ? {} : { agentSessionId: deps.agentSessionId }),
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Capture + classify the pane, fuse with the observer's belief. The capture
|
|
60
|
+
* failure is NOT swallowed: in tmux a failed capture means the session/server
|
|
61
|
+
* is gone (terminal), so `SessionGone`/`BackendUnreachable` propagates.
|
|
62
|
+
*/
|
|
63
|
+
const readBelief = async (weInterrupted) => {
|
|
64
|
+
const paneText = await deps.backend.capture(ref, CLASSIFIER_CAPTURE);
|
|
65
|
+
const pane = {
|
|
66
|
+
state: classify(paneText, deps.agent.rules),
|
|
67
|
+
interrupted: deps.agent.rules.interrupted?.(paneText) ?? false,
|
|
68
|
+
// Real, non-whitespace content (SGR stripped) — gates the drift canary so a
|
|
69
|
+
// blank/gone pane is never judged as "all channels blind."
|
|
70
|
+
nonEmpty: stripSgr(paneText).trim().length > 0,
|
|
71
|
+
};
|
|
72
|
+
return { belief: observer.belief(pane, weInterrupted), paneText };
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
name: deps.name,
|
|
76
|
+
namespace: deps.namespace,
|
|
77
|
+
...(deps.agentSessionId === undefined ? {} : { agentSessionId: deps.agentSessionId }),
|
|
78
|
+
send: (text) => mutex.run(async () => {
|
|
79
|
+
interruptPending = false; // a new turn supersedes any pending interrupt
|
|
80
|
+
const beforeIds = new Set(observer.thread().messages.map((m) => m.id));
|
|
81
|
+
await sendOnce(deps.backend, deps.agent, ref, text);
|
|
82
|
+
// Anchor the cursor on OUR OWN user record, not a positional count.
|
|
83
|
+
// A count read here is wrong: the PRIOR turn's reply may not have flushed
|
|
84
|
+
// yet (the transcript trails the done signal by ~100ms), and a human may
|
|
85
|
+
// type an interleaved turn. Anchoring on the record this send produced is
|
|
86
|
+
// immune to both. Fall back to the sentinel only if it never appears.
|
|
87
|
+
const ownId = await anchorOwnTurn(observer, beforeIds, text);
|
|
88
|
+
if (ownId !== undefined)
|
|
89
|
+
return ownId;
|
|
90
|
+
// No user record appeared. A turn sent into a BUSY session is QUEUED by
|
|
91
|
+
// the agent (accepted, runs next — do NOT re-send); report that distinctly
|
|
92
|
+
// so the consumer doesn't double-run. The agent owns the "queued" pane
|
|
93
|
+
// vocabulary; we just ask it.
|
|
94
|
+
const pane = await deps.backend.capture(ref, CLASSIFIER_CAPTURE);
|
|
95
|
+
if (deps.agent.rules.queued?.(pane))
|
|
96
|
+
return DELIVERED_QUEUED;
|
|
97
|
+
// Lost-submit recovery — but ONLY when the pane looks like an un-submitted
|
|
98
|
+
// DRAFT: it classifies `unknown` (a `❯ <text>` composer that is neither the
|
|
99
|
+
// empty idle box nor the working spinner). That is the lost-Enter signature
|
|
100
|
+
// — the paste reached the composer but the Enter didn't register. In that
|
|
101
|
+
// one case re-fire Enter ONCE (submitOnce never re-pastes, so it cannot
|
|
102
|
+
// duplicate the body) and re-anchor; the needle-match keeps it honest
|
|
103
|
+
// (confirms a NEW record matching OUR text, never a stray draft).
|
|
104
|
+
//
|
|
105
|
+
// If the pane is `working`/`idle` instead, the submit already TOOK (or
|
|
106
|
+
// there is nothing in the composer) and we merely couldn't anchor the
|
|
107
|
+
// record — e.g. an adopted session whose transcript isn't locatable. Firing
|
|
108
|
+
// a stray Enter there would inject a spurious empty turn, so we don't: the
|
|
109
|
+
// honest answer is DELIVERY_UNCONFIRMED.
|
|
110
|
+
if (classify(pane, deps.agent.rules) === "unknown") {
|
|
111
|
+
await submitOnce(deps.backend, ref);
|
|
112
|
+
const recovered = await anchorOwnTurn(observer, beforeIds, text, RETRY_ANCHOR_POLLS);
|
|
113
|
+
if (recovered !== undefined)
|
|
114
|
+
return recovered;
|
|
115
|
+
}
|
|
116
|
+
return DELIVERY_UNCONFIRMED;
|
|
117
|
+
}),
|
|
118
|
+
messagesSince: (cursor) => mutex.run(async () => {
|
|
119
|
+
if (!observer.transcriptLocatable())
|
|
120
|
+
throw new TranscriptUnlocatable(deps.name);
|
|
121
|
+
return messagesSince(observer, cursor);
|
|
122
|
+
}),
|
|
123
|
+
turnComplete: (cursor) => mutex.run(async () => {
|
|
124
|
+
if (!observer.transcriptLocatable())
|
|
125
|
+
throw new TranscriptUnlocatable(deps.name);
|
|
126
|
+
return messagesSince(observer, cursor).some((m) => m.role === "assistant");
|
|
127
|
+
}),
|
|
128
|
+
progress: () => mutex.run(async () => {
|
|
129
|
+
const { belief } = await readBelief(interruptPending);
|
|
130
|
+
// Project to the public Progress; the extra belief fields (interrupted /
|
|
131
|
+
// edge timings) are wait()'s concern, not progress()'s.
|
|
132
|
+
const { phase, toolInFlight, transcriptCount, hookChannelHealthy, agentChannelHealthy, state, } = belief;
|
|
133
|
+
return {
|
|
134
|
+
phase,
|
|
135
|
+
toolInFlight,
|
|
136
|
+
transcriptCount,
|
|
137
|
+
hookChannelHealthy,
|
|
138
|
+
agentChannelHealthy,
|
|
139
|
+
state,
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
respond: (choice) => mutex.run(async () => {
|
|
143
|
+
// Compose two sub-owners; own neither's internals. The AGENT owns the
|
|
144
|
+
// menu option-order (choice→key) — no mapping ⇒ refuse rather than guess
|
|
145
|
+
// a key (a wrong guess could pick "allow all"). The OBSERVER owns "is a
|
|
146
|
+
// prompt still showing" — injected as the confirm predicate. The io
|
|
147
|
+
// primitive owns delivery + settle.
|
|
148
|
+
const key = deps.agent.permissionPrompt?.respondKey(choice);
|
|
149
|
+
if (key === undefined)
|
|
150
|
+
throw new PromptResponseUnsupported(deps.name, deps.agent.name);
|
|
151
|
+
await respondOnce(deps.backend, ref, key, async () => (await readBelief(false)).belief.state === "permission-prompt");
|
|
152
|
+
}),
|
|
153
|
+
interrupt: () => mutex.run(async () => {
|
|
154
|
+
interruptPending = true; // authoritative: we aborted; no `stop` will come
|
|
155
|
+
await interruptOnce(deps.backend, ref);
|
|
156
|
+
}),
|
|
157
|
+
wait: (opts) => mutex.run(() => {
|
|
158
|
+
// We issued an interrupt and haven't sent since — there is no turn to
|
|
159
|
+
// wait for; report the abort rather than poll a frozen spinner to budget.
|
|
160
|
+
if (interruptPending)
|
|
161
|
+
return Promise.resolve({ kind: "aborted" });
|
|
162
|
+
return waitForOutcome(deps.backend, ref, opts ?? {}, { stabilize }, () => readBelief(false));
|
|
163
|
+
}),
|
|
164
|
+
state: () => mutex.run(async () => (await readBelief(interruptPending)).belief.state),
|
|
165
|
+
capture: (opts) => mutex.run(() => deps.backend.capture(ref, opts)),
|
|
166
|
+
kill: () => mutex.run(() => deps.backend.kill(ref)),
|
|
167
|
+
onBackendCommand: (handler) => deps.backend.onCommand(handler),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** The hook rendezvous file: the one injected at spawn, else re-derived from the id. */
|
|
171
|
+
function rendezvousPath(deps) {
|
|
172
|
+
if (deps.rendezvousPath !== undefined)
|
|
173
|
+
return deps.rendezvousPath;
|
|
174
|
+
return deps.agentSessionId === undefined ? undefined : rendezvousPathFor(deps.agentSessionId);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The messages produced since `cursor` — the shared body of `messagesSince` and
|
|
178
|
+
* `turnComplete`, read from the observer's incremental transcript cache. Causal
|
|
179
|
+
* chain when the cursor is a real message id; clean positional slice for a legacy
|
|
180
|
+
* numeric cursor; EMPTY for an unresolvable cursor (sentinel/garbage) — never the
|
|
181
|
+
* whole transcript (F40).
|
|
182
|
+
*/
|
|
183
|
+
function messagesSince(observer, cursor) {
|
|
184
|
+
const { messages: all, parentOf } = observer.thread();
|
|
185
|
+
if (all.some((m) => m.id === cursor))
|
|
186
|
+
return descendantsOf(all, parentOf, cursor);
|
|
187
|
+
const n = Number.parseInt(cursor, 10);
|
|
188
|
+
return Number.isFinite(n) && n >= 0 && String(n) === cursor.trim() ? all.slice(n) : [];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Messages causally after `ancestorId` — those whose parent chain passes through
|
|
192
|
+
* it — in file order. Uses the thread links, not file position, so the prior
|
|
193
|
+
* turn's late-flushing reply (which descends from an EARLIER user record) is
|
|
194
|
+
* excluded even if it lands after our record in the append-only file. Records
|
|
195
|
+
* with no parent chain (e.g. an agent that omits `parentId`) fall back to a
|
|
196
|
+
* positional slice so a thread-less transcript still works.
|
|
197
|
+
*
|
|
198
|
+
* **Compaction-safe.** Verified empirically on claude 2.1.162: a compaction
|
|
199
|
+
* (`/compact` or auto) summarizes the *context window* but leaves the on-disk
|
|
200
|
+
* transcript append-only with an UNBROKEN linear `parentUuid` chain, so a
|
|
201
|
+
* post-compaction turn still descends from a pre-compaction cursor — no special
|
|
202
|
+
* handling needed for the observed case. As defense-in-depth against a future
|
|
203
|
+
* record-format change (or a partial flush) that *did* drop an intermediate
|
|
204
|
+
* record, a message whose chain hits a MISSING parent (orphaned, not a clean
|
|
205
|
+
* root) and that sits positionally after the cursor is still included — we can't
|
|
206
|
+
* prove causality through a hole, so we fall back to position. This cannot
|
|
207
|
+
* re-include the late-flush prior reply: its parent record IS present (it roots
|
|
208
|
+
* cleanly at an earlier turn), so it's never an orphan. (F43/F25.)
|
|
209
|
+
*/
|
|
210
|
+
function descendantsOf(all, fullParentOf, ancestorId) {
|
|
211
|
+
// Prefer the FULL ancestry graph (every record, incl. the non-message ones an
|
|
212
|
+
// agent threads between a prompt and its reply). Without it — an agent with no
|
|
213
|
+
// `parseEdge` — fall back to links between surfaced messages only.
|
|
214
|
+
const parentOf = fullParentOf.size > 0 ? fullParentOf : new Map(all.map((m) => [m.id, m.parentId]));
|
|
215
|
+
const hasLinks = [...parentOf.values()].some((p) => p !== undefined);
|
|
216
|
+
const anchorIdx = all.findIndex((m) => m.id === ancestorId);
|
|
217
|
+
if (!hasLinks)
|
|
218
|
+
return anchorIdx >= 0 ? all.slice(anchorIdx + 1) : all.slice();
|
|
219
|
+
// Classify a message's lineage relative to the cursor by walking its parent
|
|
220
|
+
// chain: reaches the cursor → `descends`; hits a referenced-but-absent parent
|
|
221
|
+
// → `orphan` (a hole, e.g. a dropped record); reaches a clean root (no parent)
|
|
222
|
+
// → `rooted` (a different lineage — an earlier turn).
|
|
223
|
+
const lineage = (id) => {
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
let cur = parentOf.get(id);
|
|
226
|
+
while (cur !== undefined) {
|
|
227
|
+
if (cur === ancestorId)
|
|
228
|
+
return "descends";
|
|
229
|
+
if (seen.has(cur))
|
|
230
|
+
return "rooted"; // cycle guard
|
|
231
|
+
seen.add(cur);
|
|
232
|
+
if (!parentOf.has(cur))
|
|
233
|
+
return "orphan"; // parent referenced but its record is gone
|
|
234
|
+
cur = parentOf.get(cur);
|
|
235
|
+
}
|
|
236
|
+
return "rooted";
|
|
237
|
+
};
|
|
238
|
+
return all.filter((m, i) => {
|
|
239
|
+
const l = lineage(m.id);
|
|
240
|
+
return l === "descends" || (l === "orphan" && anchorIdx >= 0 && i > anchorIdx);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const ANCHOR_POLLS = 12;
|
|
244
|
+
// The post-recovery re-anchor is shorter: a lost-Enter record appears within a
|
|
245
|
+
// poll or two of the retry Enter, so a true non-delivery still reports promptly
|
|
246
|
+
// rather than paying a second full anchor window.
|
|
247
|
+
const RETRY_ANCHOR_POLLS = 8;
|
|
248
|
+
const ANCHOR_POLL_MS = 250;
|
|
249
|
+
/** Collapse whitespace so a reflowed/echoed prompt still matches. */
|
|
250
|
+
const squash = (s) => s.replace(/\s+/g, " ").trim();
|
|
251
|
+
/**
|
|
252
|
+
* The id of the user record THIS send produced — a NEW (not pre-existing) user
|
|
253
|
+
* message whose text matches what we sent. Polled because the record flushes
|
|
254
|
+
* shortly after delivery; `undefined` if it never appears (a delivery problem).
|
|
255
|
+
*/
|
|
256
|
+
async function anchorOwnTurn(observer, beforeIds, text, polls = ANCHOR_POLLS) {
|
|
257
|
+
const needle = squash(text).slice(0, 80); // prefix tolerates echo reflow / truncation
|
|
258
|
+
for (let attempt = 0; attempt < polls; attempt++) {
|
|
259
|
+
const msgs = observer.thread().messages;
|
|
260
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
261
|
+
const m = msgs[i];
|
|
262
|
+
if (m === undefined || m.role !== "user" || beforeIds.has(m.id))
|
|
263
|
+
continue;
|
|
264
|
+
if (m.parts.some((p) => p.kind === "text" && squash(p.text).includes(needle)))
|
|
265
|
+
return m.id;
|
|
266
|
+
}
|
|
267
|
+
await sleep(ANCHOR_POLL_MS);
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Build a {@link SessionHandle} that points at an existing session, without
|
|
273
|
+
* spawning or booting — the "attach to a live session" seam.
|
|
274
|
+
*
|
|
275
|
+
* Two public consumers: the stateless CLI reattaches through this on every
|
|
276
|
+
* invocation, and the public `adopt()` primitive is built directly on it —
|
|
277
|
+
* `adopt()` asserts the session EXISTS, then calls `attachHandle` (the
|
|
278
|
+
* exists-asserting mirror of `create()`'s SessionExists guard). `create()`
|
|
279
|
+
* remains the only path that *boots*; `attachHandle` is pure attach.
|
|
280
|
+
*/
|
|
281
|
+
export function attachHandle(deps) {
|
|
282
|
+
return makeHandle(deps);
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.js","sourceRoot":"","sources":["../../src/session/handle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentDef } from "../agents/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hook injection for the observe side. claudemux observes a session reliably
|
|
4
|
+
* via agent **hooks** (deterministic, harness-fired) + the transcript — never
|
|
5
|
+
* by scraping the TUI. On spawn we inject the agent's hook settings (default
|
|
6
|
+
* on) so the agent appends turn-lifecycle markers to a claudemux-owned local
|
|
7
|
+
* rendezvous file; the Observer reads them as phase edges.
|
|
8
|
+
*
|
|
9
|
+
* This module owns the claudemux-side mechanism (where the rendezvous lives,
|
|
10
|
+
* whether to inject); the agent owns the hook *vocabulary* (which events, the
|
|
11
|
+
* settings shape) behind {@link AgentDef.hooks}. No agent flag strings here.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The claudemux-owned local rendezvous file for a session's hook turn-markers.
|
|
15
|
+
* Local, absolute, no network (honors the trust posture). `.ndjson` (not the
|
|
16
|
+
* agent's `.jsonl` transcript) keeps the two stores distinct.
|
|
17
|
+
*/
|
|
18
|
+
export declare function rendezvousPathFor(sessionId: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Compute the hook injection for a spawn.
|
|
21
|
+
*
|
|
22
|
+
* @returns `args` — extra argv to prepend so the agent emits turn markers to
|
|
23
|
+
* `rendezvousPath` (the Observer reads it). Both empty/absent when hooks are
|
|
24
|
+
* disabled, the agent has none, or the consumer already supplied the agent's
|
|
25
|
+
* hook flag (we don't double-inject — deep-merging the consumer's settings
|
|
26
|
+
* with ours is a hardening follow-up).
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildHookInjection(o: {
|
|
29
|
+
agent: AgentDef;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
userExtraArgs: readonly string[];
|
|
33
|
+
}): {
|
|
34
|
+
args: string[];
|
|
35
|
+
rendezvousPath?: string;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/session/hooks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;;;;;;GAUG;AAEH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE;IACpC,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAO9C"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Hook injection for the observe side. claudemux observes a session reliably
|
|
5
|
+
* via agent **hooks** (deterministic, harness-fired) + the transcript — never
|
|
6
|
+
* by scraping the TUI. On spawn we inject the agent's hook settings (default
|
|
7
|
+
* on) so the agent appends turn-lifecycle markers to a claudemux-owned local
|
|
8
|
+
* rendezvous file; the Observer reads them as phase edges.
|
|
9
|
+
*
|
|
10
|
+
* This module owns the claudemux-side mechanism (where the rendezvous lives,
|
|
11
|
+
* whether to inject); the agent owns the hook *vocabulary* (which events, the
|
|
12
|
+
* settings shape) behind {@link AgentDef.hooks}. No agent flag strings here.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The claudemux-owned local rendezvous file for a session's hook turn-markers.
|
|
16
|
+
* Local, absolute, no network (honors the trust posture). `.ndjson` (not the
|
|
17
|
+
* agent's `.jsonl` transcript) keeps the two stores distinct.
|
|
18
|
+
*/
|
|
19
|
+
export function rendezvousPathFor(sessionId) {
|
|
20
|
+
const stateDir = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
21
|
+
return join(stateDir, "claudemux", "turns", `${sessionId}.ndjson`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute the hook injection for a spawn.
|
|
25
|
+
*
|
|
26
|
+
* @returns `args` — extra argv to prepend so the agent emits turn markers to
|
|
27
|
+
* `rendezvousPath` (the Observer reads it). Both empty/absent when hooks are
|
|
28
|
+
* disabled, the agent has none, or the consumer already supplied the agent's
|
|
29
|
+
* hook flag (we don't double-inject — deep-merging the consumer's settings
|
|
30
|
+
* with ours is a hardening follow-up).
|
|
31
|
+
*/
|
|
32
|
+
export function buildHookInjection(o) {
|
|
33
|
+
if (!o.enabled || o.agent.hooks === undefined)
|
|
34
|
+
return { args: [] };
|
|
35
|
+
const rendezvousPath = rendezvousPathFor(o.sessionId);
|
|
36
|
+
const spec = o.agent.hooks.spec({ rendezvousPath });
|
|
37
|
+
const alreadySet = o.userExtraArgs.some((a) => a === spec.flag || a.startsWith(`${spec.flag}=`));
|
|
38
|
+
if (alreadySet)
|
|
39
|
+
return { args: [] };
|
|
40
|
+
return { args: [spec.flag, spec.value], rendezvousPath };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/session/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;;;;;;;GAUG;AAEH;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,SAAS,SAAS,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAKlC;IACC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACnE,MAAM,cAAc,GAAG,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IACjG,IAAI,UAAU;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACpC,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny async mutex. Wraps an async task so calls serialize per-instance.
|
|
3
|
+
*
|
|
4
|
+
* The substrate uses one mutex per `SessionHandle` so concurrent consumer
|
|
5
|
+
* calls (e.g. `send` racing with `capture`) cannot interleave bytes.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Mutex {
|
|
8
|
+
#private;
|
|
9
|
+
/**
|
|
10
|
+
* Run `task` with exclusive access. The mutex is released whether the
|
|
11
|
+
* task resolves or rejects.
|
|
12
|
+
*/
|
|
13
|
+
run<T>(task: () => Promise<T>): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=mutex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutex.d.ts","sourceRoot":"","sources":["../../src/session/mutex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,KAAK;;IAGhB;;;OAGG;IACG,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAcjD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny async mutex. Wraps an async task so calls serialize per-instance.
|
|
3
|
+
*
|
|
4
|
+
* The substrate uses one mutex per `SessionHandle` so concurrent consumer
|
|
5
|
+
* calls (e.g. `send` racing with `capture`) cannot interleave bytes.
|
|
6
|
+
*/
|
|
7
|
+
export class Mutex {
|
|
8
|
+
#tail = Promise.resolve();
|
|
9
|
+
/**
|
|
10
|
+
* Run `task` with exclusive access. The mutex is released whether the
|
|
11
|
+
* task resolves or rejects.
|
|
12
|
+
*/
|
|
13
|
+
async run(task) {
|
|
14
|
+
const prior = this.#tail;
|
|
15
|
+
let resolveNext;
|
|
16
|
+
const next = new Promise((res) => {
|
|
17
|
+
resolveNext = res;
|
|
18
|
+
});
|
|
19
|
+
this.#tail = next;
|
|
20
|
+
try {
|
|
21
|
+
await prior;
|
|
22
|
+
return await task();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
resolveNext();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mutex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/session/mutex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,KAAK;IAChB,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE5C;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAI,IAAsB;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,WAAwB,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE;YACrC,WAAW,GAAG,GAAG,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC;YACZ,OAAO,MAAM,IAAI,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SessionHandle } from "../types.js";
|
|
2
|
+
import { type ResumeOptions } from "./resume.js";
|
|
3
|
+
/**
|
|
4
|
+
* How a session was reconnected on daemon boot — the answer to "did it survive?"
|
|
5
|
+
* - `"attached"` — the pane was **still alive** (your process restarted, the
|
|
6
|
+
* session didn't). Reconnected to the running session; nothing was lost.
|
|
7
|
+
* - `"resumed"` — the pane was **gone** (it crashed, or the box lost its tmux
|
|
8
|
+
* server). The conversation was continued in a **fresh** pane from the
|
|
9
|
+
* persisted id. A turn may have been in flight when it died — check
|
|
10
|
+
* `turnComplete(yourLastCursor)` and re-send if it reads `false`.
|
|
11
|
+
*/
|
|
12
|
+
export type RecoverStatus = "attached" | "resumed";
|
|
13
|
+
/** The result of {@link recover}: the live handle + how it was recovered. */
|
|
14
|
+
export interface RecoverResult {
|
|
15
|
+
readonly session: SessionHandle;
|
|
16
|
+
readonly status: RecoverStatus;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reconnect to a session on boot **without the consumer hand-rolling the
|
|
20
|
+
* adopt-vs-resume decision.** This is the one verb a daemon calls for each
|
|
21
|
+
* `{ name, agentSessionId }` it persisted: it tries to {@link adopt} the running
|
|
22
|
+
* pane, and if that pane is gone (`SessionGone` — a crash, or the box lost tmux)
|
|
23
|
+
* it {@link resume}s the same conversation in a fresh pane. The returned
|
|
24
|
+
* {@link RecoverStatus} tells you which happened, so "was it crashed?" is a field,
|
|
25
|
+
* not a `try`/`catch` you write yourself.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // On daemon boot, for each persisted { name, agentSessionId, cwd, lastCursor }:
|
|
30
|
+
* const { session, status } = await recover({ name, agentSessionId, cwd });
|
|
31
|
+
* if (status === "resumed" && !(await session.turnComplete(lastCursor))) {
|
|
32
|
+
* await session.send(lastInFlightPrompt); // it crashed mid-turn — re-send the lost turn
|
|
33
|
+
* }
|
|
34
|
+
* // status === "attached" ⇒ the session never went down; just keep going.
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Takes the same options as {@link resume} (the crash path needs them — `cwd`,
|
|
38
|
+
* `agentSessionId`, boot knobs). Only `SessionGone` from `adopt` triggers the
|
|
39
|
+
* resume fallback; any other adopt error (e.g. `InvalidSessionName`) propagates —
|
|
40
|
+
* those are caller mistakes, not crashes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recover(opts: ResumeOptions): Promise<RecoverResult>;
|
|
43
|
+
//# sourceMappingURL=recover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recover.d.ts","sourceRoot":"","sources":["../../src/session/recover.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;AAEnD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAczE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SessionGone } from "../errors.js";
|
|
2
|
+
import { adopt } from "./adopt.js";
|
|
3
|
+
import { resume } from "./resume.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reconnect to a session on boot **without the consumer hand-rolling the
|
|
6
|
+
* adopt-vs-resume decision.** This is the one verb a daemon calls for each
|
|
7
|
+
* `{ name, agentSessionId }` it persisted: it tries to {@link adopt} the running
|
|
8
|
+
* pane, and if that pane is gone (`SessionGone` — a crash, or the box lost tmux)
|
|
9
|
+
* it {@link resume}s the same conversation in a fresh pane. The returned
|
|
10
|
+
* {@link RecoverStatus} tells you which happened, so "was it crashed?" is a field,
|
|
11
|
+
* not a `try`/`catch` you write yourself.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // On daemon boot, for each persisted { name, agentSessionId, cwd, lastCursor }:
|
|
16
|
+
* const { session, status } = await recover({ name, agentSessionId, cwd });
|
|
17
|
+
* if (status === "resumed" && !(await session.turnComplete(lastCursor))) {
|
|
18
|
+
* await session.send(lastInFlightPrompt); // it crashed mid-turn — re-send the lost turn
|
|
19
|
+
* }
|
|
20
|
+
* // status === "attached" ⇒ the session never went down; just keep going.
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Takes the same options as {@link resume} (the crash path needs them — `cwd`,
|
|
24
|
+
* `agentSessionId`, boot knobs). Only `SessionGone` from `adopt` triggers the
|
|
25
|
+
* resume fallback; any other adopt error (e.g. `InvalidSessionName`) propagates —
|
|
26
|
+
* those are caller mistakes, not crashes.
|
|
27
|
+
*/
|
|
28
|
+
export async function recover(opts) {
|
|
29
|
+
try {
|
|
30
|
+
const session = await adopt({
|
|
31
|
+
name: opts.name,
|
|
32
|
+
...(opts.namespace === undefined ? {} : { namespace: opts.namespace }),
|
|
33
|
+
...(opts.agent === undefined ? {} : { agent: opts.agent }),
|
|
34
|
+
...(opts.backend === undefined ? {} : { backend: opts.backend }),
|
|
35
|
+
});
|
|
36
|
+
return { session, status: "attached" };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (!(e instanceof SessionGone))
|
|
40
|
+
throw e; // a real adopt error, not a crash
|
|
41
|
+
const session = await resume(opts);
|
|
42
|
+
return { session, status: "resumed" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=recover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recover.js","sourceRoot":"","sources":["../../src/session/recover.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAsB,MAAM,aAAa,CAAC;AAmBzD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAmB;IAC/C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC;YAC1B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;YACtE,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;SACjE,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IACzC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,CAAC,CAAC,YAAY,WAAW,CAAC;YAAE,MAAM,CAAC,CAAC,CAAC,kCAAkC;QAC5E,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACxC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ref.d.ts","sourceRoot":"","sources":["../../src/session/ref.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// formatSessionLabel moved to `src/backends/types.ts` so the backend layer
|
|
2
|
+
// can use it without importing from `src/session/`. This file re-exports
|
|
3
|
+
// for callers that already use the `src/session/ref` path.
|
|
4
|
+
export { formatSessionLabel } from "../backends/types.js";
|
|
5
|
+
//# sourceMappingURL=ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ref.js","sourceRoot":"","sources":["../../src/session/ref.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
|