@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -0,0 +1,79 @@
1
+ import type { SessionWatchRow, SessionWatchScopeStatus } from '../session/watch.js';
2
+ import type { ToolSetupRow } from '../setup-tool-status.js';
3
+ import type { AttentionItem } from './attention.js';
4
+ import type { ActivityEvent } from './activity.js';
5
+ import type { ToolRow } from './tools.js';
6
+ type Base = {
7
+ v: 1;
8
+ type: string;
9
+ streamId: string;
10
+ sequence: number;
11
+ scope: string;
12
+ };
13
+ export type FeedWatchEnvelope = Base & {
14
+ type: 'reset';
15
+ capturedAt: number;
16
+ agents: SessionWatchRow[];
17
+ attention: AttentionItem[];
18
+ tools: ToolRow[];
19
+ /** This device's tool-setup rows; see `setup.snapshot` for why it is a set. */
20
+ setup: ToolSetupRow[];
21
+ } | Base & {
22
+ type: 'agent.upsert';
23
+ rowKey: string;
24
+ agent: SessionWatchRow;
25
+ } | Base & {
26
+ type: 'agent.remove';
27
+ rowKey: string;
28
+ } | Base & {
29
+ type: 'attention.upsert';
30
+ rowKey: string;
31
+ attention: AttentionItem;
32
+ } | Base & {
33
+ type: 'attention.remove';
34
+ rowKey: string;
35
+ } | Base & {
36
+ type: 'activity.append';
37
+ event: ActivityEvent;
38
+ }
39
+ /** A browser task or computer run, projected by `feed/tools.ts`. */
40
+ | Base & {
41
+ type: 'tool.upsert';
42
+ rowKey: string;
43
+ tool: ToolRow;
44
+ } | Base & {
45
+ type: 'tool.remove';
46
+ rowKey: string;
47
+ }
48
+ /**
49
+ * The whole tool-setup set for one device, replaced at once.
50
+ *
51
+ * Deliberately a SNAPSHOT and not per-row upserts: `getCachedToolSetup` always
52
+ * answers for every tool in `SETUP_TOOLS`, so "browser is ready" and "computer
53
+ * needs setup" are one coherent reading of the box taken at one moment. Sending
54
+ * three independent upserts would let a consumer render a mix of two different
55
+ * readings, and there is no row to *remove* — a tool that is not installed is a
56
+ * row saying so, which is exactly what a Setup pane must show.
57
+ */
58
+ | Base & {
59
+ type: 'setup.snapshot';
60
+ capturedAt: number;
61
+ setup: ToolSetupRow[];
62
+ } | Base & {
63
+ type: 'scope';
64
+ capturedAt: number;
65
+ status: SessionWatchScopeStatus;
66
+ reason?: string;
67
+ } | Base & {
68
+ type: 'heartbeat';
69
+ capturedAt: number;
70
+ };
71
+ export type FeedWatchPayload = FeedWatchEnvelope extends infer Envelope ? Envelope extends FeedWatchEnvelope ? Omit<Envelope, 'v' | 'streamId' | 'sequence'> : never : never;
72
+ /** Per-subscriber stream identity and monotonic sequence. */
73
+ export declare class FeedWatchState {
74
+ readonly streamId: string;
75
+ private sequence;
76
+ constructor(streamId?: `${string}-${string}-${string}-${string}-${string}`);
77
+ emit(event: FeedWatchPayload): FeedWatchEnvelope;
78
+ }
79
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The feed stream's envelope contract and its sequencer.
3
+ *
4
+ * Extracted from `watch.ts` so the three modules that all speak this protocol —
5
+ * the producer (`watch.ts`), the shared collector (`hub.ts`), and the socket
6
+ * boundary (`hub-server.ts`) — can depend on the contract without depending on
7
+ * each other. `watch.ts` needs `FeedHub` to share its local collector and
8
+ * `hub.ts` needs the sequencer, so leaving both here is what keeps that from
9
+ * being an import cycle.
10
+ *
11
+ * `watch.ts` re-exports everything in this file, so every existing importer is
12
+ * unaffected.
13
+ */
14
+ import { randomUUID } from 'node:crypto';
15
+ /** Per-subscriber stream identity and monotonic sequence. */
16
+ export class FeedWatchState {
17
+ streamId;
18
+ sequence = 0;
19
+ constructor(streamId = randomUUID()) { this.streamId = streamId; }
20
+ emit(event) {
21
+ return { v: 1, streamId: this.streamId, sequence: ++this.sequence, ...event };
22
+ }
23
+ }
@@ -12,6 +12,12 @@
12
12
  * - Performance tracking: withTiming() wrapper for any async function
13
13
  */
14
14
  import type { ActorKind } from '../actor.js';
15
+ /**
16
+ * The directory today's ledger is appended to. Exported for the tool-activity
17
+ * collector, which watches it instead of re-running `agents computer sessions`
18
+ * on a timer to notice a new `computer.action` (see `feed/tool-activity.ts`).
19
+ */
20
+ export declare function getEventsDir(): string;
15
21
  export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
16
22
  export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'run.dispatched' | 'run.launch' | 'daemon.start' | 'daemon.stop' | 'daemon.error' | 'daemon.info' | 'routine.start' | 'routine.end' | 'watchdog.action' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'rotation.unresolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.failed' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
17
23
  /** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
@@ -88,6 +88,14 @@ function eventsPath(date = new Date()) {
88
88
  function eventsDir(date = new Date()) {
89
89
  return path.dirname(eventsPath(date));
90
90
  }
91
+ /**
92
+ * The directory today's ledger is appended to. Exported for the tool-activity
93
+ * collector, which watches it instead of re-running `agents computer sessions`
94
+ * on a timer to notice a new `computer.action` (see `feed/tool-activity.ts`).
95
+ */
96
+ export function getEventsDir() {
97
+ return eventsDir();
98
+ }
91
99
  /** Default retention period in days. */
92
100
  const DEFAULT_RETENTION_DAYS = 7;
93
101
  /** Default total footprint for the active log plus gzip archives (50 MiB). */
@@ -23,7 +23,38 @@ export interface MessageReceipt {
23
23
  at: string;
24
24
  /** Optional sender label for the message. */
25
25
  from?: string;
26
+ /**
27
+ * The ask this receipt is ABOUT — the block generation live when the answer
28
+ * was sent. A block id is per SESSION, so it is reused by every generation of
29
+ * that session's questions; without this a late acknowledgement for question N
30
+ * is indistinguishable from one for question N+1 and would resolve the wrong
31
+ * ask (PHNX-3999). Carried durably on the queued message so it survives the
32
+ * process that sent it.
33
+ */
34
+ generation?: string;
35
+ /** The claim (attempt) this receipt is about — `AnswerRecord.answeredAt`. */
36
+ attempt?: string;
37
+ }
38
+ /** The ask a receipt or queued message belongs to, plus the attempt that sent it. */
39
+ export interface ReceiptOrigin {
40
+ generation: string;
41
+ attempt: string;
26
42
  }
43
+ /**
44
+ * Whether a receipt describes THIS ask.
45
+ *
46
+ * Identity is the GENERATION alone, never the attempt. The question is "does
47
+ * this receipt answer this ask?", and a second attempt on the same ask carries
48
+ * the same answer -- so a stranded claim that is adopted (which necessarily
49
+ * mints a new attempt) must still recognise the message its predecessor queued,
50
+ * or it enqueues a duplicate. `attempt` rides along as provenance for the
51
+ * delivery check, not as part of identity.
52
+ *
53
+ * An UNBOUND receipt (written before these fields existed) matches NOTHING: it
54
+ * cannot name an ask, so attributing it to one would let a message queued for an
55
+ * earlier question resolve whichever question is current (PHNX-3999).
56
+ */
57
+ export declare function receiptMatchesOrigin(receipt: MessageReceipt, origin: ReceiptOrigin): boolean;
27
58
  export interface AnswerRecord {
28
59
  /** ISO-8601 timestamp of when the answer was recorded. */
29
60
  answeredAt: string;
@@ -267,7 +298,22 @@ export declare function recordAnswer(blockId: string, answer: {
267
298
  answeredFrom: string;
268
299
  operatorId?: string;
269
300
  verified?: boolean;
270
- }, root?: string): RecordAnswerResult;
301
+ }, root?: string, options?: {
302
+ pending?: boolean;
303
+ }): RecordAnswerResult;
304
+ /**
305
+ * Promote a pending claim to a resolved answer — the second half of the
306
+ * two-phase answer protocol (see `recordAnswer`'s `pending` option).
307
+ *
308
+ * Called only once a rail has reported a real {@link MessageReceipt}: that is
309
+ * the point the item stops needing a human, so that is the point the tombstone
310
+ * is written and the card may leave the feed. An unconfirmed delivery never
311
+ * reaches here, so its card stays up.
312
+ */
313
+ export declare function confirmAnswerResolution(blockId: string, root?: string, expected?: {
314
+ generation: string;
315
+ answeredAt: string;
316
+ }): boolean;
271
317
  /** Read the answer record for a block, if one exists. */
272
318
  export declare function getAnswerRecord(blockId: string, root?: string): AnswerRecord | undefined;
273
319
  /**
@@ -277,6 +323,12 @@ export declare function getAnswerRecord(blockId: string, root?: string): AnswerR
277
323
  * restoring the attention lifecycle to the state another surface observed.
278
324
  */
279
325
  export declare function rollbackAnswerClaim(blockId: string, answeredAt: string, previousBlock: OpenBlock, previousResolution: AttentionResolution | undefined, root?: string): boolean;
326
+ /**
327
+ * How long a release token may sit before it is treated as abandoned. A release
328
+ * is a handful of synchronous file operations, so anything this old belongs to a
329
+ * process that died holding it.
330
+ */
331
+ export declare const RELEASE_TOKEN_STALE_MS = 60000;
280
332
  /** True when the block has already been answered. */
281
333
  export declare function isBlockAnswered(blockId: string, root?: string): boolean;
282
334
  /**
@@ -288,6 +340,14 @@ export declare function isBlockAnswered(blockId: string, root?: string): boolean
288
340
  export declare function recordMessageReceipt(blockId: string, receipt: MessageReceipt, root?: string): void;
289
341
  /** Read the receipt list for a block. */
290
342
  export declare function getBlockReceipts(blockId: string, root?: string): MessageReceipt[];
343
+ /**
344
+ * The furthest-along receipt recorded for a block, or undefined when no rail
345
+ * ever reported one. This is the ONLY truthful evidence that an answer reached
346
+ * a delivery rail: an answer marker alone proves a claim was taken, not that
347
+ * anything was delivered, so a caller reporting on a block it did not deliver
348
+ * must read this rather than synthesize a receipt (PHNX-3999).
349
+ */
350
+ export declare function latestMessageReceipt(blockId: string, root?: string, origin?: ReceiptOrigin): MessageReceipt | undefined;
291
351
  /** Mark a block as "continued" -- the agent consumed the answer and moved on. */
292
352
  export declare function recordContinued(blockId: string, root?: string): void;
293
353
  /** Mark a decision-class block as hard-parked (no safe default existed). */
@@ -23,12 +23,32 @@
23
23
  */
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
+ import * as os from 'os';
26
27
  import * as yaml from 'yaml';
27
28
  import { stringifyDoc } from '../yaml-io.js';
28
29
  import { getFeedDir, getUserAgentsDir } from '../state.js';
29
30
  import { isAdmin, isHighConsequenceAllowed, isKnownOperator } from '../operator.js';
30
31
  import { projectKeyFromCwd } from '../project-key.js';
31
32
  import { atomicWriteJsonSync } from '../fs-atomic.js';
33
+ /**
34
+ * Whether a receipt describes THIS ask.
35
+ *
36
+ * Identity is the GENERATION alone, never the attempt. The question is "does
37
+ * this receipt answer this ask?", and a second attempt on the same ask carries
38
+ * the same answer -- so a stranded claim that is adopted (which necessarily
39
+ * mints a new attempt) must still recognise the message its predecessor queued,
40
+ * or it enqueues a duplicate. `attempt` rides along as provenance for the
41
+ * delivery check, not as part of identity.
42
+ *
43
+ * An UNBOUND receipt (written before these fields existed) matches NOTHING: it
44
+ * cannot name an ask, so attributing it to one would let a message queued for an
45
+ * earlier question resolve whichever question is current (PHNX-3999).
46
+ */
47
+ export function receiptMatchesOrigin(receipt, origin) {
48
+ if (receipt.generation === undefined)
49
+ return false;
50
+ return receipt.generation === origin.generation;
51
+ }
32
52
  function resolutionDir(root) { return path.join(root, 'resolutions'); }
33
53
  /**
34
54
  * Canonical generation for a block. A writer that stamped `generation` wins;
@@ -129,7 +149,7 @@ export function readBlock(blockId, root) {
129
149
  * High-consequence blocks require a verified operator identity. Unverified
130
150
  * answers (no operatorId or not in the registry/allowed list) are refused.
131
151
  */
132
- export function recordAnswer(blockId, answer, root) {
152
+ export function recordAnswer(blockId, answer, root, options = {}) {
133
153
  const dir = root ?? getFeedDir();
134
154
  const block = readBlock(blockId, dir);
135
155
  const operatorId = answer.operatorId;
@@ -187,24 +207,74 @@ export function recordAnswer(blockId, answer, root) {
187
207
  }
188
208
  throw err;
189
209
  }
190
- // Marker created successfully -- mirror the answer into the block file and
191
- // advance the lifecycle to `answered`. The resolution tombstone is written
192
- // first, so if a stale lifecycle re-read races the block-file update the
193
- // reconciler already refuses to resurrect this generation.
210
+ // Marker created successfully -- mirror the answer into the block file.
211
+ //
212
+ // A `pending` claim stops there: the claim is recorded so no second surface
213
+ // can take it, but the generation is NOT resolved and the lifecycle stays
214
+ // `open`, so the card remains in the operator's feed until a rail reports a
215
+ // real receipt (`confirmAnswerResolution`). A claim is not a delivery, and a
216
+ // claim whose delivery is never confirmed must not silently remove the item
217
+ // (PHNX-3999). `state` wins over `answer` in `deriveBlockState`, so the
218
+ // explicit `open` is what keeps the claimed block visible.
219
+ //
220
+ // The default one-phase path advances straight to `answered` for surfaces
221
+ // that resolve atomically (a policy default, a synchronous enqueue). The
222
+ // resolution tombstone is written first, so if a stale lifecycle re-read races
223
+ // the block-file update the reconciler already refuses to resurrect this
224
+ // generation.
194
225
  if (block) {
195
- recordResolution({
196
- blockId,
197
- generation: blockGeneration(block),
198
- resolvedAt: record.answeredAt,
199
- sourceCursor: block.sourceCursor,
200
- reason: 'answered',
201
- }, dir);
226
+ if (!options.pending) {
227
+ recordResolution({
228
+ blockId,
229
+ generation: blockGeneration(block),
230
+ resolvedAt: record.answeredAt,
231
+ sourceCursor: block.sourceCursor,
232
+ reason: 'answered',
233
+ }, dir);
234
+ }
202
235
  block.answer = record;
203
- block.state = 'answered';
236
+ block.state = options.pending ? 'open' : 'answered';
204
237
  publishBlock(block, dir);
205
238
  }
206
239
  return { ok: true };
207
240
  }
241
+ /**
242
+ * Promote a pending claim to a resolved answer — the second half of the
243
+ * two-phase answer protocol (see `recordAnswer`'s `pending` option).
244
+ *
245
+ * Called only once a rail has reported a real {@link MessageReceipt}: that is
246
+ * the point the item stops needing a human, so that is the point the tombstone
247
+ * is written and the card may leave the feed. An unconfirmed delivery never
248
+ * reaches here, so its card stays up.
249
+ */
250
+ export function confirmAnswerResolution(blockId, root, expected) {
251
+ const dir = root ?? getFeedDir();
252
+ const block = readBlock(blockId, dir);
253
+ const record = getAnswerRecord(blockId, dir);
254
+ if (!block || !record)
255
+ return false;
256
+ // One block id serves every generation of a session's asks, so a slow
257
+ // delivery for the PREVIOUS question must not resolve the one the agent has
258
+ // moved on to. A caller that knows which ask and which attempt it is
259
+ // confirming says so, and a mismatch is a no-op rather than a wrong tombstone.
260
+ // Bound to the ASK. The attempt is deliberately NOT compared: adopting a
261
+ // stranded claim mints a new attempt for the same question, and that adoption
262
+ // must still be able to resolve the ask it completed. The generation is what
263
+ // distinguishes one question from the next, which is the actual hazard.
264
+ if (expected && blockGeneration(block) !== expected.generation)
265
+ return false;
266
+ recordResolution({
267
+ blockId,
268
+ generation: blockGeneration(block),
269
+ resolvedAt: record.answeredAt,
270
+ sourceCursor: block.sourceCursor,
271
+ reason: 'answered',
272
+ }, dir);
273
+ block.answer = record;
274
+ block.state = 'answered';
275
+ publishBlock(block, dir);
276
+ return true;
277
+ }
208
278
  /** Read the answer record for a block, if one exists. */
209
279
  export function getAnswerRecord(blockId, root) {
210
280
  return safeReadJson(path.join(answeredDir(root ?? getFeedDir()), `${blockId}.json`));
@@ -221,6 +291,31 @@ export function rollbackAnswerClaim(blockId, answeredAt, previousBlock, previous
221
291
  const current = safeReadJson(marker);
222
292
  if (!current || current.answeredAt !== answeredAt)
223
293
  return false;
294
+ // Read-compare-then-unlink is NOT atomic, and two callers releasing the SAME
295
+ // claim is a real interleaving: both pass the compare, the first unlinks and
296
+ // re-claims, then the second unlinks the FIRST'S fresh marker and both end up
297
+ // holding a claim. The release is therefore gated on an O_EXCL token keyed by
298
+ // the exact claim being released -- the same primitive `recordAnswer` uses, so
299
+ // exactly one caller can ever release a given `answeredAt` (PHNX-3999).
300
+ const release = path.join(answeredDir(dir), `${blockId}.${answeredAt.replace(/[^0-9A-Za-z]/g, '')}.release`);
301
+ if (!acquireReleaseToken(release))
302
+ return false;
303
+ const dropToken = () => {
304
+ try {
305
+ fs.unlinkSync(release);
306
+ }
307
+ catch (error) {
308
+ if (error.code !== 'ENOENT')
309
+ throw error;
310
+ }
311
+ };
312
+ // Re-read INSIDE the token: a racer that released-and-re-claimed between our
313
+ // first read and the token acquisition would otherwise be clobbered.
314
+ const held = safeReadJson(marker);
315
+ if (!held || held.answeredAt !== answeredAt) {
316
+ dropToken();
317
+ return false;
318
+ }
224
319
  // Restore while the O_EXCL marker still excludes every other claimant. The
225
320
  // marker is removed LAST; once another writer can win recordAnswer, this
226
321
  // rollback has no state left to overwrite.
@@ -237,9 +332,79 @@ export function rollbackAnswerClaim(blockId, answeredAt, previousBlock, previous
237
332
  throw error;
238
333
  }
239
334
  }
240
- fs.unlinkSync(marker);
335
+ try {
336
+ fs.unlinkSync(marker);
337
+ }
338
+ catch (error) {
339
+ if (error.code !== 'ENOENT')
340
+ throw error;
341
+ }
342
+ // The token has done its job: this exact claim can never be released again,
343
+ // because the claim it names no longer exists. Leaving it would accumulate one
344
+ // dead file per released claim forever.
345
+ dropToken();
241
346
  return true;
242
347
  }
348
+ /**
349
+ * How long a release token may sit before it is treated as abandoned. A release
350
+ * is a handful of synchronous file operations, so anything this old belongs to a
351
+ * process that died holding it.
352
+ */
353
+ export const RELEASE_TOKEN_STALE_MS = 60_000;
354
+ /**
355
+ * Take the O_EXCL token that serialises releasing one specific claim.
356
+ *
357
+ * The token MUST be recoverable: a process killed between creating it and
358
+ * finishing would otherwise wedge that claim forever, and "the answer can never
359
+ * be released again" is a worse failure than the race the token prevents. So the
360
+ * token records its owner and its age, and a token whose owner is provably gone
361
+ * (same host, no such pid) or which is simply stale is reclaimed once.
362
+ */
363
+ function acquireReleaseToken(release) {
364
+ const mine = { pid: process.pid, host: os.hostname(), at: Date.now() };
365
+ const create = () => {
366
+ try {
367
+ const fd = fs.openSync(release, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
368
+ try {
369
+ fs.writeSync(fd, Buffer.from(JSON.stringify(mine), 'utf-8'));
370
+ }
371
+ finally {
372
+ fs.closeSync(fd);
373
+ }
374
+ return true;
375
+ }
376
+ catch (error) {
377
+ if (error.code === 'EEXIST')
378
+ return false;
379
+ throw error;
380
+ }
381
+ };
382
+ if (create())
383
+ return true;
384
+ const held = safeReadJson(release);
385
+ const ageMs = held?.at ? Date.now() - held.at : Number.POSITIVE_INFINITY;
386
+ let ownerGone = false;
387
+ if (held?.host === mine.host && typeof held.pid === 'number') {
388
+ // Signal 0 probes liveness without delivering anything.
389
+ try {
390
+ process.kill(held.pid, 0);
391
+ }
392
+ catch {
393
+ ownerGone = true;
394
+ }
395
+ }
396
+ if (!ownerGone && ageMs < RELEASE_TOKEN_STALE_MS)
397
+ return false; // a live peer owns it
398
+ try {
399
+ fs.unlinkSync(release);
400
+ }
401
+ catch (error) {
402
+ if (error.code !== 'ENOENT')
403
+ throw error;
404
+ }
405
+ // Exactly one reclaimer wins the re-create; the rest see EEXIST and back off.
406
+ return create();
407
+ }
243
408
  /** True when the block has already been answered. */
244
409
  export function isBlockAnswered(blockId, root) {
245
410
  return fs.existsSync(path.join(answeredDir(root ?? getFeedDir()), `${blockId}.json`));
@@ -277,11 +442,58 @@ export function recordMessageReceipt(blockId, receipt, root) {
277
442
  }
278
443
  block.receipts = receipts;
279
444
  publishBlock(block, dir);
445
+ // The AGENT's own acknowledgement is what resolves a pending claim: `queued`
446
+ // only says a rail took the answer, so it must never remove the card
447
+ // (PHNX-3999).
448
+ //
449
+ // The promotion is bound to the RECEIPT's own origin, not to whatever the
450
+ // block happens to hold now. Checking `block.answer` alone was not enough: if
451
+ // the agent moved to question N+1 AND that ask was itself claimed, a late
452
+ // acknowledgement for question N found a live claim and resolved the wrong
453
+ // question. `confirmAnswerResolution`'s own compare is what rejects it.
454
+ //
455
+ // An UNBOUND receipt never resolves. It cannot say which ask it acknowledges,
456
+ // so promoting it against whatever claim happens to be live would resolve the
457
+ // wrong question -- the exact failure this binding exists to prevent. Its
458
+ // delivery evidence is still recorded above; the card then clears through the
459
+ // ordinary session-advance path instead.
460
+ if ((receipt.status === 'consumed' || receipt.status === 'continued')
461
+ && receipt.generation !== undefined && block.answer) {
462
+ confirmAnswerResolution(blockId, dir, {
463
+ generation: receipt.generation, answeredAt: block.answer.answeredAt,
464
+ });
465
+ }
280
466
  }
281
467
  /** Read the receipt list for a block. */
282
468
  export function getBlockReceipts(blockId, root) {
283
469
  return readBlock(blockId, root)?.receipts ?? [];
284
470
  }
471
+ /**
472
+ * The furthest-along receipt recorded for a block, or undefined when no rail
473
+ * ever reported one. This is the ONLY truthful evidence that an answer reached
474
+ * a delivery rail: an answer marker alone proves a claim was taken, not that
475
+ * anything was delivered, so a caller reporting on a block it did not deliver
476
+ * must read this rather than synthesize a receipt (PHNX-3999).
477
+ */
478
+ export function latestMessageReceipt(blockId, root, origin) {
479
+ const all = getBlockReceipts(blockId, root);
480
+ // A block id is per SESSION, so its receipt list accumulates across every
481
+ // generation of that session's asks. Reading it unfiltered lets question N's
482
+ // receipt answer for question N+1 -- so a caller that knows which ask it is
483
+ // asking about passes the origin and sees only that ask's evidence.
484
+ const receipts = origin ? all.filter((receipt) => receiptMatchesOrigin(receipt, origin)) : all;
485
+ let best;
486
+ for (const receipt of receipts) {
487
+ if (!best) {
488
+ best = receipt;
489
+ continue;
490
+ }
491
+ const rank = RECEIPT_STATUS_RANK[receipt.status] - RECEIPT_STATUS_RANK[best.status];
492
+ if (rank > 0 || (rank === 0 && receipt.at >= best.at))
493
+ best = receipt;
494
+ }
495
+ return best;
496
+ }
285
497
  /** Mark a block as "continued" -- the agent consumed the answer and moved on. */
286
498
  export function recordContinued(blockId, root) {
287
499
  const dir = root ?? getFeedDir();
@@ -0,0 +1,141 @@
1
+ import { FeedHub } from './hub.js';
2
+ import type { FeedWatchEnvelope } from './envelope.js';
3
+ /**
4
+ * Live bytes a reader may leave queued, sustained past {@link HUB_BACKLOG_GRACE_MS},
5
+ * before it is dropped.
6
+ *
7
+ * `socket.write()` never blocks: when a reader stops draining — a stopped
8
+ * process, a suspended laptop, a debugger paused on a breakpoint — node buffers
9
+ * the backlog in the DAEMON's heap, without limit. A busy fleet stream is a few
10
+ * KB per second, so a reader wedged for an hour is tens of megabytes the daemon
11
+ * can never reclaim, and the daemon is the process every other surface depends
12
+ * on. A reader that cannot keep up is dropped loudly instead: it can reconnect
13
+ * and be caught up from held state, which is cheaper than the backlog.
14
+ *
15
+ * The budget is a SUSTAINED condition on queued live events, never a verdict on
16
+ * one envelope or one burst. A cold collector delivers every peer's reset as a
17
+ * live event, thirteen of them inside one tick, and a healthy reader drains
18
+ * that in milliseconds; judging the budget the instant an envelope was written
19
+ * is how a 5 MB fleet reset was cut off after 8 KiB and delivered as one
20
+ * unterminated line (the Menu activation failure this module's writer fixes).
21
+ * The catch-up snapshot is never counted: it is bounded by the held state and is
22
+ * exactly what a fresh reader is waiting for.
23
+ */
24
+ export declare const HUB_CLIENT_BACKLOG_LIMIT: number;
25
+ /**
26
+ * How long a reader may stay past {@link HUB_CLIENT_BACKLOG_LIMIT} before it is
27
+ * dropped. A healthy reader on a unix socket clears the whole budget in well
28
+ * under this; one still over it after this long is not keeping up. The live
29
+ * bytes queued for a reader are therefore bounded by the budget plus what the
30
+ * stream produces in this window; the snapshot and the frame in flight sit
31
+ * outside that figure.
32
+ */
33
+ export declare const HUB_BACKLOG_GRACE_MS = 2000;
34
+ /**
35
+ * How long a reader may leave one chunk unaccepted before it is dropped.
36
+ *
37
+ * A write that returned `false` is a kernel buffer full of bytes the reader has
38
+ * not read. A healthy reader — even one on a busy laptop — clears it in
39
+ * milliseconds; one that has not in this long is not reading at all, and the
40
+ * live-bytes budget alone would let a paused reader hold a large snapshot's
41
+ * remainder in the daemon's heap forever.
42
+ */
43
+ export declare const HUB_DRAIN_STALL_MS = 30000;
44
+ /**
45
+ * Bytes handed to the socket per write. Small enough that a reader's own
46
+ * backpressure (`write()` returning `false`, then `'drain'`) paces a multi-MB
47
+ * snapshot instead of dumping it into the daemon's heap in one copy.
48
+ */
49
+ export declare const HUB_WRITE_CHUNK_BYTES: number;
50
+ /** Optional overrides for the transport bounds. Tests exercise both bounds fast. */
51
+ export type FeedHubLimits = {
52
+ backlogBytes?: number;
53
+ backlogGraceMs?: number;
54
+ drainStallMs?: number;
55
+ chunkBytes?: number;
56
+ };
57
+ /**
58
+ * How long a reader gets to send its scope line before it is REJECTED.
59
+ *
60
+ * The handshake is required, not defaulted. Silently treating a missing or
61
+ * unparseable scope as `fleet` meant a reader that sent nothing — or sent
62
+ * garbage, or sent its line after the grace elapsed — was quietly subscribed to
63
+ * the whole-fleet collector: it started ssh children to every peer on behalf of a
64
+ * client that never asked for them, and delivered peer data to a client that may
65
+ * have wanted only this box. A boundary that guesses is worse than one that
66
+ * refuses, so an unusable handshake is reported and the connection ends.
67
+ */
68
+ export declare const HUB_HANDSHAKE_GRACE_MS = 2000;
69
+ /** The canonical socket path (POSIX) / pipe-name key (Windows). */
70
+ export declare function feedHubSocketPath(): string;
71
+ /** The address a server listens on and a client connects to. */
72
+ export declare function feedHubEndpoint(socketPath?: string): string;
73
+ /**
74
+ * Serve one {@link FeedHub} to other processes. Each accepted connection is one
75
+ * subscriber; closing it detaches, and the last detach stops the fan-out.
76
+ */
77
+ export declare class FeedHubServer {
78
+ private readonly hub;
79
+ private readonly socketPathOverride?;
80
+ private readonly localHub?;
81
+ private server;
82
+ private readonly detachers;
83
+ /** Which collector each reader is attached to, so a failure reaches only its own. */
84
+ private readonly attachedTo;
85
+ private readonly writers;
86
+ private readonly limits;
87
+ /** Readers dropped for queueing live bytes past the budget. Observability. */
88
+ droppedForBacklog: number;
89
+ /** Readers dropped for not draining a chunk within the stall deadline. Observability. */
90
+ droppedForStall: number;
91
+ /** Readers refused for a missing, invalid, or late scope line. Observability. */
92
+ rejectedHandshakes: number;
93
+ /**
94
+ * @param hub the FLEET collector (every reachable peer plus this box).
95
+ * @param localHub the LOCAL-only collector, served to a reader that asks for
96
+ * `scope: 'local'`. Optional: a server without one answers
97
+ * every reader from the fleet hub, which is what the fleet
98
+ * stream already contained.
99
+ */
100
+ constructor(hub: FeedHub, socketPathOverride?: string | undefined, localHub?: FeedHub | undefined, limits?: FeedHubLimits);
101
+ /** Report a collector failure to its readers and end those connections. */
102
+ private failReaders;
103
+ /** Subscribers currently connected. Observability + tests. */
104
+ get clientCount(): number;
105
+ /** Bytes queued for every reader and not yet handed to a socket. Observability + tests. */
106
+ get pendingBytes(): number;
107
+ start(): Promise<void>;
108
+ stop(): Promise<void>;
109
+ }
110
+ /**
111
+ * Wait until the hub accepts a connection, or the deadline passes.
112
+ *
113
+ * `ensureDaemonStarted()` returns as soon as the daemon PROCESS is spawned, which
114
+ * is well before that process has loaded its services and bound this socket.
115
+ * Retrying immediately therefore raced the bind and failed on a daemon that was
116
+ * about to be perfectly healthy — reported to the operator as "the shared feed
117
+ * stream is unavailable". Resolves true once a connect succeeds.
118
+ */
119
+ export declare function waitForHub(endpoint?: string, deadlineMs?: number, intervalMs?: number): Promise<boolean>;
120
+ /**
121
+ * Read the shared stream from the hub until `signal` aborts.
122
+ *
123
+ * Rejects when the hub is not reachable. That is deliberate: a client that
124
+ * quietly ran its own `watchFleetFeed` instead would restore the per-caller
125
+ * ssh fan-out, so the caller starts the daemon and retries rather than
126
+ * degrading into the thing this replaced.
127
+ *
128
+ * Also rejects on any close the caller did not ask for. The stream has no end
129
+ * of its own — the hub serves it until the reader leaves — so a FIN that
130
+ * arrives before `signal` aborts is the hub refusing, failing, or dropping this
131
+ * reader, and a FIN inside a line is a frame the hub never finished. Resolving
132
+ * there let `agents feed watch --json` exit 0 after 8 KiB of a 5 MB reset,
133
+ * which is indistinguishable from an empty fleet.
134
+ */
135
+ export declare function streamFeedFromHub(options: {
136
+ signal: AbortSignal;
137
+ emit: (event: FeedWatchEnvelope) => void;
138
+ endpoint?: string;
139
+ /** Which collector to attach to. Defaults to the whole fleet. */
140
+ scope?: 'fleet' | 'local';
141
+ }): Promise<void>;