flowviant 0.125.13 → 0.125.15
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/README.md +2 -1
- package/bin/cli.mjs +5 -0
- package/bin/lib/captureAuthority.mjs +82 -0
- package/bin/lib/claudePosture.mjs +9 -0
- package/bin/lib/hooks/orchestrator.mjs +1 -1
- package/bin/lib/mcpStdio.mjs +11 -3
- package/bin/lib/nativeAgentTools.mjs +4 -1
- package/bin/lib/orchestrator.mjs +46 -20
- package/bin/lib/orchestratorClient.mjs +2 -2
- package/bin/lib/orchestratorPrompt.mjs +70 -0
- package/bin/lib/readOnlyRun.mjs +172 -0
- package/bin/lib/runTurn.mjs +1 -0
- package/bin/lib/runtimeClaude.mjs +6 -5
- package/bin/lib/runtimeCodex.mjs +4 -4
- package/bin/lib/turnProfile.mjs +1 -0
- package/bin/lib/workAgentPlans.mjs +69 -43
- package/bin/lib/workCappedTurn.mjs +4 -0
- package/bin/lib/workNarration.mjs +3 -2
- package/bin/lib/workPlanRecovery.mjs +2 -21
- package/bin/lib/workSessionTurns.mjs +109 -22
- package/bin/lib/workToolLog.mjs +3 -0
- package/package.json +1 -1
|
@@ -46,6 +46,15 @@ import { sessionTurnKickoff, sessionTurnShape, sessionTurnSystem } from './workT
|
|
|
46
46
|
import { createToolLog } from './workToolLog.mjs';
|
|
47
47
|
import { createCommandAudit } from './workCommandAudit.mjs';
|
|
48
48
|
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, withRelay } from './codexRelay.mjs';
|
|
49
|
+
import { createReadOnlyCoordination, readOnlyCoordinationSafe, watchReadOnlyAttempt } from './readOnlyRun.mjs';
|
|
50
|
+
import { createCaptureAuthority } from './captureAuthority.mjs';
|
|
51
|
+
import { captureOrchestratorContract } from './orchestratorPrompt.mjs';
|
|
52
|
+
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
53
|
+
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
54
|
+
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
55
|
+
import { FLEET_URL, DAEMON_INSTANCE, MODEL } from './config.mjs';
|
|
56
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
57
|
+
const defaultWatchCapture = job => watchReadOnlyAttempt(fleetEndpoint('capture-run-state', FLEET_URL), job);
|
|
49
58
|
|
|
50
59
|
/**
|
|
51
60
|
* HOW LONG, AND HOW MANY, SETTLED TURNS THE LANE REMEMBERS (2026-09-29) — see
|
|
@@ -91,6 +100,9 @@ export function createWorkSessionTurns({
|
|
|
91
100
|
// The real CLI driver; injectable only so the lane can be driven in a test
|
|
92
101
|
// without spending a model turn (the wiki runner's shape).
|
|
93
102
|
runTurn = defaultRunTurn,
|
|
103
|
+
runtimeVersion = id => measuredRuntimeVersion(id)?.version,
|
|
104
|
+
coordinate = createReadOnlyCoordination,
|
|
105
|
+
watchCapture = defaultWatchCapture,
|
|
94
106
|
// The clock `settledHere` ages by; injectable only for its bound's test.
|
|
95
107
|
now = Date.now,
|
|
96
108
|
makeLiveMessages = createLiveMessages,
|
|
@@ -140,14 +152,17 @@ export function createWorkSessionTurns({
|
|
|
140
152
|
* be delivered by `pendingWorkReports`, and this covers the one that was.
|
|
141
153
|
*/
|
|
142
154
|
const settledHere = new Map(); // turnId -> when this lane settled it, oldest first
|
|
155
|
+
const settledAttempts = new Map(); // only measured execution generations
|
|
143
156
|
const forgetStale = (at) => {
|
|
144
157
|
for (const [id, when] of settledHere) {
|
|
145
158
|
if (settledHere.size <= SETTLED_HERE_MAX && at - when < SETTLED_HERE_TTL_MS) break;
|
|
146
159
|
settledHere.delete(id);
|
|
160
|
+
settledAttempts.delete(id);
|
|
147
161
|
}
|
|
148
162
|
};
|
|
149
|
-
const settledRecently = (turnId) => {
|
|
163
|
+
const settledRecently = (turnId, attempt) => {
|
|
150
164
|
forgetStale(now());
|
|
165
|
+
if (Number.isInteger(attempt) && settledAttempts.has(turnId) && settledAttempts.get(turnId) !== attempt) return false;
|
|
151
166
|
return settledHere.has(turnId);
|
|
152
167
|
};
|
|
153
168
|
const settleTurn = (turnId, payload) => {
|
|
@@ -156,6 +171,7 @@ export function createWorkSessionTurns({
|
|
|
156
171
|
const at = now();
|
|
157
172
|
settledHere.delete(turnId); // re-inserted at the end: the map stays in time order
|
|
158
173
|
settledHere.set(turnId, at);
|
|
174
|
+
if (Number.isInteger(run?.job?.attempt)) settledAttempts.set(turnId, run.job.attempt);
|
|
159
175
|
forgetStale(at);
|
|
160
176
|
const liveMessages = runs.get(turnId)?.live?.receipts();
|
|
161
177
|
return settleWorkTurn(turnId, { ...payload, ...(liveMessages?.ids.length ? { liveMessages } : {}) });
|
|
@@ -185,13 +201,18 @@ export function createWorkSessionTurns({
|
|
|
185
201
|
if (!job || typeof job.id !== 'string' || !job.body || !job.sessionId) continue;
|
|
186
202
|
if (rosterInterrupts.has(job.id)) continue;
|
|
187
203
|
if (workAnswering.has(job.id)) {
|
|
188
|
-
const
|
|
204
|
+
const previous = runs.get(job.id);
|
|
205
|
+
if (job.captureOrchestrationVersion === 1 && Number.isInteger(job.attempt) && Number.isInteger(previous?.job.attempt) && job.attempt !== previous.job.attempt) {
|
|
206
|
+
previous.interrupted = true; previous.abort.abort();
|
|
207
|
+
for (const child of previous.spawned) interruptThenKill(child);
|
|
208
|
+
}
|
|
209
|
+
const reason = previous?.waitingReason;
|
|
189
210
|
if (reason) reportDelivery('session', job, 'queued', reason);
|
|
190
211
|
continue;
|
|
191
212
|
}
|
|
192
213
|
// Settled by this lane already: a roster read from before the settle
|
|
193
214
|
// landed. See `settledHere`.
|
|
194
|
-
if (settledRecently(job.id)) continue;
|
|
215
|
+
if (settledRecently(job.id, job.attempt)) continue;
|
|
195
216
|
// The turn already RAN and its answer sits in the delivery queue — never
|
|
196
217
|
// run it again while the report is merely undelivered.
|
|
197
218
|
if (pendingWorkReports.has(job.id)) continue;
|
|
@@ -225,9 +246,10 @@ export function createWorkSessionTurns({
|
|
|
225
246
|
continue;
|
|
226
247
|
}
|
|
227
248
|
const releaseSlot = admit.reserve();
|
|
249
|
+
if (Number.isInteger(job.attempt) && settledAttempts.has(job.id) && settledAttempts.get(job.id) !== job.attempt) workAttempts.delete(job.id);
|
|
228
250
|
lastDeferSaid.delete(job.sessionId);
|
|
229
251
|
workAnswering.add(job.id);
|
|
230
|
-
const run = { interrupted: false, spawned: [], sessionId: job.sessionId, live: null, job };
|
|
252
|
+
const run = { interrupted: false, spawned: [], sessionId: job.sessionId, live: null, job, abort: new AbortController() };
|
|
231
253
|
runs.set(job.id, run);
|
|
232
254
|
/** The execution generation the server handed, echoed on every settle
|
|
233
255
|
* so the server can tell this execution's answer from an older one's. */
|
|
@@ -644,17 +666,29 @@ export function createWorkSessionTurns({
|
|
|
644
666
|
// without one — see workTurnShape.mjs). A plan turn's credential is
|
|
645
667
|
// still minted above: its lease answers (held elsewhere, gone) are
|
|
646
668
|
// about the SESSION, and a plan turn needs them as much as any other.
|
|
647
|
-
|
|
648
|
-
? mcpFor(rt.id, mint.token, getMcpUrl())
|
|
649
|
-
: { args: [], env: null, dir: null };
|
|
669
|
+
let mcp = { args: [], env: null, dir: null };
|
|
650
670
|
// The tab's model/effort, if it named any. Spread into turnArgs so
|
|
651
671
|
// BOTH runTurn calls below carry it — the retry is the same turn on
|
|
652
672
|
// the same brain, not a quieter second opinion.
|
|
653
673
|
const brain = brainFor(job, rt.id);
|
|
674
|
+
const childBrain = { ...brain, runtime: rt.id, model: brain.model ?? (rt.id === 'claude' ? MODEL : null),
|
|
675
|
+
...(rt.id === 'codex' ? { catalog: knownCodexModels() } : {}) };
|
|
676
|
+
const version = shape.capture && job.captureOrchestrationVersion === 1 ? runtimeVersion(rt.id) : null;
|
|
677
|
+
const delegated = shape.capture && job.captureOrchestrationVersion === 1 && Number.isSafeInteger(job.attempt)
|
|
678
|
+
&& job.attempt >= 0 && readOnlyCoordinationSafe(rt.id, version);
|
|
679
|
+
let coordination = null, control = null;
|
|
680
|
+
let captureSignal = run.abort.signal;
|
|
681
|
+
const makeCoordination = () => coordinate({ enabled: true, runtime: rt.id, version, admit,
|
|
682
|
+
identity: { scope: 'capture', id: job.id, attempt: job.attempt }, signal: captureSignal,
|
|
683
|
+
children: childBrain,
|
|
684
|
+
rootTools: createCaptureAuthority({ url: getMcpUrl(), token: mint.token, sessionId: job.sessionId,
|
|
685
|
+
turnId: job.id, attempt: job.attempt, signal: captureSignal }),
|
|
686
|
+
});
|
|
654
687
|
// Attempts count RUNS: the infra refusals above consumed nothing and
|
|
655
688
|
// settled on their own terms.
|
|
656
689
|
workAttempts.set(job.id, tries + 1);
|
|
657
690
|
let out;
|
|
691
|
+
let turnReturned = false;
|
|
658
692
|
let seenThreadId = null; // codex's conversation id, off thread.started
|
|
659
693
|
let seenClaudeSession = null; // claude's own conversation id, off system.init
|
|
660
694
|
// What a Codex turn's own events say went wrong (codexRelay.mjs):
|
|
@@ -678,9 +712,8 @@ export function createWorkSessionTurns({
|
|
|
678
712
|
const turnLog = createToolLog(dir.wt);
|
|
679
713
|
toolLog = turnLog.toolLog;
|
|
680
714
|
const { pushToolEvent } = turnLog;
|
|
681
|
-
const narrator = makeNarrator(job.sessionId, job.id, () =>
|
|
682
|
-
|
|
683
|
-
);
|
|
715
|
+
const narrator = makeNarrator(job.sessionId, job.id, () => toolLog,
|
|
716
|
+
delegated ? { attempt: job.attempt, instance: DAEMON_INSTANCE } : {});
|
|
684
717
|
|
|
685
718
|
|
|
686
719
|
// THE COMMAND AUDIT — see workCommandAudit.mjs.
|
|
@@ -691,6 +724,19 @@ export function createWorkSessionTurns({
|
|
|
691
724
|
cwd: dir.wt,
|
|
692
725
|
});
|
|
693
726
|
try {
|
|
727
|
+
if (delegated) {
|
|
728
|
+
control = await watchCapture(job);
|
|
729
|
+
captureSignal = AbortSignal.any([run.abort.signal, control.signal]);
|
|
730
|
+
const stopped = () => { run.interrupted = true; run.abort.abort(); };
|
|
731
|
+
if (captureSignal.aborted) stopped();
|
|
732
|
+
else control.signal.addEventListener('abort', stopped, { once: true });
|
|
733
|
+
if (!run.interrupted) coordination = await makeCoordination();
|
|
734
|
+
}
|
|
735
|
+
// A safe native gate replaces the credential mount, keeping the
|
|
736
|
+
// actual capture credential entirely outside the CLI's children.
|
|
737
|
+
// Unsupported/gate-unavailable turns retain the original solo path.
|
|
738
|
+
if (!coordination && shape.mountMcp) mcp = mcpFor(rt.id, mint.token, getMcpUrl());
|
|
739
|
+
const captureContract = coordination ? captureOrchestratorContract(childBrain) : '';
|
|
694
740
|
// Files first, then the message that references them: the agent
|
|
695
741
|
// must be able to open what it is being told about. Only the ones
|
|
696
742
|
// that actually landed are named.
|
|
@@ -750,7 +796,7 @@ export function createWorkSessionTurns({
|
|
|
750
796
|
knowledgeDir,
|
|
751
797
|
artifactsAccepted: getArtifactsAccepted(),
|
|
752
798
|
runtime: rt.id,
|
|
753
|
-
}) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : '');
|
|
799
|
+
}) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : '') + (captureContract ? `\n\n${captureContract}` : '');
|
|
754
800
|
contractSent = !codexContractKnown(contractPath, codexResumeId, turnSystem);
|
|
755
801
|
return contractSent ? turnSystem : '';
|
|
756
802
|
})(),
|
|
@@ -862,7 +908,43 @@ export function createWorkSessionTurns({
|
|
|
862
908
|
}
|
|
863
909
|
},
|
|
864
910
|
};
|
|
865
|
-
|
|
911
|
+
const invoke = async (args, retry = false) => {
|
|
912
|
+
if (run.interrupted) return '';
|
|
913
|
+
if (!captureContract) return runCli(args);
|
|
914
|
+
let releaseRetry = () => {};
|
|
915
|
+
if (retry) {
|
|
916
|
+
// The prior process no longer counts. Reserve its replacement
|
|
917
|
+
// against the SAME pool before awaiting a new local gate.
|
|
918
|
+
while (!run.interrupted) {
|
|
919
|
+
const hold = admit('interactive');
|
|
920
|
+
if (!hold) { releaseRetry = admit.reserve(); break; }
|
|
921
|
+
narrator.line(hold.reason);
|
|
922
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
if (retry && !run.interrupted) coordination = await makeCoordination();
|
|
927
|
+
if (run.interrupted) return '';
|
|
928
|
+
if (!coordination) {
|
|
929
|
+
if (!mcp.env) mcp = mcpFor(rt.id, mint.token, getMcpUrl());
|
|
930
|
+
return await runCli({ ...args, mcpArgs: mcp.args, mcpEnv: mcp.env,
|
|
931
|
+
onSpawn: ch => { releaseRetry(); args.onSpawn?.(ch); } });
|
|
932
|
+
}
|
|
933
|
+
const { onSpawn, onInit, ...turnArgs } = coordination.wrap({ ...args, mcpArgs: [], mcpEnv: null });
|
|
934
|
+
const result = await runCappedTurn({ runTurn: runCli, turnArgs, capMs: 15 * 60_000, workChildren,
|
|
935
|
+
releaseSlot: () => { releaseSlot(); releaseRetry(); }, onSpawn, onInit,
|
|
936
|
+
signal: captureSignal, killChild: coordination.kill,
|
|
937
|
+
// This is the ongoing conversation, never a scratch consult.
|
|
938
|
+
removeTranscript: () => {},
|
|
939
|
+
});
|
|
940
|
+
if (result.wedged && !run.interrupted) { cliFailed = true; return 'The capture turn timed out. Continue in this conversation; read list_staged before changing any proposals.'; }
|
|
941
|
+
return envScrub(result.out);
|
|
942
|
+
} finally {
|
|
943
|
+
releaseRetry();
|
|
944
|
+
await coordination?.close(run.interrupted); coordination = null;
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
out = await invoke({
|
|
866
948
|
...turnArgs,
|
|
867
949
|
resume,
|
|
868
950
|
resumeThreadId: codexResumeId || claudeResumeId || undefined,
|
|
@@ -913,9 +995,15 @@ export function createWorkSessionTurns({
|
|
|
913
995
|
cliFailed = false;
|
|
914
996
|
badExit = null;
|
|
915
997
|
contractSent = true;
|
|
916
|
-
out = await
|
|
998
|
+
out = await invoke({ ...turnArgs, resume: false, system: turnSystem }, true);
|
|
917
999
|
}
|
|
1000
|
+
turnReturned = true;
|
|
918
1001
|
} finally {
|
|
1002
|
+
await coordination?.close(run.interrupted);
|
|
1003
|
+
control?.stop();
|
|
1004
|
+
// The cap can resolve before a wedged provider closes stdout.
|
|
1005
|
+
// Its live-input relay must not keep taking the next prompt.
|
|
1006
|
+
if (delegated) await run.live?.stop();
|
|
919
1007
|
// The CLI has stopped printing, so stop relaying. The LINE itself
|
|
920
1008
|
// is cleared server-side at settle — clearing it here would race
|
|
921
1009
|
// the settle and blank the tab a beat before the reply lands.
|
|
@@ -930,15 +1018,13 @@ export function createWorkSessionTurns({
|
|
|
930
1018
|
}
|
|
931
1019
|
}
|
|
932
1020
|
if (mcp.dir) rmSync(mcp.dir, { recursive: true, force: true });
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE, seenThreadId);
|
|
941
|
-
recordCodexContract(contractPath, seenThreadId, turnSystem, contractSent);
|
|
1021
|
+
// Keep the exact native conversation on failures too. A later
|
|
1022
|
+
// capture continuation must reconcile its own staged rows, not
|
|
1023
|
+
// silently start a second conversation after a provider throws.
|
|
1024
|
+
if ((shape.capture || turnReturned) && rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
|
|
1025
|
+
pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE, seenThreadId);
|
|
1026
|
+
recordCodexContract(contractPath, seenThreadId, turnSystem, contractSent);
|
|
1027
|
+
}
|
|
942
1028
|
}
|
|
943
1029
|
/**
|
|
944
1030
|
* A PERSON INTERRUPTED IT. Whatever the CLI printed was cut off, so
|
|
@@ -1116,6 +1202,7 @@ export function createWorkSessionTurns({
|
|
|
1116
1202
|
if (run) {
|
|
1117
1203
|
if (!run.interrupted) note(`${c.cyan('tab')} ${c.dim('— interrupting the running turn')}`);
|
|
1118
1204
|
run.interrupted = true;
|
|
1205
|
+
run.abort.abort();
|
|
1119
1206
|
for (const ch of run.spawned) interruptThenKill(ch);
|
|
1120
1207
|
continue;
|
|
1121
1208
|
}
|
package/bin/lib/workToolLog.mjs
CHANGED
|
@@ -84,6 +84,8 @@ export function createToolLog(cwd) {
|
|
|
84
84
|
// past the server's limits (review, 2026-09-01).
|
|
85
85
|
const e = toolEventOf(name, input, cwd, envScrub);
|
|
86
86
|
if (!e) return;
|
|
87
|
+
if (typeof meta.agentId === 'string' && meta.agentId) e.agentId = scrubCut(meta.agentId, 160);
|
|
88
|
+
if (e.agentId && typeof meta.agentTitle === 'string' && meta.agentTitle) e.agentTitle = scrubCut(meta.agentTitle, 120);
|
|
87
89
|
if (e.t !== 'plan') {
|
|
88
90
|
if (id) { e.id = scrubCut(id, 160); correlated.set(id, e); }
|
|
89
91
|
if (name === 'Read') for (const key of ['offset', 'limit']) {
|
|
@@ -106,6 +108,7 @@ export function createToolLog(cwd) {
|
|
|
106
108
|
const last = toolLog.ev[toolLog.ev.length - 1];
|
|
107
109
|
const sameKey =
|
|
108
110
|
last &&
|
|
111
|
+
last.agentId === e.agentId &&
|
|
109
112
|
!id && !last.id &&
|
|
110
113
|
last.outcome === e.outcome &&
|
|
111
114
|
last.exitCode === e.exitCode &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.125.
|
|
3
|
+
"version": "0.125.15",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|