@songsid/agend 2.1.1-beta.17 → 2.1.1-beta.18
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/dist/backend/claude-code.d.ts +37 -0
- package/dist/backend/claude-code.js +40 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/kiro.d.ts +20 -0
- package/dist/backend/kiro.js +30 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +27 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +15 -0
- package/dist/channel/adapters/discord.js +81 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +21 -0
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +9 -1
- package/dist/channel/ipc-bridge.js +12 -3
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/types.d.ts +29 -0
- package/dist/daemon.d.ts +101 -5
- package/dist/daemon.js +304 -50
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +92 -1
- package/dist/fleet-manager.js +330 -19
- package/dist/fleet-manager.js.map +1 -1
- package/dist/hang-detector.d.ts +19 -13
- package/dist/hang-detector.js +19 -49
- package/dist/hang-detector.js.map +1 -1
- package/dist/pane-write-lock.d.ts +48 -0
- package/dist/pane-write-lock.js +73 -0
- package/dist/pane-write-lock.js.map +1 -0
- package/dist/tmux-control.d.ts +48 -1
- package/dist/tmux-control.js +79 -6
- package/dist/tmux-control.js.map +1 -1
- package/package.json +1 -1
- package/dist/channel/tool-tracker.d.ts +0 -13
- package/dist/channel/tool-tracker.js +0 -58
- package/dist/channel/tool-tracker.js.map +0 -1
- package/dist/daemon-entry.d.ts +0 -1
- package/dist/daemon-entry.js +0 -30
- package/dist/daemon-entry.js.map +0 -1
package/dist/hang-detector.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* Carries "this instance looks hung" from the daemon to the fleet manager.
|
|
4
|
+
*
|
|
5
|
+
* It is only an event bridge. Hang detection itself lives in the daemon's pane-state
|
|
6
|
+
* machine, which emits `hang` directly when a pane stops changing for the configured
|
|
7
|
+
* stuck timeout; `instance-lifecycle` subscribes to that and notifies.
|
|
8
|
+
*
|
|
9
|
+
* It used to also contain a silence-timer state machine — `start()`, `isHung()`,
|
|
10
|
+
* `hungEmitted`, and timestamps fed by `recordActivity` / `recordInbound` /
|
|
11
|
+
* `recordStatuslineUpdate`. None of it ran: `start()` was never called from
|
|
12
|
+
* anywhere, so `isHung()` was unreachable, the timestamps were written and never
|
|
13
|
+
* read, and the constructor's `timeoutMinutes` was ignored (the real stuck timeout
|
|
14
|
+
* is read separately by the pane monitor). Its tests exercised that dead logic —
|
|
15
|
+
* two had identical setup with contradictory expectations, which can only pass
|
|
16
|
+
* unnoticed when neither runs against anything real.
|
|
17
|
+
*
|
|
18
|
+
* Kept as a named class rather than a bare EventEmitter so the daemon → lifecycle
|
|
19
|
+
* wiring stays typed and greppable.
|
|
20
|
+
*/
|
|
2
21
|
export declare class HangDetector extends EventEmitter {
|
|
3
|
-
private lastActivityTs;
|
|
4
|
-
private lastStatuslineTs;
|
|
5
|
-
private lastInboundTs;
|
|
6
|
-
private hungEmitted;
|
|
7
|
-
private checkTimer;
|
|
8
|
-
private timeoutMs;
|
|
9
|
-
constructor(timeoutMinutes: number);
|
|
10
|
-
recordActivity(): void;
|
|
11
|
-
recordInbound(): void;
|
|
12
|
-
recordStatuslineUpdate(): void;
|
|
13
|
-
isHung(): boolean;
|
|
14
|
-
start(intervalMs?: number): void;
|
|
15
|
-
stop(): void;
|
|
16
22
|
}
|
package/dist/hang-detector.js
CHANGED
|
@@ -1,53 +1,23 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* Carries "this instance looks hung" from the daemon to the fleet manager.
|
|
4
|
+
*
|
|
5
|
+
* It is only an event bridge. Hang detection itself lives in the daemon's pane-state
|
|
6
|
+
* machine, which emits `hang` directly when a pane stops changing for the configured
|
|
7
|
+
* stuck timeout; `instance-lifecycle` subscribes to that and notifies.
|
|
8
|
+
*
|
|
9
|
+
* It used to also contain a silence-timer state machine — `start()`, `isHung()`,
|
|
10
|
+
* `hungEmitted`, and timestamps fed by `recordActivity` / `recordInbound` /
|
|
11
|
+
* `recordStatuslineUpdate`. None of it ran: `start()` was never called from
|
|
12
|
+
* anywhere, so `isHung()` was unreachable, the timestamps were written and never
|
|
13
|
+
* read, and the constructor's `timeoutMinutes` was ignored (the real stuck timeout
|
|
14
|
+
* is read separately by the pane monitor). Its tests exercised that dead logic —
|
|
15
|
+
* two had identical setup with contradictory expectations, which can only pass
|
|
16
|
+
* unnoticed when neither runs against anything real.
|
|
17
|
+
*
|
|
18
|
+
* Kept as a named class rather than a bare EventEmitter so the daemon → lifecycle
|
|
19
|
+
* wiring stays typed and greppable.
|
|
20
|
+
*/
|
|
2
21
|
export class HangDetector extends EventEmitter {
|
|
3
|
-
lastActivityTs = 0;
|
|
4
|
-
lastStatuslineTs = 0;
|
|
5
|
-
lastInboundTs = 0;
|
|
6
|
-
hungEmitted = false;
|
|
7
|
-
checkTimer = null;
|
|
8
|
-
timeoutMs;
|
|
9
|
-
constructor(timeoutMinutes) {
|
|
10
|
-
super();
|
|
11
|
-
this.timeoutMs = timeoutMinutes * 60 * 1000;
|
|
12
|
-
}
|
|
13
|
-
recordActivity() {
|
|
14
|
-
this.lastActivityTs = Date.now();
|
|
15
|
-
if (this.hungEmitted) {
|
|
16
|
-
this.hungEmitted = false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
recordInbound() {
|
|
20
|
-
this.lastInboundTs = Date.now();
|
|
21
|
-
}
|
|
22
|
-
recordStatuslineUpdate() {
|
|
23
|
-
this.lastStatuslineTs = Date.now();
|
|
24
|
-
}
|
|
25
|
-
isHung() {
|
|
26
|
-
if (this.lastActivityTs === 0)
|
|
27
|
-
return false;
|
|
28
|
-
if (this.lastInboundTs === 0)
|
|
29
|
-
return false;
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
// Only flag as hung if:
|
|
32
|
-
// 1. There's an inbound that hasn't been answered (no activity since inbound)
|
|
33
|
-
// 2. Timeout has elapsed since that inbound
|
|
34
|
-
const noActivitySinceInbound = this.lastActivityTs < this.lastInboundTs;
|
|
35
|
-
const stale = now - this.lastInboundTs > this.timeoutMs;
|
|
36
|
-
return stale && noActivitySinceInbound;
|
|
37
|
-
}
|
|
38
|
-
start(intervalMs = 60_000) {
|
|
39
|
-
this.checkTimer = setInterval(() => {
|
|
40
|
-
if (this.isHung() && !this.hungEmitted) {
|
|
41
|
-
this.hungEmitted = true;
|
|
42
|
-
this.emit("hang");
|
|
43
|
-
}
|
|
44
|
-
}, intervalMs);
|
|
45
|
-
}
|
|
46
|
-
stop() {
|
|
47
|
-
if (this.checkTimer) {
|
|
48
|
-
clearInterval(this.checkTimer);
|
|
49
|
-
this.checkTimer = null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
22
|
}
|
|
53
23
|
//# sourceMappingURL=hang-detector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hang-detector.js","sourceRoot":"","sources":["../src/hang-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,OAAO,YAAa,SAAQ,YAAY;
|
|
1
|
+
{"version":3,"file":"hang-detector.js","sourceRoot":"","sources":["../src/hang-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,YAAa,SAAQ,YAAY;CAAG"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutual exclusion for writes into a CLI's tmux pane.
|
|
3
|
+
*
|
|
4
|
+
* A delivery is not one tmux call, it is a transaction:
|
|
5
|
+
*
|
|
6
|
+
* set-buffer → paste-buffer → (settle delay) → send-keys Enter → confirm
|
|
7
|
+
*
|
|
8
|
+
* Anything that writes to the same pane while that transaction is mid-flight is
|
|
9
|
+
* not merely interleaved, it is *destructive*: an `Escape` injected between the
|
|
10
|
+
* paste and the Enter discards the pasted text outright, and a stray `Enter`
|
|
11
|
+
* submits a half-composed message. Both look identical to a dropped message from
|
|
12
|
+
* the user's side — the ✅ reaction never arrives and nobody can say why.
|
|
13
|
+
*
|
|
14
|
+
* `Daemon.pasteLock` already serialises inbound channel messages against each
|
|
15
|
+
* other, but three other writers reach the pane without going through it (the
|
|
16
|
+
* post-restart instructions notice, the session-snapshot injection, and the
|
|
17
|
+
* runtime-dialog auto-dismisser). This lock is the single point they all share.
|
|
18
|
+
*
|
|
19
|
+
* Two acquisition modes, because the writers have genuinely different needs:
|
|
20
|
+
*
|
|
21
|
+
* - `run()` queues. Correct for one-shot writers that must eventually happen.
|
|
22
|
+
* - `tryRun()` skips when the lock is busy. Correct for *pollers*: the dialog
|
|
23
|
+
* dismisser re-checks every 5s, so skipping this cycle costs one tick, whereas
|
|
24
|
+
* queueing it behind a delivery that is waiting on a wedged pane could defer it
|
|
25
|
+
* for as long as that wait lasts. A poller that queues is a poller that can
|
|
26
|
+
* pile up; a poller that skips cannot.
|
|
27
|
+
*/
|
|
28
|
+
export declare class PaneWriteLock {
|
|
29
|
+
private tail;
|
|
30
|
+
/** Writers queued **or** running. `run()` increments before it awaits anything,
|
|
31
|
+
* so a writer is visible to `tryRun()` from the moment it is admitted — not
|
|
32
|
+
* only once the microtask queue gets around to starting it. */
|
|
33
|
+
private pending;
|
|
34
|
+
/** True while any writer holds or is waiting for the pane. */
|
|
35
|
+
get isBusy(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn` with exclusive access, queueing behind any writer already admitted.
|
|
38
|
+
* Rejections propagate to *this* caller only — the internal chain is always
|
|
39
|
+
* left resolved so one failed write cannot wedge every later one.
|
|
40
|
+
*/
|
|
41
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Run `fn` only if the pane is free right now. Returns `false` without calling
|
|
44
|
+
* `fn` when another writer is queued or running, so periodic callers can simply
|
|
45
|
+
* try again on their next tick instead of accumulating a backlog.
|
|
46
|
+
*/
|
|
47
|
+
tryRun(fn: () => Promise<void>): Promise<boolean>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutual exclusion for writes into a CLI's tmux pane.
|
|
3
|
+
*
|
|
4
|
+
* A delivery is not one tmux call, it is a transaction:
|
|
5
|
+
*
|
|
6
|
+
* set-buffer → paste-buffer → (settle delay) → send-keys Enter → confirm
|
|
7
|
+
*
|
|
8
|
+
* Anything that writes to the same pane while that transaction is mid-flight is
|
|
9
|
+
* not merely interleaved, it is *destructive*: an `Escape` injected between the
|
|
10
|
+
* paste and the Enter discards the pasted text outright, and a stray `Enter`
|
|
11
|
+
* submits a half-composed message. Both look identical to a dropped message from
|
|
12
|
+
* the user's side — the ✅ reaction never arrives and nobody can say why.
|
|
13
|
+
*
|
|
14
|
+
* `Daemon.pasteLock` already serialises inbound channel messages against each
|
|
15
|
+
* other, but three other writers reach the pane without going through it (the
|
|
16
|
+
* post-restart instructions notice, the session-snapshot injection, and the
|
|
17
|
+
* runtime-dialog auto-dismisser). This lock is the single point they all share.
|
|
18
|
+
*
|
|
19
|
+
* Two acquisition modes, because the writers have genuinely different needs:
|
|
20
|
+
*
|
|
21
|
+
* - `run()` queues. Correct for one-shot writers that must eventually happen.
|
|
22
|
+
* - `tryRun()` skips when the lock is busy. Correct for *pollers*: the dialog
|
|
23
|
+
* dismisser re-checks every 5s, so skipping this cycle costs one tick, whereas
|
|
24
|
+
* queueing it behind a delivery that is waiting on a wedged pane could defer it
|
|
25
|
+
* for as long as that wait lasts. A poller that queues is a poller that can
|
|
26
|
+
* pile up; a poller that skips cannot.
|
|
27
|
+
*/
|
|
28
|
+
export class PaneWriteLock {
|
|
29
|
+
tail = Promise.resolve();
|
|
30
|
+
/** Writers queued **or** running. `run()` increments before it awaits anything,
|
|
31
|
+
* so a writer is visible to `tryRun()` from the moment it is admitted — not
|
|
32
|
+
* only once the microtask queue gets around to starting it. */
|
|
33
|
+
pending = 0;
|
|
34
|
+
/** True while any writer holds or is waiting for the pane. */
|
|
35
|
+
get isBusy() {
|
|
36
|
+
return this.pending > 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run `fn` with exclusive access, queueing behind any writer already admitted.
|
|
40
|
+
* Rejections propagate to *this* caller only — the internal chain is always
|
|
41
|
+
* left resolved so one failed write cannot wedge every later one.
|
|
42
|
+
*/
|
|
43
|
+
run(fn) {
|
|
44
|
+
this.pending++;
|
|
45
|
+
// Decrement inside the writer, not on the tail chain: a caller that awaits its
|
|
46
|
+
// own write and then asks `isBusy` must see the pane free, rather than busy for
|
|
47
|
+
// one extra microtask because the bookkeeping trailed the result.
|
|
48
|
+
const result = this.tail.then(async () => {
|
|
49
|
+
try {
|
|
50
|
+
return await fn();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.pending--;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Settle on the chain, not on the returned promise: the caller still sees the
|
|
57
|
+
// rejection, but the next queued writer sees a resolved predecessor.
|
|
58
|
+
this.tail = result.then(() => undefined, () => undefined);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run `fn` only if the pane is free right now. Returns `false` without calling
|
|
63
|
+
* `fn` when another writer is queued or running, so periodic callers can simply
|
|
64
|
+
* try again on their next tick instead of accumulating a backlog.
|
|
65
|
+
*/
|
|
66
|
+
async tryRun(fn) {
|
|
67
|
+
if (this.isBusy)
|
|
68
|
+
return false;
|
|
69
|
+
await this.run(fn);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=pane-write-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pane-write-lock.js","sourceRoot":"","sources":["../src/pane-write-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,OAAO,aAAa;IAChB,IAAI,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IACnD;;oEAEgE;IACxD,OAAO,GAAG,CAAC,CAAC;IAEpB,8DAA8D;IAC9D,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAI,EAAoB;QACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,+EAA+E;QAC/E,gFAAgF;QAChF,kEAAkE;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACvC,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC;YACpB,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,8EAA8E;QAC9E,qEAAqE;QACrE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM,CAAC,EAAuB;QAClC,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC9B,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
package/dist/tmux-control.d.ts
CHANGED
|
@@ -25,8 +25,12 @@ export declare class TmuxControlClient extends EventEmitter {
|
|
|
25
25
|
private lastOutputAt;
|
|
26
26
|
private paneToWindow;
|
|
27
27
|
private registeredWindows;
|
|
28
|
+
private resolveFailures;
|
|
28
29
|
private stopped;
|
|
29
30
|
private reconnectTimer;
|
|
31
|
+
/** Epoch ms of the last observation reset. Everything before it is unobservable:
|
|
32
|
+
* the pane cache was dropped, so the absence of a record proves nothing. */
|
|
33
|
+
private observationResetAt;
|
|
30
34
|
private safetySweepTimer;
|
|
31
35
|
constructor(sessionName: string, silenceMs?: number, logger?: Logger | undefined);
|
|
32
36
|
start(): void;
|
|
@@ -38,8 +42,51 @@ export declare class TmuxControlClient extends EventEmitter {
|
|
|
38
42
|
registerWindow(windowId: string): Promise<void>;
|
|
39
43
|
/** Unregister a window (call on killWindow) */
|
|
40
44
|
unregisterWindow(windowId: string): void;
|
|
41
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a window's current pane id and cache the mapping.
|
|
47
|
+
*
|
|
48
|
+
* Drops a registration that has failed to resolve `RESOLVE_FAILURES_BEFORE_DROP`
|
|
49
|
+
* times in a row. Every reconnect re-resolves every registered window, one tmux
|
|
50
|
+
* subprocess each, so a registration for a window that no longer exists is a
|
|
51
|
+
* permanent per-reconnect cost — and callers do forget to unregister (a crash
|
|
52
|
+
* respawn creates a new window id and the dead one used to stay forever).
|
|
53
|
+
*
|
|
54
|
+
* Consecutive failures rather than one: `list-panes` also fails transiently when
|
|
55
|
+
* tmux is busy, e.g. during a fleet-restart storm, and dropping a live window's
|
|
56
|
+
* registration would silence its output events.
|
|
57
|
+
*/
|
|
42
58
|
private resolvePane;
|
|
59
|
+
/**
|
|
60
|
+
* Forget everything we knew about panes, and remember that we have forgotten.
|
|
61
|
+
*
|
|
62
|
+
* Kept as one method so the grace can never be skipped: clearing the maps
|
|
63
|
+
* without arming it is precisely the bug this exists to prevent.
|
|
64
|
+
*/
|
|
65
|
+
private resetPaneObservations;
|
|
66
|
+
/**
|
|
67
|
+
* True while a freshly (re)connected client has not had time to observe output.
|
|
68
|
+
*
|
|
69
|
+
* `connect()` drops the pane cache, so for a moment afterwards *every* pane looks
|
|
70
|
+
* like it has never produced output — including panes that are mid-generation.
|
|
71
|
+
* Reading that as "idle" is the dangerous direction: a delivery would skip its
|
|
72
|
+
* busy branch and paste straight into a working CLI, where Enter is a steering
|
|
73
|
+
* interrupt rather than a new turn.
|
|
74
|
+
*
|
|
75
|
+
* `silenceMs` is the right length because it is already this class's definition
|
|
76
|
+
* of idle: a pane that produces nothing for that long counts as idle anyway, so
|
|
77
|
+
* the grace never suppresses a state the client would otherwise have reported.
|
|
78
|
+
* An actively generating pane re-registers well inside it.
|
|
79
|
+
*/
|
|
80
|
+
private inObservationGrace;
|
|
81
|
+
/**
|
|
82
|
+
* When this client last lost its view of every pane (0 if it never has).
|
|
83
|
+
*
|
|
84
|
+
* A caller reasoning about a *window* of time — "did the pane react in the two
|
|
85
|
+
* seconds after I pressed Enter?" — needs to know whether it could see for all
|
|
86
|
+
* of it. A reset inside that window makes a negative answer meaningless, and
|
|
87
|
+
* acting on it produces confident, wrong conclusions.
|
|
88
|
+
*/
|
|
89
|
+
getObservationResetAt(): number;
|
|
43
90
|
/** Check if a window's pane has been silent for at least silenceMs */
|
|
44
91
|
isIdle(windowId: string): boolean;
|
|
45
92
|
/** Timestamp of the window pane's last observed output, or undefined if unknown. */
|
package/dist/tmux-control.js
CHANGED
|
@@ -3,6 +3,12 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { getTmuxSocketName } from "./paths.js";
|
|
5
5
|
export const CONTROL_SAFETY_SWEEP_MS = 60_000;
|
|
6
|
+
/**
|
|
7
|
+
* Consecutive `list-panes` failures before a window's registration is dropped.
|
|
8
|
+
* More than one because tmux fails transiently under load, and unregistering a
|
|
9
|
+
* live window would silence the output events its daemon depends on.
|
|
10
|
+
*/
|
|
11
|
+
const RESOLVE_FAILURES_BEFORE_DROP = 3;
|
|
6
12
|
function tmuxArgs(args) {
|
|
7
13
|
const socket = getTmuxSocketName();
|
|
8
14
|
return socket ? ["-L", socket, ...args] : args;
|
|
@@ -36,8 +42,12 @@ export class TmuxControlClient extends EventEmitter {
|
|
|
36
42
|
lastOutputAt = new Map(); // paneId → timestamp
|
|
37
43
|
paneToWindow = new Map(); // paneId → windowId
|
|
38
44
|
registeredWindows = new Set(); // windowIds we should re-resolve on reconnect
|
|
45
|
+
resolveFailures = new Map(); // windowId → consecutive resolve failures
|
|
39
46
|
stopped = false;
|
|
40
47
|
reconnectTimer = null;
|
|
48
|
+
/** Epoch ms of the last observation reset. Everything before it is unobservable:
|
|
49
|
+
* the pane cache was dropped, so the absence of a record proves nothing. */
|
|
50
|
+
observationResetAt = 0;
|
|
41
51
|
safetySweepTimer = null;
|
|
42
52
|
constructor(sessionName, silenceMs = 2000, logger) {
|
|
43
53
|
super();
|
|
@@ -80,6 +90,7 @@ export class TmuxControlClient extends EventEmitter {
|
|
|
80
90
|
/** Unregister a window (call on killWindow) */
|
|
81
91
|
unregisterWindow(windowId) {
|
|
82
92
|
this.registeredWindows.delete(windowId);
|
|
93
|
+
this.resolveFailures.delete(windowId);
|
|
83
94
|
for (const [pane, win] of this.paneToWindow) {
|
|
84
95
|
if (win === windowId) {
|
|
85
96
|
this.paneToWindow.delete(pane);
|
|
@@ -88,30 +99,93 @@ export class TmuxControlClient extends EventEmitter {
|
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a window's current pane id and cache the mapping.
|
|
104
|
+
*
|
|
105
|
+
* Drops a registration that has failed to resolve `RESOLVE_FAILURES_BEFORE_DROP`
|
|
106
|
+
* times in a row. Every reconnect re-resolves every registered window, one tmux
|
|
107
|
+
* subprocess each, so a registration for a window that no longer exists is a
|
|
108
|
+
* permanent per-reconnect cost — and callers do forget to unregister (a crash
|
|
109
|
+
* respawn creates a new window id and the dead one used to stay forever).
|
|
110
|
+
*
|
|
111
|
+
* Consecutive failures rather than one: `list-panes` also fails transiently when
|
|
112
|
+
* tmux is busy, e.g. during a fleet-restart storm, and dropping a live window's
|
|
113
|
+
* registration would silence its output events.
|
|
114
|
+
*/
|
|
92
115
|
async resolvePane(windowId) {
|
|
93
116
|
try {
|
|
94
117
|
const paneId = await execTmux([
|
|
95
118
|
"list-panes", "-t", `${this.sessionName}:${windowId}`,
|
|
96
119
|
"-F", "#{pane_id}",
|
|
97
120
|
]);
|
|
121
|
+
this.resolveFailures.delete(windowId);
|
|
98
122
|
if (paneId) {
|
|
99
123
|
this.paneToWindow.set(paneId, windowId);
|
|
100
124
|
this.logger?.debug({ windowId, paneId }, "Registered window→pane mapping");
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
127
|
catch {
|
|
104
|
-
this.
|
|
128
|
+
const failures = (this.resolveFailures.get(windowId) ?? 0) + 1;
|
|
129
|
+
this.resolveFailures.set(windowId, failures);
|
|
130
|
+
if (failures >= RESOLVE_FAILURES_BEFORE_DROP && this.registeredWindows.has(windowId)) {
|
|
131
|
+
this.logger?.debug({ windowId, failures }, "Window has not resolved for several attempts — dropping its registration");
|
|
132
|
+
this.unregisterWindow(windowId);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.logger?.debug({ windowId, failures }, "Failed to resolve pane ID for window");
|
|
105
136
|
}
|
|
106
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Forget everything we knew about panes, and remember that we have forgotten.
|
|
140
|
+
*
|
|
141
|
+
* Kept as one method so the grace can never be skipped: clearing the maps
|
|
142
|
+
* without arming it is precisely the bug this exists to prevent.
|
|
143
|
+
*/
|
|
144
|
+
resetPaneObservations() {
|
|
145
|
+
this.paneToWindow.clear();
|
|
146
|
+
this.lastOutputAt.clear();
|
|
147
|
+
this.observationResetAt = Date.now();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* True while a freshly (re)connected client has not had time to observe output.
|
|
151
|
+
*
|
|
152
|
+
* `connect()` drops the pane cache, so for a moment afterwards *every* pane looks
|
|
153
|
+
* like it has never produced output — including panes that are mid-generation.
|
|
154
|
+
* Reading that as "idle" is the dangerous direction: a delivery would skip its
|
|
155
|
+
* busy branch and paste straight into a working CLI, where Enter is a steering
|
|
156
|
+
* interrupt rather than a new turn.
|
|
157
|
+
*
|
|
158
|
+
* `silenceMs` is the right length because it is already this class's definition
|
|
159
|
+
* of idle: a pane that produces nothing for that long counts as idle anyway, so
|
|
160
|
+
* the grace never suppresses a state the client would otherwise have reported.
|
|
161
|
+
* An actively generating pane re-registers well inside it.
|
|
162
|
+
*/
|
|
163
|
+
inObservationGrace() {
|
|
164
|
+
return this.observationResetAt > 0 && Date.now() < this.observationResetAt + this.silenceMs;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* When this client last lost its view of every pane (0 if it never has).
|
|
168
|
+
*
|
|
169
|
+
* A caller reasoning about a *window* of time — "did the pane react in the two
|
|
170
|
+
* seconds after I pressed Enter?" — needs to know whether it could see for all
|
|
171
|
+
* of it. A reset inside that window makes a negative answer meaningless, and
|
|
172
|
+
* acting on it produces confident, wrong conclusions.
|
|
173
|
+
*/
|
|
174
|
+
getObservationResetAt() {
|
|
175
|
+
return this.observationResetAt;
|
|
176
|
+
}
|
|
107
177
|
/** Check if a window's pane has been silent for at least silenceMs */
|
|
108
178
|
isIdle(windowId) {
|
|
109
179
|
const paneId = this.windowToPaneId(windowId);
|
|
180
|
+
// "Unknown" means unknown, not idle — but only while that ignorance is fresh.
|
|
181
|
+
// After the grace we fall back to the old optimistic answer, because a window
|
|
182
|
+
// that is genuinely untracked (never registered, or resolve failed) must not
|
|
183
|
+
// block delivery forever.
|
|
110
184
|
if (!paneId)
|
|
111
|
-
return
|
|
185
|
+
return !this.inObservationGrace();
|
|
112
186
|
const last = this.lastOutputAt.get(paneId);
|
|
113
187
|
if (last == null)
|
|
114
|
-
return
|
|
188
|
+
return !this.inObservationGrace();
|
|
115
189
|
return Date.now() - last >= this.silenceMs;
|
|
116
190
|
}
|
|
117
191
|
/** Timestamp of the window pane's last observed output, or undefined if unknown. */
|
|
@@ -217,8 +291,7 @@ export class TmuxControlClient extends EventEmitter {
|
|
|
217
291
|
// disconnect that windows churned) can leave our cached paneId →
|
|
218
292
|
// windowId mapping pointing at a stale or recycled pane. Drop the
|
|
219
293
|
// cache and re-resolve every registered window from the new server.
|
|
220
|
-
this.
|
|
221
|
-
this.lastOutputAt.clear();
|
|
294
|
+
this.resetPaneObservations();
|
|
222
295
|
// This is an observation-only client with no real terminal geometry.
|
|
223
296
|
// `ignore-size` prevents tmux's `window-size=latest` policy from treating
|
|
224
297
|
// its synthetic dimensions as authoritative. This flag is now load-bearing,
|
package/dist/tmux-control.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tmux-control.js","sourceRoot":"","sources":["../src/tmux-control.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAqB,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAQ/C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,SAAS,QAAQ,CAAC,IAAc;IAC9B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAc;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC/C,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gBAChB,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,iBAAkB,SAAQ,YAAY;
|
|
1
|
+
{"version":3,"file":"tmux-control.js","sourceRoot":"","sources":["../src/tmux-control.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ChannelAdapter } from "./types.js";
|
|
2
|
-
export declare class ToolTracker {
|
|
3
|
-
private adapter;
|
|
4
|
-
private chatId;
|
|
5
|
-
private threadId?;
|
|
6
|
-
private statusMessageId;
|
|
7
|
-
private lines;
|
|
8
|
-
constructor(adapter: ChannelAdapter, chatId: string, threadId?: string | undefined);
|
|
9
|
-
onToolUse(toolName: string, input: unknown): Promise<void>;
|
|
10
|
-
onToolResult(toolName: string, _output: unknown): Promise<void>;
|
|
11
|
-
reset(): void;
|
|
12
|
-
private summarizeTool;
|
|
13
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export class ToolTracker {
|
|
2
|
-
adapter;
|
|
3
|
-
chatId;
|
|
4
|
-
threadId;
|
|
5
|
-
statusMessageId = null;
|
|
6
|
-
lines = [];
|
|
7
|
-
constructor(adapter, chatId, threadId) {
|
|
8
|
-
this.adapter = adapter;
|
|
9
|
-
this.chatId = chatId;
|
|
10
|
-
this.threadId = threadId;
|
|
11
|
-
}
|
|
12
|
-
async onToolUse(toolName, input) {
|
|
13
|
-
const summary = this.summarizeTool(toolName, input);
|
|
14
|
-
this.lines.push(`🔧 ${summary}`);
|
|
15
|
-
if (!this.statusMessageId) {
|
|
16
|
-
// First tool — send new message
|
|
17
|
-
const sent = await this.adapter.sendText(this.chatId, this.lines.join("\n"), { threadId: this.threadId });
|
|
18
|
-
this.statusMessageId = sent.messageId;
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
// Subsequent — edit existing
|
|
22
|
-
await this.adapter.editMessage(this.chatId, this.statusMessageId, this.lines.join("\n"), this.threadId);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async onToolResult(toolName, _output) {
|
|
26
|
-
// Find the last line matching this tool and mark it done
|
|
27
|
-
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
28
|
-
if (this.lines[i].includes(toolName) && this.lines[i].startsWith("🔧")) {
|
|
29
|
-
this.lines[i] = this.lines[i].replace("🔧", "✅");
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (this.statusMessageId) {
|
|
34
|
-
await this.adapter.editMessage(this.chatId, this.statusMessageId, this.lines.join("\n"), this.threadId);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
reset() {
|
|
38
|
-
this.statusMessageId = null;
|
|
39
|
-
this.lines = [];
|
|
40
|
-
}
|
|
41
|
-
summarizeTool(name, input) {
|
|
42
|
-
const inp = input;
|
|
43
|
-
if (name === "Read")
|
|
44
|
-
return `Read: ${inp.file_path ?? ""}`;
|
|
45
|
-
if (name === "Edit")
|
|
46
|
-
return `Edit: ${inp.file_path ?? ""}`;
|
|
47
|
-
if (name === "Write")
|
|
48
|
-
return `Write: ${inp.file_path ?? ""}`;
|
|
49
|
-
if (name === "Bash")
|
|
50
|
-
return `Bash: ${String(inp.command ?? "").slice(0, 60)}`;
|
|
51
|
-
if (name === "Glob")
|
|
52
|
-
return `Glob: ${inp.pattern ?? ""}`;
|
|
53
|
-
if (name === "Grep")
|
|
54
|
-
return `Grep: ${inp.pattern ?? ""}`;
|
|
55
|
-
return name;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=tool-tracker.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-tracker.js","sourceRoot":"","sources":["../../src/channel/tool-tracker.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon-entry.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/daemon-entry.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Daemon } from "./daemon.js";
|
|
2
|
-
import { createLogger } from "./logger.js";
|
|
3
|
-
const args = process.argv.slice(2);
|
|
4
|
-
function getArg(name) {
|
|
5
|
-
const idx = args.indexOf(name);
|
|
6
|
-
return idx !== -1 ? args[idx + 1] : "";
|
|
7
|
-
}
|
|
8
|
-
const name = getArg("--instance");
|
|
9
|
-
const instanceDir = getArg("--instance-dir");
|
|
10
|
-
let config;
|
|
11
|
-
try {
|
|
12
|
-
config = JSON.parse(getArg("--config"));
|
|
13
|
-
}
|
|
14
|
-
catch (err) {
|
|
15
|
-
console.error(`Failed to parse --config JSON: ${err.message}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
const topicMode = args.includes("--topic-mode");
|
|
19
|
-
const daemon = new Daemon(name, config, instanceDir, topicMode, undefined, undefined, createLogger(config.log_level));
|
|
20
|
-
daemon.start().catch((err) => {
|
|
21
|
-
console.error("Daemon failed:", err);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
});
|
|
24
|
-
const shutdown = async () => {
|
|
25
|
-
await daemon.stop();
|
|
26
|
-
process.exit(0);
|
|
27
|
-
};
|
|
28
|
-
process.on("SIGTERM", shutdown);
|
|
29
|
-
process.on("SIGINT", shutdown);
|
|
30
|
-
//# sourceMappingURL=daemon-entry.js.map
|
package/dist/daemon-entry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-entry.js","sourceRoot":"","sources":["../src/daemon-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAClC,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAC7C,IAAI,MAAsB,CAAC;AAC3B,IAAI,CAAC;IACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;AAC1C,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,KAAK,CAAC,kCAAmC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AAChD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AAEtH,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC3B,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;IAC1B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAChC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC"}
|