@sleep2agi/agent-network 2.3.0-preview.9 → 2.3.0-preview.90

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 (113) hide show
  1. package/README.md +35 -324
  2. package/dist/bin/anet.cjs +101 -0
  3. package/dist/bin/cli.d.ts +1 -0
  4. package/dist/bin/cli.js +20 -1
  5. package/dist/bin/goal-wake-log-render.d.ts +31 -0
  6. package/dist/src/active-network-task.d.ts +28 -0
  7. package/dist/src/batch-workdir.d.ts +9 -0
  8. package/dist/src/bootstrap-password-db.d.ts +13 -0
  9. package/dist/src/channel-attachments.d.ts +29 -0
  10. package/dist/src/channel-meta.d.ts +10 -0
  11. package/dist/src/channel-task-trace.d.ts +14 -0
  12. package/dist/src/claude-vendor-env.d.ts +28 -0
  13. package/dist/src/cli-args.d.ts +8 -0
  14. package/dist/src/cli-version-display.d.ts +16 -0
  15. package/dist/src/client-task-trace.d.ts +9 -0
  16. package/dist/src/client.js +1 -1
  17. package/dist/src/codex-copresence-preflight.d.ts +65 -0
  18. package/dist/src/codex-copresence-profile.d.ts +125 -0
  19. package/dist/src/codex-copresence-recovery.d.ts +43 -0
  20. package/dist/src/codex-copresence-thread.d.ts +13 -0
  21. package/dist/src/codex-lifecycle-account.d.ts +104 -0
  22. package/dist/src/codex-lifecycle-canary.d.ts +20 -0
  23. package/dist/src/codex-lifecycle-facts.d.ts +53 -0
  24. package/dist/src/codex-lifecycle-fork.d.ts +53 -0
  25. package/dist/src/codex-lifecycle-preflight.d.ts +77 -0
  26. package/dist/src/codex-lifecycle-receipt.d.ts +44 -0
  27. package/dist/src/codex-lifecycle-restart.d.ts +86 -0
  28. package/dist/src/codex-model-default.d.ts +6 -0
  29. package/dist/src/codex-tui-client-health.d.ts +17 -0
  30. package/dist/src/commhub-response.d.ts +1 -0
  31. package/dist/src/controlled-upload.d.ts +83 -0
  32. package/dist/src/copresence-deps.d.ts +22 -0
  33. package/dist/src/copresence-identity.d.ts +339 -0
  34. package/dist/src/copresence-startup-diagnosis.d.ts +17 -0
  35. package/dist/src/daemon-capability-display.d.ts +110 -0
  36. package/dist/src/daemon-capability-display.js +21 -0
  37. package/dist/src/daemon-runtime-path-preflight.d.ts +22 -0
  38. package/dist/src/daemon-runtime-staleness.d.ts +13 -0
  39. package/dist/src/dashboard-managed-process.d.ts +35 -0
  40. package/dist/src/display-width.d.ts +27 -0
  41. package/dist/src/doctor-node-count.d.ts +28 -0
  42. package/dist/src/environ-alias.d.ts +14 -0
  43. package/dist/src/grok-attach-client.d.ts +129 -0
  44. package/dist/src/grok-build-drift.d.ts +15 -0
  45. package/dist/src/grok-copresence-disclosure.d.ts +11 -0
  46. package/dist/src/grok-copresence-orchestration.d.ts +75 -0
  47. package/dist/src/grok-copresence-profile.d.ts +65 -0
  48. package/dist/src/host-telemetry.d.ts +14 -0
  49. package/dist/src/hub-time-display.d.ts +20 -0
  50. package/dist/src/hub-version-skew.d.ts +19 -0
  51. package/dist/src/im/correlation-store.d.ts +34 -0
  52. package/dist/src/im/feishu/adapter.d.ts +80 -0
  53. package/dist/src/im/feishu/bridge.d.ts +85 -14
  54. package/dist/src/im/feishu/config.d.ts +31 -0
  55. package/dist/src/im/feishu/hub-upload.d.ts +88 -0
  56. package/dist/src/im/feishu/outbound-marker.d.ts +24 -19
  57. package/dist/src/im/feishu/outbound-paths.d.ts +50 -0
  58. package/dist/src/im/feishu/outbound-route.d.ts +62 -0
  59. package/dist/src/im/feishu/worker-lifecycle.d.ts +13 -0
  60. package/dist/src/im/feishu/worker.js +187 -144
  61. package/dist/src/im/types.d.ts +38 -1
  62. package/dist/src/local-process-state.d.ts +12 -0
  63. package/dist/src/locale-diagnostic.d.ts +12 -0
  64. package/dist/src/ls-columns.d.ts +15 -0
  65. package/dist/src/mock-llm.d.ts +12 -0
  66. package/dist/src/node-activity-log.d.ts +4 -0
  67. package/dist/src/node-not-found.d.ts +1 -0
  68. package/dist/src/node-running-guard.d.ts +9 -0
  69. package/dist/src/node-server-payload.d.ts +66 -0
  70. package/dist/src/node-server.js +7 -1
  71. package/dist/src/normalize-runtime.d.ts +29 -2
  72. package/dist/src/offline-age.d.ts +29 -0
  73. package/dist/src/one-line-cell.d.ts +20 -0
  74. package/dist/src/opencode-agent-node-pair.d.ts +25 -0
  75. package/dist/src/opencode-auth-login.d.ts +43 -0
  76. package/dist/src/opencode-launch-env.d.ts +13 -0
  77. package/dist/src/opencode-owner-mode.d.ts +6 -0
  78. package/dist/src/opencode-package-binary.d.ts +26 -0
  79. package/dist/src/opencode-pin.d.ts +44 -0
  80. package/dist/src/opencode-preset.d.ts +73 -0
  81. package/dist/src/opencode-runtime-binding.d.ts +30 -0
  82. package/dist/src/opencode-safe-root.d.ts +36 -0
  83. package/dist/src/opencode-smoke-env.d.ts +1 -0
  84. package/dist/src/outbound-attachments.d.ts +57 -0
  85. package/dist/src/outbound-tool-names.d.ts +1 -0
  86. package/dist/src/owned-roots.d.ts +58 -0
  87. package/dist/src/owner-env-file.d.ts +2 -0
  88. package/dist/src/package-mode-preflight.d.ts +41 -0
  89. package/dist/src/posix-codex-copresence.d.ts +8 -0
  90. package/dist/src/posix-modes.d.ts +36 -0
  91. package/dist/src/primary-network.d.ts +23 -0
  92. package/dist/src/private-state.d.ts +10 -0
  93. package/dist/src/process-telemetry.d.ts +12 -0
  94. package/dist/src/profile-serialize.d.ts +1 -0
  95. package/dist/src/project-key.d.ts +1 -0
  96. package/dist/src/reply-originator.d.ts +20 -0
  97. package/dist/src/resume-runtime-infer.d.ts +18 -0
  98. package/dist/src/secret-shell-guidance.d.ts +3 -0
  99. package/dist/src/session-status-class.d.ts +48 -0
  100. package/dist/src/sibling-agent-node.d.ts +23 -0
  101. package/dist/src/stale-socket.d.ts +118 -0
  102. package/dist/src/subcommand-redirect.d.ts +26 -0
  103. package/dist/src/task-trace.d.ts +33 -0
  104. package/dist/src/tmux-attach.d.ts +8 -0
  105. package/dist/src/tmux-capability.d.ts +58 -0
  106. package/dist/src/tmux-exact-target.d.ts +34 -0
  107. package/dist/src/tmux-pane-prompt.d.ts +22 -0
  108. package/dist/src/token-cli.d.ts +13 -0
  109. package/dist/src/tool-allowlist.d.ts +28 -0
  110. package/dist/src/unsafe-package-path-reason.d.ts +17 -0
  111. package/dist/src/win-launcher.d.ts +20 -0
  112. package/dist/src/windows-codex-copresence.d.ts +60 -0
  113. package/package.json +10 -5
@@ -0,0 +1,339 @@
1
+ export interface SessionInfo {
2
+ tmux: string;
3
+ pid: number;
4
+ pgid: number;
5
+ starttime_jiffies: number;
6
+ }
7
+ export interface CopresenceMarker {
8
+ marker: string;
9
+ boot_id: string;
10
+ started_at_epoch_ms: number;
11
+ owner_uid: number;
12
+ sessions: {
13
+ appsrv?: SessionInfo;
14
+ bridge?: SessionInfo;
15
+ tui?: SessionInfo;
16
+ };
17
+ }
18
+ export type RefuseCause = "MISSING" | "SYMLINK" | "NOT_REGULAR" | "WRONG_MODE" | "OWNER_MISMATCH" | "PARSE_ERROR" | "SCHEMA_INVALID" | "STALE_BOOT_ID" | "PLATFORM_UNSUPPORTED";
19
+ export type ReadMarkerResult = {
20
+ kind: "ok";
21
+ marker: CopresenceMarker;
22
+ } | {
23
+ kind: "refuse";
24
+ cause: RefuseCause;
25
+ detail: string;
26
+ };
27
+ export interface ProcStat {
28
+ pgid: number;
29
+ starttime_jiffies: number;
30
+ ppid: number;
31
+ }
32
+ /**
33
+ * Injectable primitive for /proc access. Real impl reads the filesystem;
34
+ * unit tests inject a mock so /proc-based logic is deterministically testable.
35
+ *
36
+ * Contract:
37
+ * - listAllPids: read /proc directory and return numeric-name entries.
38
+ * Throws on unrecoverable errors (permission denied on /proc itself).
39
+ * - readEnviron: read /proc/<pid>/environ as a raw string. Returns null
40
+ * if the pid has vanished (ENOENT) — that's a normal race, not a failure.
41
+ * Throws for permission errors etc.
42
+ * - readStat: read /proc/<pid>/stat and extract (pgid, starttime, ppid).
43
+ * Returns null on ENOENT (pid gone). Throws on other errors.
44
+ */
45
+ export interface ProcessEnumerator {
46
+ listAllPids(): number[];
47
+ readEnviron(pid: number): string | null;
48
+ readStat(pid: number): ProcStat | null;
49
+ /**
50
+ * REAL uid of the process itself (`Uid:` line of /proc/<pid>/status,
51
+ * field 0). Used by the environ-EACCES discriminator to tell
52
+ * "other-user process (expected EACCES)" from "our own process we could
53
+ * not inspect (must be accounted for)".
54
+ *
55
+ * MUST NOT be implemented as `statSync('/proc/<pid>/environ').uid`.
56
+ * A process that called prctl(PR_SET_DUMPABLE, 0) keeps its real uid but
57
+ * its /proc/<pid>/{environ,mem,...} nodes flip to root:root 0400 (see
58
+ * proc(5) / kernel `task_dump_owner`). Deriving ownership from the
59
+ * environ inode therefore reports uid 0 for our OWN non-dumpable
60
+ * children, which made them invisible to the scan: not in `hits`, not in
61
+ * the unreadable list, so teardown reported success and deleted the
62
+ * marker while the orphan lived on (Defect A).
63
+ *
64
+ * Returns null if the pid is gone (ENOENT) or if even /proc/<pid>/status
65
+ * is blocked (hidepid / LSM) — in that case the process cannot be ours
66
+ * to reason about and is skipped.
67
+ */
68
+ readOwnerUid(pid: number): number | null;
69
+ /**
70
+ * Single-character process state from /proc/<pid>/status (R/S/D/Z/T/...).
71
+ * `Z` means zombie — mm freed, environ returns EACCES even to owner, but
72
+ * the pid is going away shortly. Skip zombies during environ scan.
73
+ * Returns null if the pid is gone (ENOENT).
74
+ */
75
+ readState(pid: number): string | null;
76
+ }
77
+ /**
78
+ * Injectable primitive for sending signals. Real impl uses process.kill;
79
+ * tests inject a mock.
80
+ */
81
+ export interface KillPrimitive {
82
+ killPgroup(pgid: number, signal: "TERM" | "KILL"): void;
83
+ /** Returns true if ANY process in the pgroup is still alive. */
84
+ pgroupAlive(pgid: number): boolean;
85
+ }
86
+ export declare function markerFilePath(nodesDir: string, nodeId: string): string;
87
+ export declare function readBootId(): string;
88
+ /**
89
+ * Write the marker file atomically.
90
+ *
91
+ * IMPORTANT (Blocker 1 fix): the uuid parameter is the SINGLE SOURCE OF
92
+ * TRUTH. Caller is expected to inject this same uuid into every tmux
93
+ * session's ANET_NODE_MARKER env var. writeMarker does NOT generate its
94
+ * own uuid — that was the exact bug in 9f2ec282.
95
+ */
96
+ export declare function writeMarker(nodesDir: string, nodeId: string, uuid: string, sessions: CopresenceMarker["sessions"]): CopresenceMarker;
97
+ /**
98
+ * Read the marker file with structured fail-closed refuses.
99
+ *
100
+ * Never throws for expected refuse causes. Only throws if the OS itself
101
+ * is broken (e.g. EIO). Malformed JSON, wrong types, null bodies etc.
102
+ * all return { kind: "refuse", cause, detail }.
103
+ */
104
+ export declare function readMarker(nodesDir: string, nodeId: string): ReadMarkerResult;
105
+ export declare function removeMarker(nodesDir: string, nodeId: string): void;
106
+ export declare function realEnumerator(): ProcessEnumerator;
107
+ export declare function realKiller(): KillPrimitive;
108
+ /**
109
+ * Scan /proc for all pids whose environ contains ANET_NODE_MARKER=<uuid>.
110
+ * This is the authoritative identity source — NOT the marker file's stored
111
+ * pids, which may be stale (main died, workers survived under new pgids).
112
+ *
113
+ * Bytes format of /proc/PID/environ: NUL-separated key=value pairs.
114
+ *
115
+ * Blocker 1 fix (independent audit 92d53c8f, 2026-07-29): /proc/<pid>/environ
116
+ * is 0400 owner-only. Blindly reading it will hit EACCES on every other-user
117
+ * process (pid 1 is systemd/root → guaranteed EACCES). The naive fix — wrap
118
+ * in try/catch and continue — would silently miss marker-carrying processes
119
+ * whose environ we can't read for some other reason, recreating Defect A
120
+ * ("nothing killed but report success"). Right fix:
121
+ *
122
+ * 1. Try readEnviron.
123
+ * 2. On EACCES, discriminate via readOwnerUid + readState:
124
+ * - Owner uid != ours → EXPECTED skip (can't be one of our procs).
125
+ * - Own uid, state = Z → EXPECTED skip (zombie, mm freed).
126
+ * - Own uid, not zombie → FAIL-CLOSED (unexplained EACCES on our own
127
+ * live process is a real problem, must throw).
128
+ */
129
+ export interface ScanResult {
130
+ /** Pids whose environ we successfully read and matched the marker uuid. */
131
+ hits: number[];
132
+ /**
133
+ * Own-uid, live, environ-unreadable pids that are PLAUSIBLY PART OF THE
134
+ * COPRESENCE TREE (invariant 11 scope test). These might be marker-
135
+ * carrying without us being able to prove it, so the reap flow REFUSES
136
+ * marker removal while this list is non-empty — defense against Defect A
137
+ * (silently missing a marker-carrying process, then deleting the marker
138
+ * as if teardown had succeeded).
139
+ */
140
+ unreadableOwnUid: number[];
141
+ /**
142
+ * Own-uid, live, environ-unreadable pids that failed the scope test:
143
+ * their pgid belongs to no marker-carrying group and their ppid chain
144
+ * reaches no marker carrier / validated marker-file session pid.
145
+ *
146
+ * INFORMATIONAL ONLY — never fail-closed. A machine routinely has such
147
+ * processes (a same-uid process whose primary GID differs from ours fails
148
+ * the kernel's __ptrace_may_access gid check and EACCESes on environ even
149
+ * though its real uid matches). v2 lumped these into unreadableOwnUid,
150
+ * which made reapMarkerGroups structurally unable to return success on
151
+ * any such host.
152
+ *
153
+ * KNOWN RESIDUAL GAP (stated rather than papered over): a marker-carrying
154
+ * process that is BOTH non-dumpable (environ unreadable) AND fully
155
+ * detached (setsid'd into its own pgroup with ppid=1, no live recorded
156
+ * session pid above it) lands here and is therefore not reaped and does
157
+ * not block marker removal. Nothing in /proc exposes the environment of a
158
+ * non-dumpable task to a non-root reader, so no scope widening can
159
+ * recover it — only running teardown as root could. Widening scope to
160
+ * "every same-uid unreadable process" is NOT an acceptable trade: that is
161
+ * exactly the v2 behaviour that made teardown never succeed. The
162
+ * realistic copresence shape (child of a recorded pane pid, or sharing a
163
+ * marker carrier's pgroup) IS covered — see the real-/proc test.
164
+ */
165
+ unreadableOutOfScope: number[];
166
+ }
167
+ /**
168
+ * Marker-file session records used to anchor the invariant-11 scope test.
169
+ * Each entry must be re-validated (pid alive AND same starttime) before it
170
+ * is trusted — see validateAnchors.
171
+ */
172
+ export interface ScanAnchors {
173
+ sessions?: Array<{
174
+ pid: number;
175
+ starttime_jiffies: number;
176
+ }>;
177
+ }
178
+ export declare function anchorsFromMarker(marker: CopresenceMarker): ScanAnchors;
179
+ /**
180
+ * Invariant 11 scope test: is `pid` plausibly part of the copresence tree?
181
+ *
182
+ * True when the pid IS an anchor, when its pgid belongs to a marker-carrying
183
+ * group (or a validated anchor's group), or when walking its ppid chain
184
+ * reaches an anchor / marker carrier / a pid inside a relevant pgroup.
185
+ *
186
+ * Everything else is out of scope and must not participate in a fail-closed
187
+ * decision, no matter how unreadable it is.
188
+ */
189
+ export declare function isInCopresenceScope(enumer: ProcessEnumerator, pid: number, relevantPids: Set<number>, relevantPgids: Set<number>): boolean;
190
+ export declare function scanEnvironForMarker(enumer: ProcessEnumerator, markerUuid: string): number[];
191
+ export declare function scanEnvironForMarkerFull(enumer: ProcessEnumerator, markerUuid: string, anchors?: ScanAnchors): ScanResult;
192
+ export declare function groupPidsByPgid(enumer: ProcessEnumerator, pids: number[]): Map<number, number[]>;
193
+ export type HomogeneityResult = {
194
+ ok: true;
195
+ members: number[];
196
+ } | {
197
+ ok: false;
198
+ cause: "FOREIGN_MEMBER" | "ENUM_ERROR" | "EMPTY_GROUP";
199
+ foreignPids: number[];
200
+ unreadablePids: number[];
201
+ detail: string;
202
+ };
203
+ /**
204
+ * Verify that EVERY live member of `pgid` carries the marker.
205
+ *
206
+ * Blocker 2 fix: enumeration errors surface as ENUM_ERROR (fail-closed),
207
+ * never as an empty-list judged "safe". Any unreadable member also
208
+ * fails-closed. Empty group (no live members at all) also refuses — killing
209
+ * an empty pgroup can't be right and empty-list judged "ok" recreates the
210
+ * exact defect shape (independent audit 92d53c8f finding #6).
211
+ *
212
+ * Blocker 2 second half (zombie): a zombie same-uid process's environ
213
+ * returns EACCES even to owner (mm freed), but stat still exists with
214
+ * original pgid. Without discrimination, the zombie would be treated as
215
+ * "unreadable member" → ENUM_ERROR → entire group SKIPPED. Since re-verify
216
+ * happens post-SIGTERM (when zombies are most numerous), teardown would
217
+ * never escalate. Fix: apply the same owner-uid + state=Z discriminator as
218
+ * scanEnvironForMarker.
219
+ *
220
+ * Algorithm:
221
+ * 1. Enumerate all pids on the system.
222
+ * 2. Filter to pids whose /proc/PID/stat pgid == target pgid.
223
+ * 3. For each such pid, check its environ with EACCES discrimination:
224
+ * - Other-user EACCES → skip (can't be ours anyway).
225
+ * - Own-uid zombie → skip (expected, dying).
226
+ * - Own-uid live EACCES → truly unreadable → fail-closed.
227
+ * - Missing marker → foreign member → fail-closed.
228
+ */
229
+ export declare function verifyGroupHomogeneity(enumer: ProcessEnumerator, pgid: number, markerUuid: string): HomogeneityResult;
230
+ /**
231
+ * Walk the caller's ancestry (self → PPID → grandparent → ...) and check
232
+ * whether any ancestor carries the target marker. If yes → caller is
233
+ * running inside the copresence tree we're about to kill, and would take
234
+ * itself down.
235
+ */
236
+ export declare function callerCarriesMarker(enumer: ProcessEnumerator, markerUuid: string): {
237
+ self: boolean;
238
+ ancestorPid?: number;
239
+ };
240
+ export type ReapResult = {
241
+ kind: "success";
242
+ killedPgids: number[];
243
+ residualPids: number[];
244
+ unreadableOwnUid?: number[];
245
+ } | {
246
+ kind: "failed";
247
+ killedPgids: number[];
248
+ residualPids: number[];
249
+ skippedGroups: Array<{
250
+ pgid: number;
251
+ reason: string;
252
+ }>;
253
+ detail: string;
254
+ unreadableOwnUid?: number[];
255
+ };
256
+ export interface ReapOptions {
257
+ graceMs: number;
258
+ logger: (msg: string) => void;
259
+ /**
260
+ * Sleep primitive (grace period). Real code uses setTimeout; tests inject
261
+ * a fast/deterministic sleep so grace paths are actually covered. Prior
262
+ * impl was a busy-wait `while (Date.now() < end) {}` — audit 92d53c8f
263
+ * finding #3 flagged that as pinning one CPU core for graceMs and blocking
264
+ * the event loop; test coverage missed it because tests passed graceMs=0.
265
+ */
266
+ sleep?: (ms: number) => Promise<void>;
267
+ /**
268
+ * Invariant-11 scope anchors, normally `anchorsFromMarker(marker)`.
269
+ *
270
+ * Without them the only anchors are the marker carriers the scan itself
271
+ * found, so a marker-carrying process we cannot READ (non-dumpable) but
272
+ * whose parent IS a recorded session pid would be judged out of scope and
273
+ * silently dropped. Passing the marker's recorded session pids closes that
274
+ * hole; each one is re-validated (alive + same starttime) before it can
275
+ * widen scope, so a stale/recycled pid cannot.
276
+ */
277
+ anchors?: ScanAnchors;
278
+ }
279
+ /**
280
+ * Full reap flow with TOCTOU re-verification.
281
+ *
282
+ * Sequence:
283
+ * 1. Environ scan for marker → get all live pids carrying it.
284
+ * 2. Group by current PGID.
285
+ * 3. For each group: verify homogeneity. Skip on foreign/unreadable.
286
+ * 4. For each verified group: send SIGTERM to pgroup.
287
+ * 5. Wait grace.
288
+ * 6. Re-verify each group (marker still there, no new foreign) before SIGKILL.
289
+ * 7. Send SIGKILL to any group still alive.
290
+ * 8. Post-rescan: if any marker-carrying pid alive → preserve marker.
291
+ */
292
+ export declare function reapMarkerGroups(enumer: ProcessEnumerator, killer: KillPrimitive, markerUuid: string, opts: ReapOptions): Promise<ReapResult>;
293
+ export interface PrepareStartDeps {
294
+ /** Usually () => readMarker(nodesDir, nodeId). */
295
+ readMarker(): ReadMarkerResult;
296
+ /** Usually (uuid, anchors) => reapMarkerGroups(realEnumerator(), realKiller(), uuid, {...anchors}). */
297
+ reap(uuid: string, anchors: ScanAnchors): Promise<ReapResult>;
298
+ /** Usually () => removeMarker(nodesDir, nodeId). */
299
+ removeMarker(): void;
300
+ /** Usually (uuid, sessions) => writeMarker(nodesDir, nodeId, uuid, sessions). */
301
+ writeMarker(uuid: string, sessions: CopresenceMarker["sessions"]): void;
302
+ logger(msg: string): void;
303
+ }
304
+ export type PrepareStartResult = {
305
+ kind: "ok";
306
+ reclaimedUuid?: string;
307
+ } | {
308
+ kind: "blocked";
309
+ cause: "STALE_TREE_ALIVE" | "UNUSABLE_MARKER";
310
+ detail: string;
311
+ remedy: string;
312
+ };
313
+ /**
314
+ * Everything that must happen BEFORE the first `tmux new-session` of a
315
+ * copresence start. Extracted out of cli.ts so both blockers below are
316
+ * actually reachable by tests — the audit's point that the two fatal
317
+ * defects of the previous rounds both lived in an untested cli.ts seam.
318
+ *
319
+ * Blocker 5 — marker-before-tmux ordering.
320
+ * v2 wrote the marker only after the app-server had bound its port, i.e.
321
+ * after a 25s wait and after several `process.exit(1)` paths. A start that
322
+ * died in that window left a live, marker-carrying tmux session behind
323
+ * with NO marker file on disk: the exact unreclaimable-ghost this feature
324
+ * exists to prevent. The marker is now written here, before any session is
325
+ * created, with an empty `sessions` object — reap identity has always been
326
+ * the environ uuid, never the recorded pids, so an empty sessions object
327
+ * loses nothing but observability hints (which the start path fills in
328
+ * later, best-effort).
329
+ *
330
+ * Blocker 6 — never overwrite a preserved marker.
331
+ * A failed stop deliberately PRESERVES the marker so the next stop can
332
+ * retry idempotently. v2's start then overwrote it with a fresh uuid while
333
+ * only killing tmux sessions BY NAME, so the still-running subprocesses of
334
+ * the previous instance became permanently unreclaimable: their uuid was
335
+ * gone from disk forever. Now the old identity is reaped FIRST; if that
336
+ * reap does not fully succeed we refuse to start rather than destroy the
337
+ * only handle on those processes.
338
+ */
339
+ export declare function prepareIdentityForStart(newUuid: string, deps: PrepareStartDeps): Promise<PrepareStartResult>;
@@ -0,0 +1,17 @@
1
+ export interface CopresenceStartupFailure {
2
+ /** 等的那个 attach launcher 路径。 */
3
+ attachScript: string;
4
+ /** bridge 输出的落盘文件(可能还不存在——bridge 可能压根没起来)。 */
5
+ bridgeLog: string;
6
+ /** 节点自己的日志目录。 */
7
+ nodeLogDir: string;
8
+ /** 判定失败的那一刻,bridge 的 tmux 会话还在不在。 */
9
+ bridgeAlive: boolean;
10
+ /** 等了多少秒。 */
11
+ waitedSeconds: number;
12
+ /** 落盘日志的尾巴(读不到就给空串)。 */
13
+ logTail: string;
14
+ /** tmux pane 的尾巴;会话已死时必然是空。 */
15
+ paneTail: string;
16
+ }
17
+ export declare function describeCopresenceStartupFailure(f: CopresenceStartupFailure): string[];
@@ -0,0 +1,110 @@
1
+ /** hub `/api/host-supervisors` 里与本主题相关的那几格。全部可缺席。 */
2
+ export type DaemonCapabilityRow = {
3
+ readonly alias?: string;
4
+ readonly online?: boolean;
5
+ /** hub 侧的最后一次心跳(ISO 串或 epoch ms)。缺席则年龄无法计算。 */
6
+ readonly last_seen_at?: string | number | null;
7
+ readonly can_create_nodes?: boolean;
8
+ readonly create_nodes_blocked_reason?: string;
9
+ /** #1545 —— 上面那一格是**这份 report 发出前多少毫秒**测的。 */
10
+ readonly create_capability_observed_ms_ago?: number;
11
+ };
12
+ export type CapabilityView =
13
+ /** hub 上根本没有这一格 —— 旧 daemon(agent-node ≤ 2.5.0-preview.54)不上报。 */
14
+ {
15
+ readonly kind: "never-reported";
16
+ readonly line: string;
17
+ }
18
+ /** 报了 ready/blocked,且知道是多久以前测的。 */
19
+ | {
20
+ readonly kind: "ready" | "blocked";
21
+ readonly ageMs: number;
22
+ readonly line: string;
23
+ readonly fix?: CapabilityFix;
24
+ }
25
+ /** 报了 ready/blocked,但**不知道是什么时候测的**(旧 daemon 开机只算一次)。 */
26
+ | {
27
+ readonly kind: "ready-age-unknown" | "blocked-age-unknown";
28
+ readonly line: string;
29
+ readonly fix?: CapabilityFix;
30
+ };
31
+ /** 🔴 修法**按 code 给,不按上游的 detail 给**。
32
+ *
33
+ * 上游 `unsafePathHelp()` 生成的 `Fix:` 那半带完整机器路径,而它**按设计不上报**
34
+ * (那条会一路走到 Dashboard,「哪台机器的哪个路径缺什么」本身就是一张地图)。
35
+ * 所以跨机器读到的只有 code,这里给的必然是**不含机器路径的通用修法**。
36
+ *
37
+ * 🔴 四类的修法完全不同,混成一句会让人修错方向 —— 2026-08-28 有人一天里撞到
38
+ * 其中两类,看到的是同一条错误,第一反应跑去建 /etc/anet-daemon/path.conf,
39
+ * 而那次实际只要 chmod。
40
+ *
41
+ * 想要**带真实路径的那一份**,只能在那台机器上看它自己的 daemon 日志 ——
42
+ * 下面每条都会把人指过去。 */
43
+ export type CapabilityFix = {
44
+ /** 人读的一句话:这一类是什么、为什么会这样。 */
45
+ readonly explain: string;
46
+ /** 🔴 **可以整行粘进终端**的命令,或 null(这一类没有单条命令能修)。
47
+ *
48
+ * 分成两格而不是拼成一句,是因为拼起来之后**没有一条能通过 `bash -n`** ——
49
+ * 中文说明和括号注释会被 shell 当成语法。#1521 修过完全同一个形状:
50
+ * 当时 anet_bin_source 的 Fix 串 `bash -n` rc=2(`$( )` 里的 `\"` 是语法错),
51
+ * 用户粘进去只看到 `syntax error near unexpected token '('`。
52
+ * **一条"看起来是命令"的说明,比没有命令更糟。** */
53
+ readonly command: string | null;
54
+ };
55
+ /** 把毫秒渲染成人读的相对时间。**不做四舍五入到"刚刚"** ——
56
+ * 这一格存在的意义就是分辨新鲜和陈旧,含糊化等于把它的功能删掉。 */
57
+ export declare function formatAge(ms: number): string;
58
+ export declare function describeCapability(row: DaemonCapabilityRow, nowMs: number): CapabilityView;
59
+ /** 取 `/api/host-supervisors` 失败时的**原因**。以前这五种共用一个 `null`。 */
60
+ export type CapabilityFetchFailure = {
61
+ readonly why: "no-hub";
62
+ } | {
63
+ readonly why: "unauthorized";
64
+ readonly status: number;
65
+ } | {
66
+ readonly why: "http";
67
+ readonly status: number;
68
+ } | {
69
+ readonly why: "bad-body";
70
+ } | {
71
+ readonly why: "unreachable";
72
+ readonly detail?: string;
73
+ };
74
+ /** 每一种都必须说出**两件事**:到底是什么挡住了,以及下一步该敲什么。
75
+ * 尾巴统一带上「本地清单不需要网络」—— 那是这条命令没有整个失败的原因,
76
+ * 用户看到「查不到」时最先怕的就是"上面那些是不是也不可信了"。 */
77
+ export declare function describeFetchFailure(f: CapabilityFetchFailure): string;
78
+ /**
79
+ * `create_node` 被拒时 hub 返回的形状(`server/src/tools.ts` 的
80
+ * `daemon_cannot_create_nodes` 分支)。
81
+ *
82
+ * 🔴 它和 `DaemonCapabilityRow` **不是同一个形状**,所以单独一个入口 ——
83
+ * 在调用方做一次字段映射,等于开出**第二个理解这些字段含义的地方**。
84
+ */
85
+ export type CreateRejectionPayload = {
86
+ readonly error?: string;
87
+ readonly blocked_reason?: string;
88
+ /** 🔴 已经是**到现在为止**的绝对年龄(hub 在拒绝时把心跳那一段补上了)。
89
+ * null = 补不出来 —— 见 `capability_age`。 */
90
+ readonly capability_observed_ms_ago?: number | null;
91
+ readonly capability_age?: string;
92
+ };
93
+ /**
94
+ * 把拒绝载荷渲染成人能读的一段。不是这类拒绝就返回 null,调用方走它原来的通用文案。
95
+ *
96
+ * 🔴 **这里没有第二份年龄公式。** `describeCapability` 里那段
97
+ * `(now − last_seen_at) + observed` 是给「列 daemon」那条路用的;
98
+ * 拒绝这条路上,**hub 已经算好了绝对年龄**(它手里有心跳时间)。
99
+ * 所以这里只做格式化,复用同一个 `formatAge`。
100
+ * 两份年龄算法一旦并排存在,出现分歧时不会有任何东西红。
101
+ *
102
+ * 🔴 **没有新增第六种 kind**,这是刻意的:
103
+ * `kind` 是**呈现分桶**(禁不禁用、什么色调),不是完整语义。
104
+ * 「daemon 没报年龄」和「hub 没有它的心跳时间」在 UI 上要的处理**完全一样**
105
+ * (都是 blocked、都给不出年龄),差别在**说哪句话** —— 那放在 `line` 里。
106
+ * 反过来,给共享联合加第六个值,会让所有既有消费者的映射多一个
107
+ * 它们没写过的分支,而**落进 default 的那一支通常是"看起来最正常"的那个**。
108
+ * 区分没有丢:两种 unknown 的 `line` 逐字不同,并有测试钉住。
109
+ */
110
+ export declare function describeCreateRejection(payload: CreateRejectionPayload): CapabilityView | null;
@@ -0,0 +1,21 @@
1
+ var J=Object.freeze({anet_bin_identity:{explain:"该路径不是 anet 的包内 bin(若设过 ANET_BIN_ABS,先 unset 再重装)",command:"npm i -g @sleep2agi/agent-network"},anet_bin_source:{explain:"pin 没有可信来源。在该机器上写信任根 —— 两段都要 sudo,少一个 && 就会断链",command:`sudo install -d -m 755 /etc/anet-daemon && printf 'ANET_BIN_ABS=%s
2
+ ' "$(command -v anet)" | sudo tee /etc/anet-daemon/path.conf`},anet_bin_permission:{explain:"该二进制 group/other 可写。一行就能修",command:'chmod go-w "$(command -v anet)"'},anet_bin_shape:{explain:"pin 的路径形态不合法(相对路径,或路径里含软链)。取它的 realpath 重新写进 path.conf",command:'readlink -f "$(command -v anet)"'},anet_bin_unknown:{explain:"daemon 没能给出类别(例如安装后二进制被换过、校验和不符)。只能看该机器的 daemon 日志取原文",command:null},anet_bin_pin_unresolved:{explain:"旧版 daemon(agent-node 2.5.0-preview.40 及更早)只报这一个笼统原因。升级该机器的 agent-node 并重启 daemon 后才能拿到具体类别",command:null}});function G(c){if(!Number.isFinite(c)||c<0)return"?";if(c<1000)return`${Math.round(c)}ms 前`;let j=Math.floor(c/1000);if(j<60)return`${j}s 前`;let k=Math.floor(j/60);if(k<60)return`${k}m 前`;let u=Math.floor(k/60);if(u<24)return`${u}h 前`;return`${Math.floor(u/24)}d 前`}function P(c){if(typeof c==="number"&&Number.isFinite(c))return c;if(typeof c==="string"){let j=Date.parse(c);if(Number.isFinite(j))return j}return}function Q(c,j){if(typeof c.can_create_nodes!=="boolean")return{kind:"never-reported",line:`创建能力:未知 —— 这台 daemon 没报过这一格。
3
+ `+" (agent-node 版本早于 preview.55;升级后必须重启 daemon 才生效)"};let k=P(c.last_seen_at),u=c.create_capability_observed_ms_ago,q=k!==void 0&&typeof u==="number"&&Number.isFinite(u)?Math.max(0,j-k)+u:void 0;if(c.can_create_nodes){if(q===void 0)return{kind:"ready-age-unknown",line:`创建能力:可用
4
+ `+" ⚠ 不知道是什么时候测的 —— 该版本开机只算一次。重启它,或升级。"};return{kind:"ready",ageMs:q,line:`创建能力:可用(${G(q)}测)`}}let C=c.create_nodes_blocked_reason||"anet_bin_unknown",z=J[C]??{explain:`未知原因代码 ${C} —— 本 CLI 可能比那台机器的 anet 旧,升级本机 anet 或看它的 daemon 日志`,command:null},K=`完整原文只在那台机器的 daemon 日志里。
5
+ (它带真实机器路径,按设计不上报)`,H=[` 原因:${z.explain}`,z.command?` 修法(可整行粘贴):${z.command}`:" 修法:没有单条命令能修,见上。",` ${K}`].join(`
6
+ `);if(q===void 0)return{kind:"blocked-age-unknown",line:`创建能力:**不可用**(${C})
7
+ ⚠ 不知道是什么时候测的 —— 该 daemon 版本开机只算一次,之后不再重测。
8
+ ${H}`,fix:z};return{kind:"blocked",ageMs:q,line:`创建能力:**不可用**(${C},${G(q)}测)
9
+ ${H}`,fix:z}}function T(c){switch(c.why){case"no-hub":return`创建能力:查不到 —— 本机还没配 hub 地址。
10
+ `+" 跑 anet init,或这次带上 --hub <url>"+`
11
+ (本地清单不需要网络,上面那些信息仍然有效)`;case"unauthorized":return`创建能力:查不到 —— hub 拒绝了本机的身份(HTTP ${c.status})。
12
+ `+" hub 是通的,要修的是**凭据**不是网络:跑 anet login"+`
13
+ (本地清单不需要网络,上面那些信息仍然有效)`;case"http":return`创建能力:查不到 —— hub 返回 HTTP ${c.status}。
14
+ `+" hub 在,但这个接口没给出结果;看 hub 日志"+`
15
+ (本地清单不需要网络,上面那些信息仍然有效)`;case"bad-body":return`创建能力:查不到 —— hub 的应答读不懂。
16
+ `+" 多半是 hub 比本机 CLI 旧,升级 hub 或降本机 anet"+`
17
+ (本地清单不需要网络,上面那些信息仍然有效)`;case"unreachable":return"创建能力:查不到 —— 连不上 hub"+(c.detail?`(${c.detail})`:"")+"。"+`
18
+ (本地清单不需要网络,上面那些信息仍然有效)`}}function U(c){if(c?.error!=="daemon_cannot_create_nodes")return null;let j=c.blocked_reason||"anet_bin_unknown",k=J[j]??{explain:`未知原因代码 ${j} —— 本端可能比那台机器的 anet 旧,升级本端或看它的 daemon 日志`,command:null},u="完整原文(含真实路径)只在那台机器的 daemon 日志里 —— 它带机器路径,按设计不上报。",D=[`原因:${k.explain}`,k.command?`修法(可整行粘贴):${k.command}`:"修法:没有单条命令能修,见上。",u].join(`
19
+ `),q=c.capability_observed_ms_ago;if(c.capability_age==="known"&&typeof q==="number"&&Number.isFinite(q)&&q>=0)return{kind:"blocked",ageMs:q,line:`这台 daemon 报告它现在**建不了节点**(${j},${G(q)}测)
20
+ ${D}`,fix:k};let z=c.capability_age==="unknown_no_heartbeat_time"?"**它报了这个判断,但 hub 这边没有它的心跳时间**,所以算不出是多久以前的 —— 先看这台 daemon 是否还在线,别急着升级它":"**不知道是多久以前测的** —— 该 daemon 版本在开机时算一次就不再重测,这个结论可能早就过期了。重启或升级它";return{kind:"blocked-age-unknown",line:`这台 daemon 报告它现在**建不了节点**(${j});${z}
21
+ ${D}`,fix:k}}export{G as formatAge,T as describeFetchFailure,U as describeCreateRejection,Q as describeCapability};
@@ -0,0 +1,22 @@
1
+ /** 一个 runtime 依赖的外部二进制,以及它**有据可查**的安装位置。 */
2
+ export type RuntimeBinaryHint = {
3
+ /** 哪些 runtime 名需要这个二进制 */
4
+ readonly runtimes: readonly string[];
5
+ /** 在 PATH 上查的裸名 */
6
+ readonly binary: string;
7
+ /** 官方安装位置(相对 $HOME) */
8
+ readonly homeRelativeDir: string;
9
+ /** 🔴 这一条的出处 —— 没有出处就不许进这张表 */
10
+ readonly evidence: string;
11
+ };
12
+ export declare const RUNTIME_BINARY_HINTS: readonly RuntimeBinaryHint[];
13
+ export type PathPreflightInput = {
14
+ /** 这台 daemon 声称支持的 runtime */
15
+ readonly runtimes: readonly string[];
16
+ /** 二进制在 daemon 的 PATH 上能不能找到 */
17
+ readonly resolvesOnPath: (binary: string) => boolean;
18
+ /** 二进制在那个已知位置存不存在 */
19
+ readonly existsInHomeDir: (homeRelativeDir: string, binary: string) => boolean;
20
+ };
21
+ /** 返回要打给用户的警告。**没问题时返回空数组** —— 空是常态,不是失败。 */
22
+ export declare function daemonPathWarnings(input: PathPreflightInput): string[];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * daemon 的 `runtimes_supported` 是**写配置那一刻**的支持集快照,之后不会自愈。
3
+ *
4
+ * 🔴 这不是假想的:#1298 (2026-08-28) 把新建 daemon 的默认清单从 3 个放开到
5
+ * `SUPPORTED_RUNTIME_NAMES` 全体,但**它只改了写入路径**。在那之前 init 过的
6
+ * daemon,配置里仍然只有那 3 个,而且没有任何东西会说出这件事 ——
7
+ * 用户在客户端「选服务器」里看到某台机器少几个 runtime,
8
+ * 既不知道为什么,也不知道该做什么。
9
+ *
10
+ * 判据从**分发处**取(`SUPPORTED_RUNTIME_NAMES`),不在这里手写一份清单:
11
+ * 抄一份就意味着它会再漂一次(同 #1728 的教训)。
12
+ */
13
+ export declare function describeStaleRuntimeSupport(declared: readonly string[] | undefined | null, supported: readonly string[], nameHint?: string): string | null;
@@ -0,0 +1,35 @@
1
+ export type DashboardLaunchSource = "npx" | "global";
2
+ export interface DashboardLaunchRecord {
3
+ schema: 1;
4
+ port: number;
5
+ listener_pid: number;
6
+ listener_birth: string;
7
+ source: DashboardLaunchSource;
8
+ source_key: string;
9
+ recorded_at: string;
10
+ }
11
+ export type DashboardListenerDecision = {
12
+ action: "start";
13
+ } | {
14
+ action: "already_running";
15
+ pid: number;
16
+ } | {
17
+ action: "terminate_owned_stale";
18
+ pid: number;
19
+ reason: "unhealthy" | "version_changed";
20
+ } | {
21
+ action: "refuse";
22
+ reason: string;
23
+ };
24
+ export declare function isDashboardProcessCommand(command: string): boolean;
25
+ export declare function decideDashboardListener(input: {
26
+ port: number;
27
+ listenerPids: number[];
28
+ record: DashboardLaunchRecord | null;
29
+ listenerBirth: string | null;
30
+ listenerCommand: string | null;
31
+ desiredSource: DashboardLaunchSource;
32
+ desiredSourceKey: string;
33
+ healthy: boolean;
34
+ }): DashboardListenerDecision;
35
+ export declare function parseDashboardLaunchRecord(value: unknown): DashboardLaunchRecord | null;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 终端里的**显示列宽** —— CJK 字符占 2 列,而 `String.length` / `padEnd` 只数码元。
3
+ *
4
+ * 🔴 这份知识仓里本来就有,但它只活在**一个测试文件**里
5
+ * (`daemon-capability-display.test.ts` 的 `displayWidth`,2026-08-30 由 macOS 真机
6
+ * 上「合成首行约 99 列、80 列终端折行且折点落在句子中间」催生)。
7
+ * 生产代码没有它,于是所有用 `padEnd` 对齐的表在中文别名下都是歪的。
8
+ *
9
+ * 实测(2026-08-31,`anet node ls` 对着一支全中文别名的军团):
10
+ *
11
+ * NAME RUNTIME STATUS
12
+ * ──────────────────── ────────────── ────────
13
+ * 通信工程马 claude-code-cli idle ← RUNTIME 列起点
14
+ * 通信IM马 claude-code-cli idle ← 不一样
15
+ * 通信SDK牛 codex-sdk idle ← 又不一样
16
+ *
17
+ * `通信工程马`.length 是 5,`padEnd(20)` 补 15 个空格 —— 但它渲染出来是 10 列,
18
+ * 于是这一行实际占 25 列而不是 20。**别名里 CJK 越多,歪得越远。**
19
+ */
20
+ export declare function displayWidth(s: unknown): number;
21
+ /**
22
+ * 按**显示列**右补空格。已经不短于目标宽度时原样返回(与 `padEnd` 一致,不截断)。
23
+ *
24
+ * 🔴 不截断是有意的:截断会引出「截到一半的宽字符」这个新问题,
25
+ * 而调用方要的只是对齐。**一个修对齐的改动不该顺手引入截断语义。**
26
+ */
27
+ export declare function padDisplayEnd(s: unknown, width: number): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `anet doctor` 原先对「本机配置了几个节点」的判定是:
3
+ *
4
+ * check("Nodes configured", ids.length > 0, `${ids.length} node(s)`);
5
+ *
6
+ * 于是 **0 个节点 → ❌**。而一个刚装好、还没建节点的人跑 `anet doctor`,
7
+ * 看到的是:
8
+ *
9
+ * ❌ Nodes configured — 0 node(s)
10
+ * Result: 9 ok, 3 warnings, 2 errors
11
+ *
12
+ * 🔴 「还没有节点」是**全新安装的预期状态**,不是故障。把它报成 error,
13
+ * 等于在新用户的第一次诊断里制造一个假警报 —— 而这条命令存在的意义
14
+ * 正是告诉他「你这台机器现在好不好」。
15
+ *
16
+ * 🔴 但也不能反过来直接判成「一切正常」:同一个 0 对一个**本来有节点**的人
17
+ * 意味着配置目录不见了。doctor 手上只有一个数字,**分不出这两种现实**。
18
+ * 所以这里既不报错也不报好 —— 把两种都说出来,让读的人自己对号,
19
+ * 并给出各自的下一步。
20
+ */
21
+ export type NodeCountLine = {
22
+ ok: true;
23
+ detail: string;
24
+ } | {
25
+ ok: false;
26
+ info: string;
27
+ };
28
+ export declare function nodeCountLine(count: number): NodeCountLine;
@@ -0,0 +1,14 @@
1
+ /** Parse the environ blob (NUL-separated key=val) and return the
2
+ * COMMHUB_ALIAS value if present, else null. Exported for testing. */
3
+ export declare function parseEnvironAlias(environBlob: string): string | null;
4
+ /** Read /proc/<pid>/environ and return the COMMHUB_ALIAS value.
5
+ * Any file-read error (not-a-Linux, permission denied, race with
6
+ * process exit) returns null — caller must not fail-closed on that
7
+ * (the process is stale/gone/inaccessible, not our problem). */
8
+ export declare function readEnvironAlias(pid: number): string | null;
9
+ /** Scan /proc for pids whose COMMHUB_ALIAS env matches any of the
10
+ * target aliases. Linux-only (procfs); returns null on non-Linux or
11
+ * when /proc is unreadable — caller MUST fail-closed on null per
12
+ * the same #180 R2 fail-closed contract as findNodeProcessesByAlias.
13
+ * Self-pid + init (PID 1) are excluded. */
14
+ export declare function findEnvironAliasMatches(aliases: Iterable<string>, selfPid: number): number[] | null;