flowviant 0.115.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -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 +88 -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 +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- 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/readGuard.mjs +41 -7
- 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/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -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 +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- 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/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- 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 +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -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 +96 -30
- 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 +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* through the one turn-done wire (workAgentTurnReports.mjs). Null means go on.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { git, sessionBranchRef } from './git.mjs';
|
|
17
|
+
import { worktreePath } from './workPlaces.mjs';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX, AND THIS MAY NOT BE IT.
|
|
@@ -45,7 +45,11 @@ import { git } from './git.mjs';
|
|
|
45
45
|
* remedy somebody cannot carry out is worse than none.
|
|
46
46
|
*/
|
|
47
47
|
export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch }) {
|
|
48
|
-
|
|
48
|
+
// The caller validated `place` (workAgentTurns); an unsafe one would give a
|
|
49
|
+
// null directory AND a null ref below, and the null ref stands the guard
|
|
50
|
+
// down — the same fail-open direction as an unreadable repo.
|
|
51
|
+
const wtDir = worktreePath(baseDir, place);
|
|
52
|
+
const ref = sessionBranchRef(place);
|
|
49
53
|
let hasBranch = false;
|
|
50
54
|
/**
|
|
51
55
|
* THREE STATES, AND THE MIDDLE ONE IS WHY THIS IS NOT A BARE CATCH.
|
|
@@ -63,14 +67,14 @@ export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublished
|
|
|
63
67
|
* `placeWtFor` is about to fail on the same unreadable repo and say so
|
|
64
68
|
* in its own words, which is the honest sentence.
|
|
65
69
|
*/
|
|
66
|
-
let branchMeasured =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
git(['rev-parse', '--verify', '--quiet',
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let branchMeasured = ref !== null;
|
|
71
|
+
if (branchMeasured) {
|
|
72
|
+
try {
|
|
73
|
+
hasBranch = Boolean(git(['rev-parse', '--verify', '--quiet', ref], repoRoot));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e?.status === 1) hasBranch = false;
|
|
76
|
+
else branchMeasured = false;
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
if (branchMeasured && !existsSync(wtDir) && !hasBranch) {
|
|
76
80
|
/**
|
|
@@ -19,7 +19,7 @@
|
|
|
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';
|
|
@@ -27,12 +27,12 @@ import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICK
|
|
|
27
27
|
import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
|
|
28
28
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
29
29
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
30
|
-
import { canRun,
|
|
31
|
-
import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
|
|
30
|
+
import { canRun, runtimeById } from './runtimes.mjs';
|
|
32
31
|
import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS } from './runtimeEvents.mjs';
|
|
33
32
|
import { makeTraceRelay } from './trace.mjs';
|
|
34
33
|
import { readBaseTools, prepareAgentTools } from './projectTools.mjs';
|
|
35
34
|
import { runTurnResumingOnce } from './resumeLost.mjs';
|
|
35
|
+
import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
|
|
36
36
|
import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
37
37
|
import { brainFor } from './workBrain.mjs';
|
|
38
38
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
@@ -95,15 +95,8 @@ export function createAgentTurnExecution({
|
|
|
95
95
|
* reconciliation books whatever no card claimed. Missing beats fabricated
|
|
96
96
|
* and both beat a stall.
|
|
97
97
|
*/
|
|
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
|
-
: [];
|
|
98
|
+
const out = gitOrNull(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
|
|
99
|
+
return out?.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50) ?? [];
|
|
107
100
|
};
|
|
108
101
|
|
|
109
102
|
/**
|
|
@@ -147,19 +140,13 @@ export function createAgentTurnExecution({
|
|
|
147
140
|
* `runAgentMerge` learned this two commits ago and says so in its own
|
|
148
141
|
* comment; this is the same call, in the same file, left unguarded. Both
|
|
149
142
|
* 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
|
|
143
|
+
* gate the work — so failing to read them costs a null, not the turn:
|
|
144
|
+
* git.mjs's `gitOrNull`, the one spelling of that rule.
|
|
151
145
|
*/
|
|
152
|
-
const readGit = (args) => {
|
|
153
|
-
try {
|
|
154
|
-
return (git(args, wt) || '').trim() || null;
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
146
|
// WHERE THE BRANCH WAS BEFORE THIS TURN, so the commits reported are the
|
|
160
147
|
// ones this turn actually made.
|
|
161
|
-
const before =
|
|
162
|
-
const branch =
|
|
148
|
+
const before = gitOrNull(['rev-parse', 'HEAD'], wt);
|
|
149
|
+
const branch = gitOrNull(['symbolic-ref', '--quiet', '--short', 'HEAD'], wt);
|
|
163
150
|
// …and the same three facts for the lane's catch: a turn that throws past
|
|
164
151
|
// here is still settled with what was measured (workAgentTurns.mjs).
|
|
165
152
|
measured.wt = wt;
|
|
@@ -176,9 +163,9 @@ export function createAgentTurnExecution({
|
|
|
176
163
|
* posture, which can write ONLY `.flowviant/artifacts/` (claudePosture.mjs).
|
|
177
164
|
*
|
|
178
165
|
* 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
|
|
166
|
+
* Antigravity declares none of them; Codex declares `design` (since
|
|
167
|
+
* 0.115.0), `image` and `research` (0.117.0, over an isolated copy —
|
|
168
|
+
* codexResearch.mjs) (runtimeCodex.mjs, runtimeAntigravity.mjs), and the only
|
|
182
169
|
* alternative to refusing is running the card as a BUILD turn with
|
|
183
170
|
* permissions skipped — an agent writing code for an ask that was a
|
|
184
171
|
* mockup, reporting success. `nothing` puts the agent in Stuck saying so.
|
|
@@ -220,7 +207,7 @@ export function createAgentTurnExecution({
|
|
|
220
207
|
// entry in agentTaskKinds.mjs; nothing below branches on the kind name.
|
|
221
208
|
const kind = AGENT_TASK_KINDS[taskKind];
|
|
222
209
|
const posture = kind.posture;
|
|
223
|
-
if (!canRun(
|
|
210
|
+
if (!canRun(runtimeById(rt), posture)) {
|
|
224
211
|
await postAgentTurn({
|
|
225
212
|
turnId,
|
|
226
213
|
outcome: 'nothing',
|
|
@@ -393,7 +380,7 @@ export function createAgentTurnExecution({
|
|
|
393
380
|
* `parse: null` is exactly the claudeStream.mjs stream path. See
|
|
394
381
|
* CLAUDE_TOOL_PROSE_KINDS.
|
|
395
382
|
*/
|
|
396
|
-
const doubledKinds =
|
|
383
|
+
const doubledKinds = runtimeById(rt)?.parse ? null : CLAUDE_TOOL_PROSE_KINDS;
|
|
397
384
|
|
|
398
385
|
/**
|
|
399
386
|
* WHICH BRAIN THIS CONTAINER WAS PINNED TO — the same `brainFor` the tab
|
|
@@ -489,12 +476,12 @@ export function createAgentTurnExecution({
|
|
|
489
476
|
// different things about the same directory.
|
|
490
477
|
// Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
|
|
491
478
|
// told it is Codex, never Claude.
|
|
492
|
-
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind,
|
|
479
|
+
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label), {
|
|
493
480
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
494
481
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
495
482
|
// land on its page, under the facts row.
|
|
496
483
|
artifacts: getArtifactsAccepted(),
|
|
497
|
-
}) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
484
|
+
}) + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
498
485
|
agentTools: projectTools,
|
|
499
486
|
...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
|
|
500
487
|
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
@@ -502,11 +489,13 @@ export function createAgentTurnExecution({
|
|
|
502
489
|
// so a Chrome it starts to look at its page never reaches the
|
|
503
490
|
// person's (noWindowEnv.mjs).
|
|
504
491
|
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
492
|
+
// The base branch's MCP servers, for a code card: Claude's in this
|
|
493
|
+
// turn's own 0600 file, strictly; Codex's already in its private
|
|
494
|
+
// home's config.toml (projectToolRuntimes.mjs, 2026-10-10), which
|
|
495
|
+
// its fenced postures ignore — never `-c` pairs on argv.
|
|
505
496
|
...(posture === 'build' && rt === 'claude'
|
|
506
497
|
? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
|
|
507
|
-
:
|
|
508
|
-
? { mcpArgs: projectTools.codexArgs }
|
|
509
|
-
: {}),
|
|
498
|
+
: {}),
|
|
510
499
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
511
500
|
// The kind's posture, as the one turn profile (turnProfile.mjs):
|
|
512
501
|
// build for code, the fenced design/research postures otherwise.
|
|
@@ -523,7 +512,6 @@ export function createAgentTurnExecution({
|
|
|
523
512
|
},
|
|
524
513
|
// Present only when the container named one — see brainFor.
|
|
525
514
|
...brain,
|
|
526
|
-
streamJson: true,
|
|
527
515
|
answerFromResult: true,
|
|
528
516
|
label: c.cyan('[agent]'),
|
|
529
517
|
// The CLI's own tail, relayed. Throttled by the same rule the tab's
|
|
@@ -559,8 +547,9 @@ export function createAgentTurnExecution({
|
|
|
559
547
|
trace.tool(toolEventOf(name, input, wt, envScrub));
|
|
560
548
|
},
|
|
561
549
|
// The CLI's own per-turn token counts, off the `result` event. One
|
|
562
|
-
// result per
|
|
563
|
-
//
|
|
550
|
+
// result per CLI invocation. Internal-wait recovery combines those
|
|
551
|
+
// counts before this callback; project accounting still happens
|
|
552
|
+
// SERVER-side, behind the settle's idempotent win,
|
|
564
553
|
// because a retried settle must not charge the same turn twice.
|
|
565
554
|
// …tagged with WHICH CLI counted it (2026-09-24), so the server can
|
|
566
555
|
// split an agent's spend per CLI rather than labelling a codex
|
|
@@ -571,16 +560,11 @@ export function createAgentTurnExecution({
|
|
|
571
560
|
onModel: (m) => {
|
|
572
561
|
model = m;
|
|
573
562
|
},
|
|
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
|
-
},
|
|
563
|
+
// No skills or connectors are harvested here (2026-10-10): every
|
|
564
|
+
// agent turn narrows them — `--setting-sources user` and the reviewed
|
|
565
|
+
// plugin, `--strict-mcp-config` — so its init is not the machine's
|
|
566
|
+
// (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
|
|
567
|
+
// startup probe and its Terminal tabs.
|
|
584
568
|
onSpawn: (ch) => {
|
|
585
569
|
child = ch;
|
|
586
570
|
spawnedChildren.add(ch);
|
|
@@ -612,15 +596,24 @@ export function createAgentTurnExecution({
|
|
|
612
596
|
* (resumeLost.mjs, SOLID F001): a successful answer that merely
|
|
613
597
|
* MENTIONS a missing session is never run twice.
|
|
614
598
|
*/
|
|
615
|
-
out = await
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
599
|
+
out = await runAgentWithInternalWaits(
|
|
600
|
+
(args, continuing) => continuing ? runTurn(args) : runTurnResumingOnce(runTurn, args, {
|
|
601
|
+
// A Codex write-up always runs fresh over its copy (codexResearch
|
|
602
|
+
// .mjs), so there is no lost conversation to retry it for.
|
|
603
|
+
resume: rt === 'codex' && posture === 'research' ? false : args.resume,
|
|
604
|
+
runtime: rt,
|
|
605
|
+
beforeFresh: () => {
|
|
606
|
+
seenThreadId = null;
|
|
607
|
+
lastAnswer = null;
|
|
608
|
+
model = null;
|
|
609
|
+
},
|
|
610
|
+
}),
|
|
611
|
+
{ ...agentTurnArgs, onContinuation: (n) => {
|
|
620
612
|
lastAnswer = null;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
613
|
+
trace.prose('text', `The machine resumed this card after an internal wait (continuation ${n}).`);
|
|
614
|
+
void postAgentActivity(agentId, 'resuming after an internal wait');
|
|
615
|
+
} }
|
|
616
|
+
);
|
|
624
617
|
} finally {
|
|
625
618
|
// What the CLI counted, for the lane's catch if anything below throws.
|
|
626
619
|
measured.usage = usage;
|
|
@@ -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.
|
|
@@ -318,9 +318,10 @@ export function createWorkAgentTurns({
|
|
|
318
318
|
/**
|
|
319
319
|
* SETTLE EVERYTHING IN FLIGHT, because this process is about to go away.
|
|
320
320
|
*
|
|
321
|
-
* The one caller is the
|
|
322
|
-
* to another box,
|
|
323
|
-
*
|
|
321
|
+
* The one caller is the roster's stand-down (holder.mjs): the project's
|
|
322
|
+
* machine moved to another box, this box was removed from the project, or
|
|
323
|
+
* Flowviant stopped it (2026-10-10) — so nothing here will be re-offered to
|
|
324
|
+
* us and nothing else knows these turns were running. An abandoned turn sits pending until the
|
|
324
325
|
* server's six-hour expiry while the board shows an agent working on a machine
|
|
325
326
|
* that has gone — the wedge every settle path in this lane exists to avoid.
|
|
326
327
|
*
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* serves both, so the two lanes cannot disagree about what a safe name is.
|
|
10
10
|
* The name rule itself is safeFileName.mjs's.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { ensureRealSubdir, realDirState } from './ownedDirs.mjs';
|
|
14
15
|
import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
|
|
15
16
|
import { excludeInWorktree } from './git.mjs';
|
|
16
17
|
import { FLOWVIANT_OWN_PATHS } from './knowledgeLibrary.mjs';
|
|
@@ -59,13 +60,60 @@ export function createWorkAttachments() {
|
|
|
59
60
|
// and the two sides never disagree about the cut. An empty name is
|
|
60
61
|
// `attachment`, the server's own fallback.
|
|
61
62
|
const safeUploadName = (raw) => safeFileName(raw, 'attachment');
|
|
62
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* NO LINK UNDER `.flowviant/` IS FOLLOWED, AND NOTHING THERE IS REPLACED
|
|
65
|
+
* (2026-10-10, the audit's F080). This lane writes a MEMBER'S bytes under a
|
|
66
|
+
* member's name, and it was the one `.flowviant/` writer with no guard:
|
|
67
|
+
* `mkdirSync` and `writeFileSync` went through a committed `.flowviant ->
|
|
68
|
+
* ~/.ssh` or `.flowviant/uploads -> ../../../.ssh`, and `existsSync` answered
|
|
69
|
+
* "absent" for a dangling link at the file's own name, which the write then
|
|
70
|
+
* followed to create its target — an attachment named `authorized_keys`
|
|
71
|
+
* landing in the operator's `~/.ssh`. Now: both directories must be real
|
|
72
|
+
* (ownedDirs.mjs `ensureRealSubdir`, the artifacts directory's rule — refused,
|
|
73
|
+
* never replaced), every look at a name is an `lstat` or an `O_NOFOLLOW`
|
|
74
|
+
* open, and each file is CREATED (`O_EXCL`): anything already at the name,
|
|
75
|
+
* a dangling link included, is never written through.
|
|
76
|
+
*/
|
|
77
|
+
const NEW_FILE = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
78
|
+
/** Anything at all at `path` — a dangling link too. */
|
|
79
|
+
const taken = (path) => {
|
|
80
|
+
try {
|
|
81
|
+
lstatSync(path);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return e?.code !== 'ENOENT';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** The REGULAR file at `path` holds exactly `buf` — read only when the sizes
|
|
88
|
+
* agree, and never through a link. Non-blocking, so a fifo at the name is
|
|
89
|
+
* answered "no" rather than waited on. */
|
|
63
90
|
const holds = (path, buf) => {
|
|
91
|
+
let fd;
|
|
64
92
|
try {
|
|
65
|
-
|
|
93
|
+
const at = lstatSync(path);
|
|
94
|
+
if (!at.isFile() || at.size !== buf.byteLength) return false;
|
|
95
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
96
|
+
const st = fstatSync(fd);
|
|
97
|
+
return st.isFile() && st.size === buf.byteLength && readFileSync(fd).equals(buf);
|
|
66
98
|
} catch {
|
|
67
99
|
return false;
|
|
100
|
+
} finally {
|
|
101
|
+
if (fd !== undefined) closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/** A NEW regular file at `path` holding `buf`, at the mode `writeFileSync`
|
|
105
|
+
* used. Throws (EEXIST) when anything is already there; a write that fails
|
|
106
|
+
* part-way takes its own half-file with it. */
|
|
107
|
+
const createFile = (path, buf) => {
|
|
108
|
+
const fd = openSync(path, NEW_FILE, 0o666);
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(fd, buf);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
closeSync(fd);
|
|
113
|
+
rmSync(path, { force: true });
|
|
114
|
+
throw e;
|
|
68
115
|
}
|
|
116
|
+
closeSync(fd);
|
|
69
117
|
};
|
|
70
118
|
/**
|
|
71
119
|
* THE ONE DOWNLOAD LOOP. Each entry's relative path, or null where it did
|
|
@@ -81,11 +129,10 @@ export function createWorkAttachments() {
|
|
|
81
129
|
*/
|
|
82
130
|
const landFiles = async (wt, list, endpoint, sameBytesKept) => {
|
|
83
131
|
const dir = join(wt, UPLOAD_DIR);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
132
|
+
// A `.flowviant` or `uploads` that is a link or not a directory refuses
|
|
133
|
+
// the whole batch before anything is fetched: every file is a miss the
|
|
134
|
+
// turn is told about, and the repository's content is left as it is.
|
|
135
|
+
if (!ensureRealSubdir(wt, UPLOAD_DIR)) return list.map(() => null);
|
|
89
136
|
// "Never committed by us" has to be true for GIT, not just for this code:
|
|
90
137
|
// an untracked `.flowviant/` makes the whole worktree dirty, which refuses
|
|
91
138
|
// every ship, exempts the tree from closed-tab retirement forever, and
|
|
@@ -115,14 +162,22 @@ export function createWorkAttachments() {
|
|
|
115
162
|
// silently overwriting one with the other loses a file the human sent.
|
|
116
163
|
let name = safeUploadName(a.name);
|
|
117
164
|
let already = sameBytesKept && holds(join(dir, name), buf);
|
|
118
|
-
if (!already &&
|
|
165
|
+
if (!already && taken(join(dir, name))) {
|
|
119
166
|
const dot = name.lastIndexOf('.');
|
|
120
167
|
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
121
168
|
const ext = dot > 0 ? name.slice(dot) : '';
|
|
122
169
|
name = `${stem}-${String(a.id).slice(0, 6)}${ext}`;
|
|
123
|
-
|
|
170
|
+
// The id-suffixed name is this file's own: holding these bytes, it
|
|
171
|
+
// IS this file, on either lane (it used to be rewritten with them).
|
|
172
|
+
// Anything else there is a miss — never replaced, never followed.
|
|
173
|
+
already = holds(join(dir, name), buf);
|
|
174
|
+
}
|
|
175
|
+
if (!already) {
|
|
176
|
+
// Looked at again right before the write: the batch's first check
|
|
177
|
+
// is up to a download old.
|
|
178
|
+
if (realDirState(join(wt, '.flowviant')) !== 'dir' || realDirState(dir) !== 'dir') continue;
|
|
179
|
+
createFile(join(dir, name), buf);
|
|
124
180
|
}
|
|
125
|
-
if (!already) writeFileSync(join(dir, name), buf);
|
|
126
181
|
landed[landed.length - 1] = `${UPLOAD_DIR}/${name}`;
|
|
127
182
|
} catch {
|
|
128
183
|
/* one file failing must not fail the turn — the prompt lists what
|
package/bin/lib/workBrain.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* — each importing it directly — and it is pure over the job and the runtime
|
|
8
8
|
* table, so its home is a module of its own rather than either lane.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { runtimeById } from './runtimes.mjs';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The shape a per-tab model name must have before it rides argv as
|
|
@@ -15,8 +15,13 @@ import { RUNTIMES } from './runtimes.mjs';
|
|
|
15
15
|
* (below): it comes off the wire and lands in a child process's arguments —
|
|
16
16
|
* alphanumerics plus dot/dash/underscore, at most 40 characters, and NEVER a
|
|
17
17
|
* leading dash, which is an argv that parses as a flag.
|
|
18
|
+
*
|
|
19
|
+
* The server's route schemas copy it as `AGENT_MODEL_RE` (shared
|
|
20
|
+
* agent-runner.schema.ts), and the release parity gate holds the two equal
|
|
21
|
+
* (2026-10-10, audit F613): a server that accepts a name this drops has the
|
|
22
|
+
* board assert a pin the machine silently replaces with its default.
|
|
18
23
|
*/
|
|
19
|
-
const WORK_MODEL_RE = /^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,39}$/;
|
|
24
|
+
export const WORK_MODEL_RE = /^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,39}$/;
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* WHICH BRAIN, AT WHICH EFFORT — the tab's own pick, off the roster.
|
|
@@ -39,7 +44,7 @@ const WORK_MODEL_RE = /^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,39}$/;
|
|
|
39
44
|
* One global ladder once accepted `max` for every runtime, and Codex has no
|
|
40
45
|
* `max`: a skewed or previously queued Codex job reached argv as a
|
|
41
46
|
* `model_reasoning_effort` its CLI refuses. The per-CLI list lives beside each
|
|
42
|
-
* runtime's argv builder (`
|
|
47
|
+
* runtime's argv builder (`runtimeById(id).efforts`, runtime{Claude,Codex,Antigravity}.mjs); a runtime this
|
|
43
48
|
* daemon does not define runs nothing, so it spells no effort either.
|
|
44
49
|
*/
|
|
45
50
|
export function brainFor(job, runtimeId) {
|
|
@@ -47,7 +52,7 @@ export function brainFor(job, runtimeId) {
|
|
|
47
52
|
const model = typeof job?.model === 'string' ? job.model.trim() : '';
|
|
48
53
|
if (model && WORK_MODEL_RE.test(model)) out.model = model;
|
|
49
54
|
const effort = typeof job?.effort === 'string' ? job.effort.trim() : '';
|
|
50
|
-
const efforts =
|
|
55
|
+
const efforts = runtimeById(runtimeId)?.efforts ?? [];
|
|
51
56
|
if (effort && efforts.includes(effort)) out.effort = effort;
|
|
52
57
|
return out;
|
|
53
58
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE CAPPED READ-ONLY CONSULT TURN — the process plumbing the unattended
|
|
3
|
+
* one-shot lanes share: the Deploy-press planner (workAgentPlans.mjs), the AI
|
|
4
|
+
* pre-review (workAgentPrecheck.mjs) and intake (workIntake.mjs).
|
|
5
|
+
*
|
|
6
|
+
* ONE HOME (2026-10-10, the audit). Each lane carried its own forty-to-eighty
|
|
7
|
+
* lines of this, and the copies had already begun to drift. Every step below
|
|
8
|
+
* is a rule some incident taught, and a lane that copied the wrong one would
|
|
9
|
+
* show it only as a daemon that never auto-updates again:
|
|
10
|
+
* · THE CHILD IS REGISTERED IN `workChildren`, AND REMOVED IN A `finally`.
|
|
11
|
+
* It is what `workBusy()` counts and what counts against the machine's
|
|
12
|
+
* ceiling (liveTurnCount); a leaked entry keeps the daemon "busy" forever,
|
|
13
|
+
* which blocks every auto-update, silently, until a restart. No task id: a
|
|
14
|
+
* consult turn belongs to no card, and the snapshot's per-task rows must
|
|
15
|
+
* not invent one.
|
|
16
|
+
* · THE ADMISSION RESERVATION IS RELEASED AT THE SPAWN, and again in the
|
|
17
|
+
* `finally` for a turn that never spawned — release is idempotent, so
|
|
18
|
+
* doing both is the normal case (admission.mjs).
|
|
19
|
+
* · THE CAP IS ARMED AT THE SPAWN, not at entry: time spent waiting for a
|
|
20
|
+
* checkout lock is not a wedged CLI. It SIGKILLs the CHILD and never its
|
|
21
|
+
* group — the rule teardown keeps: a read-only turn starts no server, so
|
|
22
|
+
* there is nothing behind it worth signalling and everything to lose by
|
|
23
|
+
* signalling somebody else's.
|
|
24
|
+
* · THE CAP RESOLVES THE WAIT ITSELF rather than waiting for `close` after
|
|
25
|
+
* the kill: a SIGKILLed process whose stdio a grandchild still holds can be
|
|
26
|
+
* slow to emit `close`, or never emit it, and the caller is holding a
|
|
27
|
+
* claimed job (or a place's writer lock) open behind this promise.
|
|
28
|
+
* · THE TURN'S TRANSCRIPT IS REMOVED ON EVERY EXIT — settled, wedged or
|
|
29
|
+
* thrown. `claude -p` writes `~/.claude/projects/<munged-cwd>/<id>.jsonl`,
|
|
30
|
+
* and the CLI's `--continue` is cwd-keyed: left in the checkout it displaces
|
|
31
|
+
* the operator's own conversation from the `+` adopt menu and is what their
|
|
32
|
+
* `claude --continue` resumes; left in an agent's worktree it is what the
|
|
33
|
+
* agent's next turn resumes, under build permissions. The id is the one the
|
|
34
|
+
* CLI reports at `system.init`, so the file is ours by name.
|
|
35
|
+
*
|
|
36
|
+
* WHAT STAYS WITH EACH LANE is policy, never plumbing: its lock, its prompt,
|
|
37
|
+
* profile and brain, its usage callback (set or accumulate is a per-lane
|
|
38
|
+
* choice), its cap length, its narration, and the words it settles with.
|
|
39
|
+
*/
|
|
40
|
+
import { removeTranscriptSoon } from './runtimeCapabilities.mjs';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `runTurn(turnArgs)` under a `capMs` cap. Resolves
|
|
44
|
+
* `{ out, wedged, spawned, sessionId }`: `out` is the turn's answer, or `''`
|
|
45
|
+
* when the cap fired (`wedged`); `spawned` says a CLI process existed; the
|
|
46
|
+
* session id is the conversation the CLI named, or null. A turn that throws
|
|
47
|
+
* rethrows here, after the same cleanup.
|
|
48
|
+
*
|
|
49
|
+
* `onSpawn` and `onInit` are the caller's own hooks, run after this file's
|
|
50
|
+
* bookkeeping — which is why they ride beside `turnArgs` and never inside it.
|
|
51
|
+
* `removeTranscript` is a test's.
|
|
52
|
+
*/
|
|
53
|
+
export async function runCappedTurn({
|
|
54
|
+
runTurn,
|
|
55
|
+
turnArgs,
|
|
56
|
+
capMs,
|
|
57
|
+
workChildren,
|
|
58
|
+
releaseSlot = () => {},
|
|
59
|
+
transcriptCwd,
|
|
60
|
+
onSpawn = () => {},
|
|
61
|
+
onInit = () => {},
|
|
62
|
+
removeTranscript = removeTranscriptSoon,
|
|
63
|
+
}) {
|
|
64
|
+
// Wiring mistakes, refused before anything spawns: a hook inside turnArgs
|
|
65
|
+
// would be silently replaced below, and a missing cap would fire at once.
|
|
66
|
+
if (turnArgs && ('onSpawn' in turnArgs || 'onInit' in turnArgs))
|
|
67
|
+
throw new TypeError('runCappedTurn: pass onSpawn/onInit beside turnArgs, never inside it');
|
|
68
|
+
if (!(Number.isFinite(capMs) && capMs > 0)) throw new TypeError('runCappedTurn: capMs must be a positive number');
|
|
69
|
+
let child = null;
|
|
70
|
+
let timer = null;
|
|
71
|
+
let wedged = false;
|
|
72
|
+
let sessionId = null;
|
|
73
|
+
let stopWaiting = () => {};
|
|
74
|
+
const capped = new Promise((resolve) => {
|
|
75
|
+
stopWaiting = resolve;
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const turn = runTurn({
|
|
79
|
+
...turnArgs,
|
|
80
|
+
onInit: (i) => {
|
|
81
|
+
if (typeof i?.sessionId === 'string' && i.sessionId.trim()) sessionId = i.sessionId.trim();
|
|
82
|
+
onInit(i);
|
|
83
|
+
},
|
|
84
|
+
onSpawn: (ch) => {
|
|
85
|
+
child = ch;
|
|
86
|
+
workChildren.set(ch, null);
|
|
87
|
+
releaseSlot();
|
|
88
|
+
timer = setTimeout(() => {
|
|
89
|
+
wedged = true;
|
|
90
|
+
try {
|
|
91
|
+
ch.kill('SIGKILL');
|
|
92
|
+
} catch {
|
|
93
|
+
/* already gone */
|
|
94
|
+
}
|
|
95
|
+
stopWaiting('');
|
|
96
|
+
}, capMs);
|
|
97
|
+
timer.unref?.();
|
|
98
|
+
onSpawn(ch);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const out = await Promise.race([turn, capped]);
|
|
102
|
+
return { out, wedged, spawned: child !== null, sessionId };
|
|
103
|
+
} finally {
|
|
104
|
+
if (timer) clearTimeout(timer);
|
|
105
|
+
if (child) workChildren.delete(child);
|
|
106
|
+
releaseSlot();
|
|
107
|
+
if (sessionId) removeTranscript(transcriptCwd, sessionId);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/bin/lib/workChildren.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* on this box" belong together — and apart from any one lane.
|
|
11
11
|
*/
|
|
12
12
|
import { createAdmission } from './admission.mjs';
|
|
13
|
+
import { killGroup } from './processGroup.mjs';
|
|
13
14
|
|
|
14
15
|
export function createWorkChildren({ extraLiveTurns }) {
|
|
15
16
|
/**
|
|
@@ -54,11 +55,10 @@ export function createWorkChildren({ extraLiveTurns }) {
|
|
|
54
55
|
const groupKillChildren = new Set();
|
|
55
56
|
const shutdownWork = () => {
|
|
56
57
|
for (const [ch] of workChildren) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// A group that has already gone, or a pid that is no longer a leader.
|
|
58
|
+
// A group that has already gone, or a pid that is no longer a leader,
|
|
59
|
+
// falls back to the child (processGroup.mjs).
|
|
60
|
+
if (groupKillChildren.has(ch)) killGroup(ch, 'SIGTERM');
|
|
61
|
+
else {
|
|
62
62
|
try {
|
|
63
63
|
ch.kill('SIGTERM');
|
|
64
64
|
} catch {
|