@phnx-labs/agents-cli 1.22.46 → 1.22.47
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 +279 -0
- package/LICENSE +102 -182
- package/README.md +31 -18
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +4 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +18 -31
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +70 -3
- package/dist/lib/accounting/usage.js +235 -63
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +14 -6
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +5 -3
- package/dist/lib/exec.js +32 -16
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +5 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -33,7 +33,7 @@ import { planFleetTargets } from '../devices/fleet.js';
|
|
|
33
33
|
import { assertValidSshTarget, shellQuote, sshExecAsync } from '../ssh-exec.js';
|
|
34
34
|
import { machineId } from '../session/sync/config.js';
|
|
35
35
|
import { ptyRequest } from '../pty-client.js';
|
|
36
|
-
import {
|
|
36
|
+
import { showUrl } from '../open-url.js';
|
|
37
37
|
import { readAuthHealthCache, } from '../auth-health.js';
|
|
38
38
|
import { FLEET_LOGIN_FLOWS, KEYCHAIN_BOUND_ON_MAC, } from './auth-sync.js';
|
|
39
39
|
/**
|
|
@@ -500,8 +500,13 @@ export async function runFleetLogin(opts = {}) {
|
|
|
500
500
|
if (!opts.json) {
|
|
501
501
|
console.log(`Dashboard: ${dashUrl}`);
|
|
502
502
|
}
|
|
503
|
-
if (opts.open !== false)
|
|
504
|
-
|
|
503
|
+
if (opts.open !== false) {
|
|
504
|
+
const shown = await showUrl(dashUrl);
|
|
505
|
+
if (shown.via === 'none') {
|
|
506
|
+
console.error('Could not open a browser — open this yourself:');
|
|
507
|
+
console.error(` ${dashUrl}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
505
510
|
const driver = opts.driver ?? defaultPtyDriver();
|
|
506
511
|
const remotable = pending.filter((p) => p.remotable);
|
|
507
512
|
const driveOne = async (p) => {
|
|
@@ -80,6 +80,23 @@ export interface FleetManifest {
|
|
|
80
80
|
* machine's local device registry and is never committed.
|
|
81
81
|
*/
|
|
82
82
|
discovery?: Record<string, 'approved' | 'ignored'>;
|
|
83
|
+
/**
|
|
84
|
+
* Tailnet node names the user dismissed from auto-discovery, with who
|
|
85
|
+
* dismissed each and when. Lives here rather than in a per-device doc because
|
|
86
|
+
* a dismissed node is deliberately NOT a device — it never enters the
|
|
87
|
+
* registry, so it has no per-device folder. Syncs fleet-wide with the rest of
|
|
88
|
+
* `agents.yaml`, so a dismissal on one box stops the suggestion on every box.
|
|
89
|
+
*/
|
|
90
|
+
ignored?: IgnoredDeviceEntry[];
|
|
91
|
+
}
|
|
92
|
+
/** One dismissal record in {@link FleetManifest.ignored}. */
|
|
93
|
+
export interface IgnoredDeviceEntry {
|
|
94
|
+
/** Tailscale node name the user dismissed. */
|
|
95
|
+
name: string;
|
|
96
|
+
/** ISO-8601 timestamp of the dismissal. */
|
|
97
|
+
ignoredAt: string;
|
|
98
|
+
/** machineId() of the box the dismissal was made on. */
|
|
99
|
+
ignoredOn: string;
|
|
83
100
|
}
|
|
84
101
|
/**
|
|
85
102
|
* A device's desired state after merging defaults with its override and
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { AgentId, Mode } from '../types.js';
|
|
30
30
|
import type { JobConfig } from '../scheduling/routines.js';
|
|
31
|
+
import type { ConfiguredDeviceRole } from '../device-config.js';
|
|
31
32
|
/**
|
|
32
33
|
* Context for the exec-time config-env pin (mapping A, exec.ts side). The caller
|
|
33
34
|
* resolves the interactive/version facts once; the adapter only expresses the
|
|
@@ -42,6 +43,16 @@ export interface ExecConfigEnvCtx {
|
|
|
42
43
|
versionHome: string | null;
|
|
43
44
|
/** resolveInteractive(options) — computed once by the caller. */
|
|
44
45
|
interactive: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The role marked on THIS machine (worker | personal | undefined), resolved
|
|
48
|
+
* once by the caller from selfConfiguredDeviceRole(). A `personal` device is
|
|
49
|
+
* the user's own interactive box: it holds a real per-version login and the
|
|
50
|
+
* credential decision MUST defer to it for EVERY run — interactive OR headless
|
|
51
|
+
* — never the worker-only setup-token (RUSH-2395). Injected as a plain value
|
|
52
|
+
* (not imported) to keep the adapter import-leaf. Absent/undefined is treated
|
|
53
|
+
* as non-personal (worker-equivalent).
|
|
54
|
+
*/
|
|
55
|
+
deviceRole?: ConfiguredDeviceRole;
|
|
45
56
|
/**
|
|
46
57
|
* claude-account-token's resolveClaudeSetupToken, injected. The adapters MUST
|
|
47
58
|
* stay import-leaf: claude-account-token pulls in the secrets stack, which
|
|
@@ -26,35 +26,49 @@ export const claudeAdapter = {
|
|
|
26
26
|
}
|
|
27
27
|
// The `auth` bundle's setup-token exists so a run with NO human present
|
|
28
28
|
// authenticates without the Touch-ID-gated login item — usage probes,
|
|
29
|
-
// routines, dispatched runs (claude-account-token.ts).
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// `
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
// routines, dispatched runs (claude-account-token.ts). It is a WORKER
|
|
30
|
+
// credential. Two kinds of run defer to the per-version login instead:
|
|
31
|
+
//
|
|
32
|
+
// 1. An interactive run: a human is at the TTY, and their per-version login
|
|
33
|
+
// is the credential they established and expect. Overriding it made
|
|
34
|
+
// `/status` report `Auth token: CLAUDE_CODE_OAUTH_TOKEN` on a personal
|
|
35
|
+
// machine and took every hand-driven session off that login.
|
|
36
|
+
// 2. ANY run on a `personal` device — the user's own interactive box (zion),
|
|
37
|
+
// marked `config.role: personal`. That box holds a real per-version login
|
|
38
|
+
// and is the single origin of it, so every run there — interactive TUI OR
|
|
39
|
+
// a headless one-shot like `agents run claude "fix the bug"` — MUST use
|
|
40
|
+
// that login, not the setup-token. Gating on run mode ALONE
|
|
41
|
+
// (resolveInteractive = "this opens a TUI", NOT "a human is present")
|
|
42
|
+
// sent a headless run on the laptop onto the setup-token and hijacked the
|
|
43
|
+
// login. Keying the credential on DEVICE ROLE is the fix — RUSH-2395.
|
|
44
|
+
//
|
|
45
|
+
// macOS cannot cheaply confirm a home's login first (probing the Keychain
|
|
46
|
+
// raises an authorization sheet per installed version on the `agents run` hot
|
|
47
|
+
// path — agents.ts `isClaudeCredentialFileBlank`), so this path defers to
|
|
48
|
+
// Claude Code, which reads its own ACL-trusted login item without a prompt and
|
|
49
|
+
// asks a present human to log in only if the login is missing.
|
|
50
|
+
const personalDevice = ctx.deviceRole === 'personal';
|
|
51
|
+
if (ctx.interactive || personalDevice) {
|
|
52
|
+
// Drop an INHERITED copy of OUR OWN setup-token: a launch from inside a
|
|
53
|
+
// headless agent's shell inherits that agent's injected value via
|
|
54
|
+
// sanitizeProcessEnv(process.env) and would keep authenticating as it,
|
|
55
|
+
// overriding the login this branch is protecting. Matched by VALUE, so a
|
|
56
|
+
// token the user exported deliberately is a different string and is left
|
|
57
|
+
// alone (#2383). This is NARROWER than the worker path below, which
|
|
58
|
+
// overwrites-or-deletes unconditionally and never inspects the inherited
|
|
59
|
+
// value — a DIFFERENT account's inherited setup-token passing through this
|
|
60
|
+
// equality check is the adjacent hole RUSH-2360 leaves as follow-up (it does
|
|
61
|
+
// not silently run on a *shared, rotating* token, which is what caused the
|
|
62
|
+
// RUSH-1822 logout storm).
|
|
49
63
|
if (setupToken && result.CLAUDE_CODE_OAUTH_TOKEN === setupToken) {
|
|
50
64
|
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
else {
|
|
54
|
-
//
|
|
55
|
-
// path (`runner.ts
|
|
56
|
-
// setup-token when one resolves — it replaces any ambient shared
|
|
57
|
-
// inherited from the launcher. When NONE resolves, STRIP the ambient
|
|
68
|
+
// Headless run on a NON-personal device (worker, dispatched, provisioned
|
|
69
|
+
// box): mirror the routines path (`runner.ts`) UNCONDITIONALLY. Inject the
|
|
70
|
+
// per-account setup-token when one resolves — it replaces any ambient shared
|
|
71
|
+
// value inherited from the launcher. When NONE resolves, STRIP the ambient
|
|
58
72
|
// CLAUDE_CODE_OAUTH_TOKEN so a run on a provisioned box can never silently
|
|
59
73
|
// authenticate as the shared, rotating token an earlier version of this path
|
|
60
74
|
// let through — the RUSH-1822 fleet-wide-logout hazard, tracked by RUSH-2360.
|
|
@@ -63,21 +63,19 @@ fi
|
|
|
63
63
|
exec "$BINARY"${launchArgs} "$@"`;
|
|
64
64
|
},
|
|
65
65
|
execModeArgs(ctx) {
|
|
66
|
-
const policyMode = ctx.resolvedMode === 'plan' || ctx.resolvedMode === 'skip' ? ctx.resolvedMode : 'edit';
|
|
67
66
|
const writableRoots = [
|
|
68
67
|
...codexEditWritableRoots(ctx.cwd),
|
|
69
68
|
...ctx.addDirs,
|
|
70
69
|
];
|
|
71
|
-
return codexPolicyArgs(
|
|
70
|
+
return codexPolicyArgs(ctx.resolvedMode, writableRoots);
|
|
72
71
|
},
|
|
73
72
|
routineModeArgs(cmd, ctx) {
|
|
74
|
-
const policyMode = ctx.mode === 'plan' || ctx.mode === 'skip' ? ctx.mode : 'edit';
|
|
75
73
|
const routineRoots = (ctx.config.allow?.dirs ?? []).map((dir) => {
|
|
76
74
|
if (dir.startsWith('-')) {
|
|
77
75
|
throw new Error(`allow.dirs entries must not start with '-': ${JSON.stringify(dir)}`);
|
|
78
76
|
}
|
|
79
77
|
return dir.replace(/^~/, os.homedir());
|
|
80
78
|
});
|
|
81
|
-
cmd.push(...codexPolicyArgs(
|
|
79
|
+
cmd.push(...codexPolicyArgs(ctx.mode, [...codexEditWritableRoots(), ...routineRoots]));
|
|
82
80
|
},
|
|
83
81
|
};
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -275,6 +275,14 @@ def should_fire():
|
|
|
275
275
|
if not tn or tn not in allowed:
|
|
276
276
|
return False
|
|
277
277
|
|
|
278
|
+
v = m.get("permission_mode")
|
|
279
|
+
if v is not None:
|
|
280
|
+
allowed = arr(v)
|
|
281
|
+
if allowed:
|
|
282
|
+
pm = inp.get("permission_mode") or inp.get("permissionMode")
|
|
283
|
+
if pm and pm not in allowed:
|
|
284
|
+
return False
|
|
285
|
+
|
|
278
286
|
v = m.get("tool_args_match")
|
|
279
287
|
if v is not None:
|
|
280
288
|
ta = inp.get("tool_args")
|
|
@@ -20,6 +20,9 @@ export interface HookInput {
|
|
|
20
20
|
tool_name?: string;
|
|
21
21
|
tool_args?: unknown;
|
|
22
22
|
cwd?: string;
|
|
23
|
+
permission_mode?: string;
|
|
24
|
+
/** Grok-style camelCase spelling of permission_mode. */
|
|
25
|
+
permissionMode?: string;
|
|
23
26
|
}
|
|
24
27
|
export declare function isSafeHookRegex(source: string): boolean;
|
|
25
28
|
/**
|
package/dist/lib/hooks/match.js
CHANGED
|
@@ -131,6 +131,31 @@ export function shouldFire(matches, input) {
|
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
if (matches.permission_mode !== undefined) {
|
|
135
|
+
const allowed = arrayOf(matches.permission_mode);
|
|
136
|
+
if (allowed.length > 0) {
|
|
137
|
+
// Fail-open on absence: only some harnesses (Claude Code) report the
|
|
138
|
+
// live mode. An input with no mode field passes; an explicit value not
|
|
139
|
+
// in the allowed list skips.
|
|
140
|
+
const mode = input.permission_mode || input.permissionMode;
|
|
141
|
+
if (mode && !allowed.includes(mode))
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (matches.permission_mode_not !== undefined) {
|
|
146
|
+
const denied = arrayOf(matches.permission_mode_not);
|
|
147
|
+
if (denied.length > 0) {
|
|
148
|
+
// The negative form exists because the positive one cannot express
|
|
149
|
+
// "everywhere except plan" without enumerating every other mode — and an
|
|
150
|
+
// enumeration silently stops firing the moment a harness adds or renames
|
|
151
|
+
// one, which for a guard means it quietly stops guarding. Naming the mode
|
|
152
|
+
// to skip keeps every unknown mode firing, so the failure direction is
|
|
153
|
+
// "ran unnecessarily", never "did not run".
|
|
154
|
+
const mode = input.permission_mode || input.permissionMode;
|
|
155
|
+
if (mode && denied.includes(mode))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
134
159
|
if (matches.tool_args_match !== undefined) {
|
|
135
160
|
const serialized = typeof input.tool_args === 'string'
|
|
136
161
|
? input.tool_args
|
|
@@ -28,6 +28,7 @@ import { resolveRemoteOsSync } from './remote-os.js';
|
|
|
28
28
|
// 6–7× cheaper graph (RUSH-2374 proposal 2).
|
|
29
29
|
import { machineId } from '../machine-id.js';
|
|
30
30
|
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
31
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../devices/interactive-host.js';
|
|
31
32
|
import { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
32
33
|
import { loadDevices } from '../devices/registry.js';
|
|
33
34
|
import { isSelfHost } from '../devices/self-host.js';
|
|
@@ -527,6 +528,21 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
527
528
|
process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
|
|
528
529
|
hostName = plan.host;
|
|
529
530
|
}
|
|
531
|
+
// `interactive` is the second affinity sentinel: the box the human sits at
|
|
532
|
+
// (`interactive.host`). Resolved here, ahead of the isSelfHost check below, for
|
|
533
|
+
// the same reason `auto` is — a pin naming THIS machine must run locally rather
|
|
534
|
+
// than self-SSH. It never falls back to the local box when unset: rendering to
|
|
535
|
+
// a screen nobody is watching is the exact failure the sentinel prevents.
|
|
536
|
+
if (isDeviceInteractive(hostName)) {
|
|
537
|
+
const pinned = resolveInteractiveDevice();
|
|
538
|
+
if (!pinned) {
|
|
539
|
+
process.stderr.write(`${interactiveUnsetError()}\n`);
|
|
540
|
+
process.exitCode = 1;
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
|
|
544
|
+
hostName = pinned;
|
|
545
|
+
}
|
|
530
546
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
531
547
|
// Match EVERY identity the box answers to (short id, loopback, tailscale
|
|
532
548
|
// dnsName), not just machineId() — a `--device <self-dnsName>` used to slip past a
|
|
@@ -27,6 +27,7 @@ import { resolveRemoteOsSync } from './remote-os.js';
|
|
|
27
27
|
import { loadDevices } from '../devices/registry.js';
|
|
28
28
|
import { resolveDeviceProfile } from '../devices/resolve-profile.js';
|
|
29
29
|
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
30
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../devices/interactive-host.js';
|
|
30
31
|
import { localMachineId } from '../session/origin-machine.js';
|
|
31
32
|
// Re-export so existing importers (tests, commands) keep their path; the class
|
|
32
33
|
// itself lives in types.ts so providers can throw it without a circular import.
|
|
@@ -161,6 +162,12 @@ export async function matchHost(name, opts = {}) {
|
|
|
161
162
|
// returning nothing — callers that already special-case "target is this
|
|
162
163
|
// machine" (teams add/create, the passthrough self-host check) then treat it as
|
|
163
164
|
// local exactly as they would if the user had typed the local name.
|
|
165
|
+
if (isDeviceInteractive(name)) {
|
|
166
|
+
const pinned = resolveInteractiveDevice();
|
|
167
|
+
if (!pinned)
|
|
168
|
+
throw new Error(interactiveUnsetError());
|
|
169
|
+
name = pinned;
|
|
170
|
+
}
|
|
164
171
|
if (isDeviceAuto(name)) {
|
|
165
172
|
const plan = (opts.resolveAuto ?? (() => resolveDeviceAffinity({})))();
|
|
166
173
|
const picked = plan.host ?? normalizeHost(localMachineId());
|
|
Binary file
|
|
Binary file
|
|
@@ -370,6 +370,19 @@ export interface PidStaleness {
|
|
|
370
370
|
* PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
|
|
371
371
|
* to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
|
|
372
372
|
* without a live process or filesystem.
|
|
373
|
+
*
|
|
374
|
+
* The two timestamps do not have the same resolution. `pidStartTimeMs` parses
|
|
375
|
+
* `ps -o lstart`, which prints whole SECONDS, while the bundle mtime carries
|
|
376
|
+
* sub-second precision — and the restart this check exists to detect happens
|
|
377
|
+
* within a second of the swap that triggered it. A raw `<` therefore called a
|
|
378
|
+
* healthy just-restarted helper stale on essentially every upgrade: measured on
|
|
379
|
+
* zion at 1.22.46, pid start 1787441353000 vs bundle mtime 1787441353700, 700ms
|
|
380
|
+
* apart inside one second, reported as "running the OLD binary" and surfaced as
|
|
381
|
+
* `accessibilityHintNeeded` — telling the user to re-grant Accessibility after
|
|
382
|
+
* an upgrade that had already restarted the helper correctly. So the bundle
|
|
383
|
+
* mtime is truncated to the same whole second `ps` reports before comparing: a
|
|
384
|
+
* pid that started in the swap's own second is fresh, and a genuinely stale pid
|
|
385
|
+
* (a full second or more older) is still caught.
|
|
373
386
|
*/
|
|
374
387
|
export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: number, bundleMtimeMs: number): boolean;
|
|
375
388
|
/**
|
|
@@ -602,8 +602,19 @@ export function mayInstallMenubarHelper(opts) {
|
|
|
602
602
|
// No owner recorded yet (fresh or pre-`AGENTS_ENTRY` plist) — adopt it.
|
|
603
603
|
if (!opts.plistEntry)
|
|
604
604
|
return true;
|
|
605
|
+
// The recorded owner's entry path is gone from disk, so this install may adopt
|
|
606
|
+
// the helper — but a non-Developer-ID (ad-hoc/dev) source may NOT seize a
|
|
607
|
+
// healthy install this way. Recopying an ad-hoc bundle over the Developer-ID
|
|
608
|
+
// one poisons the shared Accessibility grant (an ad-hoc signature fails the
|
|
609
|
+
// grant's stored code requirement, so macOS revokes it and re-prompts on the
|
|
610
|
+
// next paste) and Gatekeeper then rejects the result as "damaged" (RUSH-2134).
|
|
611
|
+
// This does not strand a genuinely broken helper: escape (1) above
|
|
612
|
+
// (helperExecMissing / needsDevIdHeal) already lets ANY source repair a
|
|
613
|
+
// missing-or-ad-hoc install, so refusing here only declines to re-point the
|
|
614
|
+
// plist of a helper that is already present and working — the menu bar keeps
|
|
615
|
+
// running, nothing deadlocks.
|
|
605
616
|
if (!opts.ownerEntryExists)
|
|
606
|
-
return
|
|
617
|
+
return opts.sourceIsDeveloperId;
|
|
607
618
|
if (opts.installedVersion && opts.currentVersion) {
|
|
608
619
|
const versionOrder = compareVersions(opts.currentVersion, opts.installedVersion);
|
|
609
620
|
if (versionOrder > 0)
|
|
@@ -990,9 +1001,22 @@ export function getMenubarStatus() {
|
|
|
990
1001
|
* PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
|
|
991
1002
|
* to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
|
|
992
1003
|
* without a live process or filesystem.
|
|
1004
|
+
*
|
|
1005
|
+
* The two timestamps do not have the same resolution. `pidStartTimeMs` parses
|
|
1006
|
+
* `ps -o lstart`, which prints whole SECONDS, while the bundle mtime carries
|
|
1007
|
+
* sub-second precision — and the restart this check exists to detect happens
|
|
1008
|
+
* within a second of the swap that triggered it. A raw `<` therefore called a
|
|
1009
|
+
* healthy just-restarted helper stale on essentially every upgrade: measured on
|
|
1010
|
+
* zion at 1.22.46, pid start 1787441353000 vs bundle mtime 1787441353700, 700ms
|
|
1011
|
+
* apart inside one second, reported as "running the OLD binary" and surfaced as
|
|
1012
|
+
* `accessibilityHintNeeded` — telling the user to re-grant Accessibility after
|
|
1013
|
+
* an upgrade that had already restarted the helper correctly. So the bundle
|
|
1014
|
+
* mtime is truncated to the same whole second `ps` reports before comparing: a
|
|
1015
|
+
* pid that started in the swap's own second is fresh, and a genuinely stale pid
|
|
1016
|
+
* (a full second or more older) is still caught.
|
|
993
1017
|
*/
|
|
994
1018
|
export function isMenubarProcessStaleAgainstBundle(pidStartedAtMs, bundleMtimeMs) {
|
|
995
|
-
return pidStartedAtMs < bundleMtimeMs;
|
|
1019
|
+
return pidStartedAtMs < Math.floor(bundleMtimeMs / 1000) * 1000;
|
|
996
1020
|
}
|
|
997
1021
|
/** Wall-clock start time of a live pid via `ps`, or null if it can't be read. */
|
|
998
1022
|
function pidStartTimeMs(pid) {
|
package/dist/lib/open-url.d.ts
CHANGED
|
@@ -1,2 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Where a "show the human this" call actually landed. */
|
|
2
|
+
export type ShowOutcome = {
|
|
3
|
+
via: 'profile';
|
|
4
|
+
profile: string;
|
|
5
|
+
tabId?: string;
|
|
6
|
+
} | {
|
|
7
|
+
via: 'os';
|
|
8
|
+
command: string;
|
|
9
|
+
} | {
|
|
10
|
+
via: 'none';
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export interface ShowOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Force the OS default handler, ignoring `browser.viewer`. This is the
|
|
16
|
+
* The programmatic escape hatch, for a caller that must use the user's own
|
|
17
|
+
* browser regardless of configuration. There is deliberately no CLI flag for
|
|
18
|
+
* it: `agents config set browser.viewer os` is the user-facing control.
|
|
19
|
+
*/
|
|
20
|
+
osBrowser?: boolean;
|
|
21
|
+
/** Explicit profile override, ahead of `browser.viewer`. */
|
|
22
|
+
profile?: string;
|
|
23
|
+
/** Injected opener so the OS branch is testable without spawning anything. */
|
|
24
|
+
spawnOpen?: (cmd: string, args: string[]) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Launch a detached opener and report whether it actually started.
|
|
28
|
+
*
|
|
29
|
+
* Detection without blocking, which is the whole trick here. A bare detached
|
|
30
|
+
* `spawn` cannot tell success from "xdg-open is not installed" — it does not
|
|
31
|
+
* throw for a missing binary, it emits `error` asynchronously — so the failure
|
|
32
|
+
* branches of every caller were dead. But `spawnSync` is not the answer either:
|
|
33
|
+
* it waits for the child's whole lifetime, and `devices lease` opens a console
|
|
34
|
+
* and then immediately prompts for a pasted key, so a blocking open would stall
|
|
35
|
+
* that prompt behind the browser.
|
|
36
|
+
*
|
|
37
|
+
* Racing `spawn` against `error` gives both: Node emits `spawn` as soon as the
|
|
38
|
+
* child is successfully created (measured: 1ms, and it does NOT wait for exit),
|
|
39
|
+
* and `error` for ENOENT. We unref on success so the opener outlives us.
|
|
40
|
+
*/
|
|
41
|
+
export declare function trySpawn(cmd: string, args: string[]): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Decide the viewer for this call. Exported for its own test — this is the one
|
|
44
|
+
* place the policy lives.
|
|
45
|
+
*
|
|
46
|
+
* Every fall back to the OS handler prints one stderr line naming why. A silent
|
|
47
|
+
* downgrade here is what made the original bug invisible for so long: the user
|
|
48
|
+
* had configured a profile and had no way to see that it was being ignored.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveViewer(opts?: ShowOptions): Promise<'os' | {
|
|
51
|
+
profile: string;
|
|
52
|
+
}>;
|
|
53
|
+
/** Show a URL to the human at this machine. Never throws. */
|
|
54
|
+
export declare function showUrl(url: string, opts?: ShowOptions): Promise<ShowOutcome>;
|
|
55
|
+
/**
|
|
56
|
+
* Show a local file. Browser-renderable kinds go through {@link showUrl}; every
|
|
57
|
+
* other kind goes to the OS default APP, which for a screenshot or a recording
|
|
58
|
+
* is the right viewer.
|
|
59
|
+
*/
|
|
60
|
+
export declare function showFile(filePath: string, opts?: ShowOptions): Promise<ShowOutcome>;
|
package/dist/lib/open-url.js
CHANGED
|
@@ -1,19 +1,174 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The one place that decides WHERE a URL or file is shown to the human.
|
|
3
|
+
*
|
|
4
|
+
* Two browsers exist on a machine like this: the OS default handler, and the
|
|
5
|
+
* profile `agents browser` drives. They are not interchangeable. The configured
|
|
6
|
+
* profile is where the fleet's logins accumulate — `agents browser profiles
|
|
7
|
+
* logins` lists them — so a page opened there is a page the user is already
|
|
8
|
+
* signed in for, and a login acquired there is inherited by every later agent.
|
|
9
|
+
* The OS handler has none of that.
|
|
10
|
+
*
|
|
11
|
+
* Before this seam existed, `agents browser navigate` honoured the configured
|
|
12
|
+
* profile and nothing else did: `fleet login`, `devices lease`, `feedback`, and
|
|
13
|
+
* the browser-session artifact opener each shelled straight to `open`/`xdg-open`,
|
|
14
|
+
* so every one of them landed in whatever the OS handler happened to be. This
|
|
15
|
+
* module replaces all of those call sites; do not add a sixth raw `open`.
|
|
16
|
+
*
|
|
17
|
+
* Never throws. A viewer that cannot be reached degrades to the OS handler with
|
|
18
|
+
* one stderr line naming the reason, and a total failure returns `via: 'none'`
|
|
19
|
+
* so the caller can print the URL rather than silently doing nothing.
|
|
5
20
|
*/
|
|
6
21
|
import { spawn } from 'child_process';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { pathToFileURL } from 'url';
|
|
24
|
+
/**
|
|
25
|
+
* Extensions a CDP tab renders at least as well as the OS default app.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately narrow. `sessions-list.ts` EXT_KIND covers .png/.jpg/.webp/.pdf/
|
|
28
|
+
* .webm, and for those Preview and QuickTime are the better viewer — routing a
|
|
29
|
+
* screenshot into a browser tab is a downgrade, not a fix.
|
|
30
|
+
*/
|
|
31
|
+
const BROWSER_RENDERABLE = new Set(['.html', '.htm', '.svg', '.xhtml']);
|
|
32
|
+
async function osOpen(target, spawnOpen) {
|
|
33
|
+
const candidates = process.platform === 'darwin'
|
|
34
|
+
? [['open', [target]]]
|
|
35
|
+
: process.platform === 'win32'
|
|
36
|
+
? // `start` treats a lone quoted first argument as the window TITLE, so the
|
|
37
|
+
// empty title placeholder is required before the target. The three copies
|
|
38
|
+
// of this that predated the seam disagreed on it ('' vs '""').
|
|
39
|
+
[['cmd', ['/c', 'start', '', target]]]
|
|
40
|
+
: [
|
|
41
|
+
['xdg-open', [target]],
|
|
42
|
+
['gnome-open', [target]],
|
|
43
|
+
];
|
|
44
|
+
for (const [cmd, args] of candidates) {
|
|
45
|
+
if (spawnOpen) {
|
|
46
|
+
if (spawnOpen(cmd, args))
|
|
47
|
+
return { via: 'os', command: cmd };
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (await trySpawn(cmd, args))
|
|
51
|
+
return { via: 'os', command: cmd };
|
|
52
|
+
}
|
|
53
|
+
return { via: 'none', reason: 'no working OS opener on this platform' };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Launch a detached opener and report whether it actually started.
|
|
57
|
+
*
|
|
58
|
+
* Detection without blocking, which is the whole trick here. A bare detached
|
|
59
|
+
* `spawn` cannot tell success from "xdg-open is not installed" — it does not
|
|
60
|
+
* throw for a missing binary, it emits `error` asynchronously — so the failure
|
|
61
|
+
* branches of every caller were dead. But `spawnSync` is not the answer either:
|
|
62
|
+
* it waits for the child's whole lifetime, and `devices lease` opens a console
|
|
63
|
+
* and then immediately prompts for a pasted key, so a blocking open would stall
|
|
64
|
+
* that prompt behind the browser.
|
|
65
|
+
*
|
|
66
|
+
* Racing `spawn` against `error` gives both: Node emits `spawn` as soon as the
|
|
67
|
+
* child is successfully created (measured: 1ms, and it does NOT wait for exit),
|
|
68
|
+
* and `error` for ENOENT. We unref on success so the opener outlives us.
|
|
69
|
+
*/
|
|
70
|
+
export function trySpawn(cmd, args) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let settled = false;
|
|
73
|
+
const done = (ok) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
resolve(ok);
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
81
|
+
child.on('error', () => done(false));
|
|
82
|
+
child.on('spawn', () => {
|
|
83
|
+
child.unref();
|
|
84
|
+
done(true);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
done(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decide the viewer for this call. Exported for its own test — this is the one
|
|
94
|
+
* place the policy lives.
|
|
95
|
+
*
|
|
96
|
+
* Every fall back to the OS handler prints one stderr line naming why. A silent
|
|
97
|
+
* downgrade here is what made the original bug invisible for so long: the user
|
|
98
|
+
* had configured a profile and had no way to see that it was being ignored.
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveViewer(opts = {}) {
|
|
101
|
+
if (opts.osBrowser)
|
|
102
|
+
return 'os';
|
|
103
|
+
const { getConfigValue } = await import('./device-config.js');
|
|
104
|
+
const configured = opts.profile ?? (getConfigValue('browser.viewer').value || undefined);
|
|
105
|
+
// Unset means "follow the profile agents drive" — the whole point is that a
|
|
106
|
+
// machine with a configured browser stops leaking pages to the OS handler.
|
|
107
|
+
const { getConfiguredDefaultProfileName, resolveProfileRef, getProfile, isProfileLaunchableHere } = await import('./browser/profiles.js');
|
|
108
|
+
const name = configured ?? getConfiguredDefaultProfileName();
|
|
109
|
+
if (!name)
|
|
110
|
+
return 'os';
|
|
111
|
+
if (name === 'os')
|
|
112
|
+
return 'os';
|
|
113
|
+
let resolved;
|
|
11
114
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
115
|
+
resolved = await resolveProfileRef(name);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[viewer] ${name}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
|
119
|
+
return 'os';
|
|
120
|
+
}
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
console.error(`[viewer] "${name}" does not resolve to a profile — using the OS browser.`);
|
|
123
|
+
return 'os';
|
|
15
124
|
}
|
|
16
|
-
|
|
17
|
-
|
|
125
|
+
const profile = await getProfile(resolved);
|
|
126
|
+
if (!profile) {
|
|
127
|
+
console.error(`[viewer] profile "${resolved}" is not configured — using the OS browser.`);
|
|
128
|
+
return 'os';
|
|
129
|
+
}
|
|
130
|
+
if (profile.browser === 'arc') {
|
|
131
|
+
// Arc exposes no CDP page targets and crashes on tab creation, so it can be
|
|
132
|
+
// a configured profile but never a drivable viewer.
|
|
133
|
+
console.error(`[viewer] "${resolved}" is Arc, which cannot be driven — using the OS browser.`);
|
|
134
|
+
return 'os';
|
|
135
|
+
}
|
|
136
|
+
if (!isProfileLaunchableHere(profile)) {
|
|
137
|
+
console.error(`[viewer] "${resolved}" cannot launch on this machine — using the OS browser.`);
|
|
138
|
+
return 'os';
|
|
139
|
+
}
|
|
140
|
+
return { profile: resolved };
|
|
141
|
+
}
|
|
142
|
+
/** Show a URL to the human at this machine. Never throws. */
|
|
143
|
+
export async function showUrl(url, opts = {}) {
|
|
144
|
+
const viewer = await resolveViewer(opts);
|
|
145
|
+
if (viewer === 'os')
|
|
146
|
+
return osOpen(url, opts.spawnOpen);
|
|
147
|
+
try {
|
|
148
|
+
const { sendIPCRequest } = await import('./browser/ipc.js');
|
|
149
|
+
// Deliberately does NOT auto-start the browser daemon. Showing a page is a
|
|
150
|
+
// side errand — `devices lease` opens a console and immediately prompts for
|
|
151
|
+
// a pasted key — so blocking that on a daemon cold start is a surprising
|
|
152
|
+
// multi-second stall. Daemon already up: use the viewer. Not up: the OS
|
|
153
|
+
// handler is the fast, correct answer.
|
|
154
|
+
const response = await sendIPCRequest({ action: 'show', url, profile: viewer.profile }, { autoStartDaemon: false });
|
|
155
|
+
if (response.ok)
|
|
156
|
+
return { via: 'profile', profile: viewer.profile, tabId: response.tabId };
|
|
157
|
+
console.error(`[viewer] ${viewer.profile}: ${response.error} — using the OS browser.`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error(`[viewer] ${viewer.profile}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
|
161
|
+
}
|
|
162
|
+
return osOpen(url, opts.spawnOpen);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Show a local file. Browser-renderable kinds go through {@link showUrl}; every
|
|
166
|
+
* other kind goes to the OS default APP, which for a screenshot or a recording
|
|
167
|
+
* is the right viewer.
|
|
168
|
+
*/
|
|
169
|
+
export async function showFile(filePath, opts = {}) {
|
|
170
|
+
if (!BROWSER_RENDERABLE.has(path.extname(filePath).toLowerCase())) {
|
|
171
|
+
return osOpen(filePath, opts.spawnOpen);
|
|
18
172
|
}
|
|
173
|
+
return showUrl(pathToFileURL(filePath).href, opts);
|
|
19
174
|
}
|
|
@@ -15,7 +15,7 @@ export interface RefreshLeaseOptions<T> {
|
|
|
15
15
|
* Serialize refresh work across every agents-cli process on this device.
|
|
16
16
|
*
|
|
17
17
|
* In-process promise maps only protect one Node process. Factory, the daemon,
|
|
18
|
-
*
|
|
18
|
+
* and `agents view` are separate processes, so they need an
|
|
19
19
|
* OS-visible lease. The result is re-read after lock acquisition: a waiter
|
|
20
20
|
* consumes the winner's publication and never calls the provider a second
|
|
21
21
|
* time. `proper-lockfile` supplies heartbeat + stale-owner recovery while an
|
|
@@ -18,7 +18,7 @@ function lockTarget(scope, key) {
|
|
|
18
18
|
* Serialize refresh work across every agents-cli process on this device.
|
|
19
19
|
*
|
|
20
20
|
* In-process promise maps only protect one Node process. Factory, the daemon,
|
|
21
|
-
*
|
|
21
|
+
* and `agents view` are separate processes, so they need an
|
|
22
22
|
* OS-visible lease. The result is re-read after lock acquisition: a waiter
|
|
23
23
|
* consumes the winner's publication and never calls the provider a second
|
|
24
24
|
* time. `proper-lockfile` supplies heartbeat + stale-owner recovery while an
|
|
Binary file
|