@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
|
@@ -96,15 +96,31 @@ export declare function classifyUserPrompt(raw: string | undefined, opts?: {
|
|
|
96
96
|
hasImageAttachment?: boolean;
|
|
97
97
|
}): ClassifiedPrompt;
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* Reduce a harness-generated session title to something worth showing as a
|
|
100
|
+
* headline, or `undefined` when it is not (PHNX-3999 F26/F27).
|
|
101
101
|
*
|
|
102
102
|
* Claude's `ai-title` and Cursor's `chatMeta.title` are both derived from the
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
103
|
+
* FIRST turn of the transcript, so whatever the harness injected there becomes
|
|
104
|
+
* the session's name: a skill's install-directory preamble, a `!`-prefixed shell
|
|
105
|
+
* echo wrapped in `<bash-input>`, a `/clear`. The owner's recording shows both
|
|
106
|
+
* failures — one row titled with shell-command XML wrappers, others reading
|
|
107
|
+
* `/clear`. Three rungs, in order:
|
|
108
|
+
*
|
|
109
|
+
* 1. A skill invocation collapses to `/<skill>` ({@link classifyUserPrompt}
|
|
110
|
+
* reports `kind: 'skill'` only for that injected line).
|
|
111
|
+
* 2. Harness scaffolding is REJECTED, not cleaned: `<bash-input>`,
|
|
112
|
+
* `<command-name>`, `<local-command-stdout>`, `<system-reminder>` and the
|
|
113
|
+
* rest of {@link isSyntheticUserMessage}'s cross-harness list, plus a title
|
|
114
|
+
* that is only tags. Stripping the tags would leave the shell command
|
|
115
|
+
* itself as the headline, which is the same wrong answer with tidier
|
|
116
|
+
* punctuation.
|
|
117
|
+
* 3. A bare control command is rejected ({@link BARE_CONTROL_COMMAND_RE}).
|
|
118
|
+
*
|
|
119
|
+
* A rejected label returns `undefined`, so the row falls to the next rung of the
|
|
120
|
+
* canonical ladder — the daemon-generated title, then the user's own first
|
|
121
|
+
* prompt (`sessionHeadline`, `session/title.ts`). Nothing is deleted: the
|
|
122
|
+
* original turn stays on the row as `firstUserMessage` / `request`, so the
|
|
123
|
+
* details view still shows exactly what the agent was told.
|
|
108
124
|
*
|
|
109
125
|
* A user-authored title (Claude `/rename` / `custom-title`) is never passed
|
|
110
126
|
* here — the caller keeps it verbatim.
|
|
@@ -295,15 +295,45 @@ export function classifyUserPrompt(raw, opts = {}) {
|
|
|
295
295
|
return { clean: display, kind: 'text' };
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
298
|
+
* A label that is nothing but a control command — `/clear`, `/compact`,
|
|
299
|
+
* `/model`, `/exit` — with no words of its own after it.
|
|
300
|
+
*
|
|
301
|
+
* One rule instead of a per-harness list of control verbs: a title that is a
|
|
302
|
+
* bare slash token describes an action taken ON the session, never what the
|
|
303
|
+
* session is about, so whatever it names it is not a headline. A command WITH an
|
|
304
|
+
* argument (`/continue fix the parser`, `/code:commit the retry fix`) carries
|
|
305
|
+
* real intent and is kept.
|
|
306
|
+
*
|
|
307
|
+
* Skill collapse runs first in {@link cleanGeneratedSessionLabel} and returns
|
|
308
|
+
* its own `/<skill>` form, so this never swallows that deliberate shape.
|
|
309
|
+
*/
|
|
310
|
+
const BARE_CONTROL_COMMAND_RE = /^\/[\w:-]+$/;
|
|
311
|
+
/**
|
|
312
|
+
* Reduce a harness-generated session title to something worth showing as a
|
|
313
|
+
* headline, or `undefined` when it is not (PHNX-3999 F26/F27).
|
|
300
314
|
*
|
|
301
315
|
* Claude's `ai-title` and Cursor's `chatMeta.title` are both derived from the
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
316
|
+
* FIRST turn of the transcript, so whatever the harness injected there becomes
|
|
317
|
+
* the session's name: a skill's install-directory preamble, a `!`-prefixed shell
|
|
318
|
+
* echo wrapped in `<bash-input>`, a `/clear`. The owner's recording shows both
|
|
319
|
+
* failures — one row titled with shell-command XML wrappers, others reading
|
|
320
|
+
* `/clear`. Three rungs, in order:
|
|
321
|
+
*
|
|
322
|
+
* 1. A skill invocation collapses to `/<skill>` ({@link classifyUserPrompt}
|
|
323
|
+
* reports `kind: 'skill'` only for that injected line).
|
|
324
|
+
* 2. Harness scaffolding is REJECTED, not cleaned: `<bash-input>`,
|
|
325
|
+
* `<command-name>`, `<local-command-stdout>`, `<system-reminder>` and the
|
|
326
|
+
* rest of {@link isSyntheticUserMessage}'s cross-harness list, plus a title
|
|
327
|
+
* that is only tags. Stripping the tags would leave the shell command
|
|
328
|
+
* itself as the headline, which is the same wrong answer with tidier
|
|
329
|
+
* punctuation.
|
|
330
|
+
* 3. A bare control command is rejected ({@link BARE_CONTROL_COMMAND_RE}).
|
|
331
|
+
*
|
|
332
|
+
* A rejected label returns `undefined`, so the row falls to the next rung of the
|
|
333
|
+
* canonical ladder — the daemon-generated title, then the user's own first
|
|
334
|
+
* prompt (`sessionHeadline`, `session/title.ts`). Nothing is deleted: the
|
|
335
|
+
* original turn stays on the row as `firstUserMessage` / `request`, so the
|
|
336
|
+
* details view still shows exactly what the agent was told.
|
|
307
337
|
*
|
|
308
338
|
* A user-authored title (Claude `/rename` / `custom-title`) is never passed
|
|
309
339
|
* here — the caller keeps it verbatim.
|
|
@@ -313,7 +343,15 @@ export function cleanGeneratedSessionLabel(title) {
|
|
|
313
343
|
if (!trimmed)
|
|
314
344
|
return undefined;
|
|
315
345
|
const classified = classifyUserPrompt(trimmed);
|
|
316
|
-
|
|
346
|
+
if (classified.kind === 'skill')
|
|
347
|
+
return classified.clean;
|
|
348
|
+
if (isSyntheticUserMessage(trimmed))
|
|
349
|
+
return undefined;
|
|
350
|
+
if (!stripXmlLikeTags(trimmed).trim())
|
|
351
|
+
return undefined;
|
|
352
|
+
if (BARE_CONTROL_COMMAND_RE.test(trimmed))
|
|
353
|
+
return undefined;
|
|
354
|
+
return trimmed;
|
|
317
355
|
}
|
|
318
356
|
/**
|
|
319
357
|
* Extract a one-line topic from a raw user message, or undefined if the message
|
|
@@ -5,11 +5,29 @@ export declare const PEER_BACKOFF_BASE_MS = 2000;
|
|
|
5
5
|
export declare const PEER_BACKOFF_CAP_MS = 60000;
|
|
6
6
|
/** Consecutive failed spawns before the peer is parked. */
|
|
7
7
|
export declare const PEER_PARK_AFTER_FAILURES = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
|
|
10
|
+
* gives way to {@link PEER_RETIRED_RECHECK_MS}.
|
|
11
|
+
*
|
|
12
|
+
* The cap alone bounds the delay but not the total work: a box that is off for a
|
|
13
|
+
* weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
|
|
14
|
+
* per peer, each one already known to fail. Ten consecutive failures is well
|
|
15
|
+
* past any transient network event, so past that point the peer is treated as
|
|
16
|
+
* genuinely absent and re-dialed on the slow cadence instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PEER_RETIRE_AFTER_FAILURES = 10;
|
|
19
|
+
/**
|
|
20
|
+
* Re-dial cadence for a retired peer. Still bounded rather than never, because a
|
|
21
|
+
* box can come back without anything touching the device registry — waiting
|
|
22
|
+
* only on a registry change would leave it unreachable until an operator acted.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PEER_RETIRED_RECHECK_MS: number;
|
|
8
25
|
/**
|
|
9
26
|
* Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
|
|
10
|
-
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}
|
|
27
|
+
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
|
|
28
|
+
* and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
|
|
11
29
|
*/
|
|
12
|
-
export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number): number;
|
|
30
|
+
export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number, retireAfter?: number, retiredMs?: number): number;
|
|
13
31
|
interface PeerStreamOptions {
|
|
14
32
|
/** The device to subscribe to. */
|
|
15
33
|
device: DeviceProfile;
|
|
@@ -29,6 +47,10 @@ interface PeerStreamOptions {
|
|
|
29
47
|
backoffCapMs?: number;
|
|
30
48
|
/** Override the park threshold (tests). */
|
|
31
49
|
parkAfterFailures?: number;
|
|
50
|
+
/** Override the retire threshold (tests). */
|
|
51
|
+
retireAfterFailures?: number;
|
|
52
|
+
/** Override the retired re-dial cadence (tests). */
|
|
53
|
+
retiredRecheckMs?: number;
|
|
32
54
|
/** Override the ssh binary (tests). */
|
|
33
55
|
sshBin?: string;
|
|
34
56
|
/** Override the parked peer's registry re-check cadence (tests). */
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* - a peer that fails {@link PEER_PARK_AFTER_FAILURES} spawns in a row parked —
|
|
20
20
|
* it stops the reconnect cycle and re-dials only when the device registry
|
|
21
21
|
* changes or the capped backoff elapses;
|
|
22
|
+
* - a peer that fails {@link PEER_RETIRE_AFTER_FAILURES} in a row RETIRED — the
|
|
23
|
+
* capped 60 s ladder is itself unbounded in total work, so past that point the
|
|
24
|
+
* re-dial drops to {@link PEER_RETIRED_RECHECK_MS};
|
|
22
25
|
* - abort listeners removed per iteration, so a watcher open for hours does not
|
|
23
26
|
* accumulate one per reconnect on the caller's AbortSignal.
|
|
24
27
|
*/
|
|
@@ -34,17 +37,37 @@ export const PEER_BACKOFF_BASE_MS = 2_000;
|
|
|
34
37
|
export const PEER_BACKOFF_CAP_MS = 60_000;
|
|
35
38
|
/** Consecutive failed spawns before the peer is parked. */
|
|
36
39
|
export const PEER_PARK_AFTER_FAILURES = 3;
|
|
40
|
+
/**
|
|
41
|
+
* Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
|
|
42
|
+
* gives way to {@link PEER_RETIRED_RECHECK_MS}.
|
|
43
|
+
*
|
|
44
|
+
* The cap alone bounds the delay but not the total work: a box that is off for a
|
|
45
|
+
* weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
|
|
46
|
+
* per peer, each one already known to fail. Ten consecutive failures is well
|
|
47
|
+
* past any transient network event, so past that point the peer is treated as
|
|
48
|
+
* genuinely absent and re-dialed on the slow cadence instead.
|
|
49
|
+
*/
|
|
50
|
+
export const PEER_RETIRE_AFTER_FAILURES = 10;
|
|
51
|
+
/**
|
|
52
|
+
* Re-dial cadence for a retired peer. Still bounded rather than never, because a
|
|
53
|
+
* box can come back without anything touching the device registry — waiting
|
|
54
|
+
* only on a registry change would leave it unreachable until an operator acted.
|
|
55
|
+
*/
|
|
56
|
+
export const PEER_RETIRED_RECHECK_MS = 15 * 60_000;
|
|
37
57
|
/** Bytes of a peer's stderr retained for the `unavailable` reason. */
|
|
38
58
|
const PEER_STDERR_BYTES = 2_048;
|
|
39
59
|
/** How often a parked peer re-checks the device registry for a refresh. */
|
|
40
60
|
const PEER_REGISTRY_POLL_MS = 5_000;
|
|
41
61
|
/**
|
|
42
62
|
* Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
|
|
43
|
-
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}
|
|
63
|
+
* peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
|
|
64
|
+
* and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
|
|
44
65
|
*/
|
|
45
|
-
export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS) {
|
|
66
|
+
export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS, retireAfter = PEER_RETIRE_AFTER_FAILURES, retiredMs = PEER_RETIRED_RECHECK_MS) {
|
|
46
67
|
if (failures <= 0)
|
|
47
68
|
return 0;
|
|
69
|
+
if (failures >= retireAfter)
|
|
70
|
+
return retiredMs;
|
|
48
71
|
return Math.min(cap, base * 2 ** (failures - 1));
|
|
49
72
|
}
|
|
50
73
|
/** Bounded tail of a child's stderr, kept for the unavailable reason. */
|
|
@@ -133,10 +156,14 @@ export async function streamFromPeer(options) {
|
|
|
133
156
|
failures += 1;
|
|
134
157
|
const exit = code == null ? 'ssh failed' : `ssh exited ${code}`;
|
|
135
158
|
const parked = failures >= parkAfter;
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
const retireAfter = options.retireAfterFailures ?? PEER_RETIRE_AFTER_FAILURES;
|
|
160
|
+
const retired = failures >= retireAfter;
|
|
161
|
+
const delay = peerBackoffDelayMs(failures, options.backoffBaseMs, options.backoffCapMs, retireAfter, options.retiredRecheckMs);
|
|
162
|
+
options.onUnavailable(retired
|
|
163
|
+
? `${reasonFor(exit, stderr)} — retired after ${failures} failed connections, re-dialing in ${Math.round(delay / 60_000)}min or on a device refresh`
|
|
164
|
+
: parked
|
|
165
|
+
? `${reasonFor(exit, stderr)} — parked after ${failures} failed connections, retrying in ${Math.round(delay / 1000)}s or on a device refresh`
|
|
166
|
+
: reasonFor(exit, stderr));
|
|
140
167
|
await parkedWait(options, delay);
|
|
141
168
|
}
|
|
142
169
|
}
|
|
@@ -124,6 +124,26 @@ export declare function resolvePeerTarget(machine: string): Promise<{
|
|
|
124
124
|
* error, timeout, version-skewed peer with no `--json` preview envelope.
|
|
125
125
|
*/
|
|
126
126
|
export declare function fetchPeerPreviewDigest(sessionId: string, machine: string, timeoutMs?: number): Promise<unknown | undefined>;
|
|
127
|
+
export type PeerPreviewEnvelopeResult = {
|
|
128
|
+
ok: true;
|
|
129
|
+
envelope: unknown;
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
reason: 'no-target' | 'unreachable' | 'invalid-json';
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Fetch the FULL `agents sessions preview <id> --local --json` envelope
|
|
136
|
+
* (session/active/preview/error, not just the `.preview` slice
|
|
137
|
+
* {@link fetchPeerPreviewDigest} narrows to) from a session's owning peer in
|
|
138
|
+
* exactly ONE bounded {@link sshCapture} hop. This is the canonical exact
|
|
139
|
+
* ID+owner preview loader (PHNX-3999): the peer's own command already runs the
|
|
140
|
+
* owner-side bounded parsers/fold (`loadSessionPreviewDigest`) and existing
|
|
141
|
+
* redaction, so a caller that already knows the owning device needs no
|
|
142
|
+
* separate metadata round trip before this one — unlike the general fleet
|
|
143
|
+
* resolver, which fans out a `sessions <id> --json --all` metadata query first
|
|
144
|
+
* because it does NOT yet know which peer (if any) holds the id.
|
|
145
|
+
*/
|
|
146
|
+
export declare function fetchPeerPreviewEnvelope(sessionId: string, machine: string, timeoutMs?: number): Promise<PeerPreviewEnvelopeResult>;
|
|
127
147
|
/** Parse the JSON envelope returned by `sessions preview --json`. */
|
|
128
148
|
export declare function parsePeerPreviewDigest(parsed: unknown): unknown | undefined;
|
|
129
149
|
/**
|
|
@@ -540,21 +540,37 @@ const PEER_PREVIEW_TIMEOUT_MS = 15_000;
|
|
|
540
540
|
* error, timeout, version-skewed peer with no `--json` preview envelope.
|
|
541
541
|
*/
|
|
542
542
|
export async function fetchPeerPreviewDigest(sessionId, machine, timeoutMs = PEER_PREVIEW_TIMEOUT_MS) {
|
|
543
|
+
const envelope = await fetchPeerPreviewEnvelope(sessionId, machine, timeoutMs);
|
|
544
|
+
if (!envelope.ok)
|
|
545
|
+
return undefined;
|
|
546
|
+
return parsePeerPreviewDigest(envelope.envelope);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Fetch the FULL `agents sessions preview <id> --local --json` envelope
|
|
550
|
+
* (session/active/preview/error, not just the `.preview` slice
|
|
551
|
+
* {@link fetchPeerPreviewDigest} narrows to) from a session's owning peer in
|
|
552
|
+
* exactly ONE bounded {@link sshCapture} hop. This is the canonical exact
|
|
553
|
+
* ID+owner preview loader (PHNX-3999): the peer's own command already runs the
|
|
554
|
+
* owner-side bounded parsers/fold (`loadSessionPreviewDigest`) and existing
|
|
555
|
+
* redaction, so a caller that already knows the owning device needs no
|
|
556
|
+
* separate metadata round trip before this one — unlike the general fleet
|
|
557
|
+
* resolver, which fans out a `sessions <id> --json --all` metadata query first
|
|
558
|
+
* because it does NOT yet know which peer (if any) holds the id.
|
|
559
|
+
*/
|
|
560
|
+
export async function fetchPeerPreviewEnvelope(sessionId, machine, timeoutMs = PEER_PREVIEW_TIMEOUT_MS) {
|
|
543
561
|
const peer = await resolvePeerTarget(machine);
|
|
544
562
|
if (!peer)
|
|
545
|
-
return
|
|
563
|
+
return { ok: false, reason: 'no-target' };
|
|
546
564
|
const cmd = remoteListCommand(['sessions', 'preview', sessionId, '--local', '--json'], peer.os);
|
|
547
565
|
const capture = await sshCapture(peer.target, cmd, timeoutMs);
|
|
548
566
|
if (capture.code !== 0)
|
|
549
|
-
return
|
|
550
|
-
let parsed;
|
|
567
|
+
return { ok: false, reason: 'unreachable' };
|
|
551
568
|
try {
|
|
552
|
-
|
|
569
|
+
return { ok: true, envelope: JSON.parse(stripClixml(capture.stdout)) };
|
|
553
570
|
}
|
|
554
571
|
catch {
|
|
555
|
-
return
|
|
572
|
+
return { ok: false, reason: 'invalid-json' };
|
|
556
573
|
}
|
|
557
|
-
return parsePeerPreviewDigest(parsed);
|
|
558
574
|
}
|
|
559
575
|
/** Parse the JSON envelope returned by `sessions preview --json`. */
|
|
560
576
|
export function parsePeerPreviewDigest(parsed) {
|
|
@@ -59,6 +59,18 @@ export interface SessionWatchRow extends Omit<ActiveSession, 'viewingIn' | 'cont
|
|
|
59
59
|
branch?: string;
|
|
60
60
|
rowKey: string;
|
|
61
61
|
sourceDevice: string;
|
|
62
|
+
/**
|
|
63
|
+
* The registered project this session's working directory belongs to, or
|
|
64
|
+
* `null` when the association is not confirmed (PHNX-3999 F08/F09).
|
|
65
|
+
*
|
|
66
|
+
* This is what a UI groups by. It is deliberately not `project`: that field is
|
|
67
|
+
* a bucket KEY derived from the path (the basename of the cwd, so it always
|
|
68
|
+
* answers something), which is how a loose or unrelated directory became its
|
|
69
|
+
* own project group. `null` means Uncategorized — the row is still listed, it
|
|
70
|
+
* just is not filed under a project nobody bound it to. Resolved on the device
|
|
71
|
+
* that owns the path, since a definition's root is a local path.
|
|
72
|
+
*/
|
|
73
|
+
confirmedProject: string | null;
|
|
62
74
|
/** Durable index rows are kept on the stream under a distinct identity so a
|
|
63
75
|
* live row can replace/disappear without erasing its recoverable history. */
|
|
64
76
|
previous: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionProjection } from '../projection.js';
|
|
2
|
+
import { confirmedProjectForCwd } from '../../projects.js';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import * as fs from 'node:fs';
|
|
4
5
|
import * as path from 'node:path';
|
|
@@ -40,6 +41,11 @@ export function toSessionWatchRow(scope, row) {
|
|
|
40
41
|
// no computed summary reads `pending` when the summarizer is on, `skipped`
|
|
41
42
|
// when it is off (PHNX-3939). goal/checkpoints/summaryChecklist rode `...row`.
|
|
42
43
|
summaryState: resolveStreamSummaryState(row.summaryState),
|
|
44
|
+
// The CONFIRMED project this work belongs to, or null for Uncategorized
|
|
45
|
+
// (PHNX-3999 F08/F09). Only a registered project definition counts — a
|
|
46
|
+
// consumer must never group by a directory basename, which is how unrelated
|
|
47
|
+
// and unbound directories became their own "projects".
|
|
48
|
+
confirmedProject: confirmedProjectForCwd(row.cwd) ?? null,
|
|
43
49
|
rowKey,
|
|
44
50
|
sourceDevice: scope,
|
|
45
51
|
previous,
|
|
@@ -104,7 +110,10 @@ export function toPreviousSessionWatchRow(scope, session) {
|
|
|
104
110
|
...(session.harness ? { harness: session.harness } : {}),
|
|
105
111
|
sessionId: session.id,
|
|
106
112
|
...(session.cwd ? { cwd: session.cwd } : {}),
|
|
113
|
+
// `project` stays the historical bucket KEY rows are joined on; the grouping a
|
|
114
|
+
// person sees comes from the confirmed association (PHNX-3999 F08/F09).
|
|
107
115
|
...(session.project ? { project: session.project } : {}),
|
|
116
|
+
confirmedProject: confirmedProjectForCwd(session.cwd) ?? null,
|
|
108
117
|
// Same headline ladder as a live row (`deriveSessionRecap`, PHNX-3797):
|
|
109
118
|
// `/rename` label → the daemon-generated title → `request.headline` (the
|
|
110
119
|
// user's own sentence with attachment noise pulled out, PHNX-3939) →
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Bounded, single-owner preview fetch with a durable requester cache and cross-process coalescing. */
|
|
2
|
+
import { fetchPeerPreviewEnvelope } from './remote/remote-list.js';
|
|
3
|
+
export type RemotePreviewCacheState = 'fresh' | 'stale-offline' | 'stale-error' | 'no-cache-offline' | 'no-cache-error' | 'invalid-id';
|
|
4
|
+
export interface RemotePreviewOutcome {
|
|
5
|
+
envelope?: unknown;
|
|
6
|
+
cache: {
|
|
7
|
+
source: 'live' | 'cache';
|
|
8
|
+
fetchedAt: number | null;
|
|
9
|
+
stale: boolean;
|
|
10
|
+
state: RemotePreviewCacheState;
|
|
11
|
+
device: string;
|
|
12
|
+
reason: string | null;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** A changed caller cursor invalidates a good copy; failed attempts preserve both its content and cursor. */
|
|
16
|
+
export interface RemotePreviewDeps {
|
|
17
|
+
/** Defaults to the real bounded SSH transport ({@link fetchPeerPreviewEnvelope}).
|
|
18
|
+
* Overridable so the cache/backoff/revision/lease STATE MACHINE can be tested
|
|
19
|
+
* against a real SQLite DB without a live peer — this repo has no fleet in
|
|
20
|
+
* CI, and the actual bounded-transport behavior (timeout, byte cap) is
|
|
21
|
+
* already covered where `fetchPeerPreviewEnvelope`/`sshCapture` are defined. */
|
|
22
|
+
fetchEnvelope: typeof fetchPeerPreviewEnvelope;
|
|
23
|
+
}
|
|
24
|
+
export declare function getRemoteSessionPreview(sessionId: string, device: string, opts?: {
|
|
25
|
+
refresh?: boolean;
|
|
26
|
+
revision?: string;
|
|
27
|
+
now?: number;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}, deps?: RemotePreviewDeps): Promise<RemotePreviewOutcome>;
|