flowviant 0.124.0 → 0.124.1
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 +6 -0
- package/bin/cli.mjs +7 -0
- package/bin/lib/agentInternalWait.mjs +2 -1
- package/bin/lib/agentPlan.mjs +8 -0
- package/bin/lib/agentRepositories.mjs +48 -0
- package/bin/lib/checkoutIdentity.mjs +25 -0
- package/bin/lib/claudeInput.mjs +32 -0
- package/bin/lib/claudeSessions.mjs +20 -0
- package/bin/lib/credentials.mjs +52 -17
- package/bin/lib/daemonLocks.mjs +21 -3
- package/bin/lib/fleet.mjs +19 -6
- package/bin/lib/fleetRoster.mjs +1 -89
- package/bin/lib/fleetStartup.mjs +11 -14
- package/bin/lib/holder.mjs +4 -37
- package/bin/lib/instance.mjs +20 -7
- package/bin/lib/landed.mjs +2 -2
- package/bin/lib/login.mjs +39 -4
- package/bin/lib/placeLock.mjs +9 -7
- package/bin/lib/prWorkflow.mjs +15 -0
- package/bin/lib/projectLanded.mjs +16 -0
- package/bin/lib/projectRepositories.mjs +60 -0
- package/bin/lib/projectTools.mjs +2 -1
- package/bin/lib/projectWorktreeHome.mjs +25 -0
- package/bin/lib/prompts.mjs +6 -1
- package/bin/lib/repositoryDelivery.mjs +64 -0
- package/bin/lib/repositoryMerge.mjs +116 -0
- package/bin/lib/repositoryPlaces.mjs +179 -0
- package/bin/lib/repositorySetup.mjs +58 -0
- package/bin/lib/repositoryTool.mjs +71 -0
- package/bin/lib/runTurn.mjs +23 -2
- package/bin/lib/runtimeAntigravity.mjs +2 -1
- package/bin/lib/runtimeClaude.mjs +5 -2
- package/bin/lib/runtimeCodex.mjs +2 -1
- package/bin/lib/shipMerge.mjs +5 -2
- package/bin/lib/startProject.mjs +4 -4
- package/bin/lib/trace.mjs +8 -3
- package/bin/lib/turnDeclaration.mjs +88 -0
- package/bin/lib/turnLock.mjs +48 -8
- package/bin/lib/work.mjs +22 -24
- package/bin/lib/workAgentCheck.mjs +6 -5
- package/bin/lib/workAgentMerges.mjs +19 -2
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentPublish.mjs +18 -5
- package/bin/lib/workAgentRecovery.mjs +85 -0
- package/bin/lib/workAgentReview.mjs +5 -3
- package/bin/lib/workAgentTurnBegun.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +76 -9
- package/bin/lib/workAgentTurnOutcome.mjs +9 -3
- package/bin/lib/workAgentTurns.mjs +33 -9
- package/bin/lib/workDelivery.mjs +21 -0
- package/bin/lib/workDiffs.mjs +18 -11
- package/bin/lib/workLiveMessages.mjs +129 -0
- package/bin/lib/workPlaceReports.mjs +5 -0
- package/bin/lib/workPlaces.mjs +6 -1
- package/bin/lib/workRetire.mjs +5 -0
- package/bin/lib/workSessionTurns.mjs +72 -40
- package/bin/lib/workShip.mjs +17 -16
- package/package.json +1 -1
- package/npm-shrinkwrap.json +0 -69
|
@@ -27,17 +27,19 @@ export function createWorkAgentReview({ runCheck, runPrecheck }) {
|
|
|
27
27
|
* INSIDE the place's writer lock on a path whose callers settle real work —
|
|
28
28
|
* `runAgentMerge` in particular reports a claimed merge after this returns.
|
|
29
29
|
*/
|
|
30
|
-
const runReviewEntry = async (agentId, wt, agentName) => {
|
|
30
|
+
const runReviewEntry = async (agentId, wt, agentName, repository) => {
|
|
31
|
+
let result = { status: 'unknown' };
|
|
31
32
|
try {
|
|
32
|
-
await runCheck(agentId, wt);
|
|
33
|
+
result = await runCheck(agentId, wt, repository) ?? result;
|
|
33
34
|
} catch {
|
|
34
35
|
/* the row keeps its previous check answer, which is null the first time */
|
|
35
36
|
}
|
|
36
37
|
try {
|
|
37
|
-
await runPrecheck(agentId, wt, agentName);
|
|
38
|
+
await runPrecheck(agentId, wt, agentName, repository);
|
|
38
39
|
} catch {
|
|
39
40
|
/* no pre-review is posted, and absence renders nothing */
|
|
40
41
|
}
|
|
42
|
+
return result;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
return { runReviewEntry };
|
|
@@ -40,11 +40,11 @@ import { worktreePath } from './workPlaces.mjs';
|
|
|
40
40
|
* cleaned up on THIS box is same-box recovery of real committed work and
|
|
41
41
|
* behaves exactly as it did before this guard existed.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* This remains the same-box and older-server refusal. The recovery coordinator
|
|
44
|
+
* now inspects foreign work before this ordinary turn path: after a person's
|
|
45
|
+
* machine swap it can replace missing work with an explicit start from briefs.
|
|
46
46
|
*/
|
|
47
|
-
export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch }) {
|
|
47
|
+
export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch, onAvailable = () => {} }) {
|
|
48
48
|
// The caller validated `place` (workAgentTurns); an unsafe one would give a
|
|
49
49
|
// null directory AND a null ref below, and the null ref stands the guard
|
|
50
50
|
// down — the same fail-open direction as an unreadable repo.
|
|
@@ -105,9 +105,11 @@ export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublished
|
|
|
105
105
|
(fetched ? ` Its published branch could not be fetched (${fetched.why}).` : '')
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
|
+
onAvailable();
|
|
108
109
|
// The branch is here and measured. `placeWtFor`'s attach fallback
|
|
109
110
|
// opens a worktree on it below — the same path a directory somebody
|
|
110
111
|
// cleaned up already takes, on commits this box now genuinely holds.
|
|
111
112
|
}
|
|
113
|
+
if (hasBranch || (wtDir && existsSync(wtDir))) onAvailable();
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryDelivery.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
|
|
3
4
|
*
|
|
@@ -38,12 +39,17 @@ import { runTurnResumingOnce } from './resumeLost.mjs';
|
|
|
38
39
|
import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
|
|
39
40
|
import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
40
41
|
import { brainFor } from './workBrain.mjs';
|
|
42
|
+
import { refreshRestartBase } from './workAgentRecovery.mjs';
|
|
41
43
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
42
44
|
import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
43
45
|
import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
|
|
44
46
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
47
|
+
import { CLAUDE_SESSION_RE, RESUME_MARKERS } from './conversationIds.mjs';
|
|
48
|
+
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
49
|
+
import { createLiveMessages } from './workLiveMessages.mjs';
|
|
45
50
|
|
|
46
51
|
export function createAgentTurnExecution({
|
|
52
|
+
agentRepositories,
|
|
47
53
|
makeBrief,
|
|
48
54
|
baseRef,
|
|
49
55
|
baseDir,
|
|
@@ -63,7 +69,9 @@ export function createAgentTurnExecution({
|
|
|
63
69
|
postAgentActivity,
|
|
64
70
|
postAgentParked,
|
|
65
71
|
fetchAgentFiles,
|
|
72
|
+
makeLiveMessages = createLiveMessages,
|
|
66
73
|
}) {
|
|
74
|
+
const reportDelivery = createDeliveryReporter();
|
|
67
75
|
/** How long the final flush may hold the settle. Bounded because the settle
|
|
68
76
|
* is the turn's contract and the trace is a readout: a wedged uplink costs
|
|
69
77
|
* the tail of a trace, never the answer behind it. */
|
|
@@ -114,6 +122,11 @@ export function createAgentTurnExecution({
|
|
|
114
122
|
/** `run` is the lane's record of this turn (`interrupted`, set by
|
|
115
123
|
* `interruptAgentTurns`); absent, nothing can interrupt it. */
|
|
116
124
|
const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
|
|
125
|
+
let repositoryRun = null;
|
|
126
|
+
const setupController = new AbortController();
|
|
127
|
+
run.abortSetup = () => setupController.abort();
|
|
128
|
+
if (run.interrupted) setupController.abort();
|
|
129
|
+
try {
|
|
117
130
|
// WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
|
|
118
131
|
// cut, because `placeWtFor` would cut a rival branch of the same name. The
|
|
119
132
|
// rule and its three-state branch measurement: workAgentTurnBegun.mjs.
|
|
@@ -124,7 +137,10 @@ export function createAgentTurnExecution({
|
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
|
-
|
|
140
|
+
if (!(await refreshRestartBase(job, repoRoot))) return;
|
|
141
|
+
repositoryRun = await agentRepositories?.prepare(job, place, { signal: setupController.signal });
|
|
142
|
+
measured.repositoryReceipts = repositoryRun?.receipts;
|
|
143
|
+
const dir = repositoryRun ?? placeWtFor(place);
|
|
128
144
|
if (!dir) {
|
|
129
145
|
// No worktree and none could be cut. `nothing`, WITH git's own words:
|
|
130
146
|
// without them the agent sat in Stuck with no question and no reason
|
|
@@ -294,10 +310,13 @@ export function createAgentTurnExecution({
|
|
|
294
310
|
}
|
|
295
311
|
}
|
|
296
312
|
let seenThreadId = null;
|
|
313
|
+
const claudeMarker = rt === 'claude' ? sessionMetaPath(wt, RESUME_MARKERS.claude, agentId) : null;
|
|
314
|
+
let claudeResumeId = null;
|
|
315
|
+
try { const id = readFileSync(claudeMarker, 'utf8').trim(); if (CLAUDE_SESSION_RE.test(id)) claudeResumeId = id; } catch { /* unknown */ }
|
|
297
316
|
const resume =
|
|
298
317
|
rt === 'codex'
|
|
299
318
|
? Boolean(codexResumeId)
|
|
300
|
-
: rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
319
|
+
: rt === 'claude' && (Boolean(claudeResumeId) || Boolean(ranMarker && existsSync(ranMarker)));
|
|
301
320
|
/**
|
|
302
321
|
* …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
|
|
303
322
|
*
|
|
@@ -454,7 +473,14 @@ export function createAgentTurnExecution({
|
|
|
454
473
|
try {
|
|
455
474
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
456
475
|
// own worktree cannot silently change the next turn's tools.
|
|
457
|
-
|
|
476
|
+
const sources = repositoryRun?.sources ?? [];
|
|
477
|
+
const source = sources.find((r) => r.wt === wt);
|
|
478
|
+
const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef());
|
|
479
|
+
for (const other of sources.filter((r) => r !== source)) {
|
|
480
|
+
const base = readBaseTools(other.repoRoot, other.baseRef);
|
|
481
|
+
snapshot.instructions.push(...[...base.instructions, ...base.skillFiles].map((file) => ({ ...file, path: `repositories/${other.folder}/${file.path}` })));
|
|
482
|
+
}
|
|
483
|
+
projectTools = prepareAgentTools(snapshot, rt, process.env, wt, { codexHome: codexAgentHome, turnServers: repositoryRun?.tool ? { card_repositories: repositoryRun.tool.config } : {} });
|
|
458
484
|
const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
|
|
459
485
|
// A fenced posture on a runner that ignores its own file there (Codex's
|
|
460
486
|
// `--ignore-user-config`) takes the servers as `fencedMount` instead.
|
|
@@ -475,6 +501,15 @@ export function createAgentTurnExecution({
|
|
|
475
501
|
unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
|
|
476
502
|
});
|
|
477
503
|
const agentTurnArgs = {
|
|
504
|
+
...(rt === 'claude' ? { liveMessages: (run.live = makeLiveMessages({
|
|
505
|
+
scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
|
|
506
|
+
format: async (followup) => {
|
|
507
|
+
const attached = followup.attachments?.length ? await fetchAgentFiles(wt, followup.attachments.map((file) => ({ ...file, from: 'message' }))) : { message: [], card: [] };
|
|
508
|
+
return AGENT_HUMAN_KICKOFF({ agentName: job.agentName, message: followup.body + (followup.libraryPrompt ?? ''),
|
|
509
|
+
askedByName: followup.askedByName, task: job.task, cardThread: job.cardThread,
|
|
510
|
+
messageFiles: attached.message, cardFiles: files.card, position: job.position ?? 1, total: job.total ?? 1 });
|
|
511
|
+
},
|
|
512
|
+
})) } : {}),
|
|
478
513
|
prompt:
|
|
479
514
|
job.kind === 'task' && job.task
|
|
480
515
|
? AGENT_TASK_KICKOFF({
|
|
@@ -518,9 +553,10 @@ export function createAgentTurnExecution({
|
|
|
518
553
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
519
554
|
// land on its page, under the facts row.
|
|
520
555
|
artifacts: getArtifactsAccepted(),
|
|
521
|
-
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
556
|
+
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : ''),
|
|
522
557
|
agentTools: projectTools,
|
|
523
|
-
|
|
558
|
+
writableDirs: repositoryRun?.writableDirs,
|
|
559
|
+
...((projectTools.codexHome || fencedMount || repositoryRun?.tool) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}), ...(repositoryRun?.tool?.env ?? {}) } } : {}),
|
|
524
560
|
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
525
561
|
// a browser profile of the agent's own, kept beside its Codex home —
|
|
526
562
|
// so a Chrome it starts to look at its page never reaches the
|
|
@@ -542,7 +578,11 @@ export function createAgentTurnExecution({
|
|
|
542
578
|
cwd: wt,
|
|
543
579
|
runtime: rt,
|
|
544
580
|
resume,
|
|
545
|
-
resumeThreadId: codexResumeId || undefined,
|
|
581
|
+
resumeThreadId: codexResumeId || claudeResumeId || undefined,
|
|
582
|
+
onInit: (init) => {
|
|
583
|
+
if (claudeMarker && CLAUDE_SESSION_RE.test(init.sessionId ?? ''))
|
|
584
|
+
try { writeFileSync(claudeMarker, init.sessionId); } catch { /* best effort */ }
|
|
585
|
+
},
|
|
546
586
|
onThreadId: (id) => {
|
|
547
587
|
seenThreadId = String(id ?? '').trim() || seenThreadId;
|
|
548
588
|
},
|
|
@@ -558,7 +598,7 @@ export function createAgentTurnExecution({
|
|
|
558
598
|
// board past ~90 seconds.
|
|
559
599
|
onActivity: (a) => {
|
|
560
600
|
brief?.append(a?.kind ?? 'activity', a?.full ?? a?.label);
|
|
561
|
-
|
|
601
|
+
let line = a?.label;
|
|
562
602
|
if (!line) return;
|
|
563
603
|
// THE TRACE TAKES EVERY LINE; the pulse takes one every two
|
|
564
604
|
// seconds. Two channels, one stream, and the drop-sampler stays a
|
|
@@ -573,7 +613,12 @@ export function createAgentTurnExecution({
|
|
|
573
613
|
// daemon carried for all of them. The PULSE below is deliberately
|
|
574
614
|
// still `line`: it is one overwritten line on a board, and a
|
|
575
615
|
// paragraph there would be a paragraph nobody can read.
|
|
576
|
-
if (!doubledKinds || !doubledKinds.has(a.kind))
|
|
616
|
+
if (!doubledKinds || !doubledKinds.has(a.kind)) {
|
|
617
|
+
const full = a.full ?? line;
|
|
618
|
+
const readable = trace.prose(a.kind, full);
|
|
619
|
+
if (readable === null) return;
|
|
620
|
+
if (readable !== full) line = envScrub(readable).replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
621
|
+
}
|
|
577
622
|
const now = Date.now();
|
|
578
623
|
if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
|
|
579
624
|
lastAgentBeat.set(agentId, now);
|
|
@@ -612,10 +657,12 @@ export function createAgentTurnExecution({
|
|
|
612
657
|
// (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
|
|
613
658
|
// startup probe and its Terminal tabs.
|
|
614
659
|
onSpawn: (ch) => {
|
|
660
|
+
if (rt !== 'claude') reportDelivery('agent', job, 'taken');
|
|
615
661
|
brief?.running(true);
|
|
616
662
|
ch.once('close', () => { if (child === ch) brief?.running(false); });
|
|
617
663
|
child = ch;
|
|
618
664
|
spawnedChildren.add(ch);
|
|
665
|
+
measured.ranCli = true;
|
|
619
666
|
// The AGENT it serves — what the machine snapshot charges this
|
|
620
667
|
// child's memory to.
|
|
621
668
|
workChildren.set(ch, agentId);
|
|
@@ -664,12 +711,30 @@ export function createAgentTurnExecution({
|
|
|
664
711
|
void postAgentActivity(agentId, 'resuming after an internal wait');
|
|
665
712
|
} }
|
|
666
713
|
);
|
|
714
|
+
out = await finishRepositoryDelivery({
|
|
715
|
+
job, run: repositoryRun, out, answer: () => lastAnswer,
|
|
716
|
+
resume: async (prompt) => {
|
|
717
|
+
lastAnswer = null;
|
|
718
|
+
const counted = usage;
|
|
719
|
+
return runCli({
|
|
720
|
+
...agentTurnArgs, prompt, liveContinuation: true,
|
|
721
|
+
onUsage: (next) => agentTurnArgs.onUsage(Object.fromEntries(
|
|
722
|
+
['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
|
|
723
|
+
[k, (counted?.[k] ?? 0) + (next?.[k] ?? 0)])
|
|
724
|
+
)),
|
|
725
|
+
writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
|
|
726
|
+
resume: true,
|
|
727
|
+
resumeThreadId: seenThreadId ?? agentTurnArgs.resumeThreadId,
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
});
|
|
667
731
|
} finally {
|
|
668
732
|
await brief?.stop();
|
|
669
733
|
// What the CLI counted, for the lane's catch if anything below throws.
|
|
670
734
|
measured.usage = usage;
|
|
671
735
|
measured.model = model;
|
|
672
736
|
projectTools?.cleanup();
|
|
737
|
+
|
|
673
738
|
/**
|
|
674
739
|
* THE TAIL, BEFORE THE SETTLE — so the last thing the agent did is on
|
|
675
740
|
* the record by the time the board is told the turn is over.
|
|
@@ -721,7 +786,7 @@ export function createAgentTurnExecution({
|
|
|
721
786
|
}
|
|
722
787
|
|
|
723
788
|
|
|
724
|
-
const commits = commitsBetween(wt, before);
|
|
789
|
+
const commits = repositoryRun ? [...new Set(repositoryRun.receipts().map((r) => r.sha))].slice(0, 50) : commitsBetween(wt, before);
|
|
725
790
|
/**
|
|
726
791
|
* A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
|
|
727
792
|
* cut off, so there is no outcome to read: the turn reports `interrupted`
|
|
@@ -771,12 +836,14 @@ export function createAgentTurnExecution({
|
|
|
771
836
|
// reported as `nothing` — it did not deliver and it did not ask.
|
|
772
837
|
await postAgentParked(limit, rt);
|
|
773
838
|
}
|
|
839
|
+
if (job.kind === 'merge_resolve' && repositoryRun) body.mergeResolved = allResolutionsCommitted(repositoryRun.sourcesNow());
|
|
774
840
|
const reply = await postAgentTurn(body);
|
|
775
841
|
// The queue just emptied. Run the project's own check and the AI
|
|
776
842
|
// pre-review HERE, in the worktree we are already standing in and still
|
|
777
843
|
// hold the lock on — see `runReviewEntry`. Only after the settle that may
|
|
778
844
|
// say delivered: the three `nothing` settles never opened it.
|
|
779
845
|
if (final && reply?.review === true) await runReviewEntry(agentId, wt, job.agentName);
|
|
846
|
+
} finally { await repositoryRun?.tool?.close(); }
|
|
780
847
|
};
|
|
781
848
|
|
|
782
849
|
return { runAgentTurnInPlace, commitsBetween };
|
|
@@ -26,6 +26,7 @@ import { parseTurnResult } from './agentPlan.mjs';
|
|
|
26
26
|
import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
|
|
27
27
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
28
28
|
import { usableImageArtifact } from './imageArtifact.mjs';
|
|
29
|
+
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
29
30
|
|
|
30
31
|
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
|
|
31
32
|
const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
|
|
@@ -39,7 +40,9 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
39
40
|
* makes the match mean what it says: the CLI failed and this is the
|
|
40
41
|
* sentence it failed with.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
// An unreadable protocol object is not a native CLI diagnostic either.
|
|
44
|
+
const readableOut = res ? '' : withoutTurnDeclaration(out);
|
|
45
|
+
const limit = res ? null : limitLine(readableOut);
|
|
43
46
|
/** What the caller posts, and whether it is the one settle that may say
|
|
44
47
|
* delivered (the only one whose reply can open the review-entry beat).
|
|
45
48
|
* Every one carries the model the CLI said it ran on, when it said
|
|
@@ -60,13 +63,16 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
if (!res) {
|
|
66
|
+
const raw = lastAnswer || out;
|
|
67
|
+
const readable = lastAnswer ? withoutTurnDeclaration(raw) : readableOut;
|
|
63
68
|
return report({
|
|
64
69
|
turnId,
|
|
65
70
|
outcome: 'nothing',
|
|
66
71
|
// The CLI's own words when it produced any. A turn that explained why
|
|
67
72
|
// it stopped is far more use than "the agent stopped".
|
|
68
|
-
answer:
|
|
69
|
-
? envScrub(
|
|
73
|
+
answer: raw.trim()
|
|
74
|
+
? readable.trim() ? envScrub(readable).slice(-1500)
|
|
75
|
+
: 'The turn returned an unreadable outcome declaration. This work has not been delivered.'
|
|
70
76
|
: 'the turn produced no output on the machine — its CLI may be signed out',
|
|
71
77
|
...(commits.length ? { commits } : {}),
|
|
72
78
|
...(usage ? { usage } : {}),
|
|
@@ -26,9 +26,11 @@ import { createAgentTurnReports } from './workAgentTurnReports.mjs';
|
|
|
26
26
|
import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
|
|
27
27
|
import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
28
28
|
import { killGroup } from './processGroup.mjs';
|
|
29
|
+
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
29
30
|
import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
|
|
30
31
|
|
|
31
32
|
export function createWorkAgentTurns({
|
|
33
|
+
agentRepositories,
|
|
32
34
|
REJECT_RETRY_MS,
|
|
33
35
|
baseRef,
|
|
34
36
|
inPlace,
|
|
@@ -44,6 +46,7 @@ export function createWorkAgentTurns({
|
|
|
44
46
|
artifacts,
|
|
45
47
|
runReviewEntry,
|
|
46
48
|
publishAgentBranch,
|
|
49
|
+
stopAgentPublishing = () => {},
|
|
47
50
|
admit,
|
|
48
51
|
workChildren,
|
|
49
52
|
agentPublished,
|
|
@@ -52,6 +55,7 @@ export function createWorkAgentTurns({
|
|
|
52
55
|
fetchAgentFiles,
|
|
53
56
|
makeBrief = createAgentBriefRunner({ admit, workChildren }),
|
|
54
57
|
}) {
|
|
58
|
+
const reportDelivery = createDeliveryReporter();
|
|
55
59
|
const {
|
|
56
60
|
postAgentTurn,
|
|
57
61
|
postAgentTrace,
|
|
@@ -68,11 +72,9 @@ export function createWorkAgentTurns({
|
|
|
68
72
|
// A TIME. The server types the next prompt when this one lands; this side
|
|
69
73
|
// does the work and reports what happened.
|
|
70
74
|
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// pointing at whatever it liked. That is also what keeps this feature from
|
|
75
|
-
// adding a token kind to a scope map that has silently shipped empty twice.
|
|
75
|
+
// Outcomes stay in the final JSON; commits are measured from git. The
|
|
76
|
+
// local card-repository tool is a turn-scoped capability, never a machine
|
|
77
|
+
// credential handed to the CLI.
|
|
76
78
|
//
|
|
77
79
|
// UNLEASED, unlike a plan or a kill. The server hands out at most one turn
|
|
78
80
|
// per agent per poll and its settle is conditional on the row still being
|
|
@@ -110,6 +112,7 @@ export function createWorkAgentTurns({
|
|
|
110
112
|
const stoppedFor = new Map(); // agentId -> when
|
|
111
113
|
|
|
112
114
|
const { runAgentTurnInPlace, commitsBetween } = createAgentTurnExecution({
|
|
115
|
+
agentRepositories,
|
|
113
116
|
makeBrief,
|
|
114
117
|
baseRef,
|
|
115
118
|
baseDir,
|
|
@@ -184,7 +187,8 @@ export function createWorkAgentTurns({
|
|
|
184
187
|
let settled = false;
|
|
185
188
|
const settleOnce = (body) => {
|
|
186
189
|
settled = true;
|
|
187
|
-
|
|
190
|
+
const liveMessages = agentRuns.get(turnId)?.live?.receipts();
|
|
191
|
+
return postAgentTurn({ ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) });
|
|
188
192
|
};
|
|
189
193
|
|
|
190
194
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
@@ -192,14 +196,15 @@ export function createWorkAgentTurns({
|
|
|
192
196
|
// be a tab's directory, where a turn is a reader by the product's own law.
|
|
193
197
|
const run = agentRuns.get(turnId) ?? { place, interrupted: false };
|
|
194
198
|
await inPlace(place, place.startsWith('a-'), async () => {
|
|
199
|
+
run.waitingReason = null;
|
|
195
200
|
try {
|
|
196
201
|
await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
|
|
197
202
|
if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
|
|
198
203
|
} catch (error) {
|
|
199
204
|
if (settled) return;
|
|
200
|
-
const commits = measured.wt ? commitsBetween(measured.wt, measured.before) : [];
|
|
205
|
+
const commits = measured.repositoryReceipts ? [...new Set(measured.repositoryReceipts().map((r) => r.sha))].slice(0, 50) : measured.wt ? commitsBetween(measured.wt, measured.before) : [];
|
|
201
206
|
if (run.interrupted) {
|
|
202
|
-
await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
|
|
207
|
+
await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: measured.ranCli === true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
|
|
203
208
|
return;
|
|
204
209
|
}
|
|
205
210
|
await postAgentTurn(
|
|
@@ -214,6 +219,9 @@ export function createWorkAgentTurns({
|
|
|
214
219
|
})
|
|
215
220
|
);
|
|
216
221
|
}
|
|
222
|
+
}, () => {
|
|
223
|
+
run.waitingReason = 'waiting for this turn to finish';
|
|
224
|
+
reportDelivery('agent', job, 'queued', run.waitingReason);
|
|
217
225
|
});
|
|
218
226
|
/**
|
|
219
227
|
* …AND THEN PUBLISH, IF THE PROJECT PUBLISHES.
|
|
@@ -266,7 +274,12 @@ export function createWorkAgentTurns({
|
|
|
266
274
|
let saidPressure = false;
|
|
267
275
|
for (const job of list.slice(0, 4)) {
|
|
268
276
|
const id = String(job?.id || '');
|
|
269
|
-
if (!id
|
|
277
|
+
if (!id) continue;
|
|
278
|
+
if (agentTurns.has(id)) {
|
|
279
|
+
const reason = agentRuns.get(id)?.waitingReason;
|
|
280
|
+
if (reason) reportDelivery('agent', job, 'queued', reason);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
270
283
|
const held = agentReported.get(id);
|
|
271
284
|
if (reportsOnly && !held) continue;
|
|
272
285
|
// A body the server REFUSED waits out its backoff — still held, so the
|
|
@@ -321,8 +334,15 @@ export function createWorkAgentTurns({
|
|
|
321
334
|
* would all be admitted against the same stale count. The reserved slot
|
|
322
335
|
* is what the next iteration sees. See admission.mjs.
|
|
323
336
|
*/
|
|
337
|
+
const missing = agentRepositories?.missing(job);
|
|
338
|
+
if (missing) {
|
|
339
|
+
if (!saidPressure) { saidPressure = true; note(missing); }
|
|
340
|
+
void postAgentActivity(String(job.agentId), missing);
|
|
341
|
+
continue; // no CLI, no failed settle; login and the next poll remeasure
|
|
342
|
+
}
|
|
324
343
|
const hold = admit('churn');
|
|
325
344
|
if (hold) {
|
|
345
|
+
reportDelivery('agent', job, 'queued', hold.reason);
|
|
326
346
|
if (!saidPressure) {
|
|
327
347
|
saidPressure = true;
|
|
328
348
|
note(`${c.cyan('agent')} ${c.dim(`— holding off: ${hold.reason}`)}`);
|
|
@@ -367,6 +387,9 @@ export function createWorkAgentTurns({
|
|
|
367
387
|
* a wedged uplink must not hold the stand-down open.
|
|
368
388
|
*/
|
|
369
389
|
const settleAgentTurns = async (sentence) => {
|
|
390
|
+
// Stand-down precedes flushing held replies; a late accepted reply must
|
|
391
|
+
// never push this displaced machine's work after the holder has moved.
|
|
392
|
+
stopAgentPublishing();
|
|
370
393
|
const answer = String(sentence ?? '').slice(0, 1000);
|
|
371
394
|
const posts = [];
|
|
372
395
|
for (const turnId of agentTurns) {
|
|
@@ -393,6 +416,7 @@ export function createWorkAgentTurns({
|
|
|
393
416
|
if (run) {
|
|
394
417
|
if (!run.interrupted) note(`${c.cyan('agent')} ${c.dim('— interrupting the running turn')}`);
|
|
395
418
|
run.interrupted = true;
|
|
419
|
+
run.abortSetup?.();
|
|
396
420
|
const child = agentChildren.get(run.place);
|
|
397
421
|
if (child) killGroup(child);
|
|
398
422
|
continue;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A measured wait belongs to its message. Reports decorate the queue and
|
|
2
|
+
* never gate a turn; retries come with the next measurement/poll. */
|
|
3
|
+
import { DAEMON_INSTANCE, FLEET_URL } from './config.mjs';
|
|
4
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
5
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
6
|
+
|
|
7
|
+
export function createDeliveryReporter(request = fleetRequest, now = Date.now) {
|
|
8
|
+
const said = new Map();
|
|
9
|
+
return (scope, job, state, reason) => {
|
|
10
|
+
const key = `${scope}:${job.id}`;
|
|
11
|
+
const value = `${state}:${reason ?? ''}`;
|
|
12
|
+
const prior = said.get(key);
|
|
13
|
+
if (prior?.value === value && now() - prior.at < 10_000) return;
|
|
14
|
+
said.set(key, { value, at: now() });
|
|
15
|
+
// Bound a day-long daemon's decoration cache.
|
|
16
|
+
if (said.size > 500) said.delete(said.keys().next().value);
|
|
17
|
+
void request(fleetEndpoint('turn-delivery', FLEET_URL), { timeoutMs: 10_000,
|
|
18
|
+
body: { scope, turnId: job.id, instance: DAEMON_INSTANCE, ...(Number.isInteger(job.attempt) ? { attempt: job.attempt } : {}), state, ...(reason ? { reason } : {}) },
|
|
19
|
+
}).then(result => { if (!result.ok && said.get(key)?.value === value) said.delete(key); }).catch(() => { said.delete(key); });
|
|
20
|
+
};
|
|
21
|
+
}
|
package/bin/lib/workDiffs.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Commit diff requests and their bounded reports. */
|
|
2
2
|
import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
|
|
3
|
+
import { listStoredProjects } from './credentials.mjs';
|
|
3
4
|
import { git, GIT_WIDE_BUFFER, OBJECT_ID_RE } from './git.mjs';
|
|
4
5
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
5
6
|
import { scrub, scrubCut } from './uplinkScrub.mjs';
|
|
@@ -21,7 +22,9 @@ function gitBig(args, cwd) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function createWorkDiffs({ repoRoot }) {
|
|
25
|
+
export function createWorkDiffs({ repoRoot, repositories }) {
|
|
26
|
+
// The picker may have changed the active token since config was imported.
|
|
27
|
+
const roots = repositories ?? listStoredProjects().find((entry) => entry.fleetToken === FLEET_TOKEN)?.repositories ?? {};
|
|
25
28
|
const DIFF_DONE_URL = fleetEndpoint('diff-done', FLEET_URL);
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -82,24 +85,28 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
82
85
|
// In-flight guard, not a cache: the server stops offering a sha the
|
|
83
86
|
// moment it settles, so this only stops the SAME poll's job being
|
|
84
87
|
// started twice while its `git show` is still running.
|
|
85
|
-
|
|
86
|
-
servedDiffs.
|
|
88
|
+
const jobKey = `${job.repo ?? ''}:${sha}`;
|
|
89
|
+
if (servedDiffs.has(jobKey)) continue;
|
|
90
|
+
servedDiffs.add(jobKey);
|
|
91
|
+
const cwd = job.repo ? roots[job.repositoryName] : repoRoot;
|
|
92
|
+
const report = (body) => postDiff({ ...body, ...(job.repo ? { repo: job.repo } : {}) });
|
|
87
93
|
void (async () => {
|
|
88
94
|
try {
|
|
95
|
+
if (!cwd) { await report({ sha, error: 'This repository has no checkout on this machine. Run flowviant login in that checkout.' }); return; }
|
|
89
96
|
let commit = '';
|
|
90
97
|
try {
|
|
91
|
-
commit = gitBig(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`],
|
|
98
|
+
commit = gitBig(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], cwd);
|
|
92
99
|
} catch (e) {
|
|
93
100
|
// Exit 1 is `--verify --quiet`'s "not a commit here" (absent,
|
|
94
101
|
// ambiguous, or another object type); anything else is git's own
|
|
95
102
|
// failure, relayed in its words as before.
|
|
96
103
|
if (e?.status !== 1) {
|
|
97
|
-
await
|
|
104
|
+
await report({ sha, error: scrubCut(e?.message || e, 500) });
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
if (!/^[0-9a-f]{40,64}$/.test(commit)) {
|
|
102
|
-
await
|
|
109
|
+
await report({ sha, error: 'no such commit on this machine' });
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
112
|
let patch = '';
|
|
@@ -109,13 +116,13 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
109
116
|
// date already reached the card on the worktree sweep, and
|
|
110
117
|
// repeating them inside the patch would put a second copy above
|
|
111
118
|
// every hunk.
|
|
112
|
-
patch = scrub(gitBig(['show', '--patch', '--format=', commit],
|
|
119
|
+
patch = scrub(gitBig(['show', '--patch', '--format=', commit], cwd));
|
|
113
120
|
} catch (e) {
|
|
114
|
-
await
|
|
121
|
+
await report({ sha, error: scrubCut(e?.message || e, 500) });
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
117
124
|
try {
|
|
118
|
-
const raw = gitBig(['show', '--numstat', '--format=', commit],
|
|
125
|
+
const raw = gitBig(['show', '--numstat', '--format=', commit], cwd);
|
|
119
126
|
for (const line of raw.split('\n')) {
|
|
120
127
|
if (!line.trim()) continue;
|
|
121
128
|
const [a, d, ...rest] = line.split('\t');
|
|
@@ -133,7 +140,7 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
133
140
|
/* a header without counts still beats no diff */
|
|
134
141
|
}
|
|
135
142
|
const truncated = Buffer.byteLength(patch, 'utf8') > MAX_PATCH_BYTES;
|
|
136
|
-
await
|
|
143
|
+
await report({
|
|
137
144
|
sha,
|
|
138
145
|
// Cut on a LINE boundary: half a hunk header renders as garbage,
|
|
139
146
|
// and the viewer says the patch was truncated either way.
|
|
@@ -144,7 +151,7 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
144
151
|
truncated,
|
|
145
152
|
});
|
|
146
153
|
} finally {
|
|
147
|
-
servedDiffs.delete(
|
|
154
|
+
servedDiffs.delete(jobKey);
|
|
148
155
|
}
|
|
149
156
|
})();
|
|
150
157
|
}
|