@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.
Files changed (161) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +21 -8
  3. package/dist/bootstrap.js +6 -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 +50 -0
  9. package/dist/commands/feed.js +20 -7
  10. package/dist/commands/monitors.js +5 -2
  11. package/dist/commands/projects.d.ts +26 -6
  12. package/dist/commands/projects.js +55 -22
  13. package/dist/commands/repo.js +57 -19
  14. package/dist/commands/resume.d.ts +16 -0
  15. package/dist/commands/resume.js +41 -8
  16. package/dist/commands/routines.js +42 -21
  17. package/dist/commands/send.js +29 -2
  18. package/dist/commands/sessions-inject.d.ts +58 -0
  19. package/dist/commands/sessions-inject.js +143 -7
  20. package/dist/commands/sessions-optimize.js +1 -1
  21. package/dist/commands/sessions-picker.js +1 -0
  22. package/dist/commands/sessions.js +4 -11
  23. package/dist/commands/share.d.ts +5 -3
  24. package/dist/commands/share.js +73 -20
  25. package/dist/commands/ssh.js +205 -2
  26. package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
  27. package/dist/lib/accounting/account-pool-collect.js +6 -4
  28. package/dist/lib/accounting/usage-ingest.js +4 -2
  29. package/dist/lib/accounting/usage-sync.d.ts +38 -93
  30. package/dist/lib/accounting/usage-sync.js +66 -210
  31. package/dist/lib/accounting/usage.d.ts +18 -5
  32. package/dist/lib/accounting/usage.js +165 -20
  33. package/dist/lib/auth-health.d.ts +8 -0
  34. package/dist/lib/auth-health.js +4 -4
  35. package/dist/lib/boot-profile.d.ts +14 -0
  36. package/dist/lib/boot-profile.js +66 -0
  37. package/dist/lib/browser/caller-identity.d.ts +12 -0
  38. package/dist/lib/browser/caller-identity.js +19 -0
  39. package/dist/lib/browser/ipc.d.ts +37 -32
  40. package/dist/lib/browser/ipc.js +146 -94
  41. package/dist/lib/browser/task-index.d.ts +10 -2
  42. package/dist/lib/browser/task-index.js +22 -3
  43. package/dist/lib/channels/providers/desktop.d.ts +5 -4
  44. package/dist/lib/channels/providers/desktop.js +5 -4
  45. package/dist/lib/claude-account-token.js +108 -4
  46. package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
  47. package/dist/lib/daemon/account-state-daemon-service.js +81 -18
  48. package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
  49. package/dist/lib/daemon/auth-sync-service.js +17 -6
  50. package/dist/lib/daemon/catchup-service.d.ts +51 -0
  51. package/dist/lib/daemon/catchup-service.js +51 -0
  52. package/dist/lib/daemon/daemon.d.ts +12 -22
  53. package/dist/lib/daemon/daemon.js +463 -176
  54. package/dist/lib/daemon/runner.js +2 -0
  55. package/dist/lib/daemon/service.d.ts +22 -4
  56. package/dist/lib/daemon/service.js +2 -2
  57. package/dist/lib/daemon/supervisor.d.ts +55 -15
  58. package/dist/lib/daemon/supervisor.js +119 -29
  59. package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
  60. package/dist/lib/daemon/usage-sync-service.js +22 -18
  61. package/dist/lib/daemon-health.js +36 -31
  62. package/dist/lib/daemon-services.d.ts +1 -1
  63. package/dist/lib/daemon-services.js +12 -2
  64. package/dist/lib/daemon-ticks.d.ts +9 -6
  65. package/dist/lib/daemon-ticks.js +14 -8
  66. package/dist/lib/devices/health.d.ts +38 -2
  67. package/dist/lib/devices/health.js +43 -5
  68. package/dist/lib/devices/registry.js +2 -0
  69. package/dist/lib/devices/worker-pick.d.ts +1 -1
  70. package/dist/lib/devices/worker-pick.js +4 -1
  71. package/dist/lib/exec.js +15 -0
  72. package/dist/lib/feed/watch.d.ts +3 -0
  73. package/dist/lib/feed/watch.js +13 -3
  74. package/dist/lib/feed-broadcast.d.ts +64 -5
  75. package/dist/lib/feed-broadcast.js +124 -22
  76. package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
  77. package/dist/lib/fleet-shared-repo-sync.js +333 -0
  78. package/dist/lib/fleet-shared-state.d.ts +38 -0
  79. package/dist/lib/fleet-shared-state.js +105 -0
  80. package/dist/lib/hosts/remote-cmd.d.ts +2 -0
  81. package/dist/lib/hosts/remote-cmd.js +12 -3
  82. package/dist/lib/lock-compromise.d.ts +8 -0
  83. package/dist/lib/lock-compromise.js +12 -0
  84. package/dist/lib/monitors/engine.d.ts +2 -1
  85. package/dist/lib/monitors/engine.js +27 -2
  86. package/dist/lib/monitors/sources/command.js +13 -3
  87. package/dist/lib/monitors/sources/failure.d.ts +32 -0
  88. package/dist/lib/monitors/sources/failure.js +52 -0
  89. package/dist/lib/monitors/sources/types.d.ts +9 -0
  90. package/dist/lib/owner-message.d.ts +12 -0
  91. package/dist/lib/owner-message.js +44 -0
  92. package/dist/lib/refresh-coordinator.js +2 -0
  93. package/dist/lib/run-trace-sync.d.ts +28 -0
  94. package/dist/lib/run-trace-sync.js +99 -0
  95. package/dist/lib/secrets/filestore.d.ts +4 -0
  96. package/dist/lib/secrets/filestore.js +164 -3
  97. package/dist/lib/secrets/push.d.ts +10 -0
  98. package/dist/lib/secrets/push.js +86 -7
  99. package/dist/lib/secrets/remote.d.ts +18 -6
  100. package/dist/lib/secrets/remote.js +29 -4
  101. package/dist/lib/secrets/reserved-sync.d.ts +28 -27
  102. package/dist/lib/secrets/reserved-sync.js +119 -101
  103. package/dist/lib/session/active.d.ts +13 -1
  104. package/dist/lib/session/active.js +5 -0
  105. package/dist/lib/session/actor-sidecar.d.ts +12 -0
  106. package/dist/lib/session/actor-sidecar.js +2 -0
  107. package/dist/lib/session/db.d.ts +16 -2
  108. package/dist/lib/session/db.js +73 -22
  109. package/dist/lib/session/discover.d.ts +12 -3
  110. package/dist/lib/session/discover.js +187 -26
  111. package/dist/lib/session/linear.d.ts +13 -0
  112. package/dist/lib/session/linear.js +44 -0
  113. package/dist/lib/session/live-metadata.js +1 -0
  114. package/dist/lib/session/parse.js +2 -3
  115. package/dist/lib/session/prompt.d.ts +17 -0
  116. package/dist/lib/session/prompt.js +35 -0
  117. package/dist/lib/session/recovery.d.ts +43 -6
  118. package/dist/lib/session/recovery.js +80 -10
  119. package/dist/lib/session/remote/remote-list.d.ts +17 -1
  120. package/dist/lib/session/remote/remote-list.js +29 -4
  121. package/dist/lib/session/remote/watch.d.ts +25 -2
  122. package/dist/lib/session/remote/watch.js +188 -11
  123. package/dist/lib/session/session-cache.d.ts +2 -1
  124. package/dist/lib/session/session-cache.js +1 -0
  125. package/dist/lib/session/state.js +11 -13
  126. package/dist/lib/session/types.d.ts +2 -0
  127. package/dist/lib/share/backend.d.ts +2 -2
  128. package/dist/lib/share/backend.js +20 -9
  129. package/dist/lib/share/delete.d.ts +5 -1
  130. package/dist/lib/share/delete.js +7 -2
  131. package/dist/lib/share/http-error.d.ts +52 -0
  132. package/dist/lib/share/http-error.js +65 -0
  133. package/dist/lib/share/publish.d.ts +67 -11
  134. package/dist/lib/share/publish.js +98 -16
  135. package/dist/lib/share/worker-template.js +105 -9
  136. package/dist/lib/smart-launch.js +27 -4
  137. package/dist/lib/ssh-exec.d.ts +2 -0
  138. package/dist/lib/ssh-exec.js +20 -4
  139. package/dist/lib/storage/index.d.ts +14 -0
  140. package/dist/lib/storage/index.js +14 -0
  141. package/dist/lib/storage/selection.d.ts +48 -0
  142. package/dist/lib/storage/selection.js +39 -0
  143. package/dist/lib/storage/visibility.d.ts +82 -0
  144. package/dist/lib/storage/visibility.js +99 -0
  145. package/dist/lib/teams/agents.js +3 -1
  146. package/dist/lib/teams/placement-probe.js +1 -0
  147. package/dist/lib/teams/registry.js +2 -0
  148. package/dist/lib/teams/scheduler.d.ts +8 -1
  149. package/dist/lib/teams/scheduler.js +4 -1
  150. package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
  151. package/dist/lib/testdata/daemon-health-writer.js +8 -0
  152. package/dist/lib/traces/backend.js +13 -2
  153. package/dist/lib/traces/sync.d.ts +7 -0
  154. package/dist/lib/traces/sync.js +9 -0
  155. package/dist/lib/usage-refresh.d.ts +8 -2
  156. package/dist/lib/usage-refresh.js +3 -3
  157. package/dist/lib/worktree/held.d.ts +166 -0
  158. package/dist/lib/worktree/held.js +368 -0
  159. package/package.json +2 -2
  160. package/dist/lib/account-state-service.d.ts +0 -21
  161. package/dist/lib/account-state-service.js +0 -60
@@ -173,6 +173,8 @@ export function controlOpts() {
173
173
  export function sshConnectOpts(mux, hostKeyOpts) {
174
174
  return [...(hostKeyOpts ?? []), ...SSH_OPTS, ...mux];
175
175
  }
176
+ /** Grace after a timed-out child receives SIGTERM before SIGKILL enforces the bound. */
177
+ export const SSH_TIMEOUT_KILL_GRACE_MS = 250;
176
178
  /**
177
179
  * Run `remoteCmd` on `target` over ssh and capture stdout/stderr/exit.
178
180
  *
@@ -228,12 +230,28 @@ export function sshExecAsync(target, remoteCmd, opts = {}) {
228
230
  let stderr = '';
229
231
  let settled = false;
230
232
  let timedOut = false;
233
+ let killTimer = null;
231
234
  const timer = opts.timeoutMs
232
235
  ? setTimeout(() => {
233
236
  timedOut = true;
234
237
  child.kill('SIGTERM');
238
+ // SIGTERM is advisory. A wedged ssh process can ignore it and keep the
239
+ // Promise (and daemon tick) open forever, so enforce a short hard-kill
240
+ // bound. Timeout calls always disable ControlMaster above; killing this
241
+ // direct client therefore tears down the remote connection too.
242
+ killTimer = setTimeout(() => {
243
+ if (!settled)
244
+ child.kill('SIGKILL');
245
+ }, SSH_TIMEOUT_KILL_GRACE_MS);
246
+ killTimer.unref?.();
235
247
  }, opts.timeoutMs)
236
248
  : null;
249
+ const clearTimers = () => {
250
+ if (timer)
251
+ clearTimeout(timer);
252
+ if (killTimer)
253
+ clearTimeout(killTimer);
254
+ };
237
255
  child.stdout.setEncoding('utf-8');
238
256
  child.stderr.setEncoding('utf-8');
239
257
  child.stdout.on('data', (chunk) => { stdout += chunk; });
@@ -251,16 +269,14 @@ export function sshExecAsync(target, remoteCmd, opts = {}) {
251
269
  if (settled)
252
270
  return;
253
271
  settled = true;
254
- if (timer)
255
- clearTimeout(timer);
272
+ clearTimers();
256
273
  resolve({ code: null, stdout, stderr: stderr + err.message, timedOut });
257
274
  });
258
275
  child.on('close', (code) => {
259
276
  if (settled)
260
277
  return;
261
278
  settled = true;
262
- if (timer)
263
- clearTimeout(timer);
279
+ clearTimers();
264
280
  resolve({ code, stdout, stderr, timedOut });
265
281
  });
266
282
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `lib/storage` — the surface-agnostic core the managed-storage surfaces share:
3
+ *
4
+ * - {@link selection} the ONE managed-vs-BYO selection policy.
5
+ * - {@link visibility} the ONE visibility model + product default (`me`).
6
+ *
7
+ * Consume the SELECTION + VISIBILITY POLICY from here; keep each surface's typed,
8
+ * `kind`-tagged backend adapter (endpoint, namespace, covers) in that surface's
9
+ * own module. `agents artifacts share` (`lib/share/`) and `agents traces`
10
+ * (`lib/traces/`) are the two current adapters; the `sessions` sync adapter is
11
+ * the next consumer.
12
+ */
13
+ export { type StorageBackendKind, type StorageSelectionOpts, selectStorageBackendKind, isManagedSelection, } from './selection.js';
14
+ export { type ShareVisibility, type VisibilityFlags, PUBLISH_VISIBILITY_LEVELS, EDITABLE_VISIBILITY_LEVELS, MANAGED_DEFAULT_VISIBILITY, BYO_DEFAULT_VISIBILITY, defaultVisibilityForBackend, explicitVisibility, resolveVisibility, publishVisibility, } from './visibility.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `lib/storage` — the surface-agnostic core the managed-storage surfaces share:
3
+ *
4
+ * - {@link selection} the ONE managed-vs-BYO selection policy.
5
+ * - {@link visibility} the ONE visibility model + product default (`me`).
6
+ *
7
+ * Consume the SELECTION + VISIBILITY POLICY from here; keep each surface's typed,
8
+ * `kind`-tagged backend adapter (endpoint, namespace, covers) in that surface's
9
+ * own module. `agents artifacts share` (`lib/share/`) and `agents traces`
10
+ * (`lib/traces/`) are the two current adapters; the `sessions` sync adapter is
11
+ * the next consumer.
12
+ */
13
+ export { selectStorageBackendKind, isManagedSelection, } from './selection.js';
14
+ export { PUBLISH_VISIBILITY_LEVELS, EDITABLE_VISIBILITY_LEVELS, MANAGED_DEFAULT_VISIBILITY, BYO_DEFAULT_VISIBILITY, defaultVisibilityForBackend, explicitVisibility, resolveVisibility, publishVisibility, } from './visibility.js';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The ONE managed-vs-BYO storage-backend selection policy.
3
+ *
4
+ * Every surface that persists to Phoenix-managed storage (`agents artifacts
5
+ * share` today, `agents sessions` sync next) makes the SAME choice: run on OUR
6
+ * managed infrastructure when the caller is signed in to Phoenix, and only fall
7
+ * back to a bring-your-own bucket when the caller explicitly asked for it. This
8
+ * module owns that decision so it is not re-derived — and re-drifted — per
9
+ * surface.
10
+ *
11
+ * What lives here is ONLY the identity + selection policy. It deliberately does
12
+ * NOT know a surface's endpoint, namespace shape, public-read semantics, or OG
13
+ * covers — those differ (share uses an email-handle namespace with public reads
14
+ * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
+ * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
+ * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
+ * `lib/traces/backend.ts` for the two adapters.
18
+ */
19
+ import { type PhoenixSession } from '../identity/client.js';
20
+ /** The two legitimate principals any managed-capable surface can resolve to. */
21
+ export type StorageBackendKind = 'managed' | 'byo';
22
+ export interface StorageSelectionOpts {
23
+ /**
24
+ * True when the SURFACE detected an explicit bring-your-own override — a
25
+ * `--byo` flag, a caller-supplied static write token, a `…_BACKEND=byo` env,
26
+ * or a full BYO endpoint config. Detecting WHICH signals count is the
27
+ * surface's job (they differ per surface); this policy only honors the boolean.
28
+ */
29
+ byoOverride?: boolean;
30
+ /**
31
+ * DI seam for the Phoenix session. `undefined` reads the real persisted
32
+ * session (`readSession()`); `null` means "explicitly signed out".
33
+ */
34
+ session?: PhoenixSession | null;
35
+ }
36
+ /**
37
+ * Pick the storage principal. Managed when signed in (`readSession() != null`)
38
+ * AND the surface reported no explicit BYO override; otherwise BYO.
39
+ *
40
+ * This is not a fallback chain — it is a single decision. A surface that cannot
41
+ * authenticate EITHER principal (signed out AND no BYO config) still reads `byo`
42
+ * here and fails loud in its own adapter, where the actionable "run auth login
43
+ * or set up your bucket" message belongs.
44
+ */
45
+ export declare function selectStorageBackendKind(opts?: StorageSelectionOpts): StorageBackendKind;
46
+ /** True when the shared policy resolves to the managed principal. Thin sugar
47
+ * over {@link selectStorageBackendKind} for the common boolean check. */
48
+ export declare function isManagedSelection(opts?: StorageSelectionOpts): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The ONE managed-vs-BYO storage-backend selection policy.
3
+ *
4
+ * Every surface that persists to Phoenix-managed storage (`agents artifacts
5
+ * share` today, `agents sessions` sync next) makes the SAME choice: run on OUR
6
+ * managed infrastructure when the caller is signed in to Phoenix, and only fall
7
+ * back to a bring-your-own bucket when the caller explicitly asked for it. This
8
+ * module owns that decision so it is not re-derived — and re-drifted — per
9
+ * surface.
10
+ *
11
+ * What lives here is ONLY the identity + selection policy. It deliberately does
12
+ * NOT know a surface's endpoint, namespace shape, public-read semantics, or OG
13
+ * covers — those differ (share uses an email-handle namespace with public reads
14
+ * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
+ * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
+ * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
+ * `lib/traces/backend.ts` for the two adapters.
18
+ */
19
+ import { readSession } from '../identity/client.js';
20
+ /**
21
+ * Pick the storage principal. Managed when signed in (`readSession() != null`)
22
+ * AND the surface reported no explicit BYO override; otherwise BYO.
23
+ *
24
+ * This is not a fallback chain — it is a single decision. A surface that cannot
25
+ * authenticate EITHER principal (signed out AND no BYO config) still reads `byo`
26
+ * here and fails loud in its own adapter, where the actionable "run auth login
27
+ * or set up your bucket" message belongs.
28
+ */
29
+ export function selectStorageBackendKind(opts = {}) {
30
+ if (opts.byoOverride === true)
31
+ return 'byo';
32
+ const session = opts.session === undefined ? readSession() : opts.session;
33
+ return session != null ? 'managed' : 'byo';
34
+ }
35
+ /** True when the shared policy resolves to the managed principal. Thin sugar
36
+ * over {@link selectStorageBackendKind} for the common boolean check. */
37
+ export function isManagedSelection(opts = {}) {
38
+ return selectStorageBackendKind(opts) === 'managed';
39
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The ONE visibility model, identical on every surface that publishes to managed
3
+ * storage. Three levels the operator ever chooses between:
4
+ *
5
+ * - PRIVATE = `me` — owner-only, Phoenix-gated. THE DEFAULT for a new
6
+ * managed share (see {@link MANAGED_DEFAULT_VISIBILITY}).
7
+ * - TEAM = `org` — everyone at the sharer's signed-in email DOMAIN,
8
+ * Phoenix-gated. Automatic from the domain — there is
9
+ * no organization to create and no member to add.
10
+ * - PUBLIC = `public` — explicit opt-in; listed in the gallery, gets an OG
11
+ * card.
12
+ *
13
+ * Two capability-URL levels remain for the power path (unguessable link, not a
14
+ * login gate): `unlisted` (obscurity — noindex, gallery-hidden, still
15
+ * world-readable) and `private`-token-gated (`--protected`: a `?k=` key the
16
+ * Worker checks, 404 without it). `me`/`org` require a Phoenix session; the
17
+ * Worker refuses them for a bare WRITE_TOKEN (BYO) publish, which is why the BYO
18
+ * default stays `public`.
19
+ *
20
+ * The names match the Worker's own metadata vocabulary (`lib/share/worker-template.ts`),
21
+ * so this module is the single client-side source of truth for the level set and
22
+ * the DEFAULT, reusable by any surface.
23
+ */
24
+ /** Every visibility level the Worker understands. `me`/`org` are the
25
+ * Phoenix-gated (login-required) levels; `public`/`unlisted`/`private` are the
26
+ * link-reachable levels. */
27
+ export type ShareVisibility = 'public' | 'unlisted' | 'private' | 'me' | 'org';
28
+ /** The levels a publish (`share <file> --visibility`) may select. */
29
+ export declare const PUBLISH_VISIBILITY_LEVELS: readonly ShareVisibility[];
30
+ /** The levels an ALREADY-published page may be re-scoped to in place
31
+ * (`share visibility <target> <level>`). Excludes `private`: re-scoping to
32
+ * token-gated needs a fresh viewer token, which only the publish path mints. */
33
+ export declare const EDITABLE_VISIBILITY_LEVELS: readonly ShareVisibility[];
34
+ /**
35
+ * The product default for a MANAGED (signed-in) share: PRIVATE = owner-only,
36
+ * Phoenix-gated. A publish with no visibility flag lands here.
37
+ */
38
+ export declare const MANAGED_DEFAULT_VISIBILITY: ShareVisibility;
39
+ /**
40
+ * The default for a BYO (bring-your-own bucket) publish. `me`/`org` are refused
41
+ * server-side for a WRITE_TOKEN publish — there is no Phoenix owner to gate on —
42
+ * so a BYO share with no flag stays `public`, matching the pre-managed behavior.
43
+ */
44
+ export declare const BYO_DEFAULT_VISIBILITY: ShareVisibility;
45
+ import type { StorageBackendKind } from './selection.js';
46
+ /** The no-flags default for a given storage backend: `me` on managed,
47
+ * `public` on BYO. */
48
+ export declare function defaultVisibilityForBackend(kind: StorageBackendKind): ShareVisibility;
49
+ export interface VisibilityFlags {
50
+ /** An explicit `--visibility <level>`. */
51
+ visibility?: ShareVisibility;
52
+ /** `--unlisted` / `--private` (obscurity, not read-auth). */
53
+ unlisted?: boolean;
54
+ /** `--protected` — token-gated read auth (maps to `private`). */
55
+ protected?: boolean;
56
+ }
57
+ /**
58
+ * The visibility the caller EXPLICITLY asked for, or `undefined` when they
59
+ * passed no visibility signal at all. `--protected` wins over `--unlisted`
60
+ * (stronger control), which wins over an explicit `--visibility`.
61
+ *
62
+ * The distinction between "explicit" and "no preference" is what lets a caller
63
+ * apply the product default only when nothing was asked — a caller that passes
64
+ * `--visibility public` gets `public`, never the `me` default.
65
+ */
66
+ export declare function explicitVisibility(opts?: VisibilityFlags): ShareVisibility | undefined;
67
+ /**
68
+ * Resolve visibility from the caller's flags, falling back to `fallback` when no
69
+ * explicit signal was given. The library fallback stays `public` so a caller
70
+ * that hasn't opted into the managed private default (e.g. an existing lib
71
+ * consumer) is never silently flipped; the surface applies the product default
72
+ * itself via {@link defaultVisibilityForBackend}.
73
+ */
74
+ export declare function resolveVisibility(opts?: VisibilityFlags, fallback?: ShareVisibility): ShareVisibility;
75
+ /**
76
+ * The visibility a PUBLISH should stamp: the caller's explicit flag if any, else
77
+ * the product default for the resolved backend (`me` on managed, `public` on
78
+ * BYO). This is the one call a publishing surface makes — `agents artifacts
79
+ * share` today, `agents sessions` sync next — so "private by default when signed
80
+ * in" is decided in exactly one place.
81
+ */
82
+ export declare function publishVisibility(opts: VisibilityFlags, kind: StorageBackendKind): ShareVisibility;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The ONE visibility model, identical on every surface that publishes to managed
3
+ * storage. Three levels the operator ever chooses between:
4
+ *
5
+ * - PRIVATE = `me` — owner-only, Phoenix-gated. THE DEFAULT for a new
6
+ * managed share (see {@link MANAGED_DEFAULT_VISIBILITY}).
7
+ * - TEAM = `org` — everyone at the sharer's signed-in email DOMAIN,
8
+ * Phoenix-gated. Automatic from the domain — there is
9
+ * no organization to create and no member to add.
10
+ * - PUBLIC = `public` — explicit opt-in; listed in the gallery, gets an OG
11
+ * card.
12
+ *
13
+ * Two capability-URL levels remain for the power path (unguessable link, not a
14
+ * login gate): `unlisted` (obscurity — noindex, gallery-hidden, still
15
+ * world-readable) and `private`-token-gated (`--protected`: a `?k=` key the
16
+ * Worker checks, 404 without it). `me`/`org` require a Phoenix session; the
17
+ * Worker refuses them for a bare WRITE_TOKEN (BYO) publish, which is why the BYO
18
+ * default stays `public`.
19
+ *
20
+ * The names match the Worker's own metadata vocabulary (`lib/share/worker-template.ts`),
21
+ * so this module is the single client-side source of truth for the level set and
22
+ * the DEFAULT, reusable by any surface.
23
+ */
24
+ /** The levels a publish (`share <file> --visibility`) may select. */
25
+ export const PUBLISH_VISIBILITY_LEVELS = [
26
+ 'public',
27
+ 'unlisted',
28
+ 'private',
29
+ 'me',
30
+ 'org',
31
+ ];
32
+ /** The levels an ALREADY-published page may be re-scoped to in place
33
+ * (`share visibility <target> <level>`). Excludes `private`: re-scoping to
34
+ * token-gated needs a fresh viewer token, which only the publish path mints. */
35
+ export const EDITABLE_VISIBILITY_LEVELS = [
36
+ 'public',
37
+ 'unlisted',
38
+ 'me',
39
+ 'org',
40
+ ];
41
+ /**
42
+ * The product default for a MANAGED (signed-in) share: PRIVATE = owner-only,
43
+ * Phoenix-gated. A publish with no visibility flag lands here.
44
+ */
45
+ export const MANAGED_DEFAULT_VISIBILITY = 'me';
46
+ /**
47
+ * The default for a BYO (bring-your-own bucket) publish. `me`/`org` are refused
48
+ * server-side for a WRITE_TOKEN publish — there is no Phoenix owner to gate on —
49
+ * so a BYO share with no flag stays `public`, matching the pre-managed behavior.
50
+ */
51
+ export const BYO_DEFAULT_VISIBILITY = 'public';
52
+ /** The no-flags default for a given storage backend: `me` on managed,
53
+ * `public` on BYO. */
54
+ export function defaultVisibilityForBackend(kind) {
55
+ return kind === 'managed' ? MANAGED_DEFAULT_VISIBILITY : BYO_DEFAULT_VISIBILITY;
56
+ }
57
+ /**
58
+ * The visibility the caller EXPLICITLY asked for, or `undefined` when they
59
+ * passed no visibility signal at all. `--protected` wins over `--unlisted`
60
+ * (stronger control), which wins over an explicit `--visibility`.
61
+ *
62
+ * The distinction between "explicit" and "no preference" is what lets a caller
63
+ * apply the product default only when nothing was asked — a caller that passes
64
+ * `--visibility public` gets `public`, never the `me` default.
65
+ */
66
+ export function explicitVisibility(opts = {}) {
67
+ if (opts.protected === true)
68
+ return 'private';
69
+ if (opts.unlisted === true)
70
+ return 'unlisted';
71
+ if (opts.visibility === 'public' ||
72
+ opts.visibility === 'unlisted' ||
73
+ opts.visibility === 'private' ||
74
+ opts.visibility === 'me' ||
75
+ opts.visibility === 'org') {
76
+ return opts.visibility;
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Resolve visibility from the caller's flags, falling back to `fallback` when no
82
+ * explicit signal was given. The library fallback stays `public` so a caller
83
+ * that hasn't opted into the managed private default (e.g. an existing lib
84
+ * consumer) is never silently flipped; the surface applies the product default
85
+ * itself via {@link defaultVisibilityForBackend}.
86
+ */
87
+ export function resolveVisibility(opts = {}, fallback = 'public') {
88
+ return explicitVisibility(opts) ?? fallback;
89
+ }
90
+ /**
91
+ * The visibility a PUBLISH should stamp: the caller's explicit flag if any, else
92
+ * the product default for the resolved backend (`me` on managed, `public` on
93
+ * BYO). This is the one call a publishing surface makes — `agents artifacts
94
+ * share` today, `agents sessions` sync next — so "private by default when signed
95
+ * in" is decided in exactly one place.
96
+ */
97
+ export function publishVisibility(opts, kind) {
98
+ return explicitVisibility(opts) ?? defaultVisibilityForBackend(kind);
99
+ }
@@ -2361,7 +2361,9 @@ export class AgentManager {
2361
2361
  ? `at its agents.max-concurrent cap (${e.detail} running)`
2362
2362
  : e.reason === 'not-installed'
2363
2363
  ? `does not have ${this.placementAgentLabel(agent)} installed`
2364
- : e.reason;
2364
+ : e.reason === 'probe-timed-out'
2365
+ ? 'did not answer the probe in time (likely up but on a slow/relayed link)'
2366
+ : e.reason;
2365
2367
  console.error(chalk.dim(`[placement] '${e.device}' excluded from auto-pick — ${why}`));
2366
2368
  }
2367
2369
  }
@@ -137,6 +137,7 @@ export async function probePoolSignals(pool, agent, opts = {}) {
137
137
  continue; // fully unknown device — leave it out of the map
138
138
  signals.set(name, {
139
139
  reachable: s?.reachable,
140
+ timedOut: s?.timedOut,
140
141
  headroom: s ? headroom(s) : undefined,
141
142
  loadPercent: s?.loadPercent,
142
143
  memPercent: s?.memPercent,
@@ -14,6 +14,7 @@ import lockfile from 'proper-lockfile';
14
14
  import { getTeamsRegistryPath } from '../state.js';
15
15
  import { emit } from '../feed/events.js';
16
16
  import { atomicWriteJsonSync } from '../fs-atomic.js';
17
+ import { logAndContinueOnLockCompromised } from '../lock-compromise.js';
17
18
  async function registryPath() {
18
19
  return getTeamsRegistryPath();
19
20
  }
@@ -38,6 +39,7 @@ async function withRegistryLock(p, fn) {
38
39
  const release = await lockfile.lock(p, {
39
40
  retries: { retries: 60, minTimeout: 25, maxTimeout: 250, factor: 1.5 },
40
41
  stale: 10_000,
42
+ onCompromised: logAndContinueOnLockCompromised('teams registry'),
41
43
  });
42
44
  try {
43
45
  return await fn();
@@ -10,6 +10,13 @@ import type { Headroom } from '../devices/health.js';
10
10
  export interface DevicePlacementSignal {
11
11
  /** SSH probe answered. `false` → excluded (unreachable). undefined → unknown. */
12
12
  reachable?: boolean;
13
+ /**
14
+ * The probe was killed for exceeding its budget rather than failing to
15
+ * connect. Ranking treats it exactly like `reachable:false` — an
16
+ * unresponsive box is still not placeable — but the operator-facing
17
+ * exclusion reason says "probe timed out", not "unreachable" (PHNX-3682).
18
+ */
19
+ timedOut?: boolean;
13
20
  /** Headroom bucket from load+memory. `'loaded'` → excluded (overloaded). */
14
21
  headroom?: Headroom;
15
22
  /** Normalized CPU load percent (finer rank tiebreak within a headroom tier). */
@@ -60,7 +67,7 @@ export interface PlacementOptions {
60
67
  preferred?: ReadonlySet<string>;
61
68
  }
62
69
  /** Why a device was excluded from the viable set, for the fail-loud message. */
63
- export type ExclusionReason = 'unreachable' | 'overloaded' | 'capped' | 'not-installed';
70
+ export type ExclusionReason = 'unreachable' | 'probe-timed-out' | 'overloaded' | 'capped' | 'not-installed';
64
71
  /** A pool device dropped from the auto-pick, with the reason + live detail. */
65
72
  export interface ExcludedDevice {
66
73
  device: string;
@@ -199,7 +199,10 @@ export function classifyExclusions(devices, roster, opts) {
199
199
  for (const d of devices) {
200
200
  const s = signals?.get(d);
201
201
  if (s?.reachable === false) {
202
- excluded.push({ device: d, reason: 'unreachable' });
202
+ // A probe killed for exceeding its budget is a slow link, not a box that
203
+ // is down. Same exclusion either way — an unresponsive device is still not
204
+ // placeable — but the operator sees which one it was (PHNX-3682).
205
+ excluded.push({ device: d, reason: s.timedOut ? 'probe-timed-out' : 'unreachable' });
203
206
  continue;
204
207
  }
205
208
  if (s?.installed === false) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { recordSubsystemError } from '../daemon-health.js';
2
+ const [subsystem, countRaw] = process.argv.slice(2);
3
+ const count = Number(countRaw);
4
+ if (!subsystem || !Number.isInteger(count) || count < 1)
5
+ process.exit(2);
6
+ for (let i = 0; i < count; i++) {
7
+ recordSubsystemError(subsystem, `${process.pid}:${i}`);
8
+ }
@@ -10,6 +10,7 @@
10
10
  * `agents artifacts setup`.
11
11
  */
12
12
  import { readSession } from '../identity/client.js';
13
+ import { selectStorageBackendKind } from '../storage/selection.js';
13
14
  export const DEFAULT_TRACES_DOMAIN = 'traces.agents-cli.sh';
14
15
  /**
15
16
  * Resolve the backend for the current machine. Throws when not signed in.
@@ -21,11 +22,21 @@ export const DEFAULT_TRACES_DOMAIN = 'traces.agents-cli.sh';
21
22
  export function resolveTracesBackend() {
22
23
  const envBase = (process.env['AGENTS_TRACES_BASE_URL'] ?? '').replace(/\/+$/, '').trim();
23
24
  const envToken = (process.env['AGENTS_TRACES_WRITE_TOKEN'] ?? '').trim();
24
- if (envBase && envToken) {
25
- return { baseUrl: envBase, token: envToken, userId: 'byo' };
25
+ // The traces surface's only BYO signal is the full BASE_URL + WRITE_TOKEN env
26
+ // pair; the managed-vs-BYO decision itself is the shared selection policy.
27
+ const byoOverride = Boolean(envBase && envToken);
28
+ if (selectStorageBackendKind({ byoOverride }) === 'byo') {
29
+ if (byoOverride) {
30
+ return { baseUrl: envBase, token: envToken, userId: 'byo' };
31
+ }
32
+ // Not signed in and no BYO env pair — the managed principal is the only one
33
+ // this surface exposes, so fail loud with the login hint.
34
+ throw new Error("Not signed in. Run 'agents auth login' to sync traces to your Phoenix account.");
26
35
  }
27
36
  const session = readSession();
28
37
  if (!session) {
38
+ // selectStorageBackendKind read the same session and returned 'managed', so a
39
+ // null here means it was cleared between the two reads — treat as signed out.
29
40
  throw new Error("Not signed in. Run 'agents auth login' to sync traces to your Phoenix account.");
30
41
  }
31
42
  if (!session.access_token) {
@@ -390,6 +390,13 @@ interface SyncLedger {
390
390
  }
391
391
  export declare function readSyncLedger(): SyncLedger;
392
392
  export declare function writeSyncLedger(ledger: SyncLedger): void;
393
+ /**
394
+ * True once the user has run `agents traces sync` at least once (the ledger
395
+ * file exists). This is the opt-in signal the run-exit auto-sync gates on
396
+ * (PHNX-3628): a user who has never synced has not opted into the traces store,
397
+ * so an `agents run` never uploads on their behalf.
398
+ */
399
+ export declare function hasSyncedBefore(): boolean;
393
400
  export interface SyncRow {
394
401
  id: string;
395
402
  short_id: string;
@@ -816,6 +816,15 @@ export function writeSyncLedger(ledger) {
816
816
  fs.mkdirSync(path.dirname(p), { recursive: true });
817
817
  fs.writeFileSync(p, JSON.stringify(ledger, null, 2) + '\n', 'utf8');
818
818
  }
819
+ /**
820
+ * True once the user has run `agents traces sync` at least once (the ledger
821
+ * file exists). This is the opt-in signal the run-exit auto-sync gates on
822
+ * (PHNX-3628): a user who has never synced has not opted into the traces store,
823
+ * so an `agents run` never uploads on their behalf.
824
+ */
825
+ export function hasSyncedBefore() {
826
+ return fs.existsSync(ledgerPath());
827
+ }
819
828
  // ---------------------------------------------------------------------------
820
829
  // Local device name
821
830
  // ---------------------------------------------------------------------------
@@ -150,8 +150,12 @@ export declare function providerSpacingTokens(lastCallMs: number, now: number):
150
150
  export interface LocalUsageAccount {
151
151
  usageKey: string;
152
152
  agentId: AgentId;
153
- /** Live-fetch this account's usage; the daemon passes the real network fetch. */
154
- fetch: () => Promise<UsageInfo>;
153
+ /**
154
+ * Live-fetch this account's usage; the daemon passes the real network fetch.
155
+ * `signal` (the daemon tick's deadline AbortSignal) bounds the provider fetch
156
+ * so a hung refresh is aborted at deadlineMs, not just its own 5s timeout.
157
+ */
158
+ fetch: (signal?: AbortSignal) => Promise<UsageInfo>;
155
159
  }
156
160
  /**
157
161
  * Order a pass STALEST-FIRST so a scarce per-provider budget
@@ -206,6 +210,8 @@ export interface UsageRefreshDeps {
206
210
  * spending scarce provider budget re-fetching an already-current account.
207
211
  */
208
212
  readCachedSnapshot?: (usageKey: string) => UsageSnapshot | null;
213
+ /** Daemon tick deadline signal, forwarded to each account's provider fetch (PHNX-3608). */
214
+ signal?: AbortSignal;
209
215
  }
210
216
  export interface UsageRefreshResult {
211
217
  refreshed: number;
@@ -392,14 +392,14 @@ export async function buildLocalUsageAccounts() {
392
392
  // only, never the interactive login (see loadClaudeOauth); no setup-token
393
393
  // reads as "usage unavailable (no usage credential)" — "usage pending"
394
394
  // is now the cold-cache state only (#2987).
395
- fetch: async () => {
395
+ fetch: async (signal) => {
396
396
  const { getUsageInfoForIdentity } = await import('./accounting/usage.js');
397
397
  return getUsageInfoForIdentity({
398
398
  agentId,
399
399
  home: fetchInput.home,
400
400
  cliVersion: fetchInput.cliVersion,
401
401
  info: canonical,
402
- }, { forceRefresh: true, fileOnly: true });
402
+ }, { forceRefresh: true, fileOnly: true, signal });
403
403
  },
404
404
  });
405
405
  }
@@ -487,7 +487,7 @@ export async function runUsageRefresh(deps) {
487
487
  spacingUsed.set(account.agentId, (spacingUsed.get(account.agentId) ?? 0) + 1);
488
488
  }
489
489
  try {
490
- const usage = await account.fetch();
490
+ const usage = await account.fetch(deps.signal);
491
491
  if (usage.snapshot) {
492
492
  // `source` is provenance, not freshness. A forced collection that just
493
493
  // reread a local harness event returns `last_seen`; that is still a