flowviant 0.116.0 → 0.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -12
- package/bin/cli.mjs +50 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +134 -31
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +5 -8
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +194 -22
- package/bin/lib/artifacts.mjs +70 -63
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +227 -54
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +27 -13
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +193 -0
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +90 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +6 -5
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +53 -13
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +172 -171
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +54 -11
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +328 -19
- package/bin/lib/projectTools.mjs +109 -34
- package/bin/lib/prompts.mjs +205 -60
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +286 -209
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +215 -21
- package/bin/lib/runtimeClaude.mjs +78 -10
- package/bin/lib/runtimeCodex.mjs +217 -209
- package/bin/lib/runtimeDetection.mjs +8 -2
- package/bin/lib/runtimeEvents.mjs +28 -11
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +99 -14
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +16 -4
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +44 -13
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +82 -199
- package/bin/lib/workAgentPrecheck.mjs +76 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +131 -67
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +64 -7
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +119 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +18 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +113 -31
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +256 -199
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +165 -0
- package/npm-shrinkwrap.json +69 -0
- package/package.json +9 -4
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -19,20 +19,23 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { PROJECT_ID } from './config.mjs';
|
|
21
21
|
import { setRuntimeLimit } from './daemonState.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { gitOrNull } from './git.mjs';
|
|
23
23
|
import { stashCard } from './agentCards.mjs';
|
|
24
24
|
import { c } from './ui.mjs';
|
|
25
25
|
import { runTurn } from './runTurn.mjs';
|
|
26
|
+
import { killGroup } from './processGroup.mjs';
|
|
26
27
|
import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICKOFF, withProjectContext } from './prompts.mjs';
|
|
27
28
|
import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
|
|
28
29
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
29
30
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
30
|
-
import { canRun,
|
|
31
|
-
import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
|
|
31
|
+
import { canRun, runtimeById } from './runtimes.mjs';
|
|
32
32
|
import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS } from './runtimeEvents.mjs';
|
|
33
33
|
import { makeTraceRelay } from './trace.mjs';
|
|
34
|
-
import { readBaseTools, prepareAgentTools } from './projectTools.mjs';
|
|
34
|
+
import { mcpJsonServerNamesAbove, readBaseTools, prepareAgentTools } from './projectTools.mjs';
|
|
35
|
+
import { TURN_PROFILES } from './turnProfile.mjs';
|
|
36
|
+
import { postureToolsNote } from './postureTools.mjs';
|
|
35
37
|
import { runTurnResumingOnce } from './resumeLost.mjs';
|
|
38
|
+
import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
|
|
36
39
|
import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
37
40
|
import { brainFor } from './workBrain.mjs';
|
|
38
41
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
@@ -41,6 +44,7 @@ import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
|
|
|
41
44
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
42
45
|
|
|
43
46
|
export function createAgentTurnExecution({
|
|
47
|
+
makeBrief,
|
|
44
48
|
baseRef,
|
|
45
49
|
baseDir,
|
|
46
50
|
repoRoot,
|
|
@@ -95,15 +99,8 @@ export function createAgentTurnExecution({
|
|
|
95
99
|
* reconciliation books whatever no card claimed. Missing beats fabricated
|
|
96
100
|
* and both beat a stall.
|
|
97
101
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
out = git(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
|
|
101
|
-
} catch {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
return typeof out === 'string'
|
|
105
|
-
? out.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50)
|
|
106
|
-
: [];
|
|
102
|
+
const out = gitOrNull(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
|
|
103
|
+
return out?.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50) ?? [];
|
|
107
104
|
};
|
|
108
105
|
|
|
109
106
|
/**
|
|
@@ -114,7 +111,9 @@ export function createAgentTurnExecution({
|
|
|
114
111
|
* `releaseSlot` hands back the admission reservation the lane took on this
|
|
115
112
|
* turn's behalf, at the moment the CLI actually exists. Idempotent.
|
|
116
113
|
*/
|
|
117
|
-
|
|
114
|
+
/** `run` is the lane's record of this turn (`interrupted`, set by
|
|
115
|
+
* `interruptAgentTurns`); absent, nothing can interrupt it. */
|
|
116
|
+
const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
|
|
118
117
|
// WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
|
|
119
118
|
// cut, because `placeWtFor` would cut a rival branch of the same name. The
|
|
120
119
|
// rule and its three-state branch measurement: workAgentTurnBegun.mjs.
|
|
@@ -147,19 +146,13 @@ export function createAgentTurnExecution({
|
|
|
147
146
|
* `runAgentMerge` learned this two commits ago and says so in its own
|
|
148
147
|
* comment; this is the same call, in the same file, left unguarded. Both
|
|
149
148
|
* facts are OPTIONAL to a turn — they describe the branch, they do not
|
|
150
|
-
* gate the work — so failing to read them costs a null, not the turn
|
|
149
|
+
* gate the work — so failing to read them costs a null, not the turn:
|
|
150
|
+
* git.mjs's `gitOrNull`, the one spelling of that rule.
|
|
151
151
|
*/
|
|
152
|
-
const readGit = (args) => {
|
|
153
|
-
try {
|
|
154
|
-
return (git(args, wt) || '').trim() || null;
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
152
|
// WHERE THE BRANCH WAS BEFORE THIS TURN, so the commits reported are the
|
|
160
153
|
// ones this turn actually made.
|
|
161
|
-
const before =
|
|
162
|
-
const branch =
|
|
154
|
+
const before = gitOrNull(['rev-parse', 'HEAD'], wt);
|
|
155
|
+
const branch = gitOrNull(['symbolic-ref', '--quiet', '--short', 'HEAD'], wt);
|
|
163
156
|
// …and the same three facts for the lane's catch: a turn that throws past
|
|
164
157
|
// here is still settled with what was measured (workAgentTurns.mjs).
|
|
165
158
|
measured.wt = wt;
|
|
@@ -176,9 +169,9 @@ export function createAgentTurnExecution({
|
|
|
176
169
|
* posture, which can write ONLY `.flowviant/artifacts/` (claudePosture.mjs).
|
|
177
170
|
*
|
|
178
171
|
* A RUNTIME THAT CANNOT EXPRESS THE POSTURE IS REFUSED, NOT IMPROVISED.
|
|
179
|
-
* Antigravity declares none of them
|
|
180
|
-
* 0.115.0)
|
|
181
|
-
* runtimeAntigravity.mjs), and the only
|
|
172
|
+
* Antigravity declares none of them; Codex declares `design` (since
|
|
173
|
+
* 0.115.0), `image` and `research` (0.117.0, over an isolated copy —
|
|
174
|
+
* codexResearch.mjs) (runtimeCodex.mjs, runtimeAntigravity.mjs), and the only
|
|
182
175
|
* alternative to refusing is running the card as a BUILD turn with
|
|
183
176
|
* permissions skipped — an agent writing code for an ask that was a
|
|
184
177
|
* mockup, reporting success. `nothing` puts the agent in Stuck saying so.
|
|
@@ -220,7 +213,7 @@ export function createAgentTurnExecution({
|
|
|
220
213
|
// entry in agentTaskKinds.mjs; nothing below branches on the kind name.
|
|
221
214
|
const kind = AGENT_TASK_KINDS[taskKind];
|
|
222
215
|
const posture = kind.posture;
|
|
223
|
-
if (!canRun(
|
|
216
|
+
if (!canRun(runtimeById(rt), posture)) {
|
|
224
217
|
await postAgentTurn({
|
|
225
218
|
turnId,
|
|
226
219
|
outcome: 'nothing',
|
|
@@ -388,12 +381,12 @@ export function createAgentTurnExecution({
|
|
|
388
381
|
/**
|
|
389
382
|
* Prose the structured event will carry anyway, dropped so a read does
|
|
390
383
|
* not render twice — but ONLY on a runtime whose stream reaches
|
|
391
|
-
* `onToolEvent`
|
|
392
|
-
*
|
|
384
|
+
* `onToolEvent` here. Codex uses completed activity; agy has its own
|
|
385
|
+
* parser. Dropping their tool prose would blank their agents' traces.
|
|
393
386
|
* `parse: null` is exactly the claudeStream.mjs stream path. See
|
|
394
387
|
* CLAUDE_TOOL_PROSE_KINDS.
|
|
395
388
|
*/
|
|
396
|
-
const doubledKinds =
|
|
389
|
+
const doubledKinds = runtimeById(rt)?.parse ? null : CLAUDE_TOOL_PROSE_KINDS;
|
|
397
390
|
|
|
398
391
|
/**
|
|
399
392
|
* WHICH BRAIN THIS CONTAINER WAS PINNED TO — the same `brainFor` the tab
|
|
@@ -421,6 +414,9 @@ export function createAgentTurnExecution({
|
|
|
421
414
|
* ever, which holds the self-update and the tray's install for ever.
|
|
422
415
|
*/
|
|
423
416
|
const spawnedChildren = new Set();
|
|
417
|
+
/** The CLI, unless a person already asked to stop this turn (Interrupt
|
|
418
|
+
* now): then nothing starts — no fresh retry, no continuation. */
|
|
419
|
+
const runCli = (args) => (run.interrupted ? Promise.resolve('') : runTurn(args));
|
|
424
420
|
/**
|
|
425
421
|
* WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
|
|
426
422
|
*
|
|
@@ -439,6 +435,8 @@ export function createAgentTurnExecution({
|
|
|
439
435
|
/** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
|
|
440
436
|
* every settle `usage` rides, for the same reason; null sends no key. */
|
|
441
437
|
let model = null;
|
|
438
|
+
const brief = makeBrief?.({ agentId, turnId, runtime: rt, model: () => model ?? job.model });
|
|
439
|
+
brief?.append('prompt', job.task ? { title: job.task.title, message: job.body, discussion: job.cardThread, brief: job.task.brief, criteria: job.task.criteria } : job.body);
|
|
442
440
|
/**
|
|
443
441
|
* THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
|
|
444
442
|
* Codex's `out` is every message plus stderr plus error text, and
|
|
@@ -450,11 +448,32 @@ export function createAgentTurnExecution({
|
|
|
450
448
|
/** The agent's artifact directory as it stood before this turn — the
|
|
451
449
|
* tab lane's rule, in the agent's own worktree (2026-09-22). */
|
|
452
450
|
const artifactsBefore = beforeArtifacts(wt);
|
|
451
|
+
const artifactInstructions = getArtifactsAccepted()
|
|
452
|
+
? await artifacts.prepare?.({ placeDir: wt, before: artifactsBefore, agentId }) ?? '' : '';
|
|
453
453
|
let projectTools = null;
|
|
454
454
|
try {
|
|
455
455
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
456
456
|
// own worktree cannot silently change the next turn's tools.
|
|
457
457
|
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt, { codexHome: codexAgentHome });
|
|
458
|
+
const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
|
|
459
|
+
// A fenced posture on a runner that ignores its own file there (Codex's
|
|
460
|
+
// `--ignore-user-config`) takes the servers as `fencedMount` instead.
|
|
461
|
+
const fencedMount = mountsProjectMcp && posture !== 'build' ? projectTools.fencedMount : null;
|
|
462
|
+
// THE MACHINE'S OWN SERVERS ON CLAUDE (2026-10-11, the owner): the turn
|
|
463
|
+
// is not strict, and every `.mcp.json` name from the worktree up is
|
|
464
|
+
// disabled so only the base copy speaks for the project. Null (an
|
|
465
|
+
// unboundable walk) mounts strictly: the base copy alone.
|
|
466
|
+
const claudeDisabled = mountsProjectMcp && rt === 'claude' ? mcpJsonServerNamesAbove(wt) : null;
|
|
467
|
+
// What this card's posture withholds, said to the agent — with the
|
|
468
|
+
// servers it did and did not mount, and why — so it relays the fence
|
|
469
|
+
// instead of inventing a setting (postureTools.mjs). Empty for a code card.
|
|
470
|
+
const toolsNote = postureToolsNote({
|
|
471
|
+
posture,
|
|
472
|
+
runtime: rt,
|
|
473
|
+
mounted: !mountsProjectMcp ? [] : fencedMount ? fencedMount.mounted : projectTools.mcpServers,
|
|
474
|
+
machine: mountsProjectMcp && (rt === 'codex' || claudeDisabled != null),
|
|
475
|
+
unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
|
|
476
|
+
});
|
|
458
477
|
const agentTurnArgs = {
|
|
459
478
|
prompt:
|
|
460
479
|
job.kind === 'task' && job.task
|
|
@@ -464,6 +483,8 @@ export function createAgentTurnExecution({
|
|
|
464
483
|
// The card's discussion (0.106.0), printed inside THE CARD by
|
|
465
484
|
// the same builder the stash above wrote down — and the files
|
|
466
485
|
// on it (0.112.0), after it, the same way.
|
|
486
|
+
resumeBody: job.body,
|
|
487
|
+
investigationContext: job.investigationContext,
|
|
467
488
|
cardThread: job.cardThread,
|
|
468
489
|
cardFiles: files.card,
|
|
469
490
|
position: job.position ?? 1,
|
|
@@ -471,7 +492,10 @@ export function createAgentTurnExecution({
|
|
|
471
492
|
})
|
|
472
493
|
: AGENT_HUMAN_KICKOFF({
|
|
473
494
|
agentName: job.agentName,
|
|
495
|
+
// INTERRUPT NOW: the previous run was stopped to send this.
|
|
496
|
+
interrupted: job.interrupted === true,
|
|
474
497
|
message: job.body ?? '',
|
|
498
|
+
investigationContext: job.investigationContext,
|
|
475
499
|
askedByName: job.askedByName,
|
|
476
500
|
task: job.task,
|
|
477
501
|
// The discussion on the one card this message is about, in a
|
|
@@ -489,24 +513,28 @@ export function createAgentTurnExecution({
|
|
|
489
513
|
// different things about the same directory.
|
|
490
514
|
// Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
|
|
491
515
|
// told it is Codex, never Claude.
|
|
492
|
-
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind,
|
|
516
|
+
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label, { prerequisitesAccepted: job.prerequisitesAccepted === true }), {
|
|
493
517
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
494
518
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
495
519
|
// land on its page, under the facts row.
|
|
496
520
|
artifacts: getArtifactsAccepted(),
|
|
497
|
-
}) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
521
|
+
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
498
522
|
agentTools: projectTools,
|
|
499
|
-
...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
|
|
523
|
+
...((projectTools.codexHome || fencedMount) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}) } } : {}),
|
|
500
524
|
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
501
525
|
// a browser profile of the agent's own, kept beside its Codex home —
|
|
502
526
|
// so a Chrome it starts to look at its page never reaches the
|
|
503
527
|
// person's (noWindowEnv.mjs).
|
|
504
528
|
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
529
|
+
// Reviewed project servers and the machine's configured servers, on
|
|
530
|
+
// initial cards, answers and review iterations alike.
|
|
531
|
+
...(mountsProjectMcp && rt === 'claude'
|
|
532
|
+
? {
|
|
533
|
+
mcpArgs: [...(claudeDisabled ? [] : ['--strict-mcp-config']), '--mcp-config', projectTools.mcpPath],
|
|
534
|
+
projectMcpServers: projectTools.mcpServers,
|
|
535
|
+
...(claudeDisabled ? { machineMcp: claudeDisabled } : {}),
|
|
536
|
+
}
|
|
537
|
+
: fencedMount ? { mcpArgs: fencedMount.args } : {}),
|
|
510
538
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
511
539
|
// The kind's posture, as the one turn profile (turnProfile.mjs):
|
|
512
540
|
// build for code, the fenced design/research postures otherwise.
|
|
@@ -523,13 +551,13 @@ export function createAgentTurnExecution({
|
|
|
523
551
|
},
|
|
524
552
|
// Present only when the container named one — see brainFor.
|
|
525
553
|
...brain,
|
|
526
|
-
streamJson: true,
|
|
527
554
|
answerFromResult: true,
|
|
528
555
|
label: c.cyan('[agent]'),
|
|
529
556
|
// The CLI's own tail, relayed. Throttled by the same rule the tab's
|
|
530
557
|
// narrator keeps: overwritten, never appended, and ignored by the
|
|
531
558
|
// board past ~90 seconds.
|
|
532
559
|
onActivity: (a) => {
|
|
560
|
+
brief?.append(a?.kind ?? 'activity', a?.full ?? a?.label);
|
|
533
561
|
const line = a?.label;
|
|
534
562
|
if (!line) return;
|
|
535
563
|
// THE TRACE TAKES EVERY LINE; the pulse takes one every two
|
|
@@ -555,12 +583,19 @@ export function createAgentTurnExecution({
|
|
|
555
583
|
// Workbench's tool cards are built from, scrubbed at collection by
|
|
556
584
|
// the builder itself (bounded window BEFORE its caps; see
|
|
557
585
|
// toolEventOf). A tool it does not know pushes nothing.
|
|
558
|
-
onToolEvent: (name, input) => {
|
|
586
|
+
onToolEvent: (name, input, meta) => {
|
|
587
|
+
brief?.append(meta?.result ? 'tool result' : 'tool call', meta?.result
|
|
588
|
+
? { id: meta.id, exitCode: meta.exitCode, isError: meta.isError, output: meta.output }
|
|
589
|
+
: { name, input });
|
|
590
|
+
// Codex request/result pairs feed capture/planner evidence. This
|
|
591
|
+
// append-only agent trace already relays its completed activity.
|
|
592
|
+
if (meta?.runtime === 'codex') return;
|
|
559
593
|
trace.tool(toolEventOf(name, input, wt, envScrub));
|
|
560
594
|
},
|
|
561
595
|
// The CLI's own per-turn token counts, off the `result` event. One
|
|
562
|
-
// result per
|
|
563
|
-
//
|
|
596
|
+
// result per CLI invocation. Internal-wait recovery combines those
|
|
597
|
+
// counts before this callback; project accounting still happens
|
|
598
|
+
// SERVER-side, behind the settle's idempotent win,
|
|
564
599
|
// because a retried settle must not charge the same turn twice.
|
|
565
600
|
// …tagged with WHICH CLI counted it (2026-09-24), so the server can
|
|
566
601
|
// split an agent's spend per CLI rather than labelling a codex
|
|
@@ -571,17 +606,14 @@ export function createAgentTurnExecution({
|
|
|
571
606
|
onModel: (m) => {
|
|
572
607
|
model = m;
|
|
573
608
|
},
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
// probe
|
|
579
|
-
// reading "needs authorization" for the life of the process.
|
|
580
|
-
onInit: (i) => {
|
|
581
|
-
recordSkills(i.skills);
|
|
582
|
-
recordMcpServers(i.mcpServers);
|
|
583
|
-
},
|
|
609
|
+
// No skills or connectors are harvested here (2026-10-10): every
|
|
610
|
+
// agent turn narrows them — `--setting-sources user` and the reviewed
|
|
611
|
+
// plugin, `--strict-mcp-config` — so its init is not the machine's
|
|
612
|
+
// (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
|
|
613
|
+
// startup probe and its Terminal tabs.
|
|
584
614
|
onSpawn: (ch) => {
|
|
615
|
+
brief?.running(true);
|
|
616
|
+
ch.once('close', () => { if (child === ch) brief?.running(false); });
|
|
585
617
|
child = ch;
|
|
586
618
|
spawnedChildren.add(ch);
|
|
587
619
|
// The AGENT it serves — what the machine snapshot charges this
|
|
@@ -599,6 +631,8 @@ export function createAgentTurnExecution({
|
|
|
599
631
|
// and a place id IS one. It is what lets a hard stop actually reach
|
|
600
632
|
// the CLI — see `retireWorkSessions`.
|
|
601
633
|
agentChildren.set(place, ch);
|
|
634
|
+
// Asked to stop between the check and the spawn.
|
|
635
|
+
if (run.interrupted) killGroup(ch);
|
|
602
636
|
},
|
|
603
637
|
};
|
|
604
638
|
/**
|
|
@@ -612,16 +646,26 @@ export function createAgentTurnExecution({
|
|
|
612
646
|
* (resumeLost.mjs, SOLID F001): a successful answer that merely
|
|
613
647
|
* MENTIONS a missing session is never run twice.
|
|
614
648
|
*/
|
|
615
|
-
out = await
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
649
|
+
out = await runAgentWithInternalWaits(
|
|
650
|
+
(args, continuing) => continuing ? runCli(args) : runTurnResumingOnce(runCli, args, {
|
|
651
|
+
// A Codex write-up always runs fresh over its copy (codexResearch
|
|
652
|
+
// .mjs), so there is no lost conversation to retry it for.
|
|
653
|
+
resume: rt === 'codex' && posture === 'research' ? false : args.resume,
|
|
654
|
+
runtime: rt,
|
|
655
|
+
beforeFresh: () => {
|
|
656
|
+
seenThreadId = null;
|
|
657
|
+
lastAnswer = null;
|
|
658
|
+
model = null;
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
{ ...agentTurnArgs, onContinuation: (n) => {
|
|
620
662
|
lastAnswer = null;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
663
|
+
trace.prose('text', `The machine resumed this card after an internal wait (continuation ${n}).`);
|
|
664
|
+
void postAgentActivity(agentId, 'resuming after an internal wait');
|
|
665
|
+
} }
|
|
666
|
+
);
|
|
624
667
|
} finally {
|
|
668
|
+
await brief?.stop();
|
|
625
669
|
// What the CLI counted, for the lane's catch if anything below throws.
|
|
626
670
|
measured.usage = usage;
|
|
627
671
|
measured.model = model;
|
|
@@ -671,16 +715,36 @@ export function createAgentTurnExecution({
|
|
|
671
715
|
* floor: it must never delay the settle behind it.
|
|
672
716
|
*/
|
|
673
717
|
void reportSessionWorktree(place).catch(() => {});
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
void artifacts
|
|
678
|
-
.report({ placeDir: wt, before: artifactsBefore, agentId, turnId })
|
|
679
|
-
.catch(() => {});
|
|
718
|
+
// A file card delivers uploaded bytes, not an in-memory retry. Otherwise
|
|
719
|
+
// its dependent card can start while Review has nothing to show.
|
|
720
|
+
await artifacts.report({ placeDir: wt, before: artifactsBefore, agentId, turnId, requireStored: kind.artifact != null });
|
|
680
721
|
}
|
|
681
722
|
|
|
682
723
|
|
|
683
724
|
const commits = commitsBetween(wt, before);
|
|
725
|
+
/**
|
|
726
|
+
* A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
|
|
727
|
+
* cut off, so there is no outcome to read: the turn reports `interrupted`
|
|
728
|
+
* with what it measured — the commits it made, its spend, the branch —
|
|
729
|
+
* and leaves the worktree exactly as the stopped run left it. The message
|
|
730
|
+
* sent to interrupt it runs next, here, resuming this conversation. No
|
|
731
|
+
* limit, no park and no review entry: none of that happened.
|
|
732
|
+
*/
|
|
733
|
+
if (run.interrupted) {
|
|
734
|
+
measured.ranCli = spawnedChildren.size > 0;
|
|
735
|
+
await postAgentTurn({
|
|
736
|
+
turnId,
|
|
737
|
+
outcome: 'nothing',
|
|
738
|
+
interrupted: true,
|
|
739
|
+
ran: spawnedChildren.size > 0,
|
|
740
|
+
commits,
|
|
741
|
+
...(usage ? { usage } : {}),
|
|
742
|
+
...(model ? { model } : {}),
|
|
743
|
+
...(branch ? { branch } : {}),
|
|
744
|
+
worktree: wt,
|
|
745
|
+
});
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
684
748
|
// WHAT THE TURN REPORTS is one decision, off what was measured here
|
|
685
749
|
// (workAgentTurnOutcome.mjs). The side effects it implies stay on this
|
|
686
750
|
// side: the machine's limit state, the park, and the review-entry beat.
|
|
@@ -25,6 +25,7 @@ import { limitLine } from './cliLimit.mjs';
|
|
|
25
25
|
import { parseTurnResult } from './agentPlan.mjs';
|
|
26
26
|
import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
|
|
27
27
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
28
|
+
import { usableImageArtifact } from './imageArtifact.mjs';
|
|
28
29
|
|
|
29
30
|
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
|
|
30
31
|
const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
|
|
@@ -110,7 +111,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
110
111
|
*/
|
|
111
112
|
if (res.outcome === 'delivered' && kind.artifact) {
|
|
112
113
|
const want = kind.artifact.match;
|
|
113
|
-
const has = (l) => l.some((e) => want.test(e.name)
|
|
114
|
+
const has = (l) => l.some((e) => want.test(e.name)
|
|
115
|
+
&& (kind.posture !== 'image' || usableImageArtifact(e)));
|
|
114
116
|
const standing = scanArtifacts(wt);
|
|
115
117
|
const wrote = has(changedArtifacts(artifactsBefore, standing));
|
|
116
118
|
const present = wrote || ((job.kind !== 'task' || job.redo === true) && has(standing));
|
|
@@ -126,11 +128,18 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
const prerequisite = res.prerequisite;
|
|
132
|
+
const cannotRequest = prerequisite && job.prerequisitesAccepted !== true
|
|
133
|
+
? 'This server does not support prerequisite-card requests yet. This work has not been delivered.'
|
|
134
|
+
: prerequisite?.taskKind === 'image' && (job.runtime ?? 'claude') !== 'codex'
|
|
135
|
+
? 'This agent runs on a CLI that cannot generate images. Image cards require Codex’s own image generation; this agent cannot run the requested texture work.'
|
|
136
|
+
: null;
|
|
129
137
|
return report(
|
|
130
138
|
{
|
|
131
139
|
turnId,
|
|
140
|
+
...(prerequisite && !cannotRequest ? { prerequisite } : {}),
|
|
132
141
|
outcome: res.outcome,
|
|
133
|
-
answer: envScrub(res.answer ?? '').slice(0, 8000),
|
|
142
|
+
answer: cannotRequest ?? envScrub(res.answer ?? '').slice(0, 8000),
|
|
134
143
|
...(commits.length ? { commits } : {}),
|
|
135
144
|
// SCRUBBED like the answer beside it: a raised card lands in the
|
|
136
145
|
// project doc every member reads, and a brief quoting the `.env` the
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* Daemon→server reports, every one of them: no version floor, and a permanent
|
|
15
15
|
* 4xx is the server saying it will never take the body (see each POST).
|
|
16
16
|
*/
|
|
17
|
-
import { FLEET_URL
|
|
18
|
-
import { fleetEndpoint } from './fleetWire.mjs';
|
|
17
|
+
import { FLEET_URL } from './config.mjs';
|
|
18
|
+
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
19
|
+
import { fleetHeaders, postFleetJson } from './fleetPost.mjs';
|
|
19
20
|
|
|
20
21
|
export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
21
22
|
const AGENT_TURN_DONE_URL = fleetEndpoint('agent-turn-done', FLEET_URL);
|
|
@@ -53,16 +54,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
53
54
|
const turnId = String(body.turnId);
|
|
54
55
|
agentReported.set(turnId, { body, at: Date.now() });
|
|
55
56
|
try {
|
|
56
|
-
const res = await
|
|
57
|
-
method: 'POST',
|
|
58
|
-
headers: {
|
|
59
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
60
|
-
'User-Agent': USER_AGENT,
|
|
61
|
-
'Content-Type': 'application/json',
|
|
62
|
-
},
|
|
63
|
-
signal: AbortSignal.timeout(30_000),
|
|
64
|
-
body: JSON.stringify(body),
|
|
65
|
-
});
|
|
57
|
+
const res = await postFleetJson(AGENT_TURN_DONE_URL, body, { timeoutMs: 30_000 });
|
|
66
58
|
const j = await res.json().catch(() => null);
|
|
67
59
|
/**
|
|
68
60
|
* ONLY A 2xx IS DELIVERED. The server answers an already-settled,
|
|
@@ -79,7 +71,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
79
71
|
if (res.ok) {
|
|
80
72
|
agentReported.delete(turnId);
|
|
81
73
|
agentRejectedUntil.delete(turnId);
|
|
82
|
-
} else if (res.status
|
|
74
|
+
} else if (isConsideredRefusal(res.status)) {
|
|
83
75
|
agentRejectedUntil.set(turnId, Date.now() + REJECT_RETRY_MS);
|
|
84
76
|
}
|
|
85
77
|
return j?.data ?? null;
|
|
@@ -102,20 +94,8 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
102
94
|
*/
|
|
103
95
|
const postAgentTrace = async (body) => {
|
|
104
96
|
try {
|
|
105
|
-
const res = await
|
|
106
|
-
|
|
107
|
-
headers: {
|
|
108
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
109
|
-
'User-Agent': USER_AGENT,
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
},
|
|
112
|
-
signal: AbortSignal.timeout(15_000),
|
|
113
|
-
body: JSON.stringify(body),
|
|
114
|
-
});
|
|
115
|
-
return (
|
|
116
|
-
res.ok ||
|
|
117
|
-
(res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
118
|
-
);
|
|
97
|
+
const res = await postFleetJson(AGENT_TRACE_URL, body, { timeoutMs: 15_000 });
|
|
98
|
+
return res.ok || isConsideredRefusal(res.status);
|
|
119
99
|
} catch {
|
|
120
100
|
return false; // a blip — the same entries go again at the same seq
|
|
121
101
|
}
|
|
@@ -125,11 +105,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
125
105
|
try {
|
|
126
106
|
await fetch(AGENT_ACTIVITY_URL, {
|
|
127
107
|
method: 'POST',
|
|
128
|
-
headers:
|
|
129
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
130
|
-
'User-Agent': USER_AGENT,
|
|
131
|
-
'Content-Type': 'application/json',
|
|
132
|
-
},
|
|
108
|
+
headers: fleetHeaders(),
|
|
133
109
|
signal: AbortSignal.timeout(15_000),
|
|
134
110
|
body: JSON.stringify({ agentId, text }),
|
|
135
111
|
});
|
|
@@ -142,11 +118,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
142
118
|
try {
|
|
143
119
|
await fetch(AGENT_PARKED_URL, {
|
|
144
120
|
method: 'POST',
|
|
145
|
-
headers:
|
|
146
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
147
|
-
'User-Agent': USER_AGENT,
|
|
148
|
-
'Content-Type': 'application/json',
|
|
149
|
-
},
|
|
121
|
+
headers: fleetHeaders(),
|
|
150
122
|
signal: AbortSignal.timeout(15_000),
|
|
151
123
|
// `runtime` scopes the park to that CLI's agents; a server that does
|
|
152
124
|
// not know the key parks every agent, which is what it always did.
|
|
@@ -25,6 +25,8 @@ import { c, note } from './ui.mjs';
|
|
|
25
25
|
import { createAgentTurnReports } from './workAgentTurnReports.mjs';
|
|
26
26
|
import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
|
|
27
27
|
import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
28
|
+
import { killGroup } from './processGroup.mjs';
|
|
29
|
+
import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
|
|
28
30
|
|
|
29
31
|
export function createWorkAgentTurns({
|
|
30
32
|
REJECT_RETRY_MS,
|
|
@@ -48,6 +50,7 @@ export function createWorkAgentTurns({
|
|
|
48
50
|
agentRemoteAt,
|
|
49
51
|
/** The turn's files, fetched into its worktree (workAttachments.mjs, 0.112.0). */
|
|
50
52
|
fetchAgentFiles,
|
|
53
|
+
makeBrief = createAgentBriefRunner({ admit, workChildren }),
|
|
51
54
|
}) {
|
|
52
55
|
const {
|
|
53
56
|
postAgentTurn,
|
|
@@ -91,8 +94,23 @@ export function createWorkAgentTurns({
|
|
|
91
94
|
* directory names, and those are places.
|
|
92
95
|
*/
|
|
93
96
|
const agentChildren = new Map(); // placeId -> child process
|
|
97
|
+
/**
|
|
98
|
+
* INTERRUPT NOW (2026-10-13) — each turn this lane has taken: its place and
|
|
99
|
+
* whether a person asked to stop it. A stop kills the CLI's PROCESS GROUP
|
|
100
|
+
* (it is spawned detached, so the commands it started go with it); a turn
|
|
101
|
+
* asked to stop before it spawned never spawns.
|
|
102
|
+
*/
|
|
103
|
+
const agentRuns = new Map(); // turnId -> { place, interrupted }
|
|
104
|
+
/** Turns answered `ran: false` here, so a roster read made before that
|
|
105
|
+
* settle landed is not answered again at once. */
|
|
106
|
+
const notRunningSaid = new Map(); // turnId -> when
|
|
107
|
+
/** Agents whose run this lane just stopped: the next turn is the one sent to
|
|
108
|
+
* interrupt it, and is told so even when the roster that handed it over was
|
|
109
|
+
* read before the stop's settle landed. Taken once, for a little while. */
|
|
110
|
+
const stoppedFor = new Map(); // agentId -> when
|
|
94
111
|
|
|
95
112
|
const { runAgentTurnInPlace, commitsBetween } = createAgentTurnExecution({
|
|
113
|
+
makeBrief,
|
|
96
114
|
baseRef,
|
|
97
115
|
baseDir,
|
|
98
116
|
repoRoot,
|
|
@@ -172,12 +190,18 @@ export function createWorkAgentTurns({
|
|
|
172
190
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
173
191
|
// places: those are agents' by construction, and anything else here would
|
|
174
192
|
// be a tab's directory, where a turn is a reader by the product's own law.
|
|
193
|
+
const run = agentRuns.get(turnId) ?? { place, interrupted: false };
|
|
175
194
|
await inPlace(place, place.startsWith('a-'), async () => {
|
|
176
195
|
try {
|
|
177
|
-
await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured });
|
|
196
|
+
await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
|
|
197
|
+
if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
|
|
178
198
|
} catch (error) {
|
|
179
199
|
if (settled) return;
|
|
180
200
|
const commits = measured.wt ? commitsBetween(measured.wt, measured.before) : [];
|
|
201
|
+
if (run.interrupted) {
|
|
202
|
+
await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
181
205
|
await postAgentTurn(
|
|
182
206
|
failedTurnSettlement({
|
|
183
207
|
turnId,
|
|
@@ -235,7 +259,7 @@ export function createWorkAgentTurns({
|
|
|
235
259
|
}
|
|
236
260
|
};
|
|
237
261
|
|
|
238
|
-
const processAgentTurnJobs = (jobs) => {
|
|
262
|
+
const processAgentTurnJobs = (jobs, { reportsOnly = false } = {}) => {
|
|
239
263
|
const list = Array.isArray(jobs) ? jobs : [];
|
|
240
264
|
pruneHeldReports(list);
|
|
241
265
|
/** One deferral line per tick, however many turns were offered. */
|
|
@@ -244,6 +268,7 @@ export function createWorkAgentTurns({
|
|
|
244
268
|
const id = String(job?.id || '');
|
|
245
269
|
if (!id || agentTurns.has(id)) continue;
|
|
246
270
|
const held = agentReported.get(id);
|
|
271
|
+
if (reportsOnly && !held) continue;
|
|
247
272
|
// A body the server REFUSED waits out its backoff — still held, so the
|
|
248
273
|
// CLI is never re-run in the meantime.
|
|
249
274
|
if (held && (agentRejectedUntil.get(id) ?? 0) > Date.now()) continue;
|
|
@@ -306,11 +331,16 @@ export function createWorkAgentTurns({
|
|
|
306
331
|
}
|
|
307
332
|
const releaseSlot = admit.reserve();
|
|
308
333
|
agentTurns.add(id);
|
|
309
|
-
|
|
334
|
+
agentRuns.set(id, { place: String(job.placeId), interrupted: false });
|
|
335
|
+
const stopped = stoppedFor.get(String(job.agentId));
|
|
336
|
+
stoppedFor.delete(String(job.agentId));
|
|
337
|
+
const run = stopped != null && Date.now() - stopped < 120_000 ? { ...job, interrupted: true } : job;
|
|
338
|
+
void runAgentTurn(run, releaseSlot).finally(() => {
|
|
310
339
|
// Belt for every path that never reached a spawn — the release is
|
|
311
340
|
// idempotent, so the normal case releases twice.
|
|
312
341
|
releaseSlot();
|
|
313
342
|
agentTurns.delete(id);
|
|
343
|
+
agentRuns.delete(id);
|
|
314
344
|
});
|
|
315
345
|
}
|
|
316
346
|
};
|
|
@@ -318,9 +348,10 @@ export function createWorkAgentTurns({
|
|
|
318
348
|
/**
|
|
319
349
|
* SETTLE EVERYTHING IN FLIGHT, because this process is about to go away.
|
|
320
350
|
*
|
|
321
|
-
* The one caller is the
|
|
322
|
-
* to another box,
|
|
323
|
-
*
|
|
351
|
+
* The one caller is the roster's stand-down (holder.mjs): the project's
|
|
352
|
+
* machine moved to another box, this box was removed from the project, or
|
|
353
|
+
* Flowviant stopped it (2026-10-10) — so nothing here will be re-offered to
|
|
354
|
+
* us and nothing else knows these turns were running. An abandoned turn sits pending until the
|
|
324
355
|
* server's six-hour expiry while the board shows an agent working on a machine
|
|
325
356
|
* that has gone — the wedge every settle path in this lane exists to avoid.
|
|
326
357
|
*
|
|
@@ -346,5 +377,31 @@ export function createWorkAgentTurns({
|
|
|
346
377
|
await Promise.allSettled(posts);
|
|
347
378
|
};
|
|
348
379
|
|
|
349
|
-
|
|
380
|
+
/**
|
|
381
|
+
* THE AGENT RUNS A PERSON ASKED TO STOP (`roster.interruptAgentTurns`, read
|
|
382
|
+
* before the jobs). One this lane is running is stopped — the CLI's group
|
|
383
|
+
* killed — and reports `interrupted` once its CLI is gone. One that finished
|
|
384
|
+
* here keeps its own settle. One this lane is not running is answered `ran:
|
|
385
|
+
* false`, and the server puts it back in the queue untouched.
|
|
386
|
+
*/
|
|
387
|
+
const interruptAgentTurns = (ids) => {
|
|
388
|
+
const at = Date.now();
|
|
389
|
+
for (const [id, when] of notRunningSaid) if (at - when > 30_000) notRunningSaid.delete(id);
|
|
390
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
391
|
+
if (typeof id !== 'string' || !id) continue;
|
|
392
|
+
const run = agentRuns.get(id);
|
|
393
|
+
if (run) {
|
|
394
|
+
if (!run.interrupted) note(`${c.cyan('agent')} ${c.dim('— interrupting the running turn')}`);
|
|
395
|
+
run.interrupted = true;
|
|
396
|
+
const child = agentChildren.get(run.place);
|
|
397
|
+
if (child) killGroup(child);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (agentReported.has(id) || notRunningSaid.has(id)) continue;
|
|
401
|
+
notRunningSaid.set(id, at);
|
|
402
|
+
void postAgentTurn({ turnId: id, outcome: 'nothing', interrupted: true, ran: false }).catch(() => {});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return { processAgentTurnJobs, interruptAgentTurns, settleAgentTurns, agentTurns, agentChildren, agentReported };
|
|
350
407
|
}
|