flowviant 0.102.2 → 0.104.0

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 (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +93 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactPolicy.mjs +44 -0
  7. package/bin/lib/artifactPreview.mjs +52 -0
  8. package/bin/lib/artifactReporter.mjs +163 -0
  9. package/bin/lib/artifacts.mjs +47 -227
  10. package/bin/lib/authReject.mjs +16 -4
  11. package/bin/lib/authproxy.mjs +47 -304
  12. package/bin/lib/boxIdentity.mjs +245 -0
  13. package/bin/lib/childEnv.mjs +11 -9
  14. package/bin/lib/claude.mjs +14 -851
  15. package/bin/lib/claudeAuth.mjs +3 -3
  16. package/bin/lib/claudePosture.mjs +367 -0
  17. package/bin/lib/claudeSessions.mjs +411 -0
  18. package/bin/lib/claudeStream.mjs +207 -0
  19. package/bin/lib/cliLimit.mjs +34 -0
  20. package/bin/lib/commandSpecs.mjs +173 -0
  21. package/bin/lib/config.mjs +11 -42
  22. package/bin/lib/credentialDate.mjs +22 -0
  23. package/bin/lib/credentials.mjs +21 -26
  24. package/bin/lib/daemonLogging.mjs +55 -0
  25. package/bin/lib/daemonState.mjs +64 -0
  26. package/bin/lib/deploy.mjs +148 -647
  27. package/bin/lib/deployCheckout.mjs +265 -0
  28. package/bin/lib/deployConfig.mjs +142 -0
  29. package/bin/lib/deployRunner.mjs +311 -0
  30. package/bin/lib/deployWire.mjs +41 -0
  31. package/bin/lib/desktopContract.mjs +56 -105
  32. package/bin/lib/env.mjs +49 -484
  33. package/bin/lib/fleet.mjs +184 -2396
  34. package/bin/lib/fleetJobs.mjs +120 -0
  35. package/bin/lib/fleetPost.mjs +40 -0
  36. package/bin/lib/fleetReports.mjs +392 -0
  37. package/bin/lib/fleetRoster.mjs +470 -0
  38. package/bin/lib/fleetStartup.mjs +196 -0
  39. package/bin/lib/fleetWake.mjs +45 -0
  40. package/bin/lib/fleetWire.mjs +46 -0
  41. package/bin/lib/git.mjs +34 -3
  42. package/bin/lib/gitMergeIdentity.mjs +51 -0
  43. package/bin/lib/grant.mjs +5 -3
  44. package/bin/lib/help.mjs +3 -125
  45. package/bin/lib/holder.mjs +482 -0
  46. package/bin/lib/instance.mjs +124 -46
  47. package/bin/lib/knowledge.mjs +16 -799
  48. package/bin/lib/knowledgeFetch.mjs +38 -0
  49. package/bin/lib/knowledgeFileSync.mjs +112 -0
  50. package/bin/lib/knowledgeFiles.mjs +487 -0
  51. package/bin/lib/knowledgeLibrary.mjs +249 -0
  52. package/bin/lib/landed.mjs +2 -1
  53. package/bin/lib/listeners.mjs +99 -141
  54. package/bin/lib/localSessions.mjs +30 -555
  55. package/bin/lib/login.mjs +3 -2
  56. package/bin/lib/loginPath.mjs +3 -5
  57. package/bin/lib/machineDisconnect.mjs +203 -0
  58. package/bin/lib/machineEnv.mjs +22 -0
  59. package/bin/lib/machineLimits.mjs +61 -0
  60. package/bin/lib/machineListing.mjs +338 -0
  61. package/bin/lib/machines.mjs +78 -455
  62. package/bin/lib/machinesCommand.mjs +214 -0
  63. package/bin/lib/mcp-cli.mjs +2 -1
  64. package/bin/lib/patchRevert.mjs +60 -0
  65. package/bin/lib/prWorkflow.mjs +157 -0
  66. package/bin/lib/preflight.mjs +2 -1
  67. package/bin/lib/pressurePolicy.mjs +104 -0
  68. package/bin/lib/preview.mjs +2 -272
  69. package/bin/lib/previewFramePolicy.mjs +112 -0
  70. package/bin/lib/previewGatePolicy.mjs +236 -0
  71. package/bin/lib/previewRegistry.mjs +235 -0
  72. package/bin/lib/printable.mjs +2 -2
  73. package/bin/lib/procRegistry.mjs +28 -18
  74. package/bin/lib/procScan.mjs +146 -0
  75. package/bin/lib/processes.mjs +29 -37
  76. package/bin/lib/projectToolRuntimes.mjs +138 -0
  77. package/bin/lib/projectTools.mjs +13 -88
  78. package/bin/lib/prompts.mjs +28 -42
  79. package/bin/lib/repoPath.mjs +26 -0
  80. package/bin/lib/resources.mjs +7 -69
  81. package/bin/lib/resumeLost.mjs +107 -0
  82. package/bin/lib/runTurn.mjs +324 -0
  83. package/bin/lib/runtimeAntigravity.mjs +230 -0
  84. package/bin/lib/runtimeCapabilities.mjs +334 -0
  85. package/bin/lib/runtimeClaude.mjs +126 -0
  86. package/bin/lib/runtimeCodex.mjs +217 -0
  87. package/bin/lib/runtimeDetection.mjs +121 -0
  88. package/bin/lib/runtimeEvents.mjs +482 -0
  89. package/bin/lib/runtimeOfferings.mjs +4 -1
  90. package/bin/lib/runtimes.mjs +42 -1402
  91. package/bin/lib/safeFileName.mjs +74 -0
  92. package/bin/lib/shipMerge.mjs +33 -2
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +35 -12
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3037
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +207 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +92 -897
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. package/bin/lib/patch.mjs +0 -363
@@ -0,0 +1,207 @@
1
+ /**
2
+ * WHAT A TURN WHOSE CLI RAN REPORTS — decided off what was measured, never off
3
+ * the agent's word alone.
4
+ *
5
+ * Split out of workAgentTurns.mjs (SOLID F036, 2026-09-26). The four settles
6
+ * that follow a CLI (a limit, no declared outcome, a missing artifact, the
7
+ * parsed result) are one decision with its own rules — a limit only when the
8
+ * turn declared nothing, an artifact kind delivered only once its file is
9
+ * measured, every relayed string scrubbed BEFORE it is cut — and the run that
10
+ * feeds it (workAgentTurnExecution.mjs) changes for reasons none of those
11
+ * rules share. Pure but for one read-only artifact scan, so each rule is a
12
+ * unit test here instead of a source pin behind a CLI nobody spawns in tests.
13
+ *
14
+ * `failedTurnSettlement` is the fifth: a turn whose execution THREW. Its only
15
+ * caller is the lane's catch (workAgentTurns.mjs), which measures what it can
16
+ * and settles once, so a crash in the run can never leave a turn unsettled to
17
+ * be re-offered and re-run on the operator's account.
18
+ *
19
+ * `agentTurnSettlement` returns `{ res, limit, body, final }`; the caller posts
20
+ * `body` through the one turn-done wire, clears or sets the machine's limit off
21
+ * `res`/`limit`, parks on `limit`, and runs the review-entry beat only after a
22
+ * `final` settle.
23
+ */
24
+ import { limitLine } from './cliLimit.mjs';
25
+ import { parseTurnResult } from './agentPlan.mjs';
26
+ import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
27
+ import { scrub as envScrub } from './uplinkScrub.mjs';
28
+
29
+ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, commits, artifactsBefore, branch, wt }) {
30
+ const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
31
+ /**
32
+ * A LIMIT IS ONLY A LIMIT WHEN THE TURN PRODUCED NOTHING.
33
+ *
34
+ * `limitLine` is a literal phrase match over the CLI's output, and the
35
+ * output of a successful turn contains whatever the agent wrote — so a
36
+ * card about rate limiting, or a summary mentioning one, parked every
37
+ * agent on the project. Gating on "the turn declared no outcome" is what
38
+ * makes the match mean what it says: the CLI failed and this is the
39
+ * sentence it failed with.
40
+ */
41
+ const limit = res ? null : limitLine(out);
42
+ /** What the caller posts, and whether it is the one settle that may say
43
+ * delivered (the only one whose reply can open the review-entry beat). */
44
+ const report = (body, final = false) => ({ res, limit, body, final });
45
+ if (limit) {
46
+ // The turn itself is reported as `nothing` — it did not deliver and it
47
+ // did not ask. The park that goes with it is the caller's side effect
48
+ // (workAgentTurnExecution.mjs): every agent on this CLI stops.
49
+ return report({
50
+ turnId,
51
+ outcome: 'nothing',
52
+ answer: limit,
53
+ ...(usage ? { usage } : {}),
54
+ branch,
55
+ worktree: wt,
56
+ });
57
+ }
58
+
59
+ if (!res) {
60
+ return report({
61
+ turnId,
62
+ outcome: 'nothing',
63
+ // The CLI's own words when it produced any. A turn that explained why
64
+ // it stopped is far more use than "the agent stopped".
65
+ answer: out.trim()
66
+ ? envScrub(out).slice(-1500)
67
+ : 'the turn produced no output on the machine — its CLI may be signed out',
68
+ ...(commits.length ? { commits } : {}),
69
+ ...(usage ? { usage } : {}),
70
+ branch,
71
+ worktree: wt,
72
+ });
73
+ }
74
+ /**
75
+ * A DESIGN OR RESEARCH CARD IS NOT DELIVERED UNTIL ITS ARTIFACT EXISTS
76
+ * (0.97.0) — measured, never taken on the agent's word.
77
+ *
78
+ * Its whole product is one file under `.flowviant/artifacts/`: a `.html`
79
+ * mockup for design, a `.md` write-up for research. A turn that says
80
+ * "delivered" without having written one would land the agent in Review
81
+ * with nothing to look at and a summary describing a page that does not
82
+ * exist — so it settles `nothing` with the measured sentence, and the
83
+ * agent lands in Stuck with a true reason instead.
84
+ *
85
+ * WHAT COUNTS: on a TASK turn, only what the scan found NEW OR CHANGED
86
+ * this turn (`changedArtifacts` against the snapshot taken before the
87
+ * spawn) — a second design card in the same agent must not pass on the
88
+ * first card's page. On a HUMAN turn (an answer, a send-back) a matching
89
+ * file already standing in the directory also counts: "keep it as it
90
+ * is" is a legitimate answer to a question the agent asked after
91
+ * drawing, and the turn did not have to rewrite the page to deliver it.
92
+ *
93
+ * …AND ON A REDO (2026-09-23): a TASK turn re-running a card this agent
94
+ * already delivered, which is what a send-back's "Needs work" queues —
95
+ * the human turn in front of it usually rewrote the mockup already, so
96
+ * the task turn that follows truthfully says "revised last turn" with
97
+ * nothing new to write, and the task rule sent it to Stuck with a false
98
+ * sentence. The server marks such a job `redo: true` (a link delivered
99
+ * before, or carrying a `needs_work` verdict), and then a standing match
100
+ * counts, as it does for a human turn. No floor: an older server never
101
+ * sends the key, and absent keeps the stricter task rule.
102
+ *
103
+ * Commits are still reported if any exist — the posture prevents them,
104
+ * and a report never lies by omission about what is on the branch.
105
+ */
106
+ if (res.outcome === 'delivered' && kind.artifact) {
107
+ const want = kind.artifact.match;
108
+ const standing = scanArtifacts(wt);
109
+ const wrote = changedArtifacts(artifactsBefore, standing).some((e) => want.test(e.name));
110
+ const present =
111
+ wrote || ((job.kind !== 'task' || job.redo === true) && standing.some((e) => want.test(e.name)));
112
+ if (!present) {
113
+ return report({
114
+ turnId,
115
+ outcome: 'nothing',
116
+ answer: kind.artifact.missing,
117
+ ...(commits.length ? { commits } : {}),
118
+ ...(usage ? { usage } : {}),
119
+ branch,
120
+ worktree: wt,
121
+ });
122
+ }
123
+ }
124
+ return report(
125
+ {
126
+ turnId,
127
+ outcome: res.outcome,
128
+ answer: envScrub(res.answer ?? '').slice(0, 8000),
129
+ ...(commits.length ? { commits } : {}),
130
+ // SCRUBBED like the answer beside it: a raised card lands in the
131
+ // project doc every member reads, and a brief quoting the `.env` the
132
+ // agent just read ("value sk_live_… is logged in pay.ts") went out
133
+ // verbatim while the same value in the summary was redacted.
134
+ ...(res.raised?.length
135
+ ? {
136
+ raised: res.raised.map((r) => ({
137
+ title: envScrub(r.title).slice(0, 300),
138
+ ...(r.brief ? { brief: envScrub(r.brief).slice(0, 2000) } : {}),
139
+ })),
140
+ }
141
+ : {}),
142
+ ...(usage ? { usage } : {}),
143
+ /**
144
+ * THE AGENT'S RUNNING ACCOUNT OF THIS BRANCH (2026-09-22).
145
+ *
146
+ * The owner: "we should add a brief summary of what the agent has done
147
+ * overall at the top that updates." A RELAY — the agent wrote it in its
148
+ * own final JSON object and nothing here composes, narrows or infers
149
+ * one. It rides only this settle, which is the one that follows a
150
+ * PARSED result: the two `nothing` settles above it come from a turn
151
+ * that declared no outcome at all, so there is no account to carry and
152
+ * inventing one would be the machine speaking for the agent.
153
+ *
154
+ * SCRUBBED BEFORE IT IS CUT, the order that matters and the one the
155
+ * check's output lane learned the expensive way: `envScrub` replaces
156
+ * EXACT values, so a paragraph capped first hands the scrub a
157
+ * credential already cut in half — it matches nothing and the surviving
158
+ * prefix ships. `parseTurnResult` trims it and bounds it for absurdity
159
+ * at 8000, DELIBERATELY above the 1000 below, so this slice is the
160
+ * first one a value of any interest meets and the scrub has already
161
+ * run when it does. The first cut of this feature bounded the parser at
162
+ * 1000 as well, which read identically and quietly put the cap first.
163
+ *
164
+ * OMITTED WHEN THE TURN DID NOT WRITE ONE, never sent empty. Absence is
165
+ * the server's signal to KEEP the last account that was true; an empty
166
+ * string would blank the head because a model dropped a key.
167
+ */
168
+ ...(res.progress ? { progress: envScrub(res.progress).slice(0, 1000) } : {}),
169
+ branch,
170
+ worktree: wt,
171
+ },
172
+ true
173
+ );
174
+ }
175
+
176
+ /**
177
+ * A TURN WHOSE RUN THREW IS STILL SETTLED, ONCE, AS `nothing` (SOLID F036).
178
+ *
179
+ * Before this, a throw anywhere between the lock and the settle — base-branch
180
+ * agent instructions over the turn's limit (`readBaseTools` throws), a tool
181
+ * config that could not be prepared, a hook that failed after the CLI had
182
+ * committed — escaped the lane with nothing posted. The turn stayed pending,
183
+ * the agent sat in Working with nothing behind it, and every roster offer ran
184
+ * the whole turn again until the six-hour expiry: past the CLI, that is a
185
+ * second set of commits on the operator's quota, every poll.
186
+ *
187
+ * The sentence is the error's own words, SCRUBBED BEFORE IT IS CUT (a thrown
188
+ * message can quote a path or an argv carrying a value). What the run measured
189
+ * before it threw rides along: the commits already on the branch (a report
190
+ * never lies by omission), the spend if the CLI counted one, and the branch
191
+ * and worktree when a worktree existed. Nothing measured, nothing claimed.
192
+ */
193
+ export function failedTurnSettlement({ turnId, error, commits = [], usage = null, branch = null, wt = null }) {
194
+ // An Error's message, or a thrown string; anything else says nothing rather
195
+ // than "[object Object]".
196
+ const why = (typeof error === 'string' ? error : typeof error?.message === 'string' ? error.message : '').trim();
197
+ return {
198
+ turnId,
199
+ outcome: 'nothing',
200
+ answer: envScrub(
201
+ why ? `This machine could not finish the turn: ${why}` : 'This machine could not finish the turn.'
202
+ ).slice(0, 1500),
203
+ ...(commits.length ? { commits } : {}),
204
+ ...(usage ? { usage } : {}),
205
+ ...(wt ? { branch, worktree: wt } : {}),
206
+ };
207
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * AN AGENT TURN'S WIRE: the settle and its held body, the trace, the pulse and
3
+ * the park — every POST the agent-turn lane makes, and nothing that decides
4
+ * what goes in one.
5
+ *
6
+ * Split out of workAgentTurns.mjs (SOLID F036, 2026-09-26). Report delivery is
7
+ * its own vocabulary — which status counts as delivered, which refusal backs
8
+ * off, how long a finished body is held and what clock ages it — and it
9
+ * changes for reasons that have nothing to do with how a CLI is run or what a
10
+ * turn decided. The lane (workAgentTurns.mjs), the run
11
+ * (workAgentTurnExecution.mjs) and the stand-down all post through here, so a
12
+ * settle has exactly one door and one held-body map.
13
+ *
14
+ * Daemon→server reports, every one of them: no version floor, and a permanent
15
+ * 4xx is the server saying it will never take the body (see each POST).
16
+ */
17
+ import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
18
+ import { fleetEndpoint } from './fleetWire.mjs';
19
+
20
+ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
21
+ const AGENT_TURN_DONE_URL = fleetEndpoint('agent-turn-done', FLEET_URL);
22
+ const AGENT_TRACE_URL = fleetEndpoint('agent-trace', FLEET_URL);
23
+ const AGENT_ACTIVITY_URL = fleetEndpoint('agent-activity', FLEET_URL);
24
+ const AGENT_PARKED_URL = fleetEndpoint('agent-parked', FLEET_URL);
25
+
26
+ /**
27
+ * Turns whose work is DONE but whose settle has not landed, keyed to the
28
+ * finished BODY. The delivery half of what `pendingWorkReports` is for a
29
+ * tab: a settle that fails to POST must not re-run the turn — that is a
30
+ * second CLI, a second set of commits, and the operator's quota spent again
31
+ * — but a guard that only SKIPPED left the other half undone. One failed
32
+ * POST parked the agent for the server's whole six-hour expiry, holding a
33
+ * cap slot the entire time, and then expired into "nobody ran it" — a false
34
+ * sentence about a turn this machine finished. The server's settle is
35
+ * idempotent (conditional on the row still being pending), so a re-offer of
36
+ * a held turn re-POSTs the stored body instead: safe, and it lands the
37
+ * moment the network heals rather than six hours later.
38
+ *
39
+ * BOUNDED by the roster itself: a held body's clock is refreshed while the
40
+ * server keeps offering its turn, and once offering stops — settled by the
41
+ * re-POST, or expired server-side — the grace below is all that keeps it.
42
+ */
43
+ const agentReported = new Map(); // turnId -> { body, at }
44
+ const agentRejectedUntil = new Map(); // turnId -> earliest re-POST of a refused body
45
+ const AGENT_REPORT_GRACE_MS = 30 * 60_000;
46
+
47
+ /** Returns the server's reply, because it carries ONE instruction the machine
48
+ * can act on immediately: `review: true` means the agent's queue just
49
+ * emptied, so run the project's own check in the worktree we are already
50
+ * standing in. A job lane for that would need a claim, a floor and a settle
51
+ * to say something this reply already can. */
52
+ const postAgentTurn = async (body) => {
53
+ const turnId = String(body.turnId);
54
+ agentReported.set(turnId, { body, at: Date.now() });
55
+ try {
56
+ const res = await fetch(AGENT_TURN_DONE_URL, {
57
+ method: 'POST',
58
+ headers: {
59
+ Authorization: `Bearer ${FLEET_TOKEN}`,
60
+ 'User-Agent': USER_AGENT,
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ signal: AbortSignal.timeout(30_000),
64
+ body: JSON.stringify(body),
65
+ });
66
+ const j = await res.json().catch(() => null);
67
+ /**
68
+ * ONLY A 2xx IS DELIVERED. The server answers an already-settled,
69
+ * expired or unknown turn with 200 `{ settled: false }` — its only 4xx
70
+ * are a body it could not parse (deploy skew) and auth, and an edge WAF
71
+ * rule tripped by a summary that quotes an exploit string is a 403 from
72
+ * in front of it. Each of those left the turn row PENDING, and dropping
73
+ * the held body on them meant the next offer found nothing held and ran
74
+ * the CLI again: a second set of commits and the operator's quota spent,
75
+ * every poll, for six hours. The tab lane learned this as its 'reject'
76
+ * class. So a refused body stays HELD — it is the skip-guard — and its
77
+ * re-POST backs off instead of hammering a body the server just refused.
78
+ */
79
+ if (res.ok) {
80
+ agentReported.delete(turnId);
81
+ agentRejectedUntil.delete(turnId);
82
+ } else if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
83
+ agentRejectedUntil.set(turnId, Date.now() + REJECT_RETRY_MS);
84
+ }
85
+ return j?.data ?? null;
86
+ } catch {
87
+ // Unsettled — a network error, so the body STAYS held and the next
88
+ // re-offer retries the POST rather than the CLI.
89
+ return null;
90
+ }
91
+ };
92
+
93
+ /**
94
+ * ONE BATCH OF A TURN'S TRACE. See trace.mjs for the whole contract.
95
+ *
96
+ * Resolves TRUE for a permanent refusal as well as a success, and that is
97
+ * deliberate: a server with no such route 404s every batch, and a relay that
98
+ * held them would fill its buffer, shed the turn's real steps and retry the
99
+ * same rejected body for the life of the turn. There is no version floor here
100
+ * — this is a daemon→server report, so an older server simply never learns
101
+ * the trace and the board renders what it always did.
102
+ */
103
+ const postAgentTrace = async (body) => {
104
+ try {
105
+ const res = await fetch(AGENT_TRACE_URL, {
106
+ method: 'POST',
107
+ headers: {
108
+ Authorization: `Bearer ${FLEET_TOKEN}`,
109
+ 'User-Agent': USER_AGENT,
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ signal: AbortSignal.timeout(15_000),
113
+ body: JSON.stringify(body),
114
+ });
115
+ return (
116
+ res.ok ||
117
+ (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
118
+ );
119
+ } catch {
120
+ return false; // a blip — the same entries go again at the same seq
121
+ }
122
+ };
123
+
124
+ const postAgentActivity = async (agentId, text) => {
125
+ try {
126
+ await fetch(AGENT_ACTIVITY_URL, {
127
+ method: 'POST',
128
+ headers: {
129
+ Authorization: `Bearer ${FLEET_TOKEN}`,
130
+ 'User-Agent': USER_AGENT,
131
+ 'Content-Type': 'application/json',
132
+ },
133
+ signal: AbortSignal.timeout(15_000),
134
+ body: JSON.stringify({ agentId, text }),
135
+ });
136
+ } catch {
137
+ /* narration is a readout; losing a line costs nothing */
138
+ }
139
+ };
140
+
141
+ const postAgentParked = async (reason, runtime) => {
142
+ try {
143
+ await fetch(AGENT_PARKED_URL, {
144
+ method: 'POST',
145
+ headers: {
146
+ Authorization: `Bearer ${FLEET_TOKEN}`,
147
+ 'User-Agent': USER_AGENT,
148
+ 'Content-Type': 'application/json',
149
+ },
150
+ signal: AbortSignal.timeout(15_000),
151
+ // `runtime` scopes the park to that CLI's agents; a server that does
152
+ // not know the key parks every agent, which is what it always did.
153
+ body: JSON.stringify({ reason, ...(runtime ? { runtime } : {}) }),
154
+ });
155
+ } catch {
156
+ /* the next turn will hit the same limit and try again */
157
+ }
158
+ };
159
+
160
+ /**
161
+ * THE ROSTER IS THE HELD BODIES' CLOCK: an offered turn is still pending
162
+ * server-side and worth retrying; one the roster stopped naming was settled
163
+ * or expired, and holding its body past a generous grace would grow this map
164
+ * for the life of the process. The server's own expiry is the true bound —
165
+ * the grace only covers its POST racing a final offer.
166
+ */
167
+ const pruneHeldReports = (list) => {
168
+ if (!agentReported.size) return;
169
+ const offered = new Set(list.map((j) => String(j?.id || '')));
170
+ const now = Date.now();
171
+ for (const [id, held] of agentReported) {
172
+ if (offered.has(id)) held.at = now;
173
+ else if (now - held.at > AGENT_REPORT_GRACE_MS) {
174
+ agentReported.delete(id);
175
+ agentRejectedUntil.delete(id);
176
+ }
177
+ }
178
+ };
179
+
180
+ return {
181
+ postAgentTurn,
182
+ postAgentTrace,
183
+ postAgentActivity,
184
+ postAgentParked,
185
+ pruneHeldReports,
186
+ agentReported,
187
+ agentRejectedUntil,
188
+ };
189
+ }