@phnx-labs/agents-cli 1.22.66 → 1.22.68

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +7 -2
  3. package/dist/bootstrap.js +3 -3
  4. package/dist/commands/browser.js +46 -21
  5. package/dist/commands/daemon-test-harness.d.ts +1 -1
  6. package/dist/commands/daemon-test-harness.js +2 -2
  7. package/dist/commands/daemon.js +21 -22
  8. package/dist/commands/exec.js +40 -0
  9. package/dist/commands/monitors.js +2 -2
  10. package/dist/commands/repo.js +57 -19
  11. package/dist/commands/resume.d.ts +16 -0
  12. package/dist/commands/resume.js +41 -8
  13. package/dist/commands/routines.js +42 -21
  14. package/dist/commands/sessions-optimize.js +1 -1
  15. package/dist/commands/sessions.js +4 -11
  16. package/dist/commands/share.d.ts +5 -3
  17. package/dist/commands/share.js +33 -9
  18. package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
  19. package/dist/lib/accounting/account-pool-collect.js +6 -4
  20. package/dist/lib/accounting/usage-ingest.js +4 -2
  21. package/dist/lib/accounting/usage-sync.d.ts +38 -93
  22. package/dist/lib/accounting/usage-sync.js +66 -210
  23. package/dist/lib/accounting/usage.d.ts +11 -3
  24. package/dist/lib/accounting/usage.js +23 -10
  25. package/dist/lib/auth-health.d.ts +8 -0
  26. package/dist/lib/auth-health.js +4 -4
  27. package/dist/lib/browser/caller-identity.d.ts +12 -0
  28. package/dist/lib/browser/caller-identity.js +19 -0
  29. package/dist/lib/browser/ipc.d.ts +37 -32
  30. package/dist/lib/browser/ipc.js +146 -94
  31. package/dist/lib/browser/task-index.d.ts +10 -2
  32. package/dist/lib/browser/task-index.js +22 -3
  33. package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
  34. package/dist/lib/daemon/account-state-daemon-service.js +81 -18
  35. package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
  36. package/dist/lib/daemon/auth-sync-service.js +17 -6
  37. package/dist/lib/daemon/catchup-service.d.ts +51 -0
  38. package/dist/lib/daemon/catchup-service.js +51 -0
  39. package/dist/lib/daemon/daemon.d.ts +12 -22
  40. package/dist/lib/daemon/daemon.js +463 -176
  41. package/dist/lib/daemon/runner.js +2 -0
  42. package/dist/lib/daemon/service.d.ts +22 -4
  43. package/dist/lib/daemon/service.js +2 -2
  44. package/dist/lib/daemon/supervisor.d.ts +55 -15
  45. package/dist/lib/daemon/supervisor.js +119 -29
  46. package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
  47. package/dist/lib/daemon/usage-sync-service.js +22 -18
  48. package/dist/lib/daemon-health.js +36 -31
  49. package/dist/lib/daemon-services.d.ts +1 -1
  50. package/dist/lib/daemon-services.js +12 -2
  51. package/dist/lib/daemon-ticks.d.ts +9 -6
  52. package/dist/lib/daemon-ticks.js +14 -8
  53. package/dist/lib/devices/registry.js +2 -0
  54. package/dist/lib/exec.js +11 -0
  55. package/dist/lib/feed/watch.d.ts +3 -0
  56. package/dist/lib/feed/watch.js +13 -3
  57. package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
  58. package/dist/lib/fleet-shared-repo-sync.js +333 -0
  59. package/dist/lib/fleet-shared-state.d.ts +38 -0
  60. package/dist/lib/fleet-shared-state.js +105 -0
  61. package/dist/lib/hosts/remote-cmd.d.ts +2 -0
  62. package/dist/lib/hosts/remote-cmd.js +12 -3
  63. package/dist/lib/lock-compromise.d.ts +8 -0
  64. package/dist/lib/lock-compromise.js +12 -0
  65. package/dist/lib/monitors/engine.d.ts +2 -1
  66. package/dist/lib/monitors/engine.js +9 -2
  67. package/dist/lib/refresh-coordinator.js +2 -0
  68. package/dist/lib/run-trace-sync.d.ts +13 -0
  69. package/dist/lib/run-trace-sync.js +77 -0
  70. package/dist/lib/secrets/push.d.ts +10 -0
  71. package/dist/lib/secrets/push.js +86 -7
  72. package/dist/lib/secrets/remote.d.ts +18 -6
  73. package/dist/lib/secrets/remote.js +29 -4
  74. package/dist/lib/secrets/reserved-sync.d.ts +28 -27
  75. package/dist/lib/secrets/reserved-sync.js +119 -101
  76. package/dist/lib/session/active.d.ts +3 -1
  77. package/dist/lib/session/active.js +2 -0
  78. package/dist/lib/session/actor-sidecar.d.ts +12 -0
  79. package/dist/lib/session/actor-sidecar.js +2 -0
  80. package/dist/lib/session/db.d.ts +5 -2
  81. package/dist/lib/session/db.js +54 -22
  82. package/dist/lib/session/discover.d.ts +12 -3
  83. package/dist/lib/session/discover.js +106 -25
  84. package/dist/lib/session/prompt.d.ts +11 -0
  85. package/dist/lib/session/prompt.js +25 -0
  86. package/dist/lib/session/recovery.d.ts +30 -2
  87. package/dist/lib/session/recovery.js +56 -4
  88. package/dist/lib/session/remote/remote-list.d.ts +17 -1
  89. package/dist/lib/session/remote/remote-list.js +29 -4
  90. package/dist/lib/session/remote/watch.d.ts +25 -2
  91. package/dist/lib/session/remote/watch.js +185 -11
  92. package/dist/lib/session/session-cache.d.ts +2 -1
  93. package/dist/lib/session/session-cache.js +1 -0
  94. package/dist/lib/session/types.d.ts +2 -0
  95. package/dist/lib/share/publish.d.ts +55 -9
  96. package/dist/lib/share/publish.js +87 -9
  97. package/dist/lib/share/worker-template.js +100 -8
  98. package/dist/lib/ssh-exec.d.ts +2 -0
  99. package/dist/lib/ssh-exec.js +20 -4
  100. package/dist/lib/teams/registry.js +2 -0
  101. package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
  102. package/dist/lib/testdata/daemon-health-writer.js +8 -0
  103. package/dist/lib/traces/sync.d.ts +7 -0
  104. package/dist/lib/traces/sync.js +9 -0
  105. package/dist/lib/usage-refresh.d.ts +8 -2
  106. package/dist/lib/usage-refresh.js +3 -3
  107. package/package.json +1 -1
  108. package/dist/lib/account-state-service.d.ts +0 -21
  109. package/dist/lib/account-state-service.js +0 -60
@@ -23,6 +23,7 @@
23
23
  import * as fs from 'fs';
24
24
  import * as path from 'path';
25
25
  import { getDaemonDir } from './state.js';
26
+ import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
26
27
  const HEALTH_FILE = 'health.json';
27
28
  /** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
28
29
  export const SUBSYSTEM_SECRETS_BROKER = 'secrets-broker';
@@ -61,15 +62,19 @@ function readAll() {
61
62
  * dropped health update, never escape as an unhandled rejection that would
62
63
  * hit the process-wide handler and take down every OTHER service too.
63
64
  */
64
- function writeAll(records) {
65
+ function updateAll(update) {
65
66
  try {
66
67
  const healthPath = getHealthPath();
67
- fs.mkdirSync(path.dirname(healthPath), { recursive: true });
68
- fs.writeFileSync(healthPath, JSON.stringify(records), 'utf-8');
69
- try {
70
- fs.chmodSync(healthPath, 0o600);
71
- }
72
- catch { /* best effort */ }
68
+ ensureLockTarget(healthPath, '{}');
69
+ withFileLock(healthPath, () => {
70
+ const records = readAll();
71
+ update(records);
72
+ atomicWriteFileSync(healthPath, JSON.stringify(records), { encoding: 'utf-8', mode: 0o600 });
73
+ try {
74
+ fs.chmodSync(healthPath, 0o600);
75
+ }
76
+ catch { /* best effort */ }
77
+ });
73
78
  }
74
79
  catch { /* see docblock above — health recording must never crash a caller */ }
75
80
  }
@@ -78,23 +83,23 @@ function blankRecord(subsystem) {
78
83
  }
79
84
  /** Record a successful subsystem check-in — clears the failure streak. */
80
85
  export function recordSubsystemOk(subsystem, at = new Date().toISOString()) {
81
- const all = readAll();
82
- const existing = all[subsystem] ?? blankRecord(subsystem);
83
- all[subsystem] = { ...existing, subsystem, consecutiveFailures: 0, lastOkAt: at };
84
- writeAll(all);
86
+ updateAll((all) => {
87
+ const existing = all[subsystem] ?? blankRecord(subsystem);
88
+ all[subsystem] = { ...existing, subsystem, consecutiveFailures: 0, lastOkAt: at };
89
+ });
85
90
  }
86
91
  /** Record a subsystem failure — bumps the consecutive-failure streak. */
87
92
  export function recordSubsystemError(subsystem, error, at = new Date().toISOString()) {
88
- const all = readAll();
89
- const existing = all[subsystem] ?? blankRecord(subsystem);
90
- all[subsystem] = {
91
- ...existing,
92
- subsystem,
93
- lastError: error,
94
- lastErrorAt: at,
95
- consecutiveFailures: existing.consecutiveFailures + 1,
96
- };
97
- writeAll(all);
93
+ updateAll((all) => {
94
+ const existing = all[subsystem] ?? blankRecord(subsystem);
95
+ all[subsystem] = {
96
+ ...existing,
97
+ subsystem,
98
+ lastError: error,
99
+ lastErrorAt: at,
100
+ consecutiveFailures: existing.consecutiveFailures + 1,
101
+ };
102
+ });
98
103
  }
99
104
  /**
100
105
  * Refine the reason on an already-counted failure, without bumping the streak.
@@ -109,12 +114,12 @@ export function recordSubsystemError(subsystem, error, at = new Date().toISOStri
109
114
  * subsystem is left alone rather than given a blank record to decorate.
110
115
  */
111
116
  export function recordSubsystemErrorReason(subsystem, error, at = new Date().toISOString()) {
112
- const all = readAll();
113
- const existing = all[subsystem];
114
- if (!existing)
115
- return;
116
- all[subsystem] = { ...existing, lastError: error, lastErrorAt: at };
117
- writeAll(all);
117
+ updateAll((all) => {
118
+ const existing = all[subsystem];
119
+ if (!existing)
120
+ return;
121
+ all[subsystem] = { ...existing, lastError: error, lastErrorAt: at };
122
+ });
118
123
  }
119
124
  /**
120
125
  * Record a `ServiceSupervisor` lifecycle-state transition, without touching
@@ -123,10 +128,10 @@ export function recordSubsystemErrorReason(subsystem, error, at = new Date().toI
123
128
  * status` runs as a separate process from the daemon (see module docblock).
124
129
  */
125
130
  export function recordSubsystemState(subsystem, state) {
126
- const all = readAll();
127
- const existing = all[subsystem] ?? blankRecord(subsystem);
128
- all[subsystem] = { ...existing, subsystem, state };
129
- writeAll(all);
131
+ updateAll((all) => {
132
+ const existing = all[subsystem] ?? blankRecord(subsystem);
133
+ all[subsystem] = { ...existing, subsystem, state };
134
+ });
130
135
  }
131
136
  /** Read one subsystem's health record, or null if it has never reported in. */
132
137
  export function readSubsystemHealth(subsystem) {
@@ -7,7 +7,7 @@
7
7
  * enabled without pulling in the whole daemon lifecycle.
8
8
  */
9
9
  /** Every service the daemon can host. IDs are kebab-case and stable. */
10
- export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
10
+ export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
11
11
  /** Human-readable metadata for each service. */
12
12
  export interface DaemonServiceDef {
13
13
  id: DaemonServiceId;
@@ -22,6 +22,11 @@ export const DAEMON_SERVICES = [
22
22
  title: 'Routine scheduler',
23
23
  description: 'Fires cron-scheduled routines and catches up missed fires.',
24
24
  },
25
+ {
26
+ id: 'catchup',
27
+ title: 'Catch-up recovery',
28
+ description: 'Supervised pass that re-runs routines whose scheduled fire this device missed (sleep, wedge, or suspend). No-ops while the scheduler gate is off.',
29
+ },
25
30
  {
26
31
  id: 'monitors',
27
32
  title: 'Monitor engine',
@@ -49,8 +54,13 @@ export const DAEMON_SERVICES = [
49
54
  },
50
55
  {
51
56
  id: 'account-state',
52
- title: 'Account state',
53
- description: 'Refreshes account quota/usage and publishes the local fleet-status row.',
57
+ title: 'Account usage refresh',
58
+ description: 'Refreshes account quota/usage on its own tick (PHNX-3608: independent circuit breaker from account-auth).',
59
+ },
60
+ {
61
+ id: 'account-auth',
62
+ title: 'Account auth refresh',
63
+ description: 'Publishes this host\'s fleet-status row and refreshes auth health on its own slower tick (PHNX-3608: independent circuit breaker from account-state).',
54
64
  },
55
65
  {
56
66
  id: 'watchdog',
@@ -2,10 +2,12 @@
2
2
  * Daemon account-state tick bodies.
3
3
  *
4
4
  * These two bodies are the `refreshUsage` / `refreshAuth` implementations the
5
- * daemon's `account-state-service.ts` timers call directly, in-process, on a
6
- * plain `setInterval` (usage every 60s, auth every ~3 min). They are NOT
7
- * routines and are never fired through the scheduler — the daemon owns usage and
8
- * authentication health as first-party device state (RUSH-2451).
5
+ * supervised `AccountStateDaemonService` (daemon/account-state-daemon-service.ts)
6
+ * runs on its tick, in-process (usage every tick, auth on the slower ~3 min
7
+ * cadence). They are NOT routines and are never fired through the scheduler — the
8
+ * daemon owns usage and authentication health as first-party device state
9
+ * (RUSH-2451); the supervisor bounds each tick with a deadline + AbortSignal so a
10
+ * hung refresh is abandoned and restarted instead of latching (PHNX-3608).
9
11
  *
10
12
  * `refreshLocalFleetAuthState` is also called by `agents fleet`/`ssh` surfaces
11
13
  * that need a fresh local auth snapshot on demand; provider-level work is guarded
@@ -59,11 +61,12 @@ export declare function shouldReuseCachedAuthProbe(force: boolean, cached: AuthP
59
61
  */
60
62
  export declare function refreshLocalFleetAuthState(opts?: {
61
63
  force?: boolean;
64
+ signal?: AbortSignal;
62
65
  }): Promise<{
63
66
  row: FleetStatusRow;
64
67
  authRows: import('./auth-health.js').AuthProbeRow[];
65
68
  }>;
66
- export declare function runFleetCacheWarmTick(): Promise<void>;
69
+ export declare function runFleetCacheWarmTick(signal?: AbortSignal): Promise<void>;
67
70
  /**
68
71
  * Usage refresh: keep the usage cache the `agents run` router reads
69
72
  * (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
@@ -71,7 +74,7 @@ export declare function runFleetCacheWarmTick(): Promise<void>;
71
74
  * accounts it holds credentials for, straight from the provider APIs
72
75
  * (RUSH-3193 #15; no cross-host broadcast).
73
76
  */
74
- export declare function runUsageRefreshTick(): Promise<void>;
77
+ export declare function runUsageRefreshTick(signal?: AbortSignal): Promise<void>;
75
78
  /**
76
79
  * Active-sessions warm (RUSH-2062 / RUSH-2484): publish THIS host's live session
77
80
  * rows so `agents sessions watch` (and the extension that tails it) receive
@@ -2,10 +2,12 @@
2
2
  * Daemon account-state tick bodies.
3
3
  *
4
4
  * These two bodies are the `refreshUsage` / `refreshAuth` implementations the
5
- * daemon's `account-state-service.ts` timers call directly, in-process, on a
6
- * plain `setInterval` (usage every 60s, auth every ~3 min). They are NOT
7
- * routines and are never fired through the scheduler — the daemon owns usage and
8
- * authentication health as first-party device state (RUSH-2451).
5
+ * supervised `AccountStateDaemonService` (daemon/account-state-daemon-service.ts)
6
+ * runs on its tick, in-process (usage every tick, auth on the slower ~3 min
7
+ * cadence). They are NOT routines and are never fired through the scheduler — the
8
+ * daemon owns usage and authentication health as first-party device state
9
+ * (RUSH-2451); the supervisor bounds each tick with a deadline + AbortSignal so a
10
+ * hung refresh is abandoned and restarted instead of latching (PHNX-3608).
9
11
  *
10
12
  * `refreshLocalFleetAuthState` is also called by `agents fleet`/`ssh` surfaces
11
13
  * that need a fresh local auth snapshot on demand; provider-level work is guarded
@@ -62,6 +64,7 @@ export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_P
62
64
  */
63
65
  export async function refreshLocalFleetAuthState(opts) {
64
66
  const force = opts?.force === true;
67
+ const signal = opts?.signal;
65
68
  const { machineId } = await import('./machine-id.js');
66
69
  const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
67
70
  const { getCliVersion } = await import('./version.js');
@@ -89,7 +92,7 @@ export async function refreshLocalFleetAuthState(opts) {
89
92
  // Fleet status publishes every tick regardless — it does not ride that endpoint.
90
93
  const cached = readFleetAuthRows(self);
91
94
  const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
92
- const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force });
95
+ const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force, signal });
93
96
  if (!reuse)
94
97
  writeFleetAuthRows(self, authRows);
95
98
  const row = await publishLocalFleetStatus(self);
@@ -97,8 +100,8 @@ export async function refreshLocalFleetAuthState(opts) {
97
100
  },
98
101
  });
99
102
  }
100
- export async function runFleetCacheWarmTick() {
101
- const result = await refreshLocalFleetAuthState();
103
+ export async function runFleetCacheWarmTick(signal) {
104
+ const result = await refreshLocalFleetAuthState({ signal });
102
105
  // A waiter receives the already-published fleet row. The auth-row count is
103
106
  // available only to the process that performed the provider probes.
104
107
  const row = result.row;
@@ -112,7 +115,7 @@ export async function runFleetCacheWarmTick() {
112
115
  * accounts it holds credentials for, straight from the provider APIs
113
116
  * (RUSH-3193 #15; no cross-host broadcast).
114
117
  */
115
- export async function runUsageRefreshTick() {
118
+ export async function runUsageRefreshTick(signal) {
116
119
  const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
117
120
  const { writeClaudeUsageCache, readClaudeUsageCache } = await import('./accounting/usage.js');
118
121
  const { usageRateLimitedUntil } = await import('./usage-backoff.js');
@@ -124,6 +127,9 @@ export async function runUsageRefreshTick() {
124
127
  // so a recent capture means the account is already fresh at zero API cost —
125
128
  // the refresher re-derives headroom from it and skips the API fetch.
126
129
  readCachedSnapshot: (usageKey) => readClaudeUsageCache(usageKey),
130
+ // Thread the supervisor deadline into each provider fetch so the tick's I/O
131
+ // is bounded by deadlineMs, not just each fetch's own 5s timeout (PHNX-3608).
132
+ signal,
127
133
  });
128
134
  const { listProfiles } = await import('./profiles.js');
129
135
  const { refreshDueByokUsage } = await import('./byok-usage.js');
@@ -21,6 +21,7 @@ import { getDevicesRegistryPath, readMeta, updateMeta } from '../state.js';
21
21
  import { atomicWriteJsonSync } from '../fs-atomic.js';
22
22
  import { machineId } from '../machine-id.js';
23
23
  import { addIgnoredEntry, unionDeviceIgnored } from './device-docs.js';
24
+ import { logAndContinueOnLockCompromised } from '../lock-compromise.js';
24
25
  /**
25
26
  * Whether a fan-out should dial this device, honouring the preference stated on
26
27
  * {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
@@ -141,6 +142,7 @@ async function withRegistryLock(p, fn) {
141
142
  const release = await lockfile.lock(p, {
142
143
  retries: { retries: 60, minTimeout: 25, maxTimeout: 250, factor: 1.5 },
143
144
  stale: 10_000,
145
+ onCompromised: logAndContinueOnLockCompromised('devices registry'),
144
146
  });
145
147
  try {
146
148
  return await fn();
package/dist/lib/exec.js CHANGED
@@ -409,6 +409,17 @@ export function buildExecEnv(options) {
409
409
  // re-parsing harness-specific transcripts.
410
410
  result.AGENTS_RUN_MODE = resolveHeadlessMode(options.agent, normalizeMode(options.mode), resolveInteractive(options), options.modeWarningContext, options.modeWarningState);
411
411
  result.AGENTS_HISTORY_DIR = getHistoryDir();
412
+ // Durable origin version for a later native resume. The SessionStart hook joins
413
+ // this to the harness's real session id (like AGENTS_RUN_MODE), so recovery can
414
+ // pin the exact origin version even when the transcript carries no derivable one
415
+ // (codex's `.codex-homes/<version>/` home) — the "version not recorded" fallback
416
+ // to `/continue` this closes (PHNX-3626). Resolve the concrete version-home id
417
+ // the same way the spawn path does; only stamp a real one.
418
+ if (options.agent) {
419
+ const runVersion = options.version ?? resolveVersion(options.agent, options.cwd || process.cwd());
420
+ if (runVersion)
421
+ result.AGENTS_RUN_VERSION = runVersion;
422
+ }
412
423
  // So activity / feed posts stamp the right harness without re-detecting.
413
424
  // A custom-harness run (`agents run deepseek`) must stamp the PROFILE name,
414
425
  // not the host CLI (`claude`) — otherwise sessions and feed posts cannot
@@ -18,6 +18,9 @@ export type FeedWatchEnvelope = Base & {
18
18
  type: 'agent.upsert';
19
19
  rowKey: string;
20
20
  agent: SessionWatchRow;
21
+ } | Base & {
22
+ type: 'agent.remove';
23
+ rowKey: string;
21
24
  } | Base & {
22
25
  type: 'attention.upsert';
23
26
  rowKey: string;
@@ -21,13 +21,20 @@ export class FeedWatchState {
21
21
  }
22
22
  }
23
23
  async function attentionFor(agent) {
24
- if (!agent.sessionId)
24
+ // Durable Previous rows share the operator stream for Sessions history, but
25
+ // they are not live work and must never synthesize Needs-you attention.
26
+ if (!agent.sessionId || agent.previous || agent.context === 'recent')
25
27
  return undefined;
26
28
  const blockId = blockIdForSession(agent.sessionId);
27
29
  // ActiveSession.host names the terminal app; the feed contract's host is the
28
30
  // device scope. Normalize only the reconciler input so lifecycle/PR keys are
29
31
  // routable across the fleet while the projected agent row stays compatible.
30
- const session = { ...agent, host: agent.sourceDevice, viewingIn: undefined };
32
+ const session = {
33
+ ...agent,
34
+ context: agent.context,
35
+ host: agent.sourceDevice,
36
+ viewingIn: undefined,
37
+ };
31
38
  return reconcileAttention({
32
39
  block: readBlock(blockId), session,
33
40
  resolution: readResolution(blockId),
@@ -49,7 +56,10 @@ export async function projectSessionEnvelope(event, state) {
49
56
  ];
50
57
  }
51
58
  if (event.type === 'remove')
52
- return [state.emit({ type: 'attention.remove', scope: event.scope, rowKey: event.rowKey })];
59
+ return [
60
+ state.emit({ type: 'agent.remove', scope: event.scope, rowKey: event.rowKey }),
61
+ state.emit({ type: 'attention.remove', scope: event.scope, rowKey: event.rowKey }),
62
+ ];
53
63
  if (event.type === 'scope')
54
64
  return [state.emit({ type: 'scope', capturedAt: event.capturedAt, scope: event.scope, status: event.status, ...(event.reason ? { reason: event.reason } : {}) })];
55
65
  return [state.emit({ type: 'heartbeat', capturedAt: event.capturedAt, scope: event.scope })];
@@ -0,0 +1,36 @@
1
+ export declare const FLEET_SHARED_REPO_SYNC_DEADLINE_MS = 45000;
2
+ export declare const FLEET_SHARED_REPO_KILL_GRACE_MS = 250;
3
+ export interface BoundedProcessResult {
4
+ code: number | null;
5
+ stdout: string;
6
+ stderr: string;
7
+ timedOut: boolean;
8
+ }
9
+ export interface FleetSharedRepoSyncOptions {
10
+ userAgentsDir?: string;
11
+ device?: string;
12
+ timeoutMs?: number;
13
+ /** Test-only redirection for the untracked cross-process lock target. */
14
+ lockPath?: string;
15
+ }
16
+ export interface FleetSharedRepoSyncResult {
17
+ success: boolean;
18
+ committed: boolean;
19
+ pushed: boolean;
20
+ commit: string | null;
21
+ timedOut: boolean;
22
+ skipped: string | null;
23
+ error: string | null;
24
+ }
25
+ /** Run one real process asynchronously with a hard wall-clock/process-tree bound. */
26
+ export declare function runBoundedProcess(command: string, args: string[], options: {
27
+ cwd: string;
28
+ timeoutMs: number;
29
+ env?: NodeJS.ProcessEnv;
30
+ }): Promise<BoundedProcessResult>;
31
+ /**
32
+ * Commit/pull/push this device's shared state under one cross-process lock.
33
+ * Callers publish their local fields first and consume peer fields only after
34
+ * this resolves successfully.
35
+ */
36
+ export declare function syncFleetSharedStateRepo(options?: FleetSharedRepoSyncOptions): Promise<FleetSharedRepoSyncResult>;