@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-secret daemon state exchanged through the already fleet-synced user repo.
|
|
3
|
+
*
|
|
4
|
+
* Each device owns exactly one file under `~/.agents/devices/<device>/`, so
|
|
5
|
+
* normal `agents repo push/pull user` merges the fleet without devices dialing
|
|
6
|
+
* one another on every daemon tick. OAuth/setup-token values never belong here:
|
|
7
|
+
* auth publishes only a readiness verdict; the existing encrypted SSH transport
|
|
8
|
+
* remains the one path that may carry secret material.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { assertValidDeviceName } from './devices/registry.js';
|
|
13
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
14
|
+
import { getUserAgentsDir } from './state.js';
|
|
15
|
+
export const FLEET_SHARED_STATE_VERSION = 1;
|
|
16
|
+
export const FLEET_SHARED_STATE_FILE = 'daemon-state.json';
|
|
17
|
+
/** Path owned by one device in the conflict-free tracked device-doc tree. */
|
|
18
|
+
export function fleetSharedStatePath(device, userAgentsDir = getUserAgentsDir()) {
|
|
19
|
+
assertValidDeviceName(device);
|
|
20
|
+
return path.join(userAgentsDir, 'devices', device, FLEET_SHARED_STATE_FILE);
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function parseFleetSharedDeviceState(raw, owner) {
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!isRecord(parsed) || parsed.version !== FLEET_SHARED_STATE_VERSION || parsed.device !== owner) {
|
|
28
|
+
throw new Error('unrecognized shared-state envelope');
|
|
29
|
+
}
|
|
30
|
+
const usage = parsed.usage;
|
|
31
|
+
if (usage !== undefined && (!isRecord(usage) || !isRecord(usage.rows))) {
|
|
32
|
+
throw new Error('unrecognized usage snapshot');
|
|
33
|
+
}
|
|
34
|
+
const auth = parsed.auth;
|
|
35
|
+
if (auth !== undefined &&
|
|
36
|
+
(!isRecord(auth) || !['ready', 'missing', 'invalid'].includes(String(auth.status)))) {
|
|
37
|
+
throw new Error('unrecognized auth verdict');
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Merge one daemon-owned field into this device's shared file under a real
|
|
43
|
+
* inter-process lock. Stable serialization avoids dirtying the user repo when
|
|
44
|
+
* neither usage nor auth state changed.
|
|
45
|
+
*/
|
|
46
|
+
export function updateFleetSharedDeviceState(device, patch, userAgentsDir = getUserAgentsDir()) {
|
|
47
|
+
const file = fleetSharedStatePath(device, userAgentsDir);
|
|
48
|
+
ensureLockTarget(file, '');
|
|
49
|
+
return withFileLock(file, () => {
|
|
50
|
+
let current = {
|
|
51
|
+
version: FLEET_SHARED_STATE_VERSION,
|
|
52
|
+
device,
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(file, 'utf-8').trim();
|
|
56
|
+
if (raw)
|
|
57
|
+
current = parseFleetSharedDeviceState(raw, device);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// The owning device repairs its own malformed file from authoritative
|
|
61
|
+
// local state. Peer files are never repaired by this writer.
|
|
62
|
+
}
|
|
63
|
+
const next = {
|
|
64
|
+
...current,
|
|
65
|
+
...(patch.usage !== undefined ? { usage: patch.usage } : {}),
|
|
66
|
+
...(patch.auth !== undefined ? { auth: patch.auth } : {}),
|
|
67
|
+
version: FLEET_SHARED_STATE_VERSION,
|
|
68
|
+
device,
|
|
69
|
+
};
|
|
70
|
+
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
71
|
+
if (fs.readFileSync(file, 'utf-8') === serialized)
|
|
72
|
+
return { changed: false, path: file };
|
|
73
|
+
atomicWriteFileSync(file, serialized, 'utf-8');
|
|
74
|
+
return { changed: true, path: file };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Read every valid peer-owned state file; malformed peers fail separately. */
|
|
78
|
+
export function readFleetSharedDeviceStates(userAgentsDir = getUserAgentsDir()) {
|
|
79
|
+
const devicesDir = path.join(userAgentsDir, 'devices');
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(devicesDir, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err.code === 'ENOENT')
|
|
86
|
+
return { states: [], errors: [] };
|
|
87
|
+
return { states: [], errors: [{ device: '*', message: err.message }] };
|
|
88
|
+
}
|
|
89
|
+
const states = [];
|
|
90
|
+
const errors = [];
|
|
91
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
92
|
+
if (!entry.isDirectory())
|
|
93
|
+
continue;
|
|
94
|
+
const file = path.join(devicesDir, entry.name, FLEET_SHARED_STATE_FILE);
|
|
95
|
+
if (!fs.existsSync(file))
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
states.push(parseFleetSharedDeviceState(fs.readFileSync(file, 'utf-8'), entry.name));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
errors.push({ device: entry.name, message: err.message });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { states, errors };
|
|
105
|
+
}
|
|
@@ -118,6 +118,8 @@ export interface WindowsAgentsCommand {
|
|
|
118
118
|
* pass false for probes whose reachability keys off a sentinel, not the code.
|
|
119
119
|
*/
|
|
120
120
|
propagateExit?: boolean;
|
|
121
|
+
/** Remap a reached peer command's 255 to 254 so SSH's own 255 stays unambiguous. */
|
|
122
|
+
remapExit255?: boolean;
|
|
121
123
|
}
|
|
122
124
|
/**
|
|
123
125
|
* The PowerShell script (pre-encoding) that {@link buildWindowsAgentsCommand}
|
|
@@ -131,6 +131,11 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
131
131
|
// process follows the remote run to completion, so its exit handler fires at
|
|
132
132
|
// the right moment anyway.
|
|
133
133
|
notify: 'local-only',
|
|
134
|
+
// --no-trace-sync (traceSync=false) gates the LOCAL run-exit trace auto-sync,
|
|
135
|
+
// which only arms for local runs anyway (exec.ts skips it for --device/--lease).
|
|
136
|
+
// On a --device dispatch the remote box runs its own run-exit sync, so this
|
|
137
|
+
// flag is never forwarded — it is a local-exit-behavior toggle, not remote.
|
|
138
|
+
traceSync: 'local-only',
|
|
134
139
|
// Deprecated alias for --device auto; resolved on the launching box before SSH.
|
|
135
140
|
smart: 'local-only',
|
|
136
141
|
// Broadcast mode (agents run --broadcast) is its own fan-out dispatch — mutually
|
|
@@ -289,7 +294,7 @@ export function stripClixml(stdout) {
|
|
|
289
294
|
.trim();
|
|
290
295
|
}
|
|
291
296
|
export function windowsAgentsScript(cmd) {
|
|
292
|
-
const { args, env, cwd, propagateExit = true } = cmd;
|
|
297
|
+
const { args, env, cwd, propagateExit = true, remapExit255 = false } = cmd;
|
|
293
298
|
const parts = [POWERSHELL_PROGRESS_SILENCE];
|
|
294
299
|
if (env)
|
|
295
300
|
for (const [k, v] of Object.entries(env))
|
|
@@ -297,8 +302,12 @@ export function windowsAgentsScript(cmd) {
|
|
|
297
302
|
if (cwd)
|
|
298
303
|
parts.push(`Set-Location -LiteralPath ${powershellQuote(cwd)}`);
|
|
299
304
|
parts.push(`& ${['agents', ...args].map(powershellQuote).join(' ')}`);
|
|
300
|
-
if (propagateExit)
|
|
301
|
-
|
|
305
|
+
if (propagateExit) {
|
|
306
|
+
if (remapExit255)
|
|
307
|
+
parts.push('$agentsExit = $LASTEXITCODE', 'if ($agentsExit -eq 255) { exit 254 }', 'exit $agentsExit');
|
|
308
|
+
else
|
|
309
|
+
parts.push('exit $LASTEXITCODE');
|
|
310
|
+
}
|
|
302
311
|
return parts.join('; ');
|
|
303
312
|
}
|
|
304
313
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proper-lockfile invokes this callback from its background refresh timer.
|
|
3
|
+
* Its default is to throw there, outside the caller's promise chain, which can
|
|
4
|
+
* terminate the daemon. These leases are advisory serialization barriers: log
|
|
5
|
+
* a broken lease and let the owning operation finish instead of crashing the
|
|
6
|
+
* shared process.
|
|
7
|
+
*/
|
|
8
|
+
export declare function logAndContinueOnLockCompromised(scope: string): (err: Error) => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proper-lockfile invokes this callback from its background refresh timer.
|
|
3
|
+
* Its default is to throw there, outside the caller's promise chain, which can
|
|
4
|
+
* terminate the daemon. These leases are advisory serialization barriers: log
|
|
5
|
+
* a broken lease and let the owning operation finish instead of crashing the
|
|
6
|
+
* shared process.
|
|
7
|
+
*/
|
|
8
|
+
export function logAndContinueOnLockCompromised(scope) {
|
|
9
|
+
return (err) => {
|
|
10
|
+
console.warn(`[agents ${scope}] Lock was compromised; continuing without crashing: ${err.message}`);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -58,6 +58,7 @@ export declare class MonitorEngine {
|
|
|
58
58
|
private monitors;
|
|
59
59
|
private lastEval;
|
|
60
60
|
private ticking;
|
|
61
|
+
private running;
|
|
61
62
|
constructor(logFn?: LogFn);
|
|
62
63
|
/** Load owned+enabled monitors and start the tick loop. */
|
|
63
64
|
start(options?: {
|
|
@@ -65,7 +66,7 @@ export declare class MonitorEngine {
|
|
|
65
66
|
}): void;
|
|
66
67
|
/** Reload monitor configs (SIGHUP). */
|
|
67
68
|
reload(): void;
|
|
68
|
-
/** Stop the tick loop. */
|
|
69
|
+
/** Stop the tick loop. No monitor dispatches after this until `start()`. */
|
|
69
70
|
stop(): void;
|
|
70
71
|
private loadAll;
|
|
71
72
|
private intervalMs;
|
|
@@ -54,6 +54,14 @@ export function decideFire(monitor, observation) {
|
|
|
54
54
|
const raw = observation.raw;
|
|
55
55
|
const payload = observation.meta ?? {};
|
|
56
56
|
const dedupeKey = cond.dedupeKey;
|
|
57
|
+
// A snapshot the source flagged as an OBSERVATION FAILURE (a poll that exited
|
|
58
|
+
// non-zero or emitted a transport/auth/rate-limit error) is never a value
|
|
59
|
+
// change: don't fire, don't move the baseline — so an empty→error→empty flap
|
|
60
|
+
// can't read as two value changes (PHNX-3510). The engine records it as a
|
|
61
|
+
// failed check separately, feeding the drought health streak.
|
|
62
|
+
if (observation.failed) {
|
|
63
|
+
return { fire: false, value: raw, dedupeKey, persist: false, event: null };
|
|
64
|
+
}
|
|
57
65
|
if (cond.mode === 'every') {
|
|
58
66
|
// Fire on every tick that carries a real observation. An empty (or
|
|
59
67
|
// whitespace-only) observation means "nothing to report": firing an action
|
|
@@ -122,11 +130,13 @@ export class MonitorEngine {
|
|
|
122
130
|
monitors = [];
|
|
123
131
|
lastEval = new Map();
|
|
124
132
|
ticking = false;
|
|
133
|
+
running = false;
|
|
125
134
|
constructor(logFn = () => { }) {
|
|
126
135
|
this.logFn = logFn;
|
|
127
136
|
}
|
|
128
137
|
/** Load owned+enabled monitors and start the tick loop. */
|
|
129
138
|
start(options = {}) {
|
|
139
|
+
this.running = true;
|
|
130
140
|
this.loadAll();
|
|
131
141
|
this.logFn('INFO', `Monitor engine started (${this.monitors.length} monitor(s) on this device)`);
|
|
132
142
|
if (!options.externalScheduler) {
|
|
@@ -138,8 +148,9 @@ export class MonitorEngine {
|
|
|
138
148
|
this.loadAll();
|
|
139
149
|
this.logFn('INFO', `Monitor engine reloaded (${this.monitors.length} monitor(s) on this device)`);
|
|
140
150
|
}
|
|
141
|
-
/** Stop the tick loop. */
|
|
151
|
+
/** Stop the tick loop. No monitor dispatches after this until `start()`. */
|
|
142
152
|
stop() {
|
|
153
|
+
this.running = false;
|
|
143
154
|
if (this.timer) {
|
|
144
155
|
clearInterval(this.timer);
|
|
145
156
|
this.timer = null;
|
|
@@ -159,7 +170,11 @@ export class MonitorEngine {
|
|
|
159
170
|
}
|
|
160
171
|
/** Evaluate every due monitor once. Overlap-guarded so a slow cycle never stacks. */
|
|
161
172
|
async tick() {
|
|
162
|
-
|
|
173
|
+
// A stopped engine dispatches nothing (PHNX-3608): under the external
|
|
174
|
+
// scheduler the supervisor owns the timer, so a `stop()` (monitors service
|
|
175
|
+
// disabled) must be honoured HERE — otherwise the next supervised tick would
|
|
176
|
+
// still fire the last-loaded monitors even though the engine is stopped.
|
|
177
|
+
if (!this.running || this.ticking)
|
|
163
178
|
return;
|
|
164
179
|
this.ticking = true;
|
|
165
180
|
try {
|
|
@@ -196,6 +211,16 @@ export class MonitorEngine {
|
|
|
196
211
|
if (!observation) {
|
|
197
212
|
checkError = 'source produced no observation';
|
|
198
213
|
}
|
|
214
|
+
else if (observation.failed) {
|
|
215
|
+
// The poll ran but did not OBSERVE (non-zero exit, or a transport/auth/
|
|
216
|
+
// rate-limit error in its output). Skip it entirely: no decideFire, no
|
|
217
|
+
// fire, watched-state untouched — so no empty→error→empty flap dispatches
|
|
218
|
+
// an agent on a dead premise. Record it as a failed check so a sustained
|
|
219
|
+
// streak escalates as a drought, the same health surface `--postcondition`
|
|
220
|
+
// uses on the action side (PHNX-3510).
|
|
221
|
+
checkError = `poll failed: ${observation.failureReason ?? 'observation failure'}`;
|
|
222
|
+
this.logFn('WARN', `monitor '${monitor.name}' poll failed (${observation.failureReason ?? 'observation failure'}) — not treated as a value change`);
|
|
223
|
+
}
|
|
199
224
|
else {
|
|
200
225
|
const decision = decideFire(monitor, observation);
|
|
201
226
|
if (decision.fire && decision.event) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* here. No agent, no sandbox: a plain `/bin/sh -c` (or `cmd /c` on Windows).
|
|
7
7
|
*/
|
|
8
8
|
import { execFile } from 'child_process';
|
|
9
|
+
import { classifyPollFailure } from './failure.js';
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
10
11
|
/** Run the source command and return its combined stdout as the observation. */
|
|
11
12
|
export function evaluate(source) {
|
|
@@ -22,10 +23,19 @@ export function evaluate(source) {
|
|
|
22
23
|
: err
|
|
23
24
|
? 1
|
|
24
25
|
: 0;
|
|
25
|
-
//
|
|
26
|
-
// "command started failing"); surface stderr when stdout is empty.
|
|
26
|
+
// Surface stderr when stdout is empty.
|
|
27
27
|
const raw = (stdout && stdout.length > 0 ? stdout : stderr ?? '').replace(/\s+$/, '');
|
|
28
|
-
|
|
28
|
+
// A poll that failed to OBSERVE — non-zero exit, or a transport/auth/
|
|
29
|
+
// rate-limit error shape in its output (which a piped `gh … | jq`
|
|
30
|
+
// swallows the exit code of) — is not a new value. Flag it so the engine
|
|
31
|
+
// skips it instead of reading empty→error→empty as two value changes and
|
|
32
|
+
// dispatching an agent on a dead premise (PHNX-3510).
|
|
33
|
+
const failureReason = classifyPollFailure({ exitCode, text: raw });
|
|
34
|
+
resolve({
|
|
35
|
+
raw,
|
|
36
|
+
meta: { exitCode },
|
|
37
|
+
...(failureReason ? { failed: true, failureReason } : {}),
|
|
38
|
+
});
|
|
29
39
|
});
|
|
30
40
|
});
|
|
31
41
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll-failure classifier (PHNX-3510).
|
|
3
|
+
*
|
|
4
|
+
* A poll that FAILS to observe — the command exited non-zero, or its output
|
|
5
|
+
* carries a transport/auth/rate-limit error shape — is an OBSERVATION FAILURE,
|
|
6
|
+
* not a new value. Reading it as a value is the defect this closes: a
|
|
7
|
+
* `gh pr list … | jq` monitor whose gh half intermittently prints
|
|
8
|
+
* `GraphQL: API rate limit already exceeded …` flapped empty→error→empty, and an
|
|
9
|
+
* `[on-change]` monitor read that as two value changes and dispatched a full
|
|
10
|
+
* agent run on a premise that was false.
|
|
11
|
+
*
|
|
12
|
+
* The exit code alone is not enough: when gh is piped into jq the shell's exit
|
|
13
|
+
* status is jq's (0 on empty input), so the rate-limit text can ride an exit-0
|
|
14
|
+
* observation. The text patterns catch exactly that case. They stay tightly
|
|
15
|
+
* scoped to unambiguous failure shapes so a legitimate observation whose content
|
|
16
|
+
* merely mentions "timeout" is never misread as a failure.
|
|
17
|
+
*/
|
|
18
|
+
/** The failure reason matched in a poll's output text, or null when it looks clean. */
|
|
19
|
+
export declare function matchFailureText(text: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Classify one poll snapshot. Returns a short failure reason when the snapshot is
|
|
22
|
+
* an observation failure (non-zero exit, or a failure-shaped output), else null —
|
|
23
|
+
* in which case the snapshot is a genuine value the condition may diff.
|
|
24
|
+
*
|
|
25
|
+
* A failure-shaped OUTPUT is checked even on exit 0, because a piped command
|
|
26
|
+
* (`gh … | jq`) swallows the failing half's exit code. A non-zero exit is a
|
|
27
|
+
* failure regardless of output shape.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyPollFailure(input: {
|
|
30
|
+
exitCode?: number;
|
|
31
|
+
text: string;
|
|
32
|
+
}): string | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll-failure classifier (PHNX-3510).
|
|
3
|
+
*
|
|
4
|
+
* A poll that FAILS to observe — the command exited non-zero, or its output
|
|
5
|
+
* carries a transport/auth/rate-limit error shape — is an OBSERVATION FAILURE,
|
|
6
|
+
* not a new value. Reading it as a value is the defect this closes: a
|
|
7
|
+
* `gh pr list … | jq` monitor whose gh half intermittently prints
|
|
8
|
+
* `GraphQL: API rate limit already exceeded …` flapped empty→error→empty, and an
|
|
9
|
+
* `[on-change]` monitor read that as two value changes and dispatched a full
|
|
10
|
+
* agent run on a premise that was false.
|
|
11
|
+
*
|
|
12
|
+
* The exit code alone is not enough: when gh is piped into jq the shell's exit
|
|
13
|
+
* status is jq's (0 on empty input), so the rate-limit text can ride an exit-0
|
|
14
|
+
* observation. The text patterns catch exactly that case. They stay tightly
|
|
15
|
+
* scoped to unambiguous failure shapes so a legitimate observation whose content
|
|
16
|
+
* merely mentions "timeout" is never misread as a failure.
|
|
17
|
+
*/
|
|
18
|
+
const FAILURE_TEXT_PATTERNS = [
|
|
19
|
+
{ re: /\bAPI rate limit (?:already )?exceeded\b/i, reason: 'API rate limit exceeded' },
|
|
20
|
+
{ re: /\bsecondary rate limit\b/i, reason: 'secondary rate limit' },
|
|
21
|
+
{ re: /^\s*GraphQL:\s/im, reason: 'GraphQL error' },
|
|
22
|
+
{ re: /\bbad credentials\b/i, reason: 'bad credentials' },
|
|
23
|
+
{ re: /\b(?:401 Unauthorized|403 Forbidden)\b/i, reason: 'auth error' },
|
|
24
|
+
{ re: /\bcould not resolve host\b/i, reason: 'transport error (DNS)' },
|
|
25
|
+
{ re: /\bconnection (?:refused|reset|timed out)\b/i, reason: 'connection error' },
|
|
26
|
+
{ re: /\bnetwork is unreachable\b/i, reason: 'network unreachable' },
|
|
27
|
+
];
|
|
28
|
+
/** The failure reason matched in a poll's output text, or null when it looks clean. */
|
|
29
|
+
export function matchFailureText(text) {
|
|
30
|
+
for (const { re, reason } of FAILURE_TEXT_PATTERNS) {
|
|
31
|
+
if (re.test(text))
|
|
32
|
+
return reason;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Classify one poll snapshot. Returns a short failure reason when the snapshot is
|
|
38
|
+
* an observation failure (non-zero exit, or a failure-shaped output), else null —
|
|
39
|
+
* in which case the snapshot is a genuine value the condition may diff.
|
|
40
|
+
*
|
|
41
|
+
* A failure-shaped OUTPUT is checked even on exit 0, because a piped command
|
|
42
|
+
* (`gh … | jq`) swallows the failing half's exit code. A non-zero exit is a
|
|
43
|
+
* failure regardless of output shape.
|
|
44
|
+
*/
|
|
45
|
+
export function classifyPollFailure(input) {
|
|
46
|
+
const textReason = matchFailureText(input.text);
|
|
47
|
+
const badExit = typeof input.exitCode === 'number' && input.exitCode !== 0;
|
|
48
|
+
if (badExit) {
|
|
49
|
+
return textReason ? `${textReason} (exit ${input.exitCode})` : `command exited ${input.exitCode}`;
|
|
50
|
+
}
|
|
51
|
+
return textReason;
|
|
52
|
+
}
|
|
@@ -11,6 +11,15 @@ import type { MonitorSource } from '../config.js';
|
|
|
11
11
|
export interface Observation {
|
|
12
12
|
raw: string;
|
|
13
13
|
meta?: Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* The source flagged this snapshot as an OBSERVATION FAILURE (a poll that
|
|
16
|
+
* exited non-zero or emitted a transport/auth/rate-limit error), not a value.
|
|
17
|
+
* The engine skips it: no fire, watched-state untouched, counted as a failed
|
|
18
|
+
* check for drought health (PHNX-3510).
|
|
19
|
+
*/
|
|
20
|
+
failed?: boolean;
|
|
21
|
+
/** Short human reason for `failed`, surfaced in drought health and `test`. */
|
|
22
|
+
failureReason?: string;
|
|
14
23
|
}
|
|
15
24
|
/** Poll-model evaluator: return one observation, or null when none is available. */
|
|
16
25
|
export type SourceEvaluator = (source: MonitorSource) => Promise<Observation | null>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OwnerMessageOptions {
|
|
2
|
+
/** Scannable subject line, when the caller has one (feed post does; notify does not). */
|
|
3
|
+
title?: string;
|
|
4
|
+
/** Explicit session id (`--session`); otherwise resolved from the run environment. */
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shape a raw owner-send body into the composed broadcast message. Pure except
|
|
9
|
+
* for the identity/index reads that {@link resolvePostIdentity} /
|
|
10
|
+
* {@link getSessionById} already perform for feed posts.
|
|
11
|
+
*/
|
|
12
|
+
export declare function composeOwnerMessage(rawText: string, opts?: OwnerMessageOptions): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose an owner-bound phone ping through the SAME shaper `agents feed post`
|
|
3
|
+
* uses, so `agents notify` / `agents send --to owner` stop shipping a raw body
|
|
4
|
+
* dump (PHNX-3698).
|
|
5
|
+
*
|
|
6
|
+
* Before this, an owner send delivered the body verbatim: a long wall of prose,
|
|
7
|
+
* a `TEAM-N` key iMessage renders as dead text, and no way back to the session.
|
|
8
|
+
* Routing the body through {@link composeBroadcastMessage} makes an owner ping
|
|
9
|
+
* identical to an important feed post of the same event — short-shaped body,
|
|
10
|
+
* every ticket key linkified to its Linear URL, and the current session's
|
|
11
|
+
* `…/console/sessions/<id>` page as a tappable crumb.
|
|
12
|
+
*
|
|
13
|
+
* The session/agent/host are resolved the same way a feed post resolves them
|
|
14
|
+
* ({@link resolvePostIdentity} — the pid-registry / env walk), so a `notify`
|
|
15
|
+
* run inside an agent session inherits that session with no flag. Outside a
|
|
16
|
+
* session (a human at a shell) identity is undefined: the ping still gets
|
|
17
|
+
* short-shaping and ticket linkification, just no session crumb.
|
|
18
|
+
*/
|
|
19
|
+
import { composeBroadcastMessage } from './feed-broadcast.js';
|
|
20
|
+
import { resolvePostIdentity } from './feed-post.js';
|
|
21
|
+
import { getSessionById, resolveFullSessionId } from './session/db.js';
|
|
22
|
+
import { linearIssueUrl } from './session/linear.js';
|
|
23
|
+
/**
|
|
24
|
+
* Shape a raw owner-send body into the composed broadcast message. Pure except
|
|
25
|
+
* for the identity/index reads that {@link resolvePostIdentity} /
|
|
26
|
+
* {@link getSessionById} already perform for feed posts.
|
|
27
|
+
*/
|
|
28
|
+
export function composeOwnerMessage(rawText, opts = {}) {
|
|
29
|
+
const identity = resolvePostIdentity({ sessionId: opts.sessionId });
|
|
30
|
+
// A footer crumb that would 404 (an 8-char short id) is upgraded to the full
|
|
31
|
+
// indexed id so the console URL resolves; a full/native id passes through.
|
|
32
|
+
const session = resolveFullSessionId(identity?.sessionId);
|
|
33
|
+
const ticket = session ? getSessionById(session)?.ticketId : undefined;
|
|
34
|
+
const ctx = {
|
|
35
|
+
...(opts.title?.trim() ? { title: opts.title.trim() } : {}),
|
|
36
|
+
text: rawText,
|
|
37
|
+
level: 'important',
|
|
38
|
+
...(ticket ? { ticket, ticketUrl: linearIssueUrl(ticket) } : {}),
|
|
39
|
+
...(identity?.agent ? { agent: identity.agent } : {}),
|
|
40
|
+
...(identity?.host ? { host: identity.host } : {}),
|
|
41
|
+
...(session ? { session } : {}),
|
|
42
|
+
};
|
|
43
|
+
return composeBroadcastMessage(ctx);
|
|
44
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as fs from 'node:fs/promises';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import lockfile from 'proper-lockfile';
|
|
5
5
|
import { getCacheDir } from './state.js';
|
|
6
|
+
import { logAndContinueOnLockCompromised } from './lock-compromise.js';
|
|
6
7
|
const REFRESH_LOCK_STALE_MS = 2 * 60_000;
|
|
7
8
|
let refreshLockRootOverride = null;
|
|
8
9
|
export function setRefreshLockRootForTest(root) {
|
|
@@ -39,6 +40,7 @@ export async function withRefreshLease(options) {
|
|
|
39
40
|
stale: REFRESH_LOCK_STALE_MS,
|
|
40
41
|
update: REFRESH_LOCK_STALE_MS / 4,
|
|
41
42
|
retries: { retries: 240, minTimeout: 25, maxTimeout: 500, factor: 1.2 },
|
|
43
|
+
onCompromised: logAndContinueOnLockCompromised('refresh lease'),
|
|
42
44
|
});
|
|
43
45
|
try {
|
|
44
46
|
const completed = options.readCompleted();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy gate, pure and unit-testable. `disabled` is the resolved
|
|
3
|
+
* `--no-trace-sync` (commander maps it to `traceSync === false`).
|
|
4
|
+
*/
|
|
5
|
+
export declare function shouldAutoSyncTraces(disabled: boolean): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Arm a fire-and-forget `agents traces sync` for when this process exits.
|
|
8
|
+
* No-op unless {@link shouldAutoSyncTraces} passes. Best-effort by
|
|
9
|
+
* construction: a missing binary or a stalled child never affects the run.
|
|
10
|
+
* The spawn happens in the exit handler because the upload is async work Node
|
|
11
|
+
* cannot run after `exit` — a watchdog here could never fire.
|
|
12
|
+
*/
|
|
13
|
+
export declare function armRunFinishTraceSync(opts?: {
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}): void;
|
|
16
|
+
/**
|
|
17
|
+
* Fire a fire-and-forget `agents traces sync` NOW (not on exit). An important
|
|
18
|
+
* owner-bound ping (`feed post --level important`, `agents notify`,
|
|
19
|
+
* `send --to owner`) links the caller's `…/console/sessions/<id>` page, and that
|
|
20
|
+
* page only exists once the session's shard has been uploaded — trace sync fires
|
|
21
|
+
* on run exit (PHNX-3628), not when a mid-run ping is posted. This closes that
|
|
22
|
+
* gap so the tapped link resolves instead of 404ing. No-op unless
|
|
23
|
+
* {@link shouldAutoSyncTraces} passes (signed in + already opted into the store);
|
|
24
|
+
* the incremental watermark keeps the push to essentially just this session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fireTraceSyncInBackground(opts?: {
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-sync this device's just-finished session trace when a local, headless
|
|
3
|
+
* `agents run` exits (PHNX-3628).
|
|
4
|
+
*
|
|
5
|
+
* Why exit-armed + spawn, exactly like `run-notify`: the sync reads this
|
|
6
|
+
* device's `sessions.db` and PUTs redacted shards over the network — async work
|
|
7
|
+
* that CANNOT run inside a `process.on('exit')` handler (Node runs no more
|
|
8
|
+
* microtasks after exit). So, mirroring `notifyDesktop`, on exit we SPAWN a
|
|
9
|
+
* detached `agents traces sync` and unref it; it finishes after the parent is
|
|
10
|
+
* gone, and the run itself returns immediately instead of waiting on the PUTs.
|
|
11
|
+
*
|
|
12
|
+
* The upload is incremental by construction (`syncTraces` de-dups by the
|
|
13
|
+
* `traces-sync.json` mtime watermark), so the spawned sync pushes essentially
|
|
14
|
+
* just the session that finished, not the whole history.
|
|
15
|
+
*
|
|
16
|
+
* Default policy: fire ONLY when the user has ALREADY opted into the traces
|
|
17
|
+
* store — i.e. they are signed in AND have run `agents traces sync` at least
|
|
18
|
+
* once (`hasSyncedBefore`). A user who never synced is never touched. Opt out
|
|
19
|
+
* of a given run with `--no-trace-sync` or `AGENTS_NO_TRACE_SYNC=1`.
|
|
20
|
+
*
|
|
21
|
+
* Local runs only: a run placed on another machine (`--device`/`--on`/
|
|
22
|
+
* `--computer`/`--where device:`, `--lease`, `--box`, or `--cloud`) has its
|
|
23
|
+
* trajectory on the REMOTE box, not this device's `sessions.db`, so arming here
|
|
24
|
+
* would upload nothing useful — the remote box runs its own exit sync. The
|
|
25
|
+
* caller gates every remote-placement signal out before arming (it reuses the
|
|
26
|
+
* file's own `hostTargetGiven` predicate plus `--lease`/`--box`; `--cloud`
|
|
27
|
+
* returns earlier).
|
|
28
|
+
*/
|
|
29
|
+
import { spawn } from 'child_process';
|
|
30
|
+
import { getCliLaunch } from './cli-entry.js';
|
|
31
|
+
import { readSession } from './identity/client.js';
|
|
32
|
+
import { hasSyncedBefore } from './traces/sync.js';
|
|
33
|
+
/**
|
|
34
|
+
* The policy gate, pure and unit-testable. `disabled` is the resolved
|
|
35
|
+
* `--no-trace-sync` (commander maps it to `traceSync === false`).
|
|
36
|
+
*/
|
|
37
|
+
export function shouldAutoSyncTraces(disabled) {
|
|
38
|
+
if (disabled)
|
|
39
|
+
return false;
|
|
40
|
+
if (process.env.AGENTS_NO_TRACE_SYNC === '1')
|
|
41
|
+
return false;
|
|
42
|
+
if (!readSession())
|
|
43
|
+
return false; // not signed in → nothing to authenticate an upload
|
|
44
|
+
if (!hasSyncedBefore())
|
|
45
|
+
return false; // never opted into the traces store
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Spawn a detached, unref'd `agents traces sync` and return immediately. The one
|
|
50
|
+
* place the fire-and-forget child is built, shared by the run-exit arm and the
|
|
51
|
+
* feed/notify important-post trigger.
|
|
52
|
+
*
|
|
53
|
+
* Re-invoke this CLI through getCliLaunch, NOT a hand-rolled
|
|
54
|
+
* [process.execPath, process.argv[1], …] — under the compiled standalone binary
|
|
55
|
+
* argv[1] is the bun virtual entry (/$bunfs/root/agents), which would become a
|
|
56
|
+
* bogus subcommand and silently no-op the whole feature (cli-entry.ts). The
|
|
57
|
+
* spawned `traces sync` is a fresh process with its own event loop, so its own
|
|
58
|
+
* per-request network timeouts (syncTraces) bound it.
|
|
59
|
+
*/
|
|
60
|
+
function spawnDetachedTraceSync() {
|
|
61
|
+
try {
|
|
62
|
+
const { command, args } = getCliLaunch(['traces', 'sync']);
|
|
63
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
64
|
+
// A child that never starts (ENOENT) emits an async 'error'; without a
|
|
65
|
+
// listener Node re-throws it as uncaught. Swallow — this is best-effort.
|
|
66
|
+
child.on('error', () => { });
|
|
67
|
+
child.unref();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A synchronous spawn failure must never change the caller's outcome.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Arm a fire-and-forget `agents traces sync` for when this process exits.
|
|
75
|
+
* No-op unless {@link shouldAutoSyncTraces} passes. Best-effort by
|
|
76
|
+
* construction: a missing binary or a stalled child never affects the run.
|
|
77
|
+
* The spawn happens in the exit handler because the upload is async work Node
|
|
78
|
+
* cannot run after `exit` — a watchdog here could never fire.
|
|
79
|
+
*/
|
|
80
|
+
export function armRunFinishTraceSync(opts = {}) {
|
|
81
|
+
if (!shouldAutoSyncTraces(!!opts.disabled))
|
|
82
|
+
return;
|
|
83
|
+
process.on('exit', spawnDetachedTraceSync);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fire a fire-and-forget `agents traces sync` NOW (not on exit). An important
|
|
87
|
+
* owner-bound ping (`feed post --level important`, `agents notify`,
|
|
88
|
+
* `send --to owner`) links the caller's `…/console/sessions/<id>` page, and that
|
|
89
|
+
* page only exists once the session's shard has been uploaded — trace sync fires
|
|
90
|
+
* on run exit (PHNX-3628), not when a mid-run ping is posted. This closes that
|
|
91
|
+
* gap so the tapped link resolves instead of 404ing. No-op unless
|
|
92
|
+
* {@link shouldAutoSyncTraces} passes (signed in + already opted into the store);
|
|
93
|
+
* the incremental watermark keeps the push to essentially just this session.
|
|
94
|
+
*/
|
|
95
|
+
export function fireTraceSyncInBackground(opts = {}) {
|
|
96
|
+
if (!shouldAutoSyncTraces(!!opts.disabled))
|
|
97
|
+
return;
|
|
98
|
+
spawnDetachedTraceSync();
|
|
99
|
+
}
|
|
@@ -207,6 +207,10 @@ export declare function _resetFileStoreForTest(opts?: {
|
|
|
207
207
|
passphraseDir?: string | null;
|
|
208
208
|
passphrase?: string | null;
|
|
209
209
|
}): void;
|
|
210
|
+
/** Test-only: flush the in-memory metadata cache to disk now. Production flushes
|
|
211
|
+
* once at process exit; a test needs the on-disk copy mid-run to simulate the
|
|
212
|
+
* next `agents` process reading it. */
|
|
213
|
+
export declare function _flushMetaCacheForTest(): void;
|
|
210
214
|
/** Test-only: shorten the store-lock acquire timeout so a contended-lock assertion
|
|
211
215
|
* fails fast instead of waiting out the 30s production budget. */
|
|
212
216
|
export declare function _setFileStoreLockTimeoutForTest(ms: number | null): void;
|