flowviant 0.103.0 → 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.
- package/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE EXTENSION-PRESERVING 80-CHARACTER NAME, daemon side (2026-09-26, split
|
|
3
|
+
* out of knowledge.mjs, artifacts.mjs and work.mjs — SOLID F043).
|
|
4
|
+
*
|
|
5
|
+
* The rule is the server's `safeFileName` (apps/api/src/routes/
|
|
6
|
+
* sessionsAttachments.routes.ts), and it had THREE hand-kept daemon copies —
|
|
7
|
+
* one per caller — each re-stating the FNV-1a hash and the stem cut. A copy
|
|
8
|
+
* that drifted by one character would store a file under one name while the
|
|
9
|
+
* server re-derived another, which is exactly the disagreement the rule was
|
|
10
|
+
* adopted to end (2026-09-24: a bare `slice(0, 80)` cut an 84-character
|
|
11
|
+
* `….html` mockup to `….` and its bytes were discarded).
|
|
12
|
+
*
|
|
13
|
+
* Two layers, because the callers need different context rules around one
|
|
14
|
+
* common cut:
|
|
15
|
+
* - `cutKeepingExtension(clean)` — the cut alone, over an ALREADY sanitised
|
|
16
|
+
* name. Artifacts use only this: a bundle path is cut per component and
|
|
17
|
+
* keeps its safe spaces (see `safeArtifactName`).
|
|
18
|
+
* - `safeFileName(raw, fallback)` — the server's whole rule: last path
|
|
19
|
+
* component, `[^A-Za-z0-9._-]` → `_`, no leading dot or dash, then the cut.
|
|
20
|
+
* Each caller names its own fallback for an empty name ('file' for the
|
|
21
|
+
* knowledge library, 'attachment' for turn uploads — the server's).
|
|
22
|
+
*
|
|
23
|
+
* The server keeps its own independent validation (the daemon is published
|
|
24
|
+
* separately and a server deploy cannot upgrade it); `safeFileName.test.mjs`
|
|
25
|
+
* and the API's `sessionsAttachments.routes.test.ts` pin the SAME table of
|
|
26
|
+
* canonical outputs, so a change on one side fails the other's release check.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The longest name either function returns. */
|
|
30
|
+
export const SAFE_NAME_MAX = 80;
|
|
31
|
+
/** What counts as an extension worth keeping through a cut. */
|
|
32
|
+
const SAFE_EXT_RE = /^[A-Za-z0-9]{1,10}$/;
|
|
33
|
+
|
|
34
|
+
/** FNV-1a, 32-bit, as 8 hex — deterministic, so the same long name always maps
|
|
35
|
+
* to the same stored name. Verbatim against the server's `fnv1a8`. */
|
|
36
|
+
export function fnv1a8(s) {
|
|
37
|
+
let h = 0x811c9dc5;
|
|
38
|
+
for (let i = 0; i < s.length; i++) {
|
|
39
|
+
h ^= s.charCodeAt(i);
|
|
40
|
+
h = Math.imul(h, 0x01000193);
|
|
41
|
+
}
|
|
42
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An over-long name is its stem, cut, plus an 8-hex hash of the WHOLE name,
|
|
47
|
+
* plus the extension: stable across calls, at most 80 characters, and a name
|
|
48
|
+
* that already fits is returned unchanged — so it is idempotent over its own
|
|
49
|
+
* output and the server's re-application is a no-op.
|
|
50
|
+
*/
|
|
51
|
+
export function cutKeepingExtension(clean) {
|
|
52
|
+
if (clean.length <= SAFE_NAME_MAX) return clean;
|
|
53
|
+
const dot = clean.lastIndexOf('.');
|
|
54
|
+
const ext = dot > 0 && SAFE_EXT_RE.test(clean.slice(dot + 1)) ? clean.slice(dot + 1) : '';
|
|
55
|
+
const stem = ext ? clean.slice(0, dot) : clean;
|
|
56
|
+
const tag = `-${fnv1a8(clean)}`;
|
|
57
|
+
const room = SAFE_NAME_MAX - tag.length - (ext ? ext.length + 1 : 0);
|
|
58
|
+
return `${stem.slice(0, room)}${tag}${ext ? `.${ext}` : ''}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The server's `safeFileName`: drops every path separator, never starts with a
|
|
63
|
+
* dot or a dash, keeps the extension through a cut. `fallback` answers a name
|
|
64
|
+
* that sanitises to nothing.
|
|
65
|
+
*/
|
|
66
|
+
export function safeFileName(raw, fallback) {
|
|
67
|
+
const clean = String(raw ?? '')
|
|
68
|
+
.split(/[\\/]/)
|
|
69
|
+
.pop()
|
|
70
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
71
|
+
.replace(/^[.-]+/, '');
|
|
72
|
+
if (!clean) return fallback;
|
|
73
|
+
return cutKeepingExtension(clean);
|
|
74
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE WAY A STANDING-DOWN DAEMON LEAVES: AFTER ITS DEPLOYS HAVE REPORTED.
|
|
3
|
+
*
|
|
4
|
+
* ── A STAND-DOWN LETS AN IN-FLIGHT DEPLOY FINISH AND REPORT (owner ruling
|
|
5
|
+
* 2026-09-26) ──
|
|
6
|
+
*
|
|
7
|
+
* The stand-downs are Ctrl+C (SIGINT), a service manager's or a takeover's
|
|
8
|
+
* SIGTERM, a commanded stop, a displaced box, a removed box and a revoked
|
|
9
|
+
* credential. Every one of them used to reach `process.exit` straight after
|
|
10
|
+
* the teardown. The deploy's command survived (it runs in its own process
|
|
11
|
+
* group, deployRunner.mjs, and only its timeout kills that group), but the
|
|
12
|
+
* process that held the claim did not — so its outcome was never posted, the
|
|
13
|
+
* heartbeat stopped, and three minutes later the server handed the same
|
|
14
|
+
* irreversible job out again.
|
|
15
|
+
*
|
|
16
|
+
* So the exit waits. The teardown still runs first and in full (tabs, the
|
|
17
|
+
* wiki, previews — none of those is a deploy), and it closes the work lanes
|
|
18
|
+
* before it kills anything (standDownGate.mjs): no CLI turn or check starts
|
|
19
|
+
* again, and a turn it killed never hands its output back, so it settles
|
|
20
|
+
* nothing and retries nothing, exactly as when `process.exit` followed on the
|
|
21
|
+
* same tick. The roster loop stops taking work (fleet.mjs checks
|
|
22
|
+
* `leave.leaving()` at the top of every tick and after every await in it, so a
|
|
23
|
+
* signal mid-tick ends the tick too), and a deploy claim answered after the
|
|
24
|
+
* stand-down began is not run (deploy.mjs). What stays alive is only the
|
|
25
|
+
* deploy lane: the command, its heartbeat and its report. While it waits:
|
|
26
|
+
*
|
|
27
|
+
* - the instance lock stays HELD and is marked draining, so a takeover waits
|
|
28
|
+
* for it and a `flowviant stop` says so — neither signals it (instance.mjs
|
|
29
|
+
* reads the mark BEFORE it signals);
|
|
30
|
+
* - `busy` keeps being written to the daemon state file, and the lock's mark
|
|
31
|
+
* is read into `status --json` as `draining`, so the tray shows the box as
|
|
32
|
+
* working until the outcome lands. A `draining` machine event says so at
|
|
33
|
+
* once: the `stopped` event the stand-down already sent must not leave the
|
|
34
|
+
* tray reading the box as idle;
|
|
35
|
+
* - the report still goes out if the credential was rotated under it (a
|
|
36
|
+
* Disconnect, which the app now refuses while a deploy is in flight): the
|
|
37
|
+
* API's own refusal is said in words by the lease, never thrown.
|
|
38
|
+
*
|
|
39
|
+
* A SIGTERM while waiting changes nothing — a service manager, a takeover or
|
|
40
|
+
* `flowviant stop` asking again is the same stand-down asked twice. Only a
|
|
41
|
+
* person at the terminal can insist: a SECOND Ctrl+C leaves at once, after
|
|
42
|
+
* one best-effort post that settles each deploy as `unknown` ("outcome
|
|
43
|
+
* unknown: operator left mid-deploy"), so the server marks it terminal and
|
|
44
|
+
* never hands it out again. Nothing here ever kills the deploy; that is the
|
|
45
|
+
* timeout's alone.
|
|
46
|
+
*
|
|
47
|
+
* STATED RESIDUALS (no code answers them):
|
|
48
|
+
* - A service manager. No unit file is shipped or documented (checked
|
|
49
|
+
* 2026-09-26). A unit that runs this daemon must set `KillMode=mixed` (the
|
|
50
|
+
* stop's SIGTERM goes to the daemon alone, not to the deploy command in its
|
|
51
|
+
* cgroup) and `TimeoutStopSec=2100` (35 minutes, above the deploy's
|
|
52
|
+
* 30-minute timeout); systemd's defaults SIGTERM the command itself and
|
|
53
|
+
* SIGKILL the whole group after 90 s.
|
|
54
|
+
* - An older daemon's takeover or `flowviant stop` does not read the draining
|
|
55
|
+
* mark and SIGKILLs a newer draining daemon after its 20 s grace; the
|
|
56
|
+
* report is then lost and the server requeues the job, as before this
|
|
57
|
+
* ruling, until the release carrying this has spread.
|
|
58
|
+
* - A SIGKILL (or a second Ctrl+C whose `unknown` post does not land) loses
|
|
59
|
+
* the outcome the same way.
|
|
60
|
+
* - A deploy claim still on the wire when a second Ctrl+C leaves gets no
|
|
61
|
+
* `unknown` post (nothing was run, and this process has no job to settle);
|
|
62
|
+
* if the server granted it, its stale sweep hands it out again after three
|
|
63
|
+
* minutes, which is right, because nothing ran.
|
|
64
|
+
*
|
|
65
|
+
* Every dependency is injected, so the wait can be driven without a process to
|
|
66
|
+
* exit; fleet.mjs builds the live one.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
export const DRAIN_BUSY_BEAT_MS = 30_000;
|
|
70
|
+
/** How long a second Ctrl+C waits for its `unknown` post before it leaves
|
|
71
|
+
* anyway — a person insisting is not kept waiting on the network. */
|
|
72
|
+
export const ABANDON_POST_MS = 5_000;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} deps
|
|
76
|
+
* @param {() => number} deps.inFlight how many deploys this process holds
|
|
77
|
+
* @param {() => string[]} deps.labels what they deploy, `target → env`
|
|
78
|
+
* @param {() => Promise<void>} deps.settled resolves once every one reported
|
|
79
|
+
* @param {() => Promise<void>} deps.abandon settle each as `unknown` (deploy.mjs)
|
|
80
|
+
* @param {(what: string) => void} deps.markDraining mark the instance lock
|
|
81
|
+
* @param {(what: string) => void} [deps.announce] the `draining` machine event
|
|
82
|
+
* @param {() => void} deps.reportBusy write `busy` to the state file
|
|
83
|
+
* @param {(code: number) => void} deps.exit
|
|
84
|
+
* @param {{ note: (m: string) => void, warn: (m: string) => void }} deps.log
|
|
85
|
+
* @returns {((code: number, opts?: { signal?: 'SIGINT' | 'SIGTERM' | null }) => Promise<void>)
|
|
86
|
+
* & { leaving: () => boolean, drained: () => Promise<void> }}
|
|
87
|
+
*/
|
|
88
|
+
export function createLeave({
|
|
89
|
+
inFlight,
|
|
90
|
+
labels,
|
|
91
|
+
settled,
|
|
92
|
+
abandon = async () => {},
|
|
93
|
+
markDraining,
|
|
94
|
+
announce = () => {},
|
|
95
|
+
reportBusy,
|
|
96
|
+
exit,
|
|
97
|
+
log,
|
|
98
|
+
beatMs = DRAIN_BUSY_BEAT_MS,
|
|
99
|
+
abandonMs = ABANDON_POST_MS,
|
|
100
|
+
}) {
|
|
101
|
+
let waiting = null;
|
|
102
|
+
let interrupts = 0;
|
|
103
|
+
let gone = false;
|
|
104
|
+
const exitOnce = (code) => {
|
|
105
|
+
if (gone) return;
|
|
106
|
+
gone = true;
|
|
107
|
+
exit(code);
|
|
108
|
+
};
|
|
109
|
+
leave.leaving = () => waiting != null;
|
|
110
|
+
/** Resolves when the stand-down has finished waiting (at once when none began). */
|
|
111
|
+
leave.drained = () => waiting ?? Promise.resolve();
|
|
112
|
+
return leave;
|
|
113
|
+
function leave(code, { signal = null } = {}) {
|
|
114
|
+
if (signal === 'SIGINT') interrupts++;
|
|
115
|
+
if (waiting) {
|
|
116
|
+
// A SIGTERM or a second roster command while draining is the same
|
|
117
|
+
// stand-down asked again and changes nothing. Only a person at the
|
|
118
|
+
// terminal insists, and only with a SECOND Ctrl+C.
|
|
119
|
+
if (signal !== 'SIGINT' || gone) return waiting;
|
|
120
|
+
if (interrupts < 2) {
|
|
121
|
+
log.note('still waiting for the deploy to report. Ctrl+C again leaves now, without its outcome.');
|
|
122
|
+
return waiting;
|
|
123
|
+
}
|
|
124
|
+
log.warn(
|
|
125
|
+
'leaving without seeing the deploy end — its command keeps running on this box; ' +
|
|
126
|
+
'the app is told its outcome is unknown, so it is never run again.'
|
|
127
|
+
);
|
|
128
|
+
const bounded = new Promise((r) => {
|
|
129
|
+
const t = setTimeout(r, abandonMs);
|
|
130
|
+
t.unref?.();
|
|
131
|
+
});
|
|
132
|
+
Promise.race([Promise.resolve().then(abandon).catch(() => {}), bounded]).then(() => exitOnce(code));
|
|
133
|
+
return waiting;
|
|
134
|
+
}
|
|
135
|
+
if (inFlight() === 0) {
|
|
136
|
+
waiting = Promise.resolve();
|
|
137
|
+
exitOnce(code);
|
|
138
|
+
return waiting;
|
|
139
|
+
}
|
|
140
|
+
const n = inFlight();
|
|
141
|
+
const what = labels().join(', ');
|
|
142
|
+
log.note(
|
|
143
|
+
`waiting for ${n === 1 ? 'a deploy' : `${n} deploys`} (${what}) to finish and report before stopping — ` +
|
|
144
|
+
`the command is not stopped.${signal === 'SIGINT' ? ' Ctrl+C again leaves now, without its outcome.' : ''}`
|
|
145
|
+
);
|
|
146
|
+
try {
|
|
147
|
+
markDraining(what);
|
|
148
|
+
} catch {
|
|
149
|
+
/* a lock we cannot mark still drains; an older daemon's takeover may force it */
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
announce(what);
|
|
153
|
+
} catch {
|
|
154
|
+
/* a local readout must never interrupt a stand-down */
|
|
155
|
+
}
|
|
156
|
+
const beat = () => {
|
|
157
|
+
try {
|
|
158
|
+
reportBusy();
|
|
159
|
+
} catch {
|
|
160
|
+
/* a local readout must never interrupt a stand-down */
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
beat();
|
|
164
|
+
const timer = setInterval(beat, beatMs);
|
|
165
|
+
timer.unref?.();
|
|
166
|
+
waiting = settled().then(
|
|
167
|
+
() => {
|
|
168
|
+
clearInterval(timer);
|
|
169
|
+
if (gone) return;
|
|
170
|
+
beat();
|
|
171
|
+
log.note(`${n === 1 ? 'the deploy is' : 'the deploys are'} done — stopping.`);
|
|
172
|
+
exitOnce(code);
|
|
173
|
+
},
|
|
174
|
+
() => {
|
|
175
|
+
clearInterval(timer);
|
|
176
|
+
exitOnce(code);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
return waiting;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE WORK LANES STOP AT THE TEARDOWN, EVEN WHILE THE PROCESS LIVES ON (second
|
|
3
|
+
* review of ruling 2026-09-26: deploy during stand-down).
|
|
4
|
+
*
|
|
5
|
+
* Before the drain, every stand-down ran the teardown and then `process.exit`
|
|
6
|
+
* on the same tick. The teardown SIGTERMed the live CLIs, and the code waiting
|
|
7
|
+
* on them never ran again, so a killed turn never settled: it stayed pending on
|
|
8
|
+
* the server and resumed in place on the next run ("Worktrees are kept:
|
|
9
|
+
* in-flight work resumes next run").
|
|
10
|
+
*
|
|
11
|
+
* The drain keeps the process alive after the teardown for as long as a
|
|
12
|
+
* deploy is in flight (standDownExit.mjs). Every continuation that was waiting
|
|
13
|
+
* on a killed CLI would then run. A tab turn's resume-fresh retry would spawn a
|
|
14
|
+
* new CLI that nobody kills. An agent turn would settle as `nothing` and send
|
|
15
|
+
* the agent to Stuck. The wiki would sync a half-written vault.
|
|
16
|
+
*
|
|
17
|
+
* So the teardown closes this gate FIRST, and the two places the daemon spawns
|
|
18
|
+
* a lane's work ask it: runTurn.mjs (every CLI turn: tab, agent, plan,
|
|
19
|
+
* pre-review, wiki) and workAgentCheck.mjs (the project check). Once it is
|
|
20
|
+
* closed, a lane's spawn never starts, and a lane's child that ends never hands
|
|
21
|
+
* back its output. The promise the lane awaits stays pending, which is exactly
|
|
22
|
+
* where `process.exit` used to leave it: no settle, no retry, no sync. A pending
|
|
23
|
+
* promise holds no handle, so it never keeps the process up on its own.
|
|
24
|
+
*
|
|
25
|
+
* The deploy lane runs past the gate: it is what the drain exists for. It asks
|
|
26
|
+
* the gate once, when a claim's answer comes back after the stand-down began,
|
|
27
|
+
* and then does not run the deploy (deploy.mjs).
|
|
28
|
+
*
|
|
29
|
+
* Process-wide by design: there is one teardown per process, and it closes
|
|
30
|
+
* once and for good.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
let stopped = false;
|
|
34
|
+
|
|
35
|
+
/** The teardown's first act (fleet.mjs). Idempotent; never reopens. */
|
|
36
|
+
export function stopWorkLanes() {
|
|
37
|
+
stopped = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether the teardown has run in this process. */
|
|
41
|
+
export function workLanesStopped() {
|
|
42
|
+
return stopped;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What a lane awaits once the gate is closed: an answer that never comes. */
|
|
46
|
+
export function never() {
|
|
47
|
+
return new Promise(() => {});
|
|
48
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE START PATH'S PROJECT: which stored credential a bare `flowviant` serves
|
|
3
|
+
* here, said, asked, or refused — never guessed — and the repo binding the
|
|
4
|
+
* person consented to, handed back DEFERRED.
|
|
5
|
+
*
|
|
6
|
+
* SPLIT OUT OF cli.mjs (SOLID audit 2026-09-26, F053): the entrypoint changed
|
|
7
|
+
* for terminal dispatch, for the `machines` menu, and for this policy. The
|
|
8
|
+
* picker, the single-unbound confirm, every refusal and the tray folder check
|
|
9
|
+
* are one decision with one reason to change, so they live here and cli.mjs
|
|
10
|
+
* asks `resolveStartProject` once and passes its answer to runFleetDaemon.
|
|
11
|
+
* Moved verbatim; the refusals still end the process in the same words.
|
|
12
|
+
*
|
|
13
|
+
* THE BINDING IS NEVER WRITTEN HERE. What this returns is `afterLock` — the
|
|
14
|
+
* store write, or null — and runFleetDaemon runs it only once the instance
|
|
15
|
+
* lock refused nothing (startBind.test.mjs pins the order and that this is
|
|
16
|
+
* the one home of both writes).
|
|
17
|
+
*/
|
|
18
|
+
import { FLEET_TOKEN, CREDENTIAL, adoptStoredCredential } from './config.mjs';
|
|
19
|
+
import { runLogin } from './login.mjs';
|
|
20
|
+
import { terminalCommand } from './launchCommand.mjs';
|
|
21
|
+
import { canPrompt, askWithTimeout, selectMenu, menuSupported } from './tty.mjs';
|
|
22
|
+
|
|
23
|
+
/** Re-exec a plain `flowviant` after an inline login — the login command's own
|
|
24
|
+
* pattern: config.mjs read the store at IMPORT time, before the credential
|
|
25
|
+
* existed, so this process cannot serve; the child can. */
|
|
26
|
+
async function reexecAfterLogin() {
|
|
27
|
+
const login = await runLogin({ thenStart: false });
|
|
28
|
+
if (!login?.saved) process.exit(1); // refused: the directory already serves another project
|
|
29
|
+
const { spawn } = await import('node:child_process');
|
|
30
|
+
const child = spawn(process.execPath, process.argv[1]?.startsWith('/$bunfs/') ? [] : [process.argv[1]], { stdio: 'inherit', env: process.env });
|
|
31
|
+
process.exit(await new Promise((resolve) => child.on('exit', (code) => resolve(code ?? 0))));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TWO ROWS THAT READ ALIKE ARE NOT A CHOICE. `projectRowLabel` adds the id and
|
|
35
|
+
// the connected date to the COLLIDING rows only — production held two projects
|
|
36
|
+
// both called "BRIF AI", both bound to the same checkout, so this picker
|
|
37
|
+
// offered two identical lines and the only way to answer was to guess. See
|
|
38
|
+
// credentials.mjs for why the suffix is not on every row. (That exact case is
|
|
39
|
+
// a refusal now rather than a picker — a directory serves one project — but
|
|
40
|
+
// two same-named projects can still meet in the no-match picker.)
|
|
41
|
+
function listLines(entries, { projectRowLabel }) {
|
|
42
|
+
return entries
|
|
43
|
+
.map(
|
|
44
|
+
(e, i) =>
|
|
45
|
+
` ${i + 1}. ${projectRowLabel(e, entries)}` +
|
|
46
|
+
(e.repoRoot ? ` — connected for ${e.repoRoot}` : ' — not tied to a repo yet')
|
|
47
|
+
)
|
|
48
|
+
.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the project this start serves. Returns the deferred binding (or
|
|
53
|
+
* null); every refusal exits the process in words, as the entrypoint did.
|
|
54
|
+
* `dirGiven`: the start named its checkout with `--dir` (the tray's form).
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveStartProject({ dirGiven }) {
|
|
57
|
+
// ── WHICH PROJECT THIS START SERVES — said, asked, or refused; never guessed.
|
|
58
|
+
//
|
|
59
|
+
// The store holds many projects since 0.55.0 and resolution is BY REPO
|
|
60
|
+
// (credentials.mjs). What is left here is the human half: an ambiguous store
|
|
61
|
+
// on a TTY becomes a PICKER, a single unbound credential gets ONE confirm that
|
|
62
|
+
// binds it, and a headless start with no unambiguous answer refuses in words
|
|
63
|
+
// that name every stored project and every way out. The one thing this block
|
|
64
|
+
// must never do is serve a project the resolution did not name — "it said
|
|
65
|
+
// skadooble in my calendar repo" is the confusion this exists to end.
|
|
66
|
+
// A RESTART IS NOT A PERSON. `reexec` (update.mjs) inherits stdio, so an
|
|
67
|
+
// auto-updated daemon's child sees two TTYs; without this it would stop on the
|
|
68
|
+
// binding confirm below and the machine would stay dark until somebody typed a
|
|
69
|
+
// key. Same reasoning as the headless case, and the same answer.
|
|
70
|
+
// `canPrompt()`, not a bare isTTY pair: a BACKGROUNDED job (`flowviant &`) has
|
|
71
|
+
// two TTYs and cannot be asked anything — the first read raises SIGTTIN and the
|
|
72
|
+
// kernel STOPS the process, which is why 0.55.2's timeout did not save it (a
|
|
73
|
+
// stopped process runs no timers). See tty.mjs.
|
|
74
|
+
const interactive = canPrompt() && process.env.FLOWVIANT_REEXEC !== '1';
|
|
75
|
+
|
|
76
|
+
/** NO ANSWER TIME LIMIT on the start path (2026-09-20, the owner: "why is
|
|
77
|
+
* there an answer time limit, remove that"). The confirm below carried 20s
|
|
78
|
+
* and the picker 60s, each arguing that a restart nobody is watching must
|
|
79
|
+
* not sit dark on a prompt — but `interactive` already excludes exactly that
|
|
80
|
+
* case (no foreground terminal, or a self-update re-exec, and nothing is
|
|
81
|
+
* asked). What was left was a person reading a list being told "no answer
|
|
82
|
+
* in 60s — nothing started". A prompt a person can see waits for the
|
|
83
|
+
* person; tty.mjs's header carries the argument. */
|
|
84
|
+
const externalToken =
|
|
85
|
+
process.argv.includes('--fleet') ||
|
|
86
|
+
Boolean(process.env.FLOWVIANT_MACHINE_TOKEN) ||
|
|
87
|
+
Boolean(process.env.FLOWVIANT_FLEET);
|
|
88
|
+
|
|
89
|
+
/** A repo binding the person just consented to, persisted by runFleetDaemon
|
|
90
|
+
* only after the instance lock is taken — never before a refusal. */
|
|
91
|
+
let afterLock = null;
|
|
92
|
+
|
|
93
|
+
if (!FLEET_TOKEN) {
|
|
94
|
+
if (CREDENTIAL.error) {
|
|
95
|
+
console.error(`error: ${CREDENTIAL.error}. \`${terminalCommand('projects')}\` lists what is stored.`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
// A DIRECTORY SERVES ONE PROJECT (2026-09-23, the owner, verbatim: "a
|
|
99
|
+
// directory cannot have more than one project on flowviant. if one already
|
|
100
|
+
// exists, it would warn the user and ask them to delete the project on
|
|
101
|
+
// flowviant first. because 2 projects shouldnt be able to edit a directory
|
|
102
|
+
// at the same time."). This used to be a PICKER — two projects bound to one
|
|
103
|
+
// checkout, choose which this daemon serves — which is a control offering to
|
|
104
|
+
// do the forbidden thing politely. It is a refusal now, on a TTY and
|
|
105
|
+
// headless alike, naming both projects and the remedy. `credentialRefusal`
|
|
106
|
+
// is the same sentence login prints when it declines to bind a second one.
|
|
107
|
+
if (CREDENTIAL.reason === 'multiple-bound') {
|
|
108
|
+
const { directoryTakenRefusal } = await import('./credentials.mjs');
|
|
109
|
+
console.error(directoryTakenRefusal(CREDENTIAL.choices, CREDENTIAL.repoRoot));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (CREDENTIAL.choices?.length && interactive) {
|
|
113
|
+
const creds = await import('./credentials.mjs');
|
|
114
|
+
const { originSlug } = await import('./git.mjs');
|
|
115
|
+
const { basename } = await import('node:path');
|
|
116
|
+
const { choices, repoRoot } = CREDENTIAL;
|
|
117
|
+
console.log(
|
|
118
|
+
CREDENTIAL.reason === 'outside-repo'
|
|
119
|
+
? 'flowviant is not inside a git repo, and more than one project is connected here.'
|
|
120
|
+
: `This repo (${repoRoot}) is not connected to any project yet. Connected on this machine:`
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const loginLabel = `connect ${repoRoot ? 'this repo' : 'a repo'} to a different project (${terminalCommand('login')})`;
|
|
124
|
+
// WHICH ONE LOOKS RIGHT — a pre-selection, never an auto-serve. The resolver
|
|
125
|
+
// refuses to serve a project the repo PATH did not name (the skadooble law);
|
|
126
|
+
// this only decides which row the cursor starts on, using the repo's folder
|
|
127
|
+
// name and its github repo-name against the stored project names. A unique
|
|
128
|
+
// match becomes ONE keypress; a wrong guess costs nothing, because the human
|
|
129
|
+
// still confirms. (Two projects bound to THIS repo never reach here: that
|
|
130
|
+
// is a refusal above, since 2026-09-23.)
|
|
131
|
+
const slug = repoRoot ? originSlug(repoRoot) : null;
|
|
132
|
+
const likely = creds.likelyChoiceIndex(choices, {
|
|
133
|
+
repoBasename: repoRoot ? basename(repoRoot) : null,
|
|
134
|
+
repoSlugName: slug ? slug.split('/')[1] : null,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Bounded like the confirm below, and for the same reason — but silence
|
|
138
|
+
// means something DIFFERENT here and the difference is load-bearing. There
|
|
139
|
+
// is a real ambiguity to resolve; serving a guess is the skadooble bug.
|
|
140
|
+
// So no answer REFUSES, which is exactly what this branch already does
|
|
141
|
+
// headless, and the message says how to answer without being present.
|
|
142
|
+
let chosen = null; // 0-based into [...choices, login]
|
|
143
|
+
if (menuSupported()) {
|
|
144
|
+
const rowLabel = (e) =>
|
|
145
|
+
creds.projectRowLabel(e, choices) +
|
|
146
|
+
(e.repoRoot ? ` — connected for ${e.repoRoot}` : ' — not tied to a repo yet');
|
|
147
|
+
const options = [...choices.map(rowLabel), loginLabel];
|
|
148
|
+
// Say WHY the cursor starts where it does — "intuitive" made visible.
|
|
149
|
+
if (likely >= 0) options[likely] += ' ← looks like this repo';
|
|
150
|
+
const res = await selectMenu({
|
|
151
|
+
options,
|
|
152
|
+
defaultIndex: likely >= 0 ? likely : 0,
|
|
153
|
+
});
|
|
154
|
+
if (res.cancelled) {
|
|
155
|
+
console.error('nothing chosen — nothing started.');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
if (!res.unsupported) chosen = res.index;
|
|
159
|
+
}
|
|
160
|
+
if (chosen === null) {
|
|
161
|
+
// Numeric fallback — a pipe, or a terminal without raw mode. Empty Enter
|
|
162
|
+
// takes the likely default when there is one, so it is one keystroke here
|
|
163
|
+
// too.
|
|
164
|
+
console.log(listLines(choices, creds));
|
|
165
|
+
console.log(` ${choices.length + 1}. ${loginLabel}`);
|
|
166
|
+
const hint = likely >= 0 ? ` (enter for ${creds.projectLabel(choices[likely])})` : '';
|
|
167
|
+
const raw = await askWithTimeout(
|
|
168
|
+
`Which project should this daemon serve? [1-${choices.length + 1}]${hint} `
|
|
169
|
+
);
|
|
170
|
+
if (raw === null) {
|
|
171
|
+
// The read itself failed — stdin closed, or the terminal went away
|
|
172
|
+
// mid-question. Not a timeout: there is none.
|
|
173
|
+
console.error(
|
|
174
|
+
`\ncould not read an answer — nothing started. ` +
|
|
175
|
+
`Name one with \`--project <name|id>\`, or run \`flowviant\` here in the foreground and pick.`
|
|
176
|
+
);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const n = raw === '' && likely >= 0 ? likely + 1 : Number.parseInt(raw, 10);
|
|
180
|
+
chosen = Number.isInteger(n) ? n - 1 : -1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (chosen === choices.length) await reexecAfterLogin();
|
|
184
|
+
const picked = chosen >= 0 ? choices[chosen] : undefined;
|
|
185
|
+
if (!picked) {
|
|
186
|
+
console.error('nothing chosen — nothing started.');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
// An answered question is consent: adopt it, and BIND it to this repo so
|
|
190
|
+
// the next start needs no prompt. Repointing is legitimate and said aloud.
|
|
191
|
+
if (repoRoot && picked.repoRoot && picked.repoRoot !== repoRoot) {
|
|
192
|
+
console.log(`note: ${creds.projectLabel(picked)} was connected for ${picked.repoRoot} — now serving ${repoRoot} instead.`);
|
|
193
|
+
}
|
|
194
|
+
adoptStoredCredential(picked);
|
|
195
|
+
// BOUND ONLY ONCE THE START HAS THE LOCK. Written here, the answer moved
|
|
196
|
+
// the binding even when the instance lock then refused the start — a live
|
|
197
|
+
// daemon for the same project in its own checkout — and that daemon's next
|
|
198
|
+
// unattended self-update re-exec'd into a store that no longer bound its
|
|
199
|
+
// repo, found no match headless, and exited with the machine dark.
|
|
200
|
+
afterLock = () => creds.selectStoredProject(picked.projectId, { bindRepoRoot: repoRoot ?? undefined });
|
|
201
|
+
console.log(`serving ${creds.projectLabel(picked)}${repoRoot ? ` from ${repoRoot}` : ''}.`);
|
|
202
|
+
} else if (CREDENTIAL.choices?.length) {
|
|
203
|
+
const creds = await import('./credentials.mjs');
|
|
204
|
+
console.error(
|
|
205
|
+
'error: more than one project is connected on this machine and this repo is not bound to any of them:\n' +
|
|
206
|
+
listLines(CREDENTIAL.choices, creds) +
|
|
207
|
+
`\nPick one with \`--project <name|id>\`, bind this repo by running \`${terminalCommand()}\` here in a terminal once,\n` +
|
|
208
|
+
`or connect this repo to its own project with \`${terminalCommand('login')}\`.`
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
} else {
|
|
212
|
+
console.error(
|
|
213
|
+
'error: no credential found. Easiest:\n' +
|
|
214
|
+
` ${terminalCommand('login')} (approve in the app — recommended)\n` +
|
|
215
|
+
'Or set:\n' +
|
|
216
|
+
' FLOWVIANT_FLEET=fva_… (machine token, from the app)'
|
|
217
|
+
);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
} else if (!externalToken && CREDENTIAL.needsConfirm && interactive) {
|
|
221
|
+
// ONE stored project, never tied to a repo — the pre-0.55.0 world. Ask once;
|
|
222
|
+
// yes binds and every later start is silent. This is the exact question
|
|
223
|
+
// whose absence had a calendar checkout serving skadooble.
|
|
224
|
+
//
|
|
225
|
+
// AND IT TIMES OUT, because a prompt on a start path is a way for a machine
|
|
226
|
+
// to go dark. `FLOWVIANT_REEXEC` above covers the restart THIS version
|
|
227
|
+
// performs, but it cannot cover the one that matters most: the hop that
|
|
228
|
+
// installs a fixed daemon is spawned by the OLD one, which never sets it.
|
|
229
|
+
// 0.55.0 → 0.55.1 was exactly that — an auto-update landing unattended would
|
|
230
|
+
// stop here with the machine serving nothing. A guard that only works once
|
|
231
|
+
// everyone already has it is not a guard.
|
|
232
|
+
//
|
|
233
|
+
// IF THE READ FAILS WE SERVE, AND WE DO NOT BIND. (This used to say "on
|
|
234
|
+
// timeout"; there is no timeout since 2026-09-20 — a person at the prompt
|
|
235
|
+
// is waited for. `null` now means stdin closed under the question.) Those
|
|
236
|
+
// are two decisions:
|
|
237
|
+
// · SERVE, because it is what every version before 0.55.0 did with this
|
|
238
|
+
// exact store, so the silent path is the status quo rather than a new
|
|
239
|
+
// risk — and a daemon that answers is strictly better than one that does
|
|
240
|
+
// not, which is the whole reason this product has exactly one refusal.
|
|
241
|
+
// · DO NOT BIND, because binding is the thing the question was FOR. Nobody
|
|
242
|
+
// answered, so nothing is cemented; the next human start asks again. That
|
|
243
|
+
// keeps the skadooble case fixed for the person who is actually looking,
|
|
244
|
+
// which is the only person it could ever have been fixed for.
|
|
245
|
+
const creds = await import('./credentials.mjs');
|
|
246
|
+
const label = creds.projectLabel(CREDENTIAL.entry);
|
|
247
|
+
const answered = await askWithTimeout(
|
|
248
|
+
`This machine's one connected project is ${label}. Serve this repo (${CREDENTIAL.repoRoot}) as ${label}? [Y/n] `
|
|
249
|
+
);
|
|
250
|
+
const raw = answered === null ? null : answered.toLowerCase(); // null = the read failed
|
|
251
|
+
if (raw === null) {
|
|
252
|
+
console.log(
|
|
253
|
+
`\n could not read an answer — serving ${label} for this run ` +
|
|
254
|
+
`without tying it to this repo. Run \`${terminalCommand()}\` here and answer to make it stick.`
|
|
255
|
+
);
|
|
256
|
+
} else if (raw === '' || raw === 'y' || raw === 'yes') {
|
|
257
|
+
// After the lock, for the reason the picker's bind gives above.
|
|
258
|
+
afterLock = () => creds.bindStoredRepo(CREDENTIAL.entry.projectId, CREDENTIAL.repoRoot);
|
|
259
|
+
} else {
|
|
260
|
+
console.error(
|
|
261
|
+
`nothing started. Connect this repo to its own project with \`${terminalCommand('login')}\`, ` +
|
|
262
|
+
`or see what is stored with \`${terminalCommand('projects')}\`.`
|
|
263
|
+
);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (process.argv.includes('--json-events') && dirGiven && CREDENTIAL?.entry) {
|
|
269
|
+
const { detectRepoRoot, listStoredProjects, boundElsewhere } = await import('./credentials.mjs');
|
|
270
|
+
const repo = detectRepoRoot();
|
|
271
|
+
const entry = CREDENTIAL.entry;
|
|
272
|
+
if (!repo || entry.repoRoot !== repo || boundElsewhere(listStoredProjects(), repo, entry.projectId).length) {
|
|
273
|
+
console.error('error: this project is not connected to this repository. Run `flowviant login` for the selected folder.');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return afterLock;
|
|
279
|
+
}
|
package/bin/lib/trace.mjs
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* tail of a trace, never the settle behind it.
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
import { THINK_MARKER } from './
|
|
62
|
+
import { THINK_MARKER } from './runtimeEvents.mjs';
|
|
63
63
|
|
|
64
64
|
/** One batch every two seconds — the discipline the tab's narrator keeps, for
|
|
65
65
|
* the same reason: a turn emits hundreds of entries and nobody is reading them
|
|
@@ -190,7 +190,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
190
190
|
return {
|
|
191
191
|
/**
|
|
192
192
|
* A prose line from the stream. `kind` is the daemon's activity vocabulary
|
|
193
|
-
* (
|
|
193
|
+
* (runtimeEvents.mjs); anything that is not thinking or the model speaking is a
|
|
194
194
|
* `note` — the honest bucket for a codex error line or an agy tool name,
|
|
195
195
|
* rather than a wire value invented per runtime.
|
|
196
196
|
*
|
|
@@ -225,7 +225,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
225
225
|
* real transcripts: 93 blocks, all of them empty) carries no text in any
|
|
226
226
|
* of them — so the trace filled with the identical marker repeated, which
|
|
227
227
|
* is noise standing exactly where the thought would have been. The wiki
|
|
228
|
-
* feed collapses the same run for the same reason (
|
|
228
|
+
* feed collapses the same run for the same reason (wikiRunner.mjs: "Collapse
|
|
229
229
|
* runs of bare 'thinking…' so the feed doesn't fill with it").
|
|
230
230
|
*
|
|
231
231
|
* ONLY THE BARE MARKER, and that is the whole safety of it: a think WITH
|