flowviant 0.125.12 → 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 +24 -1
- package/bin/cli.mjs +5 -0
- package/bin/lib/agentFiles.mjs +1 -1
- package/bin/lib/agentPrerequisiteContract.mjs +8 -0
- package/bin/lib/agentTaskKinds.mjs +4 -4
- package/bin/lib/artifactContracts.mjs +15 -11
- package/bin/lib/artifactPreview.mjs +2 -2
- package/bin/lib/captureAuthority.mjs +82 -0
- package/bin/lib/claudePosture.mjs +9 -0
- package/bin/lib/codexResearch.mjs +12 -1
- package/bin/lib/hooks/orchestrator.mjs +1 -1
- package/bin/lib/intermediateArtifacts.mjs +228 -0
- package/bin/lib/mcpStdio.mjs +11 -3
- package/bin/lib/modelExport.mjs +19 -0
- package/bin/lib/modelExportProtocol.mjs +80 -0
- 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/pinnedCapture.mjs +35 -7
- package/bin/lib/prompts.mjs +10 -3
- 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/work.mjs +5 -5
- package/bin/lib/workAgentMerges.mjs +8 -8
- package/bin/lib/workAgentPlans.mjs +69 -43
- package/bin/lib/workAgentReview.mjs +24 -1
- package/bin/lib/workAgentTurnExecution.mjs +29 -6
- package/bin/lib/workAgentTurnOutcome.mjs +15 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -5
- package/bin/lib/workAgentTurns.mjs +4 -0
- package/bin/lib/workAttachments.mjs +4 -2
- package/bin/lib/workCappedTurn.mjs +4 -0
- package/bin/lib/workLiveMessages.mjs +2 -2
- 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
|
@@ -47,7 +47,7 @@ import { runAgentWithInternalWaits } from './agentInternalWait.mjs';
|
|
|
47
47
|
import { brainFor } from './workBrain.mjs';
|
|
48
48
|
import { refreshRestartBase } from './workAgentRecovery.mjs';
|
|
49
49
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
50
|
-
import { agentTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
50
|
+
import { agentDeclaredDelivery, agentTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
51
51
|
import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
|
|
52
52
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
53
53
|
import { agentTurnResume } from './workAgentResume.mjs';
|
|
@@ -59,6 +59,7 @@ import { ORCHESTRATED_POSTURES, orchestratesPosture } from './orchestratorPrompt
|
|
|
59
59
|
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
60
60
|
import { codexSubagentsSafe } from './runtimeCodex.mjs';
|
|
61
61
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
62
|
+
import { createIntermediateArtifacts } from './intermediateArtifacts.mjs';
|
|
62
63
|
|
|
63
64
|
export function createAgentTurnExecution({
|
|
64
65
|
admit,
|
|
@@ -76,6 +77,7 @@ export function createAgentTurnExecution({
|
|
|
76
77
|
reportSessionWorktree,
|
|
77
78
|
artifacts,
|
|
78
79
|
runReviewEntry,
|
|
80
|
+
runMergeCheck,
|
|
79
81
|
workChildren,
|
|
80
82
|
agentChildren,
|
|
81
83
|
postAgentTrace,
|
|
@@ -84,6 +86,8 @@ export function createAgentTurnExecution({
|
|
|
84
86
|
postAgentParked,
|
|
85
87
|
fetchAgentFiles,
|
|
86
88
|
makeLiveMessages = createLiveMessages,
|
|
89
|
+
runTurnImpl = runTurn,
|
|
90
|
+
exportModel,
|
|
87
91
|
}) {
|
|
88
92
|
const reportDelivery = createDeliveryReporter();
|
|
89
93
|
/** How long the final flush may hold the settle. Bounded because the settle
|
|
@@ -276,6 +280,10 @@ export function createAgentTurnExecution({
|
|
|
276
280
|
* prompt is byte-for-byte what it was.
|
|
277
281
|
*/
|
|
278
282
|
await fetchResultInputs(wt, job.resultInputs);
|
|
283
|
+
const intermediate = createIntermediateArtifacts({ wt, agentId,
|
|
284
|
+
path: sessionMetaPath(wt, 'flowviant-agent-artifacts', agentId),
|
|
285
|
+
...(exportModel ? { exportPage: exportModel } : {}) });
|
|
286
|
+
const intermediateFiles = intermediate.inputs();
|
|
279
287
|
const files =
|
|
280
288
|
Array.isArray(job.attachments) && job.attachments.length > 0
|
|
281
289
|
? await fetchAgentFiles(wt, job.attachments)
|
|
@@ -380,7 +388,7 @@ export function createAgentTurnExecution({
|
|
|
380
388
|
const spawnedChildren = new Set();
|
|
381
389
|
/** The CLI, unless a person already asked to stop this turn (Interrupt
|
|
382
390
|
* now): then nothing starts — no fresh retry, no continuation. */
|
|
383
|
-
const runCli = (args) => (run.interrupted ? Promise.resolve('') :
|
|
391
|
+
const runCli = (args) => (run.interrupted ? Promise.resolve('') : runTurnImpl(args));
|
|
384
392
|
/**
|
|
385
393
|
* WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
|
|
386
394
|
*
|
|
@@ -420,6 +428,7 @@ export function createAgentTurnExecution({
|
|
|
420
428
|
}
|
|
421
429
|
let projectTools = null;
|
|
422
430
|
let orchestrator = null;
|
|
431
|
+
let intermediateDelivery = null;
|
|
423
432
|
try {
|
|
424
433
|
if (orchestrating) {
|
|
425
434
|
orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
|
|
@@ -441,15 +450,16 @@ export function createAgentTurnExecution({
|
|
|
441
450
|
format: async (followup) => {
|
|
442
451
|
orchestrator?.note(followup);
|
|
443
452
|
const attached = followup.attachments?.length ? await fetchAgentFiles(wt, followup.attachments.map((file) => ({ ...file, from: 'message' }))) : { message: [], card: [] };
|
|
444
|
-
return agentFollowupKickoff(job, followup, { messageFiles: attached.message, cardFiles: files.card, orchestrated: Boolean(orchestrator) });
|
|
453
|
+
return agentFollowupKickoff(job, followup, { messageFiles: attached.message, cardFiles: files.card, intermediateFiles, orchestrated: Boolean(orchestrator) });
|
|
445
454
|
},
|
|
446
455
|
})) } : {}),
|
|
447
456
|
// The kickoff and the system prompt, composed by the agent lane's
|
|
448
457
|
// shape (workAgentTurnShape.mjs) from what this run measured.
|
|
449
458
|
researchHandoff: job.researchHandoff,
|
|
450
459
|
resultInputs: job.resultInputs,
|
|
460
|
+
intermediateFiles,
|
|
451
461
|
researchIdentity: { projectId: PROJECT_ID, agentId, turnId, taskId: job.task?.id ?? job.repositoryTaskId },
|
|
452
|
-
prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message }),
|
|
462
|
+
prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message, intermediateFiles }),
|
|
453
463
|
system: agentTurnSystem({ taskKind, taskType: job.task?.taskType, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun, orchestration: orchestrator ? { posture, runtime: rt, ...cardBrain } : null }),
|
|
454
464
|
agentTools: projectTools,
|
|
455
465
|
writableDirs: repositoryRun?.writableDirs,
|
|
@@ -733,7 +743,18 @@ export function createAgentTurnExecution({
|
|
|
733
743
|
void reportSessionWorktree(place).catch(() => {});
|
|
734
744
|
// A file card delivers uploaded bytes, not an in-memory retry. Otherwise
|
|
735
745
|
// its dependent card can start while Review has nothing to show.
|
|
736
|
-
|
|
746
|
+
if (!run.interrupted && agentDeclaredDelivery(out, lastAnswer)) {
|
|
747
|
+
try { intermediateDelivery = await intermediate.prepare({ job, turnId, taskKind, before: artifactsBefore, signal: setupController.signal }); }
|
|
748
|
+
catch (error) {
|
|
749
|
+
if (run.interrupted) throw error;
|
|
750
|
+
// The failed page still has its preview/files for repair. Reporting
|
|
751
|
+
// those bytes cannot turn a failed export into a delivered card.
|
|
752
|
+
try { await artifacts.report({ placeDir: wt, before: artifactsBefore, agentId, turnId, requireStored: kind.artifact != null }); }
|
|
753
|
+
catch (uploadError) { throw new Error(`${error.message}; artifact reporting also failed: ${uploadError.message}`); }
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
await artifacts.report({ placeDir: wt, before: intermediateDelivery?.uploadBefore ?? artifactsBefore, agentId, turnId, requireStored: kind.artifact != null });
|
|
737
758
|
}
|
|
738
759
|
|
|
739
760
|
|
|
@@ -766,7 +787,9 @@ export function createAgentTurnExecution({
|
|
|
766
787
|
artifactsBefore,
|
|
767
788
|
branch,
|
|
768
789
|
wt,
|
|
790
|
+
modelExports: intermediateDelivery?.modelExports,
|
|
769
791
|
});
|
|
792
|
+
if (body.outcome === 'delivered') intermediateDelivery?.commit();
|
|
770
793
|
if (res) setRuntimeLimit(PROJECT_ID, rt, null);
|
|
771
794
|
if (body.outcome === 'delivered' && job.task?.taskType === 'tests' && res?.testFindings) {
|
|
772
795
|
body.testFindings = measureTestFindings(res.testFindings, repositoryRun?.sourcesNow() ?? [{ wt, baseRef: baseRef(), id: job.repos?.length === 1 ? job.repos[0] : null }], envScrub);
|
|
@@ -789,7 +812,7 @@ export function createAgentTurnExecution({
|
|
|
789
812
|
const rows = repositoryRun?.sourcesNow() ?? [{ wt }];
|
|
790
813
|
for (const r of rows) {
|
|
791
814
|
const head = gitOrNull(['rev-parse', 'HEAD'], r.wt);
|
|
792
|
-
const check = await
|
|
815
|
+
const check = await runMergeCheck(agentId, r.wt, repositoryRun ? r : undefined);
|
|
793
816
|
if (check?.status === 'failed') {
|
|
794
817
|
body.mergeResolved = false;
|
|
795
818
|
body.mergeDetail = relayedText(check.text || check.output || 'The repository check failed during merge recovery.').slice(0, 2000);
|
|
@@ -33,8 +33,14 @@ import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
|
33
33
|
import { usableImageArtifact } from './imageArtifact.mjs';
|
|
34
34
|
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
35
35
|
import { ANSWER_DELIVERED_USES, researchUseOf, withResearchHeader } from './researchUse.mjs';
|
|
36
|
+
import { measuredModelExports } from './intermediateArtifacts.mjs';
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/** Materialization runs only for a declared delivery; settlement remains the
|
|
39
|
+
* sole parser and the sole authority deciding whether it really delivered. */
|
|
40
|
+
export const agentDeclaredDelivery = (out, lastAnswer) =>
|
|
41
|
+
((lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out))?.outcome === 'delivered';
|
|
42
|
+
|
|
43
|
+
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt, modelExports = [] }) {
|
|
38
44
|
const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
|
|
39
45
|
/**
|
|
40
46
|
* A LIMIT IS ONLY A LIMIT WHEN THE TURN PRODUCED NOTHING.
|
|
@@ -94,7 +100,7 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
94
100
|
* Its product is under `.flowviant/artifacts/`, and the kind's table
|
|
95
101
|
* (agentTaskKinds.mjs) says which file proves it: a `.html` mockup for
|
|
96
102
|
* design, a `.html` deck, a `.md` write-up for research, and a `.html`
|
|
97
|
-
* page for a 3D model
|
|
103
|
+
* page for a 3D model plus its measured page-bound local GLB export. A turn that
|
|
98
104
|
* says "delivered" without having written one would land the agent in Review
|
|
99
105
|
* with nothing to look at and a summary describing a page that does not
|
|
100
106
|
* exist — so it settles `nothing` with the measured sentence, and the
|
|
@@ -135,7 +141,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
135
141
|
&& (kind.posture !== 'image' || usableImageArtifact(e)));
|
|
136
142
|
const standing = scanArtifacts(wt);
|
|
137
143
|
const wrote = has(changedArtifacts(artifactsBefore, standing));
|
|
138
|
-
const
|
|
144
|
+
const exported = kind.exportModel && measuredModelExports(wt, modelExports);
|
|
145
|
+
const present = wrote || exported || ((job.kind !== 'task' || job.redo === true) && has(standing));
|
|
139
146
|
if (!present) {
|
|
140
147
|
return report({
|
|
141
148
|
turnId,
|
|
@@ -147,6 +154,11 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
147
154
|
worktree: wt,
|
|
148
155
|
});
|
|
149
156
|
}
|
|
157
|
+
if (kind.exportModel && !exported) {
|
|
158
|
+
return report({ turnId, outcome: 'nothing',
|
|
159
|
+
answer: 'The model page has no verified local .glb export. Retry this producer card to export its exact page before dependent work starts; no manual download or Library keeping is required.',
|
|
160
|
+
...(usage ? { usage } : {}), branch, worktree: wt });
|
|
161
|
+
}
|
|
150
162
|
}
|
|
151
163
|
const prerequisite = res.prerequisite;
|
|
152
164
|
const cannotRequest = prerequisite && job.prerequisitesAccepted !== true
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { AGENT_TASK_KICKOFF, AGENT_HUMAN_KICKOFF, SYSTEM_AGENT_FOR, withProjectContext } from './prompts.mjs';
|
|
26
26
|
import { INTERNAL_WAIT_CONTRACT } from './agentInternalWait.mjs';
|
|
27
27
|
import { orchestratorContract } from './orchestratorPrompt.mjs';
|
|
28
|
+
import { intermediateArtifactContext } from './intermediateArtifacts.mjs';
|
|
28
29
|
|
|
29
30
|
/** What every kickoff of this job is handed: the agent, the card and its
|
|
30
31
|
* discussion, the job's context handoffs, the card's files, the place in
|
|
@@ -43,8 +44,8 @@ const shared = (job, cardFiles) => ({
|
|
|
43
44
|
|
|
44
45
|
/** The turn's first prompt: the card, on a task turn; the person's words
|
|
45
46
|
* otherwise. `cardFiles` and `messageFiles` are the files already on disk. */
|
|
46
|
-
export const agentKickoff = (job, { cardFiles, messageFiles }) =>
|
|
47
|
-
job.kind === 'task' && job.task
|
|
47
|
+
export const agentKickoff = (job, { cardFiles, messageFiles, intermediateFiles }) =>
|
|
48
|
+
(job.kind === 'task' && job.task
|
|
48
49
|
? AGENT_TASK_KICKOFF({
|
|
49
50
|
// The card's discussion (0.106.0), printed inside THE CARD by
|
|
50
51
|
// the same builder the run's stash wrote down — and the files
|
|
@@ -63,17 +64,17 @@ export const agentKickoff = (job, { cardFiles, messageFiles }) =>
|
|
|
63
64
|
// The files (0.112.0): the person's inside their words, the
|
|
64
65
|
// card's at the foot of its discussion.
|
|
65
66
|
messageFiles,
|
|
66
|
-
});
|
|
67
|
+
})) + intermediateArtifactContext(intermediateFiles);
|
|
67
68
|
|
|
68
69
|
/** A live message sent while the turn runs, as the CLI is handed it — on an
|
|
69
70
|
* orchestrated card with its id, so the route it takes is reported. */
|
|
70
|
-
export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, orchestrated }) =>
|
|
71
|
+
export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, intermediateFiles, orchestrated }) =>
|
|
71
72
|
AGENT_HUMAN_KICKOFF({
|
|
72
73
|
...shared(job, cardFiles),
|
|
73
74
|
message: followup.body + (followup.libraryPrompt ?? '') + (orchestrated ? `\nMessage id: ${followup.id}. Choose and report its route with card_orchestrator.route_message.` : ''),
|
|
74
75
|
askedByName: followup.askedByName,
|
|
75
76
|
messageFiles,
|
|
76
|
-
});
|
|
77
|
+
}) + intermediateArtifactContext(intermediateFiles);
|
|
77
78
|
|
|
78
79
|
/** The turn's system prompt, in its fixed section order. `orchestration` is
|
|
79
80
|
* null on a solo run, else what the contract names: `{ posture, runtime,
|
|
@@ -48,6 +48,7 @@ export function createWorkAgentTurns({
|
|
|
48
48
|
reportSessionWorktree,
|
|
49
49
|
artifacts,
|
|
50
50
|
runReviewEntry,
|
|
51
|
+
runMergeCheck,
|
|
51
52
|
publishAgentBranch,
|
|
52
53
|
stopAgentPublishing = () => {},
|
|
53
54
|
admit,
|
|
@@ -59,6 +60,7 @@ export function createWorkAgentTurns({
|
|
|
59
60
|
makeBrief = createAgentBriefRunner({ admit, workChildren }),
|
|
60
61
|
/** The run's live-message relay (workLiveMessages.mjs); a test's stand-in. */
|
|
61
62
|
makeLiveMessages,
|
|
63
|
+
exportModel,
|
|
62
64
|
}) {
|
|
63
65
|
const reportDelivery = createDeliveryReporter();
|
|
64
66
|
const {
|
|
@@ -133,6 +135,8 @@ export function createWorkAgentTurns({
|
|
|
133
135
|
reportSessionWorktree,
|
|
134
136
|
artifacts,
|
|
135
137
|
runReviewEntry,
|
|
138
|
+
runMergeCheck,
|
|
139
|
+
exportModel,
|
|
136
140
|
workChildren,
|
|
137
141
|
agentChildren,
|
|
138
142
|
postAgentTrace,
|
|
@@ -17,7 +17,7 @@ import { excludeInWorktree } from './git.mjs';
|
|
|
17
17
|
import { FLOWVIANT_OWN_PATHS } from './knowledgeLibrary.mjs';
|
|
18
18
|
import { safeFileName } from './safeFileName.mjs';
|
|
19
19
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
20
|
-
import { readAgentFiles } from './agentFiles.mjs';
|
|
20
|
+
import { AGENT_FILES_PER_MESSAGE_MAX, readAgentFiles } from './agentFiles.mjs';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* ONE FILE'S CEILING, for both lanes (0.113.0, the owner 2026-09-29: "20 MB
|
|
@@ -27,6 +27,8 @@ import { readAgentFiles } from './agentFiles.mjs';
|
|
|
27
27
|
* is what 0.112.0's 10 MB did to anything bigger, and why the server floors a
|
|
28
28
|
* file over 10 MB at this release (`DAEMON_LARGE_FILES_MIN`).
|
|
29
29
|
*/
|
|
30
|
+
export const SESSION_ATTACHMENTS_PER_TURN_MAX = AGENT_FILES_PER_MESSAGE_MAX;
|
|
31
|
+
|
|
30
32
|
export const ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
31
33
|
|
|
32
34
|
export function createWorkAttachments() {
|
|
@@ -206,7 +208,7 @@ export function createWorkAttachments() {
|
|
|
206
208
|
*/
|
|
207
209
|
const fetchTabFiles = async (wt, attachments) => {
|
|
208
210
|
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
209
|
-
const list = attachments.slice(0,
|
|
211
|
+
const list = attachments.slice(0, SESSION_ATTACHMENTS_PER_TURN_MAX);
|
|
210
212
|
const landed = await landFiles(wt, list, 'attachment', false);
|
|
211
213
|
return list.map((a, i) => (landed[i] ? { path: landed[i] } : { missed: safeUploadName(a?.name) }));
|
|
212
214
|
};
|
|
@@ -73,6 +73,7 @@ export async function runCappedTurn({
|
|
|
73
73
|
let timer = null;
|
|
74
74
|
let wedged = false;
|
|
75
75
|
let sessionId = null;
|
|
76
|
+
let finished = false;
|
|
76
77
|
let stopWaiting = () => {};
|
|
77
78
|
const capped = new Promise((resolve) => {
|
|
78
79
|
stopWaiting = resolve;
|
|
@@ -88,10 +89,12 @@ export async function runCappedTurn({
|
|
|
88
89
|
const turn = runTurn({
|
|
89
90
|
...turnArgs,
|
|
90
91
|
onInit: (i) => {
|
|
92
|
+
if (finished) return;
|
|
91
93
|
if (typeof i?.sessionId === 'string' && i.sessionId.trim()) sessionId = i.sessionId.trim();
|
|
92
94
|
onInit(i);
|
|
93
95
|
},
|
|
94
96
|
onSpawn: (ch) => {
|
|
97
|
+
if (finished) { try { if (killChild) killChild(ch); else ch?.kill('SIGKILL'); } catch { /* already gone */ } return; }
|
|
95
98
|
child = ch;
|
|
96
99
|
workChildren.set(ch, null);
|
|
97
100
|
releaseSlot();
|
|
@@ -112,6 +115,7 @@ export async function runCappedTurn({
|
|
|
112
115
|
const out = await Promise.race([turn, capped]);
|
|
113
116
|
return { out, wedged, spawned: child !== null, sessionId };
|
|
114
117
|
} finally {
|
|
118
|
+
finished = true;
|
|
115
119
|
signal?.removeEventListener('abort', cancel);
|
|
116
120
|
if (timer) clearTimeout(timer);
|
|
117
121
|
if (child) workChildren.delete(child);
|
|
@@ -6,12 +6,12 @@ import { readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { fleetRequest } from './fleetPost.mjs';
|
|
8
8
|
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
9
|
-
import { DAEMON_INSTANCE, FLEET_URL } from './config.mjs';
|
|
9
|
+
import { DAEMON_INSTANCE, FLEET_URL, VERSION } from './config.mjs';
|
|
10
10
|
import { messageIdsForSession } from './claudeSessions.mjs';
|
|
11
11
|
|
|
12
12
|
export function createLiveMessages({ scope, turnId, attempt, cwd, sessionMetaPath, format, observe = () => {}, interrupted = () => false,
|
|
13
13
|
request = fleetRequest, nativeIds = messageIdsForSession, setTimer = setInterval, clearTimer = clearInterval, finalFlushMs = 1_000 }) {
|
|
14
|
-
const identity = { scope, turnId, instance: DAEMON_INSTANCE, ...(attempt !== undefined ? { attempt } : {}) };
|
|
14
|
+
const identity = { scope, turnId, daemonVersion: VERSION, instance: DAEMON_INSTANCE, ...(attempt !== undefined ? { attempt } : {}) };
|
|
15
15
|
const file = sessionMetaPath(cwd, 'flowviant-live-messages', turnId);
|
|
16
16
|
let sessionId = null;
|
|
17
17
|
let timer = null;
|
|
@@ -35,7 +35,7 @@ export function createWorkNarration() {
|
|
|
35
35
|
* line over the finished reply — the server drops narration for a turn
|
|
36
36
|
* that is no longer pending. (A session-level pending count can't tell the
|
|
37
37
|
* settled turn's stale line from the queued NEXT turn's fresh one.) */
|
|
38
|
-
const makeNarrator = (sessionId, turnId, getTools) => {
|
|
38
|
+
const makeNarrator = (sessionId, turnId, getTools, identity = {}) => {
|
|
39
39
|
const recent = [];
|
|
40
40
|
let lastSent = 0;
|
|
41
41
|
let dirty = false;
|
|
@@ -60,6 +60,7 @@ export function createWorkNarration() {
|
|
|
60
60
|
body: JSON.stringify({
|
|
61
61
|
sessionId,
|
|
62
62
|
turnId,
|
|
63
|
+
...identity,
|
|
63
64
|
lines,
|
|
64
65
|
// The structured tool log so far, riding the same throttled beat.
|
|
65
66
|
// Same lifecycle as the lines: overwritten as the turn moves,
|
|
@@ -101,7 +102,7 @@ export function createWorkNarration() {
|
|
|
101
102
|
'Content-Type': 'application/json',
|
|
102
103
|
},
|
|
103
104
|
signal: AbortSignal.timeout(10_000),
|
|
104
|
-
body: JSON.stringify({ sessionId, turnId, lines: [], context }),
|
|
105
|
+
body: JSON.stringify({ sessionId, turnId, ...identity, lines: [], context }),
|
|
105
106
|
});
|
|
106
107
|
} catch {
|
|
107
108
|
/* a readout is decoration, like the line */
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/** The person's own CLI diagnoses planning, under the ordinary consult fence. */
|
|
2
2
|
import { candidateObjects } from './modelJson.mjs';
|
|
3
3
|
import { scrub } from './uplinkScrub.mjs';
|
|
4
|
-
|
|
5
|
-
import { DAEMON_INSTANCE } from './config.mjs';
|
|
4
|
+
export { watchReadOnlyAttempt as watchPlanAttempt } from './readOnlyRun.mjs';
|
|
6
5
|
|
|
7
6
|
export const PLAN_RECOVERY_SYSTEM = `You are recovering the person's retained Plan press.
|
|
8
7
|
Read the original selection, failure, retained evidence, answers and repository.
|
|
9
8
|
Establish the cause before claiming a repair. Diagnose/debug the planning failure
|
|
10
9
|
with the available read-only tools. Repair the planning approach, context or output
|
|
11
|
-
instructions. Do not change repository files, start
|
|
10
|
+
instructions. Do not change repository files, start Board work or approve work.
|
|
12
11
|
Retry count is never a reason to ask the person to click Plan again.
|
|
13
12
|
Only ask for an answer, credential action or changed-scope decision that reading
|
|
14
13
|
and diagnosis cannot supply. Ask one specific question and explain what it settles.
|
|
@@ -30,21 +29,3 @@ export function parsePlanDiagnosis(out) {
|
|
|
30
29
|
}
|
|
31
30
|
return null;
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
/** Unknown transport state never means Stop. Only an answered holder read does. */
|
|
35
|
-
export async function watchPlanAttempt(url, job) {
|
|
36
|
-
const controller = new AbortController();
|
|
37
|
-
let busy = false;
|
|
38
|
-
const check = async () => {
|
|
39
|
-
if (busy) return;
|
|
40
|
-
busy = true;
|
|
41
|
-
try {
|
|
42
|
-
const r = await fleetRequest(url, { body: { id: job.id, attempt: job.attempt, instance: DAEMON_INSTANCE }, timeoutMs: 15_000 });
|
|
43
|
-
if (r.ok && r.json?.data?.active === false) controller.abort();
|
|
44
|
-
} finally { busy = false; }
|
|
45
|
-
};
|
|
46
|
-
await check();
|
|
47
|
-
const timer = setInterval(check, 5_000);
|
|
48
|
-
timer.unref?.();
|
|
49
|
-
return { signal: controller.signal, check, stop: () => clearInterval(timer) };
|
|
50
|
-
}
|
|
@@ -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": {
|