@phnx-labs/agents-cli 1.22.114 → 1.22.116
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 +245 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +210 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +214 -148
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/feed.js +65 -14
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +158 -11
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +29 -11
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +93 -302
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +3 -9
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +6 -5
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +229 -37
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +141 -0
- package/dist/lib/feed/hub-server.js +586 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +72 -1
- package/dist/lib/hosts/remote-cmd.js +151 -10
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +27 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +220 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One fleet fan-out, many readers.
|
|
3
|
+
*
|
|
4
|
+
* THE COST THIS EXISTS TO AVOID. `watchFleetFeed` opens a persistent
|
|
5
|
+
* `ssh <peer> agents feed watch --json --local` per dialable device — that is
|
|
6
|
+
* correct, and it is also per CALLER. Every consumer that wanted the operator
|
|
7
|
+
* stream ran its own copy: the extension's leader child, a menu-bar helper, an
|
|
8
|
+
* operator's `agents feed watch --json`. Three readers on a thirteen-device
|
|
9
|
+
* fleet is thirty-nine long-lived ssh children carrying byte-identical NDJSON,
|
|
10
|
+
* three copies of the local activity cursor, and three independent backoff
|
|
11
|
+
* ladders that each re-dial the same offline box.
|
|
12
|
+
*
|
|
13
|
+
* WHAT REPLACES IT. The hub owns exactly ONE {@link watchFleetFeed} — so one ssh
|
|
14
|
+
* child per reachable peer, one collector, one backoff ladder — and broadcasts
|
|
15
|
+
* to every subscriber. The fan-out starts on the FIRST subscriber and stops on
|
|
16
|
+
* the LAST, so an idle box with no reader open holds no peer connections at all.
|
|
17
|
+
*
|
|
18
|
+
* A LATE SUBSCRIBER COSTS NOTHING. The hub keeps the per-scope row state the
|
|
19
|
+
* stream has delivered so far, so subscriber two is served a synthesized reset
|
|
20
|
+
* per scope out of that state — no second fan-out, no re-dial, no waiting for
|
|
21
|
+
* the peers to re-announce. Its `streamId`/`sequence` are its own and start at
|
|
22
|
+
* 1, which is exactly what the published contract ("order by streamId +
|
|
23
|
+
* sequence") lets a consumer rely on.
|
|
24
|
+
*
|
|
25
|
+
* RESET SEMANTICS ARE PRESERVED, NOT REINVENTED. A peer's reset replaces that
|
|
26
|
+
* ONE scope's rows in the held state and is forwarded verbatim; a peer going
|
|
27
|
+
* unavailable forwards the `scope` event and leaves its rows in place, so a
|
|
28
|
+
* reader that attaches while a box is offline still sees that box's last-known
|
|
29
|
+
* rows marked unavailable rather than an empty fleet.
|
|
30
|
+
*/
|
|
31
|
+
import { machineId, normalizeHost } from '../machine-id.js';
|
|
32
|
+
import { FeedWatchState } from './envelope.js';
|
|
33
|
+
/** Activity events replayed to a late subscriber. The lane is a rolling view,
|
|
34
|
+
* so a bounded tail is the honest amount of history to hand over. */
|
|
35
|
+
export const HUB_ACTIVITY_REPLAY = 50;
|
|
36
|
+
function emptyScope() {
|
|
37
|
+
return { agents: new Map(), attention: new Map(), tools: new Map(), setup: [], capturedAt: 0 };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The per-scope row state the hub has observed. This is a projection of the
|
|
41
|
+
* events already delivered, never an independent gather: nothing here reads a
|
|
42
|
+
* file, runs a command, or dials a peer.
|
|
43
|
+
*/
|
|
44
|
+
export class FeedHubState {
|
|
45
|
+
scopes = new Map();
|
|
46
|
+
activity = [];
|
|
47
|
+
scope(name) {
|
|
48
|
+
const key = normalizeHost(name);
|
|
49
|
+
let scope = this.scopes.get(key);
|
|
50
|
+
if (!scope) {
|
|
51
|
+
scope = emptyScope();
|
|
52
|
+
this.scopes.set(key, scope);
|
|
53
|
+
}
|
|
54
|
+
return scope;
|
|
55
|
+
}
|
|
56
|
+
apply(event) {
|
|
57
|
+
const scope = this.scope(event.scope);
|
|
58
|
+
switch (event.type) {
|
|
59
|
+
case 'reset':
|
|
60
|
+
// One scope only. A peer reconnecting must not erase another's rows.
|
|
61
|
+
scope.agents = new Map(event.agents.map((agent) => [agent.rowKey, agent]));
|
|
62
|
+
scope.attention = new Map(event.attention.map((item) => [item.key, item]));
|
|
63
|
+
scope.tools = new Map(event.tools.map((tool) => [tool.rowKey, tool]));
|
|
64
|
+
scope.setup = event.setup;
|
|
65
|
+
scope.capturedAt = event.capturedAt;
|
|
66
|
+
break;
|
|
67
|
+
case 'agent.upsert':
|
|
68
|
+
scope.agents.set(event.rowKey, event.agent);
|
|
69
|
+
break;
|
|
70
|
+
case 'agent.remove':
|
|
71
|
+
scope.agents.delete(event.rowKey);
|
|
72
|
+
break;
|
|
73
|
+
case 'attention.upsert':
|
|
74
|
+
scope.attention.set(event.rowKey, event.attention);
|
|
75
|
+
break;
|
|
76
|
+
case 'attention.remove':
|
|
77
|
+
scope.attention.delete(event.rowKey);
|
|
78
|
+
break;
|
|
79
|
+
case 'tool.upsert':
|
|
80
|
+
scope.tools.set(event.rowKey, event.tool);
|
|
81
|
+
break;
|
|
82
|
+
case 'tool.remove':
|
|
83
|
+
scope.tools.delete(event.rowKey);
|
|
84
|
+
break;
|
|
85
|
+
case 'setup.snapshot':
|
|
86
|
+
scope.setup = event.setup;
|
|
87
|
+
break;
|
|
88
|
+
case 'scope':
|
|
89
|
+
// Rows are deliberately RETAINED: transient fleet loss is not session
|
|
90
|
+
// death, the same rule `watchLocalSessions` states for its own scope.
|
|
91
|
+
scope.status = { status: event.status, ...(event.reason ? { reason: event.reason } : {}) };
|
|
92
|
+
break;
|
|
93
|
+
case 'activity.append':
|
|
94
|
+
this.activity.push(event.event);
|
|
95
|
+
if (this.activity.length > HUB_ACTIVITY_REPLAY)
|
|
96
|
+
this.activity.shift();
|
|
97
|
+
break;
|
|
98
|
+
case 'heartbeat': break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The envelopes that bring a fresh subscriber to the current state. */
|
|
102
|
+
snapshot(state) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const [name, scope] of this.scopes) {
|
|
105
|
+
out.push(state.emit({
|
|
106
|
+
type: 'reset', scope: name, capturedAt: scope.capturedAt || Date.now(),
|
|
107
|
+
agents: [...scope.agents.values()], attention: [...scope.attention.values()],
|
|
108
|
+
tools: [...scope.tools.values()], setup: scope.setup,
|
|
109
|
+
}));
|
|
110
|
+
if (scope.status)
|
|
111
|
+
out.push(state.emit({ type: 'scope', scope: name, capturedAt: Date.now(), ...scope.status }));
|
|
112
|
+
}
|
|
113
|
+
// Chronological, matching the order they were first delivered in.
|
|
114
|
+
for (const event of this.activity) {
|
|
115
|
+
out.push(state.emit({ type: 'activity.append', scope: normalizeHost(machineId()), event }));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/** Scopes the hub has seen. Observability + tests. */
|
|
120
|
+
get scopeNames() { return [...this.scopes.keys()]; }
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The shared collector. Construct one per process; call {@link subscribe} per
|
|
124
|
+
* reader.
|
|
125
|
+
*/
|
|
126
|
+
export class FeedHub {
|
|
127
|
+
options;
|
|
128
|
+
subscribers = new Set();
|
|
129
|
+
held = new FeedHubState();
|
|
130
|
+
controller = null;
|
|
131
|
+
running = null;
|
|
132
|
+
/**
|
|
133
|
+
* Bumped on every start/stop. A fan-out's completion handler only clears state
|
|
134
|
+
* when its own generation is still current, so a run winding down cannot clear
|
|
135
|
+
* a newer one's controller.
|
|
136
|
+
*/
|
|
137
|
+
generation = 0;
|
|
138
|
+
/** The most recent fan-out failure, if the current generation hit one. */
|
|
139
|
+
lastFailure = null;
|
|
140
|
+
/**
|
|
141
|
+
* Called when the fan-out rejects. Without this the failure lived only in a
|
|
142
|
+
* dropped promise, so readers sat attached to a collector that had already died
|
|
143
|
+
* and saw an idle stream instead of an error. Settable so the socket server can
|
|
144
|
+
* attach after construction.
|
|
145
|
+
*/
|
|
146
|
+
onFailure = null;
|
|
147
|
+
watch;
|
|
148
|
+
constructor(options) {
|
|
149
|
+
this.options = options;
|
|
150
|
+
this.watch = options.watch;
|
|
151
|
+
}
|
|
152
|
+
/** Readers currently attached. The fan-out runs iff this is > 0. */
|
|
153
|
+
get readerCount() { return this.subscribers.size; }
|
|
154
|
+
/** Is the single shared fan-out running right now? */
|
|
155
|
+
get active() { return this.controller !== null; }
|
|
156
|
+
/** The held per-scope state, for observability and tests. */
|
|
157
|
+
get state() { return this.held; }
|
|
158
|
+
/**
|
|
159
|
+
* Attach a reader. It is immediately served a snapshot of the held state, then
|
|
160
|
+
* every later event. Returns the detach function; the fan-out stops when the
|
|
161
|
+
* last reader detaches.
|
|
162
|
+
*/
|
|
163
|
+
subscribe(emit) {
|
|
164
|
+
const subscriber = { emit, state: new FeedWatchState() };
|
|
165
|
+
this.subscribers.add(subscriber);
|
|
166
|
+
for (const event of this.held.snapshot(subscriber.state))
|
|
167
|
+
emit(event);
|
|
168
|
+
this.start();
|
|
169
|
+
let detached = false;
|
|
170
|
+
return () => {
|
|
171
|
+
if (detached)
|
|
172
|
+
return;
|
|
173
|
+
detached = true;
|
|
174
|
+
this.subscribers.delete(subscriber);
|
|
175
|
+
if (this.subscribers.size === 0)
|
|
176
|
+
this.stop();
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Stop the fan-out and detach every reader. */
|
|
180
|
+
async close() {
|
|
181
|
+
this.subscribers.clear();
|
|
182
|
+
this.stop();
|
|
183
|
+
const running = this.running;
|
|
184
|
+
this.running = null;
|
|
185
|
+
if (running)
|
|
186
|
+
await running.catch(() => { });
|
|
187
|
+
}
|
|
188
|
+
/** Await the in-flight fan-out's teardown. Tests assert no overlap with it. */
|
|
189
|
+
async settled() {
|
|
190
|
+
await this.running?.catch(() => { });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Start the single fan-out, waiting for any previous one to finish first.
|
|
194
|
+
*
|
|
195
|
+
* The wait is the whole point. `stop()` aborts and returns immediately, but the
|
|
196
|
+
* fan-out it aborted is still tearing down ssh children. A reader that detaches
|
|
197
|
+
* and immediately reattaches — a VS Code window reloading, a menu-bar popover
|
|
198
|
+
* closing and reopening — therefore used to start a SECOND fan-out alongside
|
|
199
|
+
* the dying one: two ssh children per peer, two collectors, for as long as the
|
|
200
|
+
* overlap lasted. Serializing on the previous run makes that impossible.
|
|
201
|
+
*/
|
|
202
|
+
start() {
|
|
203
|
+
if (this.controller)
|
|
204
|
+
return;
|
|
205
|
+
const generation = ++this.generation;
|
|
206
|
+
this.lastFailure = null;
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
this.controller = controller;
|
|
209
|
+
const previous = this.running ?? Promise.resolve();
|
|
210
|
+
this.running = previous
|
|
211
|
+
.catch(() => { })
|
|
212
|
+
.then(() => {
|
|
213
|
+
// The readers may all have left while we waited for the old run to
|
|
214
|
+
// drain; starting then would dial every peer for nobody.
|
|
215
|
+
if (generation !== this.generation || controller.signal.aborted)
|
|
216
|
+
return;
|
|
217
|
+
return this.watch({
|
|
218
|
+
signal: controller.signal,
|
|
219
|
+
...(this.options.reconnectMs !== undefined ? { reconnectMs: this.options.reconnectMs } : {}),
|
|
220
|
+
// Generation-guarded: an aborted fan-out can still emit while it drains
|
|
221
|
+
// (a peer's last buffered line, a pending promise resolving). Those
|
|
222
|
+
// envelopes describe the OLD subscription and must not reach the new
|
|
223
|
+
// generation's readers or mutate its held state.
|
|
224
|
+
emit: (event) => { if (generation === this.generation)
|
|
225
|
+
this.broadcast(event); },
|
|
226
|
+
});
|
|
227
|
+
})
|
|
228
|
+
.catch((error) => {
|
|
229
|
+
// Surfaced, never swallowed: a reader attached to a dead collector would
|
|
230
|
+
// otherwise be indistinguishable from a quiet fleet.
|
|
231
|
+
if (generation === this.generation) {
|
|
232
|
+
this.lastFailure = error instanceof Error ? error : new Error(String(error));
|
|
233
|
+
this.onFailure?.(this.lastFailure);
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
.finally(() => {
|
|
237
|
+
// Only clear if this is still the live run: a stop/start cycle may have
|
|
238
|
+
// replaced it, and clearing then would strand the newer controller.
|
|
239
|
+
if (generation === this.generation)
|
|
240
|
+
this.controller = null;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
stop() {
|
|
244
|
+
this.generation += 1;
|
|
245
|
+
this.controller?.abort();
|
|
246
|
+
this.controller = null;
|
|
247
|
+
}
|
|
248
|
+
broadcast(event) {
|
|
249
|
+
this.held.apply(event);
|
|
250
|
+
const { v: _v, streamId: _streamId, sequence: _sequence, ...payload } = event;
|
|
251
|
+
for (const subscriber of this.subscribers) {
|
|
252
|
+
subscriber.emit(subscriber.state.emit(payload));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -7,9 +7,17 @@ export interface PullRequestStatus extends PullRequestAttentionSignal {
|
|
|
7
7
|
statusCheckRollup?: unknown[];
|
|
8
8
|
}
|
|
9
9
|
/** CLI-owned bounded-TTL source shared by feed attention and PR-board consumers. */
|
|
10
|
+
export declare const PR_STATUS_DEFAULT_TIMEOUT_MS = 15000;
|
|
11
|
+
/**
|
|
12
|
+
* @param options.timeoutMs bound for the underlying `gh` call. An operator-facing
|
|
13
|
+
* caller working against a deadline (the feed answer path) passes its remaining
|
|
14
|
+
* budget so one unreachable PR cannot consume the whole operation; a timed-out
|
|
15
|
+
* read is negative-cached for the TTL like any other failure.
|
|
16
|
+
*/
|
|
10
17
|
export declare function readPullRequestStatus(session: ActiveSession, options?: {
|
|
11
18
|
nowMs?: number;
|
|
12
19
|
ttlMs?: number;
|
|
20
|
+
timeoutMs?: number;
|
|
13
21
|
gh?: GhExec;
|
|
14
22
|
}): Promise<PullRequestStatus | undefined>;
|
|
15
23
|
export declare function resetPullRequestStatusCache(): void;
|
|
@@ -15,6 +15,13 @@ function needsHuman(value) {
|
|
|
15
15
|
return value.reviewDecision !== 'APPROVED' || (value.mergeable === 'MERGEABLE' && checksSettled);
|
|
16
16
|
}
|
|
17
17
|
/** CLI-owned bounded-TTL source shared by feed attention and PR-board consumers. */
|
|
18
|
+
export const PR_STATUS_DEFAULT_TIMEOUT_MS = 15_000;
|
|
19
|
+
/**
|
|
20
|
+
* @param options.timeoutMs bound for the underlying `gh` call. An operator-facing
|
|
21
|
+
* caller working against a deadline (the feed answer path) passes its remaining
|
|
22
|
+
* budget so one unreachable PR cannot consume the whole operation; a timed-out
|
|
23
|
+
* read is negative-cached for the TTL like any other failure.
|
|
24
|
+
*/
|
|
18
25
|
export async function readPullRequestStatus(session, options = {}) {
|
|
19
26
|
const ref = session.pr?.url ?? session.pr?.number;
|
|
20
27
|
if (!ref || !session.cwd)
|
|
@@ -25,7 +32,8 @@ export async function readPullRequestStatus(session, options = {}) {
|
|
|
25
32
|
if (hit && hit.expiresAt > now)
|
|
26
33
|
return hit.value;
|
|
27
34
|
const cwd = session.cwd;
|
|
28
|
-
const
|
|
35
|
+
const timeout = options.timeoutMs ?? PR_STATUS_DEFAULT_TIMEOUT_MS;
|
|
36
|
+
const gh = options.gh ?? (async (args) => (await execFileAsync('gh', args, { cwd, timeout, maxBuffer: 1024 * 1024 })).stdout);
|
|
29
37
|
try {
|
|
30
38
|
const stdout = await gh(['pr', 'view', String(ref), '--json', PR_STATUS_FIELDS]);
|
|
31
39
|
const raw = JSON.parse(stdout);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type BrowserSessionRow } from '../browser/sessions-list.js';
|
|
2
|
+
import { type ComputerRunRow } from '../computer/sessions-list.js';
|
|
3
|
+
import type { LiveBrowserTask } from './tools.js';
|
|
4
|
+
import { type ToolRow } from './tools.js';
|
|
5
|
+
/** Re-projection cadence used only while no directory watcher could arm. */
|
|
6
|
+
export declare const TOOL_SWEEP_MS = 5000;
|
|
7
|
+
/** What one re-projection changed. Empty on both sides means nothing moved. */
|
|
8
|
+
export interface ToolDiff {
|
|
9
|
+
upserts: ToolRow[];
|
|
10
|
+
removes: string[];
|
|
11
|
+
}
|
|
12
|
+
interface ToolSources {
|
|
13
|
+
browserRows?: () => BrowserSessionRow[];
|
|
14
|
+
computerRows?: () => ComputerRunRow[];
|
|
15
|
+
bindings?: () => Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
device?: string;
|
|
18
|
+
profile?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
createdAt?: number;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
launchId?: string;
|
|
23
|
+
}>;
|
|
24
|
+
liveTasks?: () => LiveBrowserTask[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One projection attempt. `complete` is false when any source threw.
|
|
28
|
+
*
|
|
29
|
+
* The flag is load-bearing, not diagnostic. A transient read failure — the
|
|
30
|
+
* browser rewriting `tasks.json`, a rotating ledger, an EMFILE — used to yield an
|
|
31
|
+
* EMPTY row list, which the differ then read as "every task closed" and published
|
|
32
|
+
* as a remove for every row. The operator watched their live tasks vanish and
|
|
33
|
+
* come back. An incomplete projection is not evidence of absence, so the caller
|
|
34
|
+
* keeps the state it already had.
|
|
35
|
+
*/
|
|
36
|
+
export interface ToolSnapshot {
|
|
37
|
+
rows: ToolRow[];
|
|
38
|
+
complete: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Project every browser task and computer run this machine knows about into
|
|
42
|
+
* canonical tool rows, newest first. Impure by design — the three readers are
|
|
43
|
+
* injectable so a test drives real temp stores rather than a mocked service.
|
|
44
|
+
*/
|
|
45
|
+
export declare function collectToolRows(scope: string, sources?: ToolSources): ToolSnapshot;
|
|
46
|
+
/**
|
|
47
|
+
* Holds the last projected row set and answers "what changed?".
|
|
48
|
+
*
|
|
49
|
+
* Row identity is the projection's own `rowKey`, so a browser task that gains a
|
|
50
|
+
* capture upserts under the same key, and a closed task — gone from both the
|
|
51
|
+
* index and the capture tree — comes back as a remove.
|
|
52
|
+
*/
|
|
53
|
+
export declare class ToolRowSet {
|
|
54
|
+
private readonly rows;
|
|
55
|
+
/** The diff from the current set to `next`, and adopt `next` as current. */
|
|
56
|
+
diff(next: ToolRow[]): ToolDiff;
|
|
57
|
+
/** Adopt `rows` as the current set without emitting a diff (a reset). */
|
|
58
|
+
reset(rows: ToolRow[]): void;
|
|
59
|
+
}
|
|
60
|
+
interface ToolWatchOptions {
|
|
61
|
+
scope: string;
|
|
62
|
+
signal: AbortSignal;
|
|
63
|
+
/** Called with every non-empty diff. */
|
|
64
|
+
onDiff: (diff: ToolDiff) => void;
|
|
65
|
+
/** Re-projection cadence while no watcher is armed. */
|
|
66
|
+
sweepMs?: number;
|
|
67
|
+
/** Roots to watch (tests pass temp dirs). */
|
|
68
|
+
roots?: string[];
|
|
69
|
+
/** Row sources, forwarded to {@link collectToolRows}. */
|
|
70
|
+
sources?: ToolSources;
|
|
71
|
+
/** Seed the set so the first diff reports only later changes. */
|
|
72
|
+
initial?: ToolRow[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The directory roots whose contents back the tool rows.
|
|
76
|
+
*
|
|
77
|
+
* The standalone computer ledger is its OWN root: the engine writes there
|
|
78
|
+
* directly, without going through agents-cli, so nothing under the event-ledger
|
|
79
|
+
* or browser roots changes when an operator runs `computer` by hand. Omitting it
|
|
80
|
+
* meant those actions were only ever noticed on a sweep triggered by unrelated
|
|
81
|
+
* activity.
|
|
82
|
+
*/
|
|
83
|
+
export declare function toolWatchRoots(): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Every live browser task on this machine, across every profile runtime dir.
|
|
86
|
+
*
|
|
87
|
+
* THROWS for the same reason {@link readLiveTasksFor} does. A runtime dir that
|
|
88
|
+
* does not exist means no browser has ever run here — genuinely no tasks. A
|
|
89
|
+
* readdir that fails for any other reason (EACCES, EMFILE) is a failure, and
|
|
90
|
+
* returning `[]` for it would tell the differ every task had closed.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readLiveBrowserTasks(root?: string): LiveBrowserTask[];
|
|
93
|
+
/**
|
|
94
|
+
* Watch the tool roots and report diffs until `signal` aborts.
|
|
95
|
+
*
|
|
96
|
+
* `armed()` reports whether every root currently has a live watcher. It is a
|
|
97
|
+
* FUNCTION, not a flag captured at setup: a watcher can die later (its directory
|
|
98
|
+
* is removed and recreated, an inotify limit is hit), and a frozen `armed: true`
|
|
99
|
+
* meant the tick kept short-circuiting on `!dirty` from a watcher that would
|
|
100
|
+
* never report again — changes were then missed permanently, with the handle
|
|
101
|
+
* still claiming to be armed. Each tick re-arms whatever is missing and sweeps
|
|
102
|
+
* until everything is watched again.
|
|
103
|
+
*/
|
|
104
|
+
export declare function watchToolActivity(options: ToolWatchOptions): {
|
|
105
|
+
armed: () => boolean;
|
|
106
|
+
stop: () => void;
|
|
107
|
+
};
|
|
108
|
+
export {};
|