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
|
@@ -12,53 +12,34 @@
|
|
|
12
12
|
* the credential (workSessionTokens.mjs), the runtime pin (workSessionRuntime
|
|
13
13
|
* .mjs), the adopt carry (workAdoptCarry.mjs), attachments
|
|
14
14
|
* (workAttachments.mjs), the live line (workNarration.mjs), the tool log
|
|
15
|
-
* (workToolLog.mjs)
|
|
15
|
+
* (workToolLog.mjs), the command audit (workCommandAudit.mjs) and — pure —
|
|
16
|
+
* what the turn IS, plain, plan, capture or build, with its prompt pair
|
|
17
|
+
* (workTurnShape.mjs, audit 2026-10-10 F159).
|
|
16
18
|
*/
|
|
17
19
|
import { existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
18
|
-
import { git
|
|
20
|
+
import { git } from './git.mjs';
|
|
21
|
+
import { measureRepositoryBaseline } from './repositoryBaseline.mjs';
|
|
22
|
+
import { REPOSITORY_BASELINE_PARAGRAPH } from './prompts.mjs';
|
|
19
23
|
import { processStartTime } from './procRegistry.mjs';
|
|
20
24
|
import { c, note, ok, warn } from './ui.mjs';
|
|
21
25
|
import { mcpFor } from './claude.mjs';
|
|
22
26
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
23
27
|
import { addUsage } from './turnUsage.mjs';
|
|
24
|
-
import {
|
|
25
|
-
SYSTEM_WORK,
|
|
26
|
-
SYSTEM_CAPTURE,
|
|
27
|
-
WORK_TURN_KICKOFF,
|
|
28
|
-
CAPTURE_TURN_KICKOFF,
|
|
29
|
-
SYSTEM_WORK_PLAIN,
|
|
30
|
-
WORK_TURN_KICKOFF_PLAIN,
|
|
31
|
-
withProjectContext,
|
|
32
|
-
} from './prompts.mjs';
|
|
33
28
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
34
29
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
35
|
-
import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
|
|
36
30
|
import { isTerminalSessionLive } from './claudeSessions.mjs';
|
|
37
31
|
import { isAgyConversationLive } from './agySessions.mjs';
|
|
32
|
+
import { adoptFence } from './localSessions.mjs';
|
|
38
33
|
import { resumeRetriesFresh } from './resumeLost.mjs';
|
|
39
34
|
import { turnLockedByLivePid } from './turnLock.mjs';
|
|
35
|
+
import { killGroup } from './processGroup.mjs';
|
|
36
|
+
import { printable } from './printable.mjs';
|
|
40
37
|
import { brainFor } from './workBrain.mjs';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
38
|
+
import { AGY_CONV_RE, CLAUDE_SESSION_RE, CODEX_THREAD_RE, RESUME_MARKERS } from './conversationIds.mjs';
|
|
39
|
+
import { sessionTurnKickoff, sessionTurnShape, sessionTurnSystem } from './workTurnShape.mjs';
|
|
43
40
|
import { createToolLog } from './workToolLog.mjs';
|
|
44
41
|
import { createCommandAudit } from './workCommandAudit.mjs';
|
|
45
|
-
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, withRelay } from './codexRelay.mjs';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* THE ONE SENTENCE A PLAN TURN'S SYSTEM PROMPT GAINS (0.97.0), composed here
|
|
49
|
-
* rather than in prompts.mjs because it is a MODIFIER on whichever contract
|
|
50
|
-
* the tab runs, not a contract of its own.
|
|
51
|
-
*
|
|
52
|
-
* The enforcement is not this sentence — it is `--permission-mode plan`, under
|
|
53
|
-
* which the CLI itself refuses every write (claudePosture.mjs, PLAN_MODE_PERM). What
|
|
54
|
-
* the sentence buys is the ANSWER: the plain contract above it says "edit
|
|
55
|
-
* freely, commit", and a turn told only that would spend itself discovering
|
|
56
|
-
* refusals. It also says not to reach for `ExitPlanMode` (measured: disabled
|
|
57
|
-
* under `-p`, and the probe turn then asked the person to leave plan mode
|
|
58
|
-
* themselves) — leaving plan mode is the person's switch in the tab.
|
|
59
|
-
*/
|
|
60
|
-
export const PLAN_TURN_SENTENCE =
|
|
61
|
-
'THIS IS A PLANNING TURN: the person switched this tab to plan mode, so read what you need, decide, and answer with the plan itself as your reply — change nothing (the CLI refuses every write this turn, whatever the mechanics above say about editing, committing or artifacts), and do not try to leave plan mode; they switch it off in the tab when they want the plan carried out.';
|
|
42
|
+
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, SESSION_ANSWER_MAX, withRelay } from './codexRelay.mjs';
|
|
62
43
|
|
|
63
44
|
/**
|
|
64
45
|
* HOW LONG, AND HOW MANY, SETTLED TURNS THE LANE REMEMBERS (2026-09-29) — see
|
|
@@ -70,6 +51,7 @@ export const SETTLED_HERE_MAX = 500;
|
|
|
70
51
|
|
|
71
52
|
export function createWorkSessionTurns({
|
|
72
53
|
repoRoot,
|
|
54
|
+
baseRef = () => null,
|
|
73
55
|
baseDir,
|
|
74
56
|
getMcpUrl,
|
|
75
57
|
getArtifactsAccepted,
|
|
@@ -77,7 +59,7 @@ export function createWorkSessionTurns({
|
|
|
77
59
|
inPlace,
|
|
78
60
|
workChildren,
|
|
79
61
|
workAttempts,
|
|
80
|
-
|
|
62
|
+
notePlace,
|
|
81
63
|
placeOf,
|
|
82
64
|
placeWtFor,
|
|
83
65
|
sessionMetaPath,
|
|
@@ -106,6 +88,31 @@ export function createWorkSessionTurns({
|
|
|
106
88
|
const workAnswering = new Set(); // turn ids currently queued/running here
|
|
107
89
|
const MAX_WORK_TRIES = 3;
|
|
108
90
|
|
|
91
|
+
/**
|
|
92
|
+
* INTERRUPT NOW (2026-10-13) — each turn this lane has taken, with the CLI
|
|
93
|
+
* children it spawned and whether a person asked to stop it. A stop kills
|
|
94
|
+
* each child's PROCESS GROUP: the CLI is spawned detached, so everything it
|
|
95
|
+
* started (a test run, a dev server) is in that group and goes with it —
|
|
96
|
+
* which is what interrupting a prompt means, and what Pause does too. A
|
|
97
|
+
* turn asked to stop before it spawned never spawns.
|
|
98
|
+
*/
|
|
99
|
+
const runs = new Map(); // turnId -> { interrupted, spawned: [] }
|
|
100
|
+
/** Turns this process answered `ran: false` for, so the roster's next read
|
|
101
|
+
* (made before that settle landed) is not answered again at once. */
|
|
102
|
+
const notRunningSaid = new Map(); // turnId -> when
|
|
103
|
+
/**
|
|
104
|
+
* Sessions whose run this lane just stopped. The next turn there is the one
|
|
105
|
+
* sent to interrupt it, and the roster that hands it over can have been read
|
|
106
|
+
* before the stop's settle landed — so the note does not wait on the
|
|
107
|
+
* server's `interrupted` alone. Taken once, and only for a little while.
|
|
108
|
+
*/
|
|
109
|
+
const stoppedIn = new Map(); // sessionId -> when
|
|
110
|
+
const tookStop = (sessionId) => {
|
|
111
|
+
const when = stoppedIn.get(sessionId);
|
|
112
|
+
stoppedIn.delete(sessionId);
|
|
113
|
+
return when != null && now() - when < 120_000;
|
|
114
|
+
};
|
|
115
|
+
|
|
109
116
|
/**
|
|
110
117
|
* A TURN THIS LANE SETTLED IS NEVER RUN AGAIN (2026-09-29).
|
|
111
118
|
*
|
|
@@ -153,10 +160,14 @@ export function createWorkSessionTurns({
|
|
|
153
160
|
* and it is released in the turn's outer `finally`, after its settle.
|
|
154
161
|
*/
|
|
155
162
|
const startingIn = new Set();
|
|
163
|
+
// A roster can contain a turn read before its stop was requested. Never
|
|
164
|
+
// start that stale job after acknowledging the same roster's interrupt.
|
|
165
|
+
let rosterInterrupts = new Set();
|
|
156
166
|
|
|
157
167
|
const processWorkTurns = (jobs) => {
|
|
158
168
|
for (const job of jobs ?? []) {
|
|
159
169
|
if (!job || typeof job.id !== 'string' || !job.body || !job.sessionId) continue;
|
|
170
|
+
if (rosterInterrupts.has(job.id)) continue;
|
|
160
171
|
if (workAnswering.has(job.id)) continue;
|
|
161
172
|
// Settled by this lane already: a roster read from before the settle
|
|
162
173
|
// landed. See `settledHere`.
|
|
@@ -189,20 +200,33 @@ export function createWorkSessionTurns({
|
|
|
189
200
|
const releaseSlot = admit.reserve();
|
|
190
201
|
lastDeferSaid.delete(job.sessionId);
|
|
191
202
|
workAnswering.add(job.id);
|
|
203
|
+
const run = { interrupted: false, spawned: [] };
|
|
204
|
+
runs.set(job.id, run);
|
|
205
|
+
/** The execution generation the server handed, echoed on every settle
|
|
206
|
+
* so the server can tell this execution's answer from an older one's. */
|
|
207
|
+
const attempt = Number.isInteger(job.attempt) ? { attempt: job.attempt } : {};
|
|
208
|
+
/** The CLI, unless a person already asked to stop this turn — then no
|
|
209
|
+
* run starts, the fresh retry after an empty resume included. */
|
|
210
|
+
const runCli = (args) => (run.interrupted ? Promise.resolve('') : runTurn(args));
|
|
192
211
|
const place = job.place || job.sessionId;
|
|
193
212
|
/**
|
|
194
|
-
* VALIDATED AT THE TRUST BOUNDARY,
|
|
195
|
-
* the roster's map
|
|
196
|
-
*
|
|
213
|
+
* VALIDATED AT THE TRUST BOUNDARY, through the same door `learnPlaces`
|
|
214
|
+
* uses for the roster's map (`notePlace`, workPlaces.mjs) — in the same
|
|
215
|
+
* reconcile tick, BEFORE the preview jobs and the sweep read it. An
|
|
197
216
|
* unchecked value stored here reaches every `placeDir` consumer: the
|
|
198
217
|
* turn is spawned in it, `burstListeners` measures it, and a share would
|
|
199
218
|
* publicly tunnel a directory OUTSIDE the checkout — the exact traversal
|
|
200
219
|
* the preview feature's port attribution exists to prevent. Settled out
|
|
201
220
|
* loud rather than skipped, because a silently dropped turn strands the
|
|
202
221
|
* tab for the server's whole expiry window.
|
|
222
|
+
*
|
|
223
|
+
* Stored on success: remembered for every other beat — the sweep, ship,
|
|
224
|
+
* the preview re-check — so they all ask the same directory this turn
|
|
225
|
+
* runs in.
|
|
203
226
|
*/
|
|
204
|
-
if (
|
|
227
|
+
if (!notePlace(job.sessionId, place)) {
|
|
205
228
|
void settleTurn(job.id, {
|
|
229
|
+
...attempt,
|
|
206
230
|
ok: false,
|
|
207
231
|
answer:
|
|
208
232
|
'the server named a working directory this machine refuses to use — close and reopen the tab, then send the message again',
|
|
@@ -210,9 +234,6 @@ export function createWorkSessionTurns({
|
|
|
210
234
|
releaseSlot();
|
|
211
235
|
continue;
|
|
212
236
|
}
|
|
213
|
-
// Remembered for every other beat — the sweep, ship, the preview
|
|
214
|
-
// re-check — so they all ask the same directory this turn runs in.
|
|
215
|
-
sessionPlaces.set(job.sessionId, place);
|
|
216
237
|
// A READER: other turns in this place run alongside it. See `inPlace`.
|
|
217
238
|
inPlace(place, false, async () => {
|
|
218
239
|
/** This turn's artifact snapshot — set once the place is resolved and
|
|
@@ -240,6 +261,20 @@ export function createWorkSessionTurns({
|
|
|
240
261
|
* out here beside `usage`, and spread wherever it is. Last write wins
|
|
241
262
|
* across a fresh retry: that spawn is the one that answered. */
|
|
242
263
|
let model = null;
|
|
264
|
+
/**
|
|
265
|
+
* WHAT THIS TURN SPENT AND DID, as every settle after a CLI may have
|
|
266
|
+
* run reports it (audit 2026-10-10, F164): its tool log when it has
|
|
267
|
+
* rows, its usage and its model, each only when measured. ONE
|
|
268
|
+
* null-safe spread for the empty adoption, the answer and the catch —
|
|
269
|
+
* the catch's own copy is where the 2026-09-26 ReferenceError lived,
|
|
270
|
+
* and a field added to one copy and not the others drops exactly when
|
|
271
|
+
* a turn fails.
|
|
272
|
+
*/
|
|
273
|
+
const spent = () => ({
|
|
274
|
+
...(toolLog != null ? { tools: toolLog } : {}),
|
|
275
|
+
...(usage ? { usage } : {}),
|
|
276
|
+
...(model ? { model } : {}),
|
|
277
|
+
});
|
|
243
278
|
/** The `startingIn` key this turn claimed — released in the outer
|
|
244
279
|
* `finally`, and only by the turn that took it. */
|
|
245
280
|
let startClaim = null;
|
|
@@ -249,13 +284,17 @@ export function createWorkSessionTurns({
|
|
|
249
284
|
// Out of local tries: SETTLE, don't skip — a silently skipped turn
|
|
250
285
|
// strands the tab for the server's whole 24h expiry window.
|
|
251
286
|
await settleTurn(job.id, {
|
|
287
|
+
...attempt,
|
|
252
288
|
ok: false,
|
|
253
289
|
answer: `the turn failed ${tries} times on this machine — check the daemon log, then send the message again`,
|
|
254
290
|
});
|
|
255
291
|
return;
|
|
256
292
|
}
|
|
293
|
+
// A person's name and their tab's, both member-written, printed on
|
|
294
|
+
// the operator's console: scrubbed (2026-10-10, the audit's F097;
|
|
295
|
+
// see printable.mjs).
|
|
257
296
|
note(
|
|
258
|
-
`${c.cyan('tab')} ${c.dim(`— ${job.askedByName || 'the owner'} in "${job.sessionName || 'a session'}"`)}`
|
|
297
|
+
`${c.cyan('tab')} ${c.dim(`— ${printable(job.askedByName) || 'the owner'} in "${printable(job.sessionName) || 'a session'}"`)}`
|
|
259
298
|
);
|
|
260
299
|
// ── ADOPTION: a tab born from a TERMINAL session ────────────────
|
|
261
300
|
// The server sends `adopt {id, cwd}` only while the session has no
|
|
@@ -275,6 +314,7 @@ export function createWorkSessionTurns({
|
|
|
275
314
|
!/^[0-9a-f][0-9a-f-]{6,62}$/i.test(job.adopt.id)
|
|
276
315
|
) {
|
|
277
316
|
await settleTurn(job.id, {
|
|
317
|
+
...attempt,
|
|
278
318
|
ok: false,
|
|
279
319
|
answer: 'that terminal session id is not one this machine can resume',
|
|
280
320
|
});
|
|
@@ -289,6 +329,7 @@ export function createWorkSessionTurns({
|
|
|
289
329
|
}
|
|
290
330
|
if (!srcCwd) {
|
|
291
331
|
await settleTurn(job.id, {
|
|
332
|
+
...attempt,
|
|
292
333
|
ok: false,
|
|
293
334
|
answer: "the terminal session's directory no longer exists on the machine",
|
|
294
335
|
});
|
|
@@ -297,29 +338,20 @@ export function createWorkSessionTurns({
|
|
|
297
338
|
// Inside the repo, outside the daemon's own worktrees: an adopt
|
|
298
339
|
// source is a HUMAN's checkout, and one of our directories showing
|
|
299
340
|
// up here means a stale or confused offer, not a session to fork.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
realRoot = realpathSync(repoRoot);
|
|
306
|
-
} catch {
|
|
307
|
-
/* keep the literal path */
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
realBase = realpathSync(baseDir);
|
|
311
|
-
} catch {
|
|
312
|
-
/* keep the literal path */
|
|
313
|
-
}
|
|
314
|
-
if (!under(srcCwd, realRoot)) {
|
|
341
|
+
// The SAME fence the offer was made through (`adoptFence`,
|
|
342
|
+
// localSessions.mjs), so the two doors cannot disagree.
|
|
343
|
+
const fenced = adoptFence({ repoRoot, excludeDirs: [baseDir] }).check(srcCwd);
|
|
344
|
+
if (fenced === 'outside') {
|
|
315
345
|
await settleTurn(job.id, {
|
|
346
|
+
...attempt,
|
|
316
347
|
ok: false,
|
|
317
348
|
answer: "the terminal session's directory is outside this project's repository",
|
|
318
349
|
});
|
|
319
350
|
return;
|
|
320
351
|
}
|
|
321
|
-
if (
|
|
352
|
+
if (fenced === 'ours') {
|
|
322
353
|
await settleTurn(job.id, {
|
|
354
|
+
...attempt,
|
|
323
355
|
ok: false,
|
|
324
356
|
answer:
|
|
325
357
|
"that directory is one of the daemon's own worktrees — its session is already a tab, not something to adopt",
|
|
@@ -330,6 +362,7 @@ export function createWorkSessionTurns({
|
|
|
330
362
|
srcHead = git(['rev-parse', 'HEAD'], srcCwd);
|
|
331
363
|
} catch {
|
|
332
364
|
await settleTurn(job.id, {
|
|
365
|
+
...attempt,
|
|
333
366
|
ok: false,
|
|
334
367
|
answer:
|
|
335
368
|
"the terminal session's directory is not a usable git checkout (no HEAD to branch from)",
|
|
@@ -347,6 +380,7 @@ export function createWorkSessionTurns({
|
|
|
347
380
|
: isTerminalSessionLive(job.adopt.id);
|
|
348
381
|
if (adoptLive) {
|
|
349
382
|
await settleTurn(job.id, {
|
|
383
|
+
...attempt,
|
|
350
384
|
ok: false,
|
|
351
385
|
answer:
|
|
352
386
|
'That terminal session is still open on the machine — close it there first, then adopt.',
|
|
@@ -363,6 +397,7 @@ export function createWorkSessionTurns({
|
|
|
363
397
|
const dir = placeWtFor(place, adopting ? srcHead : undefined);
|
|
364
398
|
if (!dir) {
|
|
365
399
|
await settleTurn(job.id, {
|
|
400
|
+
...attempt,
|
|
366
401
|
ok: false,
|
|
367
402
|
answer:
|
|
368
403
|
'the session worktree could not be opened on the machine — check the daemon log',
|
|
@@ -381,7 +416,7 @@ export function createWorkSessionTurns({
|
|
|
381
416
|
const startKey = lockPath || dir.wt;
|
|
382
417
|
if (startingIn.has(startKey) || turnLockedByLivePid(lockPath)) {
|
|
383
418
|
warn(
|
|
384
|
-
`a turn is already running in "${job.sessionName || job.sessionId}" — waiting for it to finish`
|
|
419
|
+
`a turn is already running in "${printable(job.sessionName) || job.sessionId}" — waiting for it to finish`
|
|
385
420
|
);
|
|
386
421
|
return;
|
|
387
422
|
}
|
|
@@ -394,12 +429,17 @@ export function createWorkSessionTurns({
|
|
|
394
429
|
// What the artifact directory held BEFORE this turn — the capture chat
|
|
395
430
|
// excepted: it runs read-only, is never told about artifacts, and
|
|
396
431
|
// whatever sibling tabs wrote in this shared place is theirs.
|
|
397
|
-
if (job.capture !== true)
|
|
432
|
+
if (job.capture !== true) {
|
|
433
|
+
artifactScan = { dir: dir.wt, before: beforeArtifacts(dir.wt), reported: false };
|
|
434
|
+
artifactScan.instructions = getArtifactsAccepted()
|
|
435
|
+
? await artifacts.prepare?.({ placeDir: dir.wt, before: artifactScan.before, sessionId: job.sessionId }) ?? '' : '';
|
|
436
|
+
}
|
|
398
437
|
const rt = sessionRuntime(dir.wt, job.runtime || null, job.sessionId);
|
|
399
438
|
if (rt.mismatch) {
|
|
400
439
|
// Something upstream changed this tab's identity mid-life. A held
|
|
401
440
|
// context must never be answered by a different brain — say so.
|
|
402
441
|
await settleTurn(job.id, {
|
|
442
|
+
...attempt,
|
|
403
443
|
ok: false,
|
|
404
444
|
answer: `this tab is pinned to ${rt.mismatch.pin} but the server says it is a ${rt.mismatch.runtime} tab — reopen a new tab`,
|
|
405
445
|
});
|
|
@@ -407,6 +447,7 @@ export function createWorkSessionTurns({
|
|
|
407
447
|
}
|
|
408
448
|
if (rt.missing) {
|
|
409
449
|
await settleTurn(job.id, {
|
|
450
|
+
...attempt,
|
|
410
451
|
ok: false,
|
|
411
452
|
answer: `this session runs on ${rt.missing}, which is no longer installed on the machine — reinstall it, or open a new tab`,
|
|
412
453
|
});
|
|
@@ -419,6 +460,7 @@ export function createWorkSessionTurns({
|
|
|
419
460
|
// whole control plane rides one). An honest sentence beats the
|
|
420
461
|
// mcpFor throw this used to crash into every turn.
|
|
421
462
|
await settleTurn(job.id, {
|
|
463
|
+
...attempt,
|
|
422
464
|
ok: false,
|
|
423
465
|
answer: `this session runs on ${rt.unsupported}, which cannot drive a Terminal tab on this machine — open a new tab`,
|
|
424
466
|
});
|
|
@@ -426,49 +468,30 @@ export function createWorkSessionTurns({
|
|
|
426
468
|
}
|
|
427
469
|
if (!rt.id) {
|
|
428
470
|
await settleTurn(job.id, {
|
|
471
|
+
...attempt,
|
|
429
472
|
ok: false,
|
|
430
473
|
answer:
|
|
431
474
|
'No coding CLI is installed on the machine — install Claude Code (or another supported CLI), then send the message again',
|
|
432
475
|
});
|
|
433
476
|
return;
|
|
434
477
|
}
|
|
435
|
-
if (adopting && rt.id !== 'claude' && rt.id !== 'antigravity') {
|
|
436
|
-
// An adopt id names a conversation in ITS OWN CLI's store: claude
|
|
437
|
-
// forks it (--resume --fork-session), agy moves it
|
|
438
|
-
// (--conversation). Codex has no adoptable store yet, and its
|
|
439
|
-
// args builder backstops this with a loud throw — but a sentence
|
|
440
|
-
// here beats a stack there.
|
|
441
|
-
await settleTurn(job.id, {
|
|
442
|
-
ok: false,
|
|
443
|
-
answer:
|
|
444
|
-
'adopting this terminal session needs its own CLI on the machine — install it, then try again',
|
|
445
|
-
});
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
478
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
479
|
+
* WHAT THIS TURN IS — plain, plan, capture or build — decided ONCE,
|
|
480
|
+
* off the runtime's row and the job's own switches
|
|
481
|
+
* (workTurnShape.mjs, audit 2026-10-10 F159); everything below
|
|
482
|
+
* reads it instead of re-deciding. Its refusal (an adoption this
|
|
483
|
+
* CLI cannot take, then plan mode on a CLI that cannot spell it)
|
|
484
|
+
* settles here, in its words, in the order the lane always said
|
|
485
|
+
* them.
|
|
455
486
|
*/
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
await settleTurn(job.id, {
|
|
459
|
-
ok: false,
|
|
460
|
-
answer: `plan mode runs on Claude Code only, and this tab is ${rt.id} — turn plan off in the tab, then send the message again`,
|
|
461
|
-
});
|
|
487
|
+
const shape = sessionTurnShape({ runtime: rt.id, adopting, job });
|
|
488
|
+
if (shape.refusal) {
|
|
489
|
+
await settleTurn(job.id, { ...attempt, ok: false, answer: shape.refusal });
|
|
462
490
|
return;
|
|
463
491
|
}
|
|
464
|
-
// A PLAIN tab (agy) mounts no MCP: no credential to mint
|
|
465
|
-
// to write. The trade is stated in SYSTEM_WORK_PLAIN — no cards, no
|
|
466
|
-
// streaming — and the honesty survives on the existing rails: the
|
|
467
|
-
// answer lands via work-turn-done, the rail says "no card yet", and
|
|
468
|
-
// ship-time reconciliation books every branch commit.
|
|
469
|
-
const plainTab = rt.id === 'antigravity';
|
|
492
|
+
// A PLAIN tab (agy) mounts no MCP: no credential to mint.
|
|
470
493
|
let mint = null;
|
|
471
|
-
if (!
|
|
494
|
+
if (!shape.plain) {
|
|
472
495
|
mint = await mintWorkToken(job.sessionId);
|
|
473
496
|
if (!mint) mint = await mintWorkToken(job.sessionId, true); // one transient blip ≠ a dead turn
|
|
474
497
|
// Another daemon on this credential holds the session. Return
|
|
@@ -482,6 +505,7 @@ export function createWorkSessionTurns({
|
|
|
482
505
|
}
|
|
483
506
|
if (mint?.gone) {
|
|
484
507
|
await settleTurn(job.id, {
|
|
508
|
+
...attempt,
|
|
485
509
|
ok: false,
|
|
486
510
|
answer:
|
|
487
511
|
'Flowviant no longer offers this session to this machine — the tab may have been closed or moved',
|
|
@@ -490,6 +514,7 @@ export function createWorkSessionTurns({
|
|
|
490
514
|
}
|
|
491
515
|
if (!mint?.token) {
|
|
492
516
|
await settleTurn(job.id, {
|
|
517
|
+
...attempt,
|
|
493
518
|
ok: false,
|
|
494
519
|
answer:
|
|
495
520
|
'the machine could not mint a session credential from Flowviant — check its connection, then send the message again',
|
|
@@ -528,11 +553,11 @@ export function createWorkSessionTurns({
|
|
|
528
553
|
*/
|
|
529
554
|
let claudeResumeId = null;
|
|
530
555
|
if (rt.id === 'claude') {
|
|
531
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
556
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
|
|
532
557
|
if (convMarker && existsSync(convMarker)) {
|
|
533
558
|
try {
|
|
534
559
|
const v = readFileSync(convMarker, 'utf8').trim();
|
|
535
|
-
if (
|
|
560
|
+
if (CLAUDE_SESSION_RE.test(v)) claudeResumeId = v;
|
|
536
561
|
} catch {
|
|
537
562
|
/* unreadable marker — run fresh */
|
|
538
563
|
}
|
|
@@ -540,7 +565,7 @@ export function createWorkSessionTurns({
|
|
|
540
565
|
}
|
|
541
566
|
let codexResumeId = null;
|
|
542
567
|
if (rt.id === 'codex') {
|
|
543
|
-
const threadMarker = sessionMetaPath(dir.wt,
|
|
568
|
+
const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
|
|
544
569
|
if (threadMarker && existsSync(threadMarker)) {
|
|
545
570
|
try {
|
|
546
571
|
const v = readFileSync(threadMarker, 'utf8').trim();
|
|
@@ -558,7 +583,7 @@ export function createWorkSessionTurns({
|
|
|
558
583
|
// dispatch sharing the machine could overwrite that between turns.
|
|
559
584
|
let agyConvId = null;
|
|
560
585
|
if (rt.id === 'antigravity' && !adopting) {
|
|
561
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
586
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
|
|
562
587
|
if (convMarker && existsSync(convMarker)) {
|
|
563
588
|
try {
|
|
564
589
|
const v = readFileSync(convMarker, 'utf8').trim();
|
|
@@ -605,17 +630,13 @@ export function createWorkSessionTurns({
|
|
|
605
630
|
const adoptNote = adopting
|
|
606
631
|
? '[ADOPTED SESSION — this conversation was brought in from a terminal. Begin your reply with a 2-3 sentence recap of where it left off and what state carried over, then answer the message.]'
|
|
607
632
|
: '';
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const mcp =
|
|
616
|
-
plainTab || planTurn
|
|
617
|
-
? { args: [], env: null, dir: null }
|
|
618
|
-
: mcpFor(rt.id, mint.token, getMcpUrl());
|
|
633
|
+
// Mounted only where the shape says (a plain tab and a plan turn run
|
|
634
|
+
// without one — see workTurnShape.mjs). A plan turn's credential is
|
|
635
|
+
// still minted above: its lease answers (held elsewhere, gone) are
|
|
636
|
+
// about the SESSION, and a plan turn needs them as much as any other.
|
|
637
|
+
const mcp = shape.mountMcp
|
|
638
|
+
? mcpFor(rt.id, mint.token, getMcpUrl())
|
|
639
|
+
: { args: [], env: null, dir: null };
|
|
619
640
|
// The tab's model/effort, if it named any. Spread into turnArgs so
|
|
620
641
|
// BOTH runTurn calls below carry it — the retry is the same turn on
|
|
621
642
|
// the same brain, not a quieter second opinion.
|
|
@@ -637,7 +658,7 @@ export function createWorkSessionTurns({
|
|
|
637
658
|
toolLog = turnLog.toolLog;
|
|
638
659
|
const { pushToolEvent } = turnLog;
|
|
639
660
|
const narrator = makeNarrator(job.sessionId, job.id, () =>
|
|
640
|
-
toolLog
|
|
661
|
+
toolLog
|
|
641
662
|
);
|
|
642
663
|
|
|
643
664
|
|
|
@@ -661,42 +682,22 @@ export function createWorkSessionTurns({
|
|
|
661
682
|
const message = [job.body, filesNote, adoptNote, carryNote]
|
|
662
683
|
.filter(Boolean)
|
|
663
684
|
.join('\n\n');
|
|
664
|
-
// A CAPTURE chat (the board's New task conversation): its own
|
|
665
|
-
// system prompt, its own kickoff, and the scratch planner's
|
|
666
|
-
// READ-ONLY permission profile — the prompt says stage-never-file
|
|
667
|
-
// and the profile is what makes "read-only" true rather than
|
|
668
|
-
// asserted. Server-flagged per job; a server too old to flag it
|
|
669
|
-
// simply runs an ordinary tab, which the web's version floor
|
|
670
|
-
// prevents ever being offered.
|
|
671
|
-
const captureTab = job.capture === true;
|
|
672
685
|
const turnArgs = {
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
: WORK_TURN_KICKOFF({
|
|
689
|
-
sessionId: job.sessionId,
|
|
690
|
-
sessionName: job.sessionName,
|
|
691
|
-
message,
|
|
692
|
-
askedByName: job.askedByName,
|
|
693
|
-
}),
|
|
694
|
-
// ONE PROFILE (turnProfile.mjs): the capture chat's read-only
|
|
695
|
-
// `plan` list; or a plan turn's `plan-mode` — `--permission-mode
|
|
696
|
-
// plan` INSTEAD of the build posture, never beside
|
|
697
|
-
// `--dangerously-skip-permissions`, which silently wins
|
|
698
|
-
// (claudePosture.mjs, PLAN_MODE_PERM); else the build posture.
|
|
699
|
-
profile: captureTab ? 'plan' : planTurn ? 'plan-mode' : 'build',
|
|
686
|
+
// The kickoff under the turn's contract: a plain tab (or a plan
|
|
687
|
+
// turn) has no tools to name and no session id to pass, and asks
|
|
688
|
+
// for one complete report instead of a stream; a capture chat
|
|
689
|
+
// restates its posture.
|
|
690
|
+
prompt: sessionTurnKickoff(shape, {
|
|
691
|
+
sessionId: job.sessionId,
|
|
692
|
+
sessionName: job.sessionName,
|
|
693
|
+
message,
|
|
694
|
+
askedByName: job.askedByName,
|
|
695
|
+
// INTERRUPT NOW: the previous run was stopped to send this.
|
|
696
|
+
interrupted: tookStop(job.sessionId) || job.interrupted === true,
|
|
697
|
+
}),
|
|
698
|
+
// ONE PROFILE (turnProfile.mjs), the shape's: `plan` for the
|
|
699
|
+
// capture chat, `plan-mode` for a plan turn, else `build`.
|
|
700
|
+
profile: shape.profile,
|
|
700
701
|
// The adopt turn resumes the TERMINAL conversation by forking it
|
|
701
702
|
// into this cwd (claude: --resume <id> --fork-session). After it
|
|
702
703
|
// speaks once, the fork lives natively here and turn 2+ is the
|
|
@@ -708,19 +709,17 @@ export function createWorkSessionTurns({
|
|
|
708
709
|
// library that synced a second ago is in THIS turn.
|
|
709
710
|
...(() => {
|
|
710
711
|
const knowledgeDir = knowledgeDirFor(repoRoot);
|
|
712
|
+
if (job.capture === true) toolLog.baseline = measureRepositoryBaseline({ cwd: dir.wt, baseRef, knowledgeDir });
|
|
711
713
|
return {
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
// paragraph
|
|
715
|
-
//
|
|
716
|
-
system:
|
|
717
|
-
|
|
718
|
-
:
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
// chat, and only while the server can show one.
|
|
722
|
-
{ knowledgeDir, artifacts: !captureTab && getArtifactsAccepted() }
|
|
723
|
-
),
|
|
714
|
+
// The shape's contract (a plan turn: the PLAIN one, no
|
|
715
|
+
// artifacts paragraph, the planning sentence appended). The
|
|
716
|
+
// artifacts paragraph also waits on the server — ARTIFACTS
|
|
717
|
+
// (0.94.0): only while it can show one, read here at spawn.
|
|
718
|
+
system: sessionTurnSystem(shape, {
|
|
719
|
+
knowledgeDir,
|
|
720
|
+
artifactsAccepted: getArtifactsAccepted(),
|
|
721
|
+
runtime: rt.id,
|
|
722
|
+
}) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : ''),
|
|
724
723
|
// The directory is OUTSIDE a worktree's cwd; Claude Code is
|
|
725
724
|
// told it may read there (`--add-dir`) rather than left to
|
|
726
725
|
// refuse a read-only capture turn a path it was just handed.
|
|
@@ -729,16 +728,16 @@ export function createWorkSessionTurns({
|
|
|
729
728
|
})(),
|
|
730
729
|
// Present only when the tab named one — see brainFor.
|
|
731
730
|
...brain,
|
|
732
|
-
// The tab watches the CLI work
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
streamJson: true,
|
|
731
|
+
// The tab watches the CLI work (every CLI speaks events);
|
|
732
|
+
// `answerFromResult` keeps `out` — which IS the reply posted to
|
|
733
|
+
// the transcript — to the final result, so streamed prose is
|
|
734
|
+
// narrated once and posted once. Every line goes to the narrator
|
|
735
|
+
// above, throttled.
|
|
738
736
|
answerFromResult: true,
|
|
739
737
|
onActivity: (a) => {
|
|
740
738
|
narrator.line(a?.label);
|
|
741
|
-
|
|
739
|
+
// Claude/Codex commands are audited below with their correlated outcome.
|
|
740
|
+
if (rt.id !== 'claude' && rt.id !== 'codex') auditCommand(a);
|
|
742
741
|
if (a?.kind === 'tool' && APPROVAL_REFUSED.test(a.error ?? '')) approvalRefused.push(a.label);
|
|
743
742
|
},
|
|
744
743
|
// One Codex agent MESSAGE (runtimeEvents.mjs `answer`): the turn
|
|
@@ -747,7 +746,13 @@ export function createWorkSessionTurns({
|
|
|
747
746
|
if (String(a ?? '').trim()) codexSpoke = true;
|
|
748
747
|
},
|
|
749
748
|
// The structured twin of the line above — see toolLog.
|
|
750
|
-
onToolEvent:
|
|
749
|
+
onToolEvent: (name, input, meta) => {
|
|
750
|
+
const e = pushToolEvent(name, input, meta);
|
|
751
|
+
if (e?.t === 'bash') auditCommand({
|
|
752
|
+
kind: 'bash', command: input?.command ?? e.c,
|
|
753
|
+
observation: { id: e.id, outcome: e.outcome, exitCode: e.exitCode, errorCategory: e.errorCategory },
|
|
754
|
+
});
|
|
755
|
+
},
|
|
751
756
|
// The CLI's own token counts, tagged with which CLI counted them
|
|
752
757
|
// — ADDED, not set: both runTurn calls below share these args.
|
|
753
758
|
onUsage: (u) => {
|
|
@@ -756,16 +761,12 @@ export function createWorkSessionTurns({
|
|
|
756
761
|
onModel: (m) => {
|
|
757
762
|
model = m;
|
|
758
763
|
},
|
|
759
|
-
// What this CLI says it can be asked for by name
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
+
// What this CLI says it can be asked for by name, and which of
|
|
765
|
+
// its connectors need a sign-in, are harvested off this init by
|
|
766
|
+
// runTurn itself (`initHarvestFor`, runtimeCapabilities.mjs) —
|
|
767
|
+
// a tab's build turn is the machine's measurement; its fenced
|
|
768
|
+
// capture chat is not.
|
|
764
769
|
onInit: (i) => {
|
|
765
|
-
recordSkills(i.skills);
|
|
766
|
-
// The CLI's mounted MCP servers and connectors (0.97.0) — the
|
|
767
|
-
// same free fact off the same event; see recordMcpServers.
|
|
768
|
-
recordMcpServers(i.mcpServers);
|
|
769
770
|
// The conversation this turn is actually speaking under. Held
|
|
770
771
|
// and persisted after the turn ends, so the NEXT one resumes
|
|
771
772
|
// this exact thread rather than whatever the directory saw
|
|
@@ -777,11 +778,9 @@ export function createWorkSessionTurns({
|
|
|
777
778
|
cwd: dir.wt,
|
|
778
779
|
mcpArgs: mcp.args,
|
|
779
780
|
mcpEnv: mcp.env,
|
|
780
|
-
// A
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
// talking to it may be on a phone (noWindowEnv.mjs).
|
|
784
|
-
display: !captureTab,
|
|
781
|
+
// A Terminal tab keeps its environment's display; the capture
|
|
782
|
+
// chat does not (workTurnShape.mjs).
|
|
783
|
+
display: shape.display,
|
|
785
784
|
runtime: rt.id,
|
|
786
785
|
label: c.cyan('[tab]'),
|
|
787
786
|
// Only codex announces one (thread.started); held here so the id
|
|
@@ -794,6 +793,9 @@ export function createWorkSessionTurns({
|
|
|
794
793
|
onSpawn: (ch) => {
|
|
795
794
|
if (!ch) return;
|
|
796
795
|
spawned.push(ch);
|
|
796
|
+
run.spawned.push(ch);
|
|
797
|
+
// Asked to stop between the check and the spawn.
|
|
798
|
+
if (run.interrupted) killGroup(ch);
|
|
797
799
|
// Keyed to the SESSION it serves — that id is what the machine
|
|
798
800
|
// snapshot charges this child's memory to.
|
|
799
801
|
workChildren.set(ch, job.sessionId);
|
|
@@ -817,7 +819,7 @@ export function createWorkSessionTurns({
|
|
|
817
819
|
}
|
|
818
820
|
},
|
|
819
821
|
};
|
|
820
|
-
out = await
|
|
822
|
+
out = await runCli({
|
|
821
823
|
...turnArgs,
|
|
822
824
|
resume,
|
|
823
825
|
resumeThreadId: codexResumeId || claudeResumeId || undefined,
|
|
@@ -858,7 +860,7 @@ export function createWorkSessionTurns({
|
|
|
858
860
|
!adopting &&
|
|
859
861
|
resumeRetriesFresh({ resume, out, runtime: rt.id, sawInit: Boolean(seenClaudeSession) })
|
|
860
862
|
) {
|
|
861
|
-
out = await
|
|
863
|
+
out = await runCli({ ...turnArgs, resume: false });
|
|
862
864
|
}
|
|
863
865
|
} finally {
|
|
864
866
|
// The CLI has stopped printing, so stop relaying. The LINE itself
|
|
@@ -888,9 +890,9 @@ export function createWorkSessionTurns({
|
|
|
888
890
|
if (
|
|
889
891
|
rt.id === 'claude' &&
|
|
890
892
|
seenClaudeSession &&
|
|
891
|
-
|
|
893
|
+
CLAUDE_SESSION_RE.test(seenClaudeSession)
|
|
892
894
|
) {
|
|
893
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
895
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
|
|
894
896
|
if (convMarker) {
|
|
895
897
|
try {
|
|
896
898
|
writeFileSync(convMarker, seenClaudeSession);
|
|
@@ -900,7 +902,7 @@ export function createWorkSessionTurns({
|
|
|
900
902
|
}
|
|
901
903
|
}
|
|
902
904
|
if (rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
|
|
903
|
-
const threadMarker = sessionMetaPath(dir.wt,
|
|
905
|
+
const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
|
|
904
906
|
if (threadMarker) {
|
|
905
907
|
try {
|
|
906
908
|
writeFileSync(threadMarker, seenThreadId);
|
|
@@ -909,6 +911,21 @@ export function createWorkSessionTurns({
|
|
|
909
911
|
}
|
|
910
912
|
}
|
|
911
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* A PERSON INTERRUPTED IT. Whatever the CLI printed was cut off, so
|
|
916
|
+
* it is not an answer: the turn settles `interrupted`, with what it
|
|
917
|
+
* spent and did, and the message sent to interrupt it runs next in
|
|
918
|
+
* this same conversation — the markers above already hold its id.
|
|
919
|
+
* Never spawned, it settles `ran: false`, unremembered, so the
|
|
920
|
+
* server's re-offer runs it as it was.
|
|
921
|
+
*/
|
|
922
|
+
if (run.interrupted) {
|
|
923
|
+
const body = { ...attempt, ok: false, interrupted: true, ran: run.spawned.length > 0, answer: 'the turn was interrupted', ...spent() };
|
|
924
|
+
if (body.ran) stoppedIn.set(job.sessionId, now());
|
|
925
|
+
await (body.ran ? settleTurn(job.id, body) : settleWorkTurn(job.id, body));
|
|
926
|
+
ok(`${c.cyan('tab')} ${c.dim('— interrupted')}`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
912
929
|
const answer = (out || '').trim();
|
|
913
930
|
// No output at all smells like a dead MCP credential (the lane
|
|
914
931
|
// workers' no-sentinel case) — drop the cached token so the next
|
|
@@ -920,14 +937,13 @@ export function createWorkSessionTurns({
|
|
|
920
937
|
// exactly that; no sessionRef is recorded, so the server keeps
|
|
921
938
|
// offering the adoption and a retry after the user checks is cheap.
|
|
922
939
|
await settleTurn(job.id, {
|
|
940
|
+
...attempt,
|
|
923
941
|
ok: false,
|
|
924
942
|
answer: "Couldn't resume the terminal session — it may have been removed.",
|
|
925
943
|
// Whatever it DID before coming back empty is exactly the
|
|
926
|
-
// question a failed turn's log answers
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
...(usage ? { usage } : {}),
|
|
930
|
-
...(model ? { model } : {}),
|
|
944
|
+
// question a failed turn's log answers — and an empty fork
|
|
945
|
+
// still spent what it spent.
|
|
946
|
+
...spent(),
|
|
931
947
|
});
|
|
932
948
|
warn('adopt turn produced no output — settled as failed');
|
|
933
949
|
return;
|
|
@@ -938,7 +954,7 @@ export function createWorkSessionTurns({
|
|
|
938
954
|
// cwd registry. From here on the marker is the tab's identity and
|
|
939
955
|
// the registry is never trusted again.
|
|
940
956
|
if (rt.id === 'antigravity' && answer.length > 0) {
|
|
941
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
957
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
|
|
942
958
|
if (convMarker && !existsSync(convMarker)) {
|
|
943
959
|
const learned = adopting ? job.adopt.id : agyRegistryLookup(dir.wt);
|
|
944
960
|
if (learned && AGY_CONV_RE.test(learned)) {
|
|
@@ -971,16 +987,26 @@ export function createWorkSessionTurns({
|
|
|
971
987
|
*/
|
|
972
988
|
const loginFailure = rt.id === 'codex' && !codexSpoke ? codexLoginFailure(answer) : null;
|
|
973
989
|
const refusedLine = rt.id === 'codex' ? codexApprovalSentence(approvalRefused) : null;
|
|
990
|
+
let artifactFailure = null;
|
|
991
|
+
if (artifactScan) {
|
|
992
|
+
artifactScan.reported = true;
|
|
993
|
+
try {
|
|
994
|
+
await artifacts.report({ placeDir: artifactScan.dir, before: artifactScan.before, sessionId: job.sessionId, turnId: job.id });
|
|
995
|
+
} catch (error) {
|
|
996
|
+
artifactFailure = `Artifact delivery is incomplete: ${error.message}`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
974
999
|
const spoke = answer.length > 0 && !loginFailure;
|
|
975
1000
|
await settleTurn(job.id, {
|
|
976
|
-
|
|
977
|
-
|
|
1001
|
+
...attempt,
|
|
1002
|
+
ok: spoke && !artifactFailure,
|
|
1003
|
+
answer: artifactFailure ? withRelay(artifactFailure, answer) : loginFailure
|
|
978
1004
|
? withRelay(codexLoginSentence(loginFailure), answer)
|
|
979
1005
|
: answer.length > 0
|
|
980
1006
|
? refusedLine
|
|
981
1007
|
? withRelay(refusedLine, answer)
|
|
982
1008
|
: // Scrub: a reply can quote config or env-adjacent code.
|
|
983
|
-
envScrub(answer).slice(0,
|
|
1009
|
+
envScrub(answer).slice(0, SESSION_ANSWER_MAX)
|
|
984
1010
|
: (refusedLine ?? noWords),
|
|
985
1011
|
// Only a turn that actually SPOKE proves a conversation lives
|
|
986
1012
|
// here. Recording the path unconditionally is how a crashed first
|
|
@@ -989,30 +1015,34 @@ export function createWorkSessionTurns({
|
|
|
989
1015
|
// The turn's tool log, in final form — the durable copy that lands
|
|
990
1016
|
// on the settled message (the live copy on the record is cleared
|
|
991
1017
|
// at settle). Already scrubbed at collection.
|
|
992
|
-
...(
|
|
993
|
-
...(usage ? { usage } : {}),
|
|
994
|
-
...(model ? { model } : {}),
|
|
1018
|
+
...spent(),
|
|
995
1019
|
});
|
|
996
1020
|
if (spoke) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
997
1021
|
else if (loginFailure) warn('codex answered 401 for this session turn — settled as failed, in words');
|
|
998
1022
|
else warn('session turn produced no output — settled as failed');
|
|
999
1023
|
} catch (e) {
|
|
1024
|
+
if (toolLog) toolLog.state = 'partial';
|
|
1025
|
+
if (run.interrupted) {
|
|
1026
|
+
const body = { ...attempt, ok: false, interrupted: true, ran: run.spawned.length > 0, answer: 'the turn was interrupted', ...spent() };
|
|
1027
|
+
await (body.ran ? settleTurn(job.id, body) : settleWorkTurn(job.id, body));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1000
1030
|
await settleTurn(job.id, {
|
|
1031
|
+
...attempt,
|
|
1001
1032
|
ok: false,
|
|
1002
1033
|
// Scrub, like every string that leaves this machine: an exception
|
|
1003
1034
|
// routinely quotes command output, and command output can quote a
|
|
1004
1035
|
// synced secret.
|
|
1005
1036
|
answer: envScrub(String(e?.message ?? 'the session turn failed')).slice(0, 2000),
|
|
1006
1037
|
// "What did it do before it failed" is exactly the question a
|
|
1007
|
-
// crashed turn's log answers
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
...(usage ? { usage } : {}),
|
|
1011
|
-
...(model ? { model } : {}),
|
|
1038
|
+
// crashed turn's log answers, and a turn that threw after its CLI
|
|
1039
|
+
// ran still spent what it spent — the main settle's own spread.
|
|
1040
|
+
...spent(),
|
|
1012
1041
|
});
|
|
1013
1042
|
warn(`session turn failed: ${e?.message ?? e}`);
|
|
1014
1043
|
} finally {
|
|
1015
1044
|
workAnswering.delete(job.id);
|
|
1045
|
+
runs.delete(job.id);
|
|
1016
1046
|
if (startClaim) startingIn.delete(startClaim);
|
|
1017
1047
|
// Nothing spawned, or everything already has: releasing twice is the
|
|
1018
1048
|
// normal case and costs nothing. A reservation that leaked would
|
|
@@ -1028,7 +1058,7 @@ export function createWorkSessionTurns({
|
|
|
1028
1058
|
// …and relay what it wrote to SHOW its owner (2026-09-22) — the same
|
|
1029
1059
|
// beat, the same reason it is not awaited. A failed turn's half-drawn
|
|
1030
1060
|
// page is still what the turn left, and the tab should see it.
|
|
1031
|
-
if (artifactScan) {
|
|
1061
|
+
if (artifactScan && !artifactScan.reported) {
|
|
1032
1062
|
void artifacts
|
|
1033
1063
|
.report({
|
|
1034
1064
|
placeDir: artifactScan.dir,
|
|
@@ -1043,5 +1073,32 @@ export function createWorkSessionTurns({
|
|
|
1043
1073
|
}
|
|
1044
1074
|
};
|
|
1045
1075
|
|
|
1046
|
-
|
|
1076
|
+
/**
|
|
1077
|
+
* THE RUNS A PERSON ASKED TO STOP (`roster.interruptTurns`, read before the
|
|
1078
|
+
* jobs). One this lane is running is stopped — its process groups killed —
|
|
1079
|
+
* and settles `interrupted` when its CLI is gone. One it finished already
|
|
1080
|
+
* keeps its own settle. One it is not running at all is answered `ran:
|
|
1081
|
+
* false`: nothing was running here to stop, and the server puts it back in
|
|
1082
|
+
* the queue.
|
|
1083
|
+
*/
|
|
1084
|
+
const interruptWorkTurns = (ids) => {
|
|
1085
|
+
rosterInterrupts = new Set(Array.isArray(ids) ? ids : []);
|
|
1086
|
+
const at = now();
|
|
1087
|
+
for (const [id, when] of notRunningSaid) if (at - when > 30_000) notRunningSaid.delete(id);
|
|
1088
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
1089
|
+
if (typeof id !== 'string' || !id) continue;
|
|
1090
|
+
const run = runs.get(id);
|
|
1091
|
+
if (run) {
|
|
1092
|
+
if (!run.interrupted) note(`${c.cyan('tab')} ${c.dim('— interrupting the running turn')}`);
|
|
1093
|
+
run.interrupted = true;
|
|
1094
|
+
for (const ch of run.spawned) killGroup(ch);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (pendingWorkReports.has(id) || settledRecently(id) || notRunningSaid.has(id)) continue;
|
|
1098
|
+
notRunningSaid.set(id, at);
|
|
1099
|
+
void Promise.resolve(settleWorkTurn(id, { ok: false, interrupted: true, ran: false, answer: '' })).catch(() => {});
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
return { processWorkTurns, interruptWorkTurns, workAnswering, settledHere };
|
|
1047
1104
|
}
|