@phnx-labs/agents-cli 1.22.83 → 1.22.85
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 +96 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-ticks.js +23 -2
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +10 -4
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/fleet-shared-state.d.ts +6 -0
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +65 -2
- package/dist/lib/session/db.js +178 -18
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -170
- package/dist/lib/accounts/connect.js +0 -383
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental reader for the activity log directory.
|
|
3
|
+
*
|
|
4
|
+
* `readRecentActivity` answers "what happened since T?" by tailing and parsing
|
|
5
|
+
* every session log in the directory. That is the right shape for a one-shot
|
|
6
|
+
* `agents feed` render and the wrong shape for `agents feed watch`, which asks
|
|
7
|
+
* the same question twice a second for as long as a VS Code window is open: on
|
|
8
|
+
* a real operator box the directory holds 1,437 logs / 64 MB, so every tick
|
|
9
|
+
* re-read and re-parsed the whole corpus to emit, almost always, nothing.
|
|
10
|
+
*
|
|
11
|
+
* This reader keeps a per-file cursor instead. The opening scan opens no files
|
|
12
|
+
* at all — it records each log's size, inode, and mtime — so only bytes appended
|
|
13
|
+
* *after* the stream started are ever read. Steady-state cost is one `stat` per
|
|
14
|
+
* changed file plus a parse of exactly the appended bytes.
|
|
15
|
+
*
|
|
16
|
+
* The emitted events, their order, and the `sinceMs` filter match
|
|
17
|
+
* `readRecentActivity` for everything appended while the stream is open; the
|
|
18
|
+
* equivalence is pinned by `activity-stream.test.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { getActivityDir } from '../state.js';
|
|
23
|
+
import { ACTIVITY_TAIL_BYTES, parseActivityLine } from './activity.js';
|
|
24
|
+
/** How often the stream falls back to a full directory stat sweep. */
|
|
25
|
+
export const ACTIVITY_SWEEP_MS = 5_000;
|
|
26
|
+
/** Bytes behind the cursor re-verified before appended bytes are trusted. */
|
|
27
|
+
export const ACTIVITY_ANCHOR_BYTES = 64;
|
|
28
|
+
const NEWLINE = 0x0a;
|
|
29
|
+
const EMPTY = Buffer.alloc(0);
|
|
30
|
+
/** Could this file have changed since its cursor last looked? */
|
|
31
|
+
function changed(cursor, stat) {
|
|
32
|
+
return stat.identity !== cursor.identity
|
|
33
|
+
|| stat.size !== cursor.size
|
|
34
|
+
|| stat.mtimeNs !== cursor.mtimeNs
|
|
35
|
+
|| stat.ctimeNs !== cursor.ctimeNs;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A cursor over the activity directory. Construct it at the moment the caller's
|
|
39
|
+
* activity cursor starts, then call {@link read} once per tick.
|
|
40
|
+
*/
|
|
41
|
+
export class ActivityStream {
|
|
42
|
+
dir;
|
|
43
|
+
maxBytesPerRead;
|
|
44
|
+
sweepMs;
|
|
45
|
+
cursors = new Map();
|
|
46
|
+
dirty = new Set();
|
|
47
|
+
watcher;
|
|
48
|
+
watchRequested;
|
|
49
|
+
lastSweepMs = 0;
|
|
50
|
+
/** False during the opening scan, so it registers history without reading it. */
|
|
51
|
+
started = false;
|
|
52
|
+
/** Bytes read from activity logs since construction. Observability + tests. */
|
|
53
|
+
bytesRead = 0;
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.dir = options.root ?? getActivityDir();
|
|
56
|
+
this.maxBytesPerRead = options.maxBytesPerRead ?? ACTIVITY_TAIL_BYTES;
|
|
57
|
+
this.sweepMs = options.sweepMs ?? ACTIVITY_SWEEP_MS;
|
|
58
|
+
this.watchRequested = options.watch ?? true;
|
|
59
|
+
this.sweep(Date.now());
|
|
60
|
+
this.armWatcher();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Events appended since the last call, newest first, filtered to `sinceMs`
|
|
64
|
+
* inclusive — the same shape and order `readRecentActivity({ sinceMs })`
|
|
65
|
+
* returns for those events.
|
|
66
|
+
*/
|
|
67
|
+
read(sinceMs, nowMs = Date.now()) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const name of this.candidates(nowMs)) {
|
|
70
|
+
for (const event of this.readFile(name)) {
|
|
71
|
+
const at = Date.parse(event.ts);
|
|
72
|
+
if (Number.isFinite(at) && at >= sinceMs)
|
|
73
|
+
out.push(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
out.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Release the directory watcher. Safe to call more than once. */
|
|
80
|
+
close() {
|
|
81
|
+
this.watchRequested = false;
|
|
82
|
+
this.watcher?.close();
|
|
83
|
+
this.watcher = undefined;
|
|
84
|
+
}
|
|
85
|
+
/** Which log names could have changed since the previous tick. */
|
|
86
|
+
candidates(nowMs) {
|
|
87
|
+
// A watcher that never armed (unsupported filesystem, or a directory that
|
|
88
|
+
// did not exist at construction) means every tick sweeps. That is the
|
|
89
|
+
// fallback: never a silent no-op that would drop events.
|
|
90
|
+
if (!this.watcher)
|
|
91
|
+
this.armWatcher();
|
|
92
|
+
if (!this.watcher || nowMs - this.lastSweepMs >= this.sweepMs)
|
|
93
|
+
this.sweep(nowMs);
|
|
94
|
+
const names = [...this.dirty];
|
|
95
|
+
this.dirty.clear();
|
|
96
|
+
return names;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stat every log, marking the ones that could have changed. Opens nothing: a
|
|
100
|
+
* log the opening scan sees is registered past its own bytes, so history is
|
|
101
|
+
* never replayed onto the stream.
|
|
102
|
+
*/
|
|
103
|
+
sweep(nowMs) {
|
|
104
|
+
this.lastSweepMs = nowMs;
|
|
105
|
+
let names;
|
|
106
|
+
try {
|
|
107
|
+
names = fs.readdirSync(this.dir).filter((name) => name.endsWith('.jsonl'));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return; // The directory appears with the first logged event.
|
|
111
|
+
}
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
seen.add(name);
|
|
115
|
+
const stat = this.statOf(name);
|
|
116
|
+
if (!stat)
|
|
117
|
+
continue;
|
|
118
|
+
const cursor = this.cursors.get(name);
|
|
119
|
+
if (!cursor) {
|
|
120
|
+
// A log first seen after the opening scan is new work: left
|
|
121
|
+
// unregistered so `readFile` opens it from a bounded tail.
|
|
122
|
+
if (this.started)
|
|
123
|
+
this.dirty.add(name);
|
|
124
|
+
else
|
|
125
|
+
this.cursors.set(name, {
|
|
126
|
+
identity: stat.identity, offset: stat.size, partial: EMPTY, partialIsFragment: false,
|
|
127
|
+
anchor: EMPTY, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (changed(cursor, stat))
|
|
132
|
+
this.dirty.add(name);
|
|
133
|
+
}
|
|
134
|
+
// A cursor is dropped only when its log is gone. There is deliberately no
|
|
135
|
+
// size cap: the map cannot outgrow the directory this sweep already had to
|
|
136
|
+
// enumerate, so a cap bounds nothing the readdir does not — while dropping
|
|
137
|
+
// a live log's cursor would re-register it as new work on the next sweep
|
|
138
|
+
// and replay a bounded tail of it onto the stream as duplicates.
|
|
139
|
+
for (const name of [...this.cursors.keys()])
|
|
140
|
+
if (!seen.has(name))
|
|
141
|
+
this.cursors.delete(name);
|
|
142
|
+
this.started = true;
|
|
143
|
+
}
|
|
144
|
+
statOf(name) {
|
|
145
|
+
try {
|
|
146
|
+
const st = fs.statSync(path.join(this.dir, name), { bigint: true });
|
|
147
|
+
return {
|
|
148
|
+
identity: `${st.dev}:${st.ino}`,
|
|
149
|
+
size: Number(st.size),
|
|
150
|
+
mtimeNs: Number(st.mtimeNs),
|
|
151
|
+
ctimeNs: Number(st.ctimeNs),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined; // Deleted between readdir and stat.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** A cursor starting at a bounded tail of the file as it stands right now. */
|
|
159
|
+
freshCursor(stat) {
|
|
160
|
+
const offset = Math.max(0, stat.size - this.maxBytesPerRead);
|
|
161
|
+
return {
|
|
162
|
+
identity: stat.identity, offset, partial: EMPTY, partialIsFragment: offset > 0,
|
|
163
|
+
anchor: EMPTY, size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Read and parse only the bytes appended to one log since its cursor. */
|
|
167
|
+
readFile(name, restarted = false) {
|
|
168
|
+
const stat = this.statOf(name);
|
|
169
|
+
if (!stat) {
|
|
170
|
+
this.cursors.delete(name);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
let cursor = this.cursors.get(name);
|
|
174
|
+
// Unseen, replaced, truncated, or rewritten in place at the same length:
|
|
175
|
+
// restart from a bounded tail of the file as it now stands. The caller's
|
|
176
|
+
// `sinceMs` drops whatever predates the stream.
|
|
177
|
+
//
|
|
178
|
+
// The same-length case is why ctime is tracked. Growth is caught by the
|
|
179
|
+
// 64-byte anchor and a shrink by the offset compare, but a rewrite that
|
|
180
|
+
// lands on exactly the previous size moves neither, and the early return
|
|
181
|
+
// below would otherwise retire the file for good with content unread.
|
|
182
|
+
if (!cursor || cursor.identity !== stat.identity || stat.size < cursor.offset
|
|
183
|
+
|| (stat.size === cursor.size && stat.ctimeNs !== cursor.ctimeNs)) {
|
|
184
|
+
cursor = this.freshCursor(stat);
|
|
185
|
+
this.cursors.set(name, cursor);
|
|
186
|
+
}
|
|
187
|
+
cursor.size = stat.size;
|
|
188
|
+
cursor.mtimeNs = stat.mtimeNs;
|
|
189
|
+
cursor.ctimeNs = stat.ctimeNs;
|
|
190
|
+
if (stat.size <= cursor.offset)
|
|
191
|
+
return [];
|
|
192
|
+
// A burst larger than the budget keeps the newest bytes; the skipped span is
|
|
193
|
+
// exactly what the bounded-tail reader would have dropped as well.
|
|
194
|
+
const start = Math.max(cursor.offset, stat.size - this.maxBytesPerRead);
|
|
195
|
+
if (start > cursor.offset) {
|
|
196
|
+
cursor.partial = EMPTY;
|
|
197
|
+
cursor.partialIsFragment = true;
|
|
198
|
+
cursor.anchor = EMPTY;
|
|
199
|
+
}
|
|
200
|
+
const verify = Math.min(cursor.anchor.length, start);
|
|
201
|
+
const buf = this.readRange(name, start - verify, stat.size - start + verify);
|
|
202
|
+
if (buf === undefined)
|
|
203
|
+
return []; // Transient I/O error: retry next tick.
|
|
204
|
+
if (verify > 0 && !buf.subarray(0, verify).equals(cursor.anchor.subarray(cursor.anchor.length - verify))) {
|
|
205
|
+
// The bytes behind the cursor changed, so this file was rewritten rather
|
|
206
|
+
// than appended to. Restart it once from a bounded tail.
|
|
207
|
+
if (restarted)
|
|
208
|
+
return [];
|
|
209
|
+
this.cursors.delete(name);
|
|
210
|
+
return this.readFile(name, true);
|
|
211
|
+
}
|
|
212
|
+
const fresh = buf.subarray(verify);
|
|
213
|
+
cursor.offset = stat.size;
|
|
214
|
+
cursor.anchor = Buffer.concat([cursor.anchor, fresh]).subarray(-ACTIVITY_ANCHOR_BYTES);
|
|
215
|
+
const chunk = Buffer.concat([cursor.partial, fresh]);
|
|
216
|
+
const fragment = cursor.partialIsFragment;
|
|
217
|
+
const end = chunk.lastIndexOf(NEWLINE);
|
|
218
|
+
// Hold an unterminated trailing line: the writer appends whole
|
|
219
|
+
// newline-terminated records (`appendActivityEvent`), so a tail without a
|
|
220
|
+
// newline is a write in progress and completes on a later tick.
|
|
221
|
+
cursor.partial = end < 0 ? chunk : chunk.subarray(end + 1);
|
|
222
|
+
cursor.partialIsFragment = end < 0 ? fragment : false;
|
|
223
|
+
if (end < 0)
|
|
224
|
+
return [];
|
|
225
|
+
const lines = chunk.subarray(0, end).toString('utf-8').split('\n');
|
|
226
|
+
// A range that began mid-file starts inside a record; that leading fragment
|
|
227
|
+
// is not one, exactly as the bounded-tail reader drops it.
|
|
228
|
+
if (fragment)
|
|
229
|
+
lines.shift();
|
|
230
|
+
const events = [];
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const event = parseActivityLine(line);
|
|
233
|
+
if (event)
|
|
234
|
+
events.push(event);
|
|
235
|
+
}
|
|
236
|
+
return events;
|
|
237
|
+
}
|
|
238
|
+
readRange(name, start, length) {
|
|
239
|
+
let fd;
|
|
240
|
+
try {
|
|
241
|
+
fd = fs.openSync(path.join(this.dir, name), 'r');
|
|
242
|
+
const buf = Buffer.allocUnsafe(length);
|
|
243
|
+
const read = fs.readSync(fd, buf, 0, length, start);
|
|
244
|
+
this.bytesRead += read;
|
|
245
|
+
return buf.subarray(0, read);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (fd !== undefined)
|
|
252
|
+
fs.closeSync(fd);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
armWatcher() {
|
|
256
|
+
if (!this.watchRequested || this.watcher)
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
this.watcher = fs.watch(this.dir, (_event, name) => {
|
|
260
|
+
if (typeof name === 'string' && name.endsWith('.jsonl'))
|
|
261
|
+
this.dirty.add(name);
|
|
262
|
+
});
|
|
263
|
+
// A watch error (the directory is removed) degrades to sweeping rather
|
|
264
|
+
// than taking down the watcher process.
|
|
265
|
+
this.watcher.on('error', () => { this.watcher?.close(); this.watcher = undefined; });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
this.watcher = undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -129,6 +129,13 @@ export declare function appendActivityEvent(event: Omit<ActivityEvent, 'v' | 'ti
|
|
|
129
129
|
v?: number;
|
|
130
130
|
tier?: ActivityTier;
|
|
131
131
|
}, root?: string): void;
|
|
132
|
+
/**
|
|
133
|
+
* Parse one activity log line. Tolerant by design: a blank, corrupt, or
|
|
134
|
+
* half-written line is skipped rather than failing the whole read.
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseActivityLine(line: string): ActivityEvent | undefined;
|
|
137
|
+
/** Per-session byte budget for a bounded tail read of an activity log. */
|
|
138
|
+
export declare const ACTIVITY_TAIL_BYTES: number;
|
|
132
139
|
/** Read all events for one session (bounded tail). */
|
|
133
140
|
export declare function readSessionActivity(sessionId: string, root?: string, maxBytes?: number): ActivityEvent[];
|
|
134
141
|
/** List session ids that have an activity log. */
|
|
@@ -119,7 +119,11 @@ export function appendActivityEvent(event, root) {
|
|
|
119
119
|
};
|
|
120
120
|
fs.appendFileSync(activityPath(dir, event.sessionId), `${JSON.stringify(record)}\n`, { mode: 0o644 });
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Parse one activity log line. Tolerant by design: a blank, corrupt, or
|
|
124
|
+
* half-written line is skipped rather than failing the whole read.
|
|
125
|
+
*/
|
|
126
|
+
export function parseActivityLine(line) {
|
|
123
127
|
const trimmed = line.trim();
|
|
124
128
|
if (!trimmed)
|
|
125
129
|
return undefined;
|
|
@@ -160,6 +164,8 @@ function parseLine(line) {
|
|
|
160
164
|
return undefined; // skip corrupt / partial lines (fail-open reader)
|
|
161
165
|
}
|
|
162
166
|
}
|
|
167
|
+
/** Per-session byte budget for a bounded tail read of an activity log. */
|
|
168
|
+
export const ACTIVITY_TAIL_BYTES = 256 * 1024;
|
|
163
169
|
/** Read the tail of a file as UTF-8, bounded to the last `maxBytes`. */
|
|
164
170
|
function readTail(file, maxBytes) {
|
|
165
171
|
let fd;
|
|
@@ -232,7 +238,7 @@ function readActivityTail(file, maxBytes, sinceMs = -Infinity) {
|
|
|
232
238
|
const events = [];
|
|
233
239
|
let newestMs = -Infinity;
|
|
234
240
|
for (const line of text.split('\n')) {
|
|
235
|
-
const event =
|
|
241
|
+
const event = parseActivityLine(line);
|
|
236
242
|
if (!event)
|
|
237
243
|
continue;
|
|
238
244
|
events.push(event);
|
|
@@ -277,7 +283,7 @@ function readActivityTail(file, maxBytes, sinceMs = -Infinity) {
|
|
|
277
283
|
return result;
|
|
278
284
|
}
|
|
279
285
|
/** Read all events for one session (bounded tail). */
|
|
280
|
-
export function readSessionActivity(sessionId, root, maxBytes =
|
|
286
|
+
export function readSessionActivity(sessionId, root, maxBytes = ACTIVITY_TAIL_BYTES) {
|
|
281
287
|
const dir = root ?? getActivityDir();
|
|
282
288
|
return structuredClone(readActivityTail(activityPath(dir, sessionId), maxBytes).events ?? []);
|
|
283
289
|
}
|
|
@@ -301,7 +307,7 @@ export function readRecentActivity(opts = {}) {
|
|
|
301
307
|
const wanted = opts.events && opts.events.length > 0 ? new Set(opts.events) : null;
|
|
302
308
|
const all = [];
|
|
303
309
|
for (const sessionId of listActivitySessions(dir)) {
|
|
304
|
-
const tail = readActivityTail(activityPath(dir, sessionId), opts.maxBytesPerSession ??
|
|
310
|
+
const tail = readActivityTail(activityPath(dir, sessionId), opts.maxBytesPerSession ?? ACTIVITY_TAIL_BYTES, sinceMs);
|
|
305
311
|
if (tail.newestMs < sinceMs)
|
|
306
312
|
continue;
|
|
307
313
|
for (const ev of tail.events ?? []) {
|
package/dist/lib/feed/watch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SessionWatchEnvelope, type SessionWatchRow, type SessionWatchScopeStatus } from '../session/watch.js';
|
|
1
|
+
import { type SessionWatchEnvelope, type SessionWatchRow, type SessionWatchScopeStatus, type WatchLocalOptions } from '../session/watch.js';
|
|
2
2
|
import { type AttentionItem } from './attention.js';
|
|
3
3
|
import { type ActivityEvent } from './activity.js';
|
|
4
4
|
export declare const FEED_WATCH_VERSION: 1;
|
|
@@ -48,12 +48,18 @@ export declare class FeedWatchState {
|
|
|
48
48
|
emit(event: FeedWatchPayload): FeedWatchEnvelope;
|
|
49
49
|
}
|
|
50
50
|
export declare function projectSessionEnvelope(event: SessionWatchEnvelope, state: FeedWatchState): Promise<FeedWatchEnvelope[]>;
|
|
51
|
-
export
|
|
51
|
+
export interface WatchLocalFeedOptions {
|
|
52
52
|
scope: string;
|
|
53
53
|
signal: AbortSignal;
|
|
54
54
|
emit: (event: FeedWatchEnvelope) => void;
|
|
55
|
+
/** Activity drain cadence. */
|
|
55
56
|
activityPollMs?: number;
|
|
56
|
-
|
|
57
|
+
/** Attention reconcile cadence when nothing has announced a change. */
|
|
58
|
+
reconcileMs?: number;
|
|
59
|
+
/** Session-watch inputs, forwarded verbatim to {@link watchLocalSessions}. */
|
|
60
|
+
sessions?: Pick<WatchLocalOptions, 'readCache' | 'readPrevious' | 'journalPath' | 'journalPollMs' | 'heartbeatMs'>;
|
|
61
|
+
}
|
|
62
|
+
export declare function watchLocalFeed(options: WatchLocalFeedOptions): Promise<void>;
|
|
57
63
|
export declare function watchFleetFeed(options: {
|
|
58
64
|
signal: AbortSignal;
|
|
59
65
|
emit: (event: FeedWatchEnvelope) => void;
|
package/dist/lib/feed/watch.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
4
|
import { loadDevices, isDialableDevice } from '../devices/registry.js';
|
|
5
|
-
import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
|
|
6
5
|
import { machineId, normalizeHost } from '../machine-id.js';
|
|
7
|
-
import {
|
|
6
|
+
import { shellQuote } from '../ssh-exec.js';
|
|
8
7
|
import { buildWindowsAgentsCommand, remoteShellFor } from '../hosts/remote-cmd.js';
|
|
8
|
+
import { getFeedDir } from '../state.js';
|
|
9
|
+
import { streamFromPeer } from '../session/remote/peer-stream.js';
|
|
9
10
|
import { watchLocalSessions } from '../session/watch.js';
|
|
10
11
|
import { readBlock, readResolution, blockIdForSession } from './feed.js';
|
|
11
12
|
import { reconcileAttention } from './attention.js';
|
|
12
|
-
import {
|
|
13
|
-
import { readPullRequestStatus } from './pr-status.js';
|
|
13
|
+
import { ActivityStream } from './activity-stream.js';
|
|
14
|
+
import { PR_STATUS_TTL_MS, readPullRequestStatus } from './pr-status.js';
|
|
14
15
|
export const FEED_WATCH_VERSION = 1;
|
|
15
16
|
export class FeedWatchState {
|
|
16
17
|
streamId;
|
|
@@ -64,8 +65,37 @@ export async function projectSessionEnvelope(event, state) {
|
|
|
64
65
|
return [state.emit({ type: 'scope', capturedAt: event.capturedAt, scope: event.scope, status: event.status, ...(event.reason ? { reason: event.reason } : {}) })];
|
|
65
66
|
return [state.emit({ type: 'heartbeat', capturedAt: event.capturedAt, scope: event.scope })];
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Watch the feed dirs that can change attention without a session event: an
|
|
70
|
+
* `agents feed post --blocked` writes a block, `feed answer` writes a
|
|
71
|
+
* resolution. Both are external processes, so nothing on the session stream
|
|
72
|
+
* announces them — this is what lets the reconcile pass be event-driven instead
|
|
73
|
+
* of a poll over every row twice a second.
|
|
74
|
+
*/
|
|
75
|
+
function watchAttentionStores(onChange) {
|
|
76
|
+
const feedDir = getFeedDir();
|
|
77
|
+
const watchers = [];
|
|
78
|
+
for (const dir of [feedDir, path.join(feedDir, 'resolutions')]) {
|
|
79
|
+
try {
|
|
80
|
+
const watcher = fs.watch(dir, () => onChange());
|
|
81
|
+
// A directory that disappears must not take the watcher process down; the
|
|
82
|
+
// PR-status cadence below still reconciles on its own timer.
|
|
83
|
+
watcher.on('error', () => watcher.close());
|
|
84
|
+
watchers.push(watcher);
|
|
85
|
+
}
|
|
86
|
+
catch { /* the dir appears with the first block; the timed pass covers it */ }
|
|
87
|
+
}
|
|
88
|
+
return () => { for (const watcher of watchers)
|
|
89
|
+
watcher.close(); };
|
|
90
|
+
}
|
|
67
91
|
export async function watchLocalFeed(options) {
|
|
68
92
|
const state = new FeedWatchState();
|
|
93
|
+
// The stream's opening scan registers every log past its own bytes, so the
|
|
94
|
+
// cursor has to be taken *after* it: a record appended while the scan was
|
|
95
|
+
// walking the directory is unreadable from the byte cursor, and only a
|
|
96
|
+
// timestamp taken after the scan classifies it as history rather than
|
|
97
|
+
// dropping it from a window the caller believes was covered.
|
|
98
|
+
const activity = new ActivityStream();
|
|
69
99
|
let activityCursor = Date.now();
|
|
70
100
|
const agents = new Map();
|
|
71
101
|
const attention = new Map();
|
|
@@ -82,20 +112,34 @@ export async function watchLocalFeed(options) {
|
|
|
82
112
|
: state.emit({ type: 'attention.remove', scope: options.scope, rowKey }));
|
|
83
113
|
}
|
|
84
114
|
};
|
|
115
|
+
// Attention is reconciled when something can actually have changed it — a row
|
|
116
|
+
// moved, a block/resolution file was written — or when the PR-status TTL has
|
|
117
|
+
// expired and the cached verdicts are stale. Re-running it every 500 ms cost
|
|
118
|
+
// two file reads per row per tick and changed nothing.
|
|
119
|
+
const reconcileMs = options.reconcileMs ?? PR_STATUS_TTL_MS;
|
|
120
|
+
let attentionDirty = true;
|
|
121
|
+
let lastReconcileMs = 0;
|
|
122
|
+
const markAttentionDirty = () => { attentionDirty = true; };
|
|
123
|
+
const stopAttentionWatch = watchAttentionStores(markAttentionDirty);
|
|
85
124
|
const activityTimer = setInterval(() => {
|
|
86
125
|
pending = pending.then(async () => {
|
|
87
|
-
const
|
|
126
|
+
const nowMs = Date.now();
|
|
127
|
+
const events = activity.read(activityCursor + 1, nowMs).reverse();
|
|
88
128
|
for (const event of events) {
|
|
89
129
|
activityCursor = Math.max(activityCursor, Date.parse(event.ts));
|
|
90
130
|
options.emit(state.emit({ type: 'activity.append', scope: options.scope, event }));
|
|
91
131
|
}
|
|
132
|
+
if (!attentionDirty && nowMs - lastReconcileMs < reconcileMs)
|
|
133
|
+
return;
|
|
134
|
+
attentionDirty = false;
|
|
135
|
+
lastReconcileMs = nowMs;
|
|
92
136
|
await reconcileRows();
|
|
93
137
|
});
|
|
94
138
|
}, options.activityPollMs ?? 500);
|
|
95
|
-
const stopActivity = () => clearInterval(activityTimer);
|
|
139
|
+
const stopActivity = () => { clearInterval(activityTimer); stopAttentionWatch(); activity.close(); };
|
|
96
140
|
options.signal.addEventListener('abort', stopActivity, { once: true });
|
|
97
141
|
try {
|
|
98
|
-
await watchLocalSessions({ scope: options.scope, signal: options.signal, emit: (event) => {
|
|
142
|
+
await watchLocalSessions({ ...options.sessions, scope: options.scope, signal: options.signal, emit: (event) => {
|
|
99
143
|
if (event.type === 'reset') {
|
|
100
144
|
agents.clear();
|
|
101
145
|
for (const row of event.rows)
|
|
@@ -156,35 +200,27 @@ export async function watchFleetFeed(options) {
|
|
|
156
200
|
}
|
|
157
201
|
const self = machineId();
|
|
158
202
|
const peers = Object.values(devices).filter((device) => isDialableDevice(device) && normalizeHost(device.name) !== self && ['windows', 'linux', 'macos'].includes(device.platform));
|
|
159
|
-
const tasks = peers.map(
|
|
203
|
+
const tasks = peers.map((device) => {
|
|
160
204
|
const scope = normalizeHost(device.name);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const stop = () => child.kill('SIGTERM');
|
|
172
|
-
options.signal.addEventListener('abort', stop, { once: true });
|
|
173
|
-
const reader = createInterface({ input: child.stdout });
|
|
174
|
-
reader.on('line', (line) => { try {
|
|
175
|
-
const event = JSON.parse(line);
|
|
176
|
-
if (event.v === 1)
|
|
205
|
+
return streamFromPeer({
|
|
206
|
+
device,
|
|
207
|
+
signal: options.signal,
|
|
208
|
+
command: remoteFeedWatchCommand(device.platform),
|
|
209
|
+
backoffBaseMs: options.reconnectMs,
|
|
210
|
+
onLine: (line) => {
|
|
211
|
+
try {
|
|
212
|
+
const event = JSON.parse(line);
|
|
213
|
+
if (event.v !== 1)
|
|
214
|
+
return false;
|
|
177
215
|
forward(event);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await new Promise((resolve) => { const timer = setTimeout(resolve, options.reconnectMs ?? 2_000); options.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true }); });
|
|
187
|
-
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false; /* protocol only */
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
onUnavailable: (reason) => options.emit(coordinator.emit({ type: 'scope', capturedAt: Date.now(), scope, status: 'unavailable', reason })),
|
|
223
|
+
});
|
|
188
224
|
});
|
|
189
225
|
await Promise.all([local, ...tasks]);
|
|
190
226
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import type { DeviceProfile } from '../devices/registry.js';
|
|
11
|
-
import {
|
|
11
|
+
import type { RemoteBackend } from '../secrets-types.js';
|
|
12
12
|
import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
|
|
13
13
|
/** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
|
|
14
14
|
export declare function agentIdOf(spec: string): string;
|
|
@@ -149,7 +149,7 @@ export interface ExecContext {
|
|
|
149
149
|
dryRun?: boolean;
|
|
150
150
|
}
|
|
151
151
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
152
|
-
export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): DeviceApplyResult
|
|
152
|
+
export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): Promise<DeviceApplyResult>;
|
|
153
153
|
/** Run a pool of async tasks with a concurrency cap, preserving input order. */
|
|
154
154
|
export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
155
155
|
/** Reconcile every device row in parallel (capped). */
|
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import * as os from 'os';
|
|
11
|
-
import { pushBundleToHost } from '../secrets
|
|
12
|
-
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets
|
|
11
|
+
import { pushBundleToHost } from '../secrets-client.js';
|
|
12
|
+
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets-policy.js';
|
|
13
13
|
import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
|
|
14
14
|
import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
|
|
15
15
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
@@ -371,7 +371,7 @@ export function parseRemoteBundles(stdout) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
374
|
-
export function reconcileDevice(row, device, ctx) {
|
|
374
|
+
export async function reconcileDevice(row, device, ctx) {
|
|
375
375
|
if (!row.probe.reachable) {
|
|
376
376
|
return { device: row.device, ok: false, steps: [], note: row.probe.note ?? 'unreachable' };
|
|
377
377
|
}
|
|
@@ -438,7 +438,7 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
438
438
|
}
|
|
439
439
|
const backend = isReservedBundleName(bundle) || device.platform === 'linux' ? 'file' : 'keychain';
|
|
440
440
|
try {
|
|
441
|
-
const out = pushBundleToHost(bundle, target, {
|
|
441
|
+
const out = await pushBundleToHost(bundle, target, {
|
|
442
442
|
remoteBackend: backend,
|
|
443
443
|
operation: `fleet apply ${row.device}`,
|
|
444
444
|
// No passphrase, ever, from this path. On the file backend the remote
|
|
@@ -138,9 +138,10 @@ export const FLEET_LOGIN_FLOWS = {
|
|
|
138
138
|
// claude: interactive `/login` inside the TUI is a loopback browser flow, and
|
|
139
139
|
// the token is keychain-bound on macOS. That native login stays per-box and
|
|
140
140
|
// is never copied. The shareable `claude setup-token` mint is a different
|
|
141
|
-
// credential class —
|
|
142
|
-
// (`lib/auth-mint.ts`), not as a
|
|
143
|
-
// is account-scoped, not
|
|
141
|
+
// credential class — minted by `agents accounts add claude [name]` /
|
|
142
|
+
// `agents accounts login claude#<name>` (`lib/auth-mint.ts`), not as a
|
|
143
|
+
// fleet-login device-code flow (a setup-token is account-scoped, not
|
|
144
|
+
// per-machine).
|
|
144
145
|
claude: {
|
|
145
146
|
loginCommand: 'claude',
|
|
146
147
|
flowType: 'loopback',
|
|
@@ -267,15 +267,12 @@ async function performFleetSharedRepoSync(root, device, timeoutMs) {
|
|
|
267
267
|
}
|
|
268
268
|
const ownedFile = fleetSharedStatePath(device, root);
|
|
269
269
|
const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
|
|
270
|
-
// The central `agents.yaml` is fleet-shared state too —
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// rows instead of stashing them, which is also what `accounts label --help`
|
|
277
|
-
// already promises ("labels live on the central account rows in agents.yaml,
|
|
278
|
-
// which repo push/pull already syncs fleet-wide").
|
|
270
|
+
// The central `agents.yaml` is fleet-shared state too — native account rows
|
|
271
|
+
// land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
|
|
272
|
+
// commit. Publishing only the per-device file and then `rebase --autostash`-ing
|
|
273
|
+
// over a dirty central file silently destroyed those names: every box lost its
|
|
274
|
+
// account names on its next daemon publish (PHNX-3887). Commit it alongside the
|
|
275
|
+
// device doc so the rebase carries the rows instead of stashing them.
|
|
279
276
|
const centralFile = path.join(root, 'agents.yaml');
|
|
280
277
|
const publishPaths = [relativeOwnedFile];
|
|
281
278
|
if (fs.existsSync(centralFile))
|
|
@@ -34,6 +34,12 @@ export interface SessionMirrorRow {
|
|
|
34
34
|
summaryChecklist?: import('./session/types.js').SessionChecklistItem[];
|
|
35
35
|
/** Lifecycle of the daemon-computed summary (PHNX-3939). */
|
|
36
36
|
summaryState?: import('./session/types.js').SummaryState;
|
|
37
|
+
/** Tidied latest user turn, so a peer row shows what the agent was asked (PHNX-3939). */
|
|
38
|
+
request?: import('./session/types.js').SessionRequest;
|
|
39
|
+
/** Bounded narration-anchored steps, so a peer row shows what the agent did. */
|
|
40
|
+
timeline?: import('./session/types.js').SessionTimeline;
|
|
41
|
+
/** Bounded file-change list for the peer row. */
|
|
42
|
+
files?: import('./session/types.js').SessionFiles;
|
|
37
43
|
capturedAt: number;
|
|
38
44
|
}
|
|
39
45
|
export interface FleetSharedDeviceState {
|
|
@@ -150,16 +150,16 @@ export interface HarnessAdapter {
|
|
|
150
150
|
routineModeArgs?(cmd: string[], ctx: RoutineLaunchCtx): void;
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
* per-harness branch in
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
153
|
+
* Config-dir env keys a harness pins to its slot / version home. Every
|
|
154
|
+
* per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
|
|
155
|
+
* slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
|
|
156
|
+
* OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
|
|
157
|
+
* parent agent's pin leak into a sibling slot.
|
|
158
158
|
*/
|
|
159
|
-
export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME"];
|
|
159
|
+
export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME", "GROK_HOME", "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME"];
|
|
160
160
|
/**
|
|
161
161
|
* Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
|
|
162
|
-
* (the default / no-config-dir harness) deletes all
|
|
162
|
+
* (the default / no-config-dir harness) deletes all of them — the old `else` arm.
|
|
163
163
|
*/
|
|
164
164
|
export declare function stripForeignConfigDir(result: NodeJS.ProcessEnv, keep?: readonly string[]): void;
|
|
165
165
|
export declare function registerHarnessAdapter(adapter: HarnessAdapter): void;
|