flowviant 0.114.1 → 0.115.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 +9 -0
- package/bin/lib/agentTaskKinds.mjs +10 -6
- package/bin/lib/artifacts.mjs +43 -1
- package/bin/lib/claudePersonal.mjs +145 -0
- package/bin/lib/codexPersonal.mjs +467 -0
- package/bin/lib/codexRelay.mjs +63 -0
- package/bin/lib/prompts.mjs +74 -22
- package/bin/lib/resumeLost.mjs +16 -2
- package/bin/lib/runTurn.mjs +24 -1
- package/bin/lib/runtimeClaude.mjs +44 -11
- package/bin/lib/runtimeCodex.mjs +165 -4
- package/bin/lib/runtimeEvents.mjs +9 -2
- package/bin/lib/turnProfile.mjs +3 -2
- package/bin/lib/workAgentTurnExecution.mjs +23 -3
- package/bin/lib/workSessionTurns.mjs +145 -30
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
|
37
37
|
import { brainFor } from './workBrain.mjs';
|
|
38
38
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
39
39
|
import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
40
|
-
import { ensureArtifactDir } from './artifacts.mjs';
|
|
40
|
+
import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
|
|
41
41
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
42
42
|
|
|
43
43
|
export function createAgentTurnExecution({
|
|
@@ -176,7 +176,8 @@ export function createAgentTurnExecution({
|
|
|
176
176
|
* posture, which can write ONLY `.flowviant/artifacts/` (claudePosture.mjs).
|
|
177
177
|
*
|
|
178
178
|
* A RUNTIME THAT CANNOT EXPRESS THE POSTURE IS REFUSED, NOT IMPROVISED.
|
|
179
|
-
*
|
|
179
|
+
* Antigravity declares none of them and Codex declares `design` (since
|
|
180
|
+
* 0.115.0) and `image` but not `research` (runtimeCodex.mjs,
|
|
180
181
|
* runtimeAntigravity.mjs), and the only
|
|
181
182
|
* alternative to refusing is running the card as a BUILD turn with
|
|
182
183
|
* permissions skipped — an agent writing code for an ask that was a
|
|
@@ -248,6 +249,23 @@ export function createAgentTurnExecution({
|
|
|
248
249
|
});
|
|
249
250
|
return;
|
|
250
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* …AND ITS SCRATCH BESIDE IT, EMPTY (0.115.0): a fenced shell's temp files
|
|
254
|
+
* (a here-document, `mktemp`) go under `.flowviant/tmp`, which Codex's
|
|
255
|
+
* design fence grants because `/tmp` is read-only in there
|
|
256
|
+
* (`ensureFenceScratch`). Made for every file kind — one rule, no kind
|
|
257
|
+
* named — and refused in words where it cannot stand as a real directory.
|
|
258
|
+
*/
|
|
259
|
+
if (kind.artifact && !ensureFenceScratch(wt)) {
|
|
260
|
+
await postAgentTurn({
|
|
261
|
+
turnId,
|
|
262
|
+
outcome: 'nothing',
|
|
263
|
+
answer: "this worktree's .flowviant/tmp is not a real directory, so the fenced turn has no scratch to work in",
|
|
264
|
+
branch,
|
|
265
|
+
worktree: wt,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
251
269
|
|
|
252
270
|
// ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly
|
|
253
271
|
// right here — the ambiguity that forced per-tab session pinning in the
|
|
@@ -469,7 +487,9 @@ export function createAgentTurnExecution({
|
|
|
469
487
|
// The knowledge paragraph, when this box holds a library — the same
|
|
470
488
|
// composer the tabs use, so an agent and a tab can never be told two
|
|
471
489
|
// different things about the same directory.
|
|
472
|
-
|
|
490
|
+
// Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
|
|
491
|
+
// told it is Codex, never Claude.
|
|
492
|
+
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, RUNTIMES[rt]?.label), {
|
|
473
493
|
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
474
494
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
475
495
|
// land on its page, under the facts row.
|
|
@@ -42,6 +42,7 @@ import { REPO_PLACE } from './workPlaces.mjs';
|
|
|
42
42
|
import { CODEX_THREAD_RE, AGY_CONV_RE } from './workSessionRuntime.mjs';
|
|
43
43
|
import { createToolLog } from './workToolLog.mjs';
|
|
44
44
|
import { createCommandAudit } from './workCommandAudit.mjs';
|
|
45
|
+
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, withRelay } from './codexRelay.mjs';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* THE ONE SENTENCE A PLAN TURN'S SYSTEM PROMPT GAINS (0.97.0), composed here
|
|
@@ -59,6 +60,14 @@ import { createCommandAudit } from './workCommandAudit.mjs';
|
|
|
59
60
|
export const PLAN_TURN_SENTENCE =
|
|
60
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.';
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* HOW LONG, AND HOW MANY, SETTLED TURNS THE LANE REMEMBERS (2026-09-29) — see
|
|
65
|
+
* `settledHere` below. Ten minutes is far past any roster a poll could still
|
|
66
|
+
* be holding; the cap keeps a box that answers all day from growing the map.
|
|
67
|
+
*/
|
|
68
|
+
export const SETTLED_HERE_TTL_MS = 10 * 60 * 1000;
|
|
69
|
+
export const SETTLED_HERE_MAX = 500;
|
|
70
|
+
|
|
62
71
|
export function createWorkSessionTurns({
|
|
63
72
|
repoRoot,
|
|
64
73
|
baseDir,
|
|
@@ -91,14 +100,67 @@ export function createWorkSessionTurns({
|
|
|
91
100
|
// The real CLI driver; injectable only so the lane can be driven in a test
|
|
92
101
|
// without spending a model turn (the wiki runner's shape).
|
|
93
102
|
runTurn = defaultRunTurn,
|
|
103
|
+
// The clock `settledHere` ages by; injectable only for its bound's test.
|
|
104
|
+
now = Date.now,
|
|
94
105
|
}) {
|
|
95
106
|
const workAnswering = new Set(); // turn ids currently queued/running here
|
|
96
107
|
const MAX_WORK_TRIES = 3;
|
|
97
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A TURN THIS LANE SETTLED IS NEVER RUN AGAIN (2026-09-29).
|
|
111
|
+
*
|
|
112
|
+
* A delivered settle forgets the turn everywhere: the report queue drops it
|
|
113
|
+
* from `pendingWorkReports` and `workAttempts`, and the turn's `finally`
|
|
114
|
+
* drops it from `workAnswering`. A roster READ made before that settle
|
|
115
|
+
* landed still offers it — the poll was in flight while the POST went out —
|
|
116
|
+
* and nothing was left to say it had run: the turn ran twice, settled
|
|
117
|
+
* twice, the rerun's reply overwrote the first, and the person's follow-up
|
|
118
|
+
* waited behind a turn nobody asked for. So every settle this lane makes
|
|
119
|
+
* goes through `settleTurn`, which remembers the turn id, and a job in the
|
|
120
|
+
* map is skipped where jobs are picked. Remembered BEFORE the POST: a
|
|
121
|
+
* settle still in flight is covered by `workAnswering`, one that could not
|
|
122
|
+
* be delivered by `pendingWorkReports`, and this covers the one that was.
|
|
123
|
+
*/
|
|
124
|
+
const settledHere = new Map(); // turnId -> when this lane settled it, oldest first
|
|
125
|
+
const forgetStale = (at) => {
|
|
126
|
+
for (const [id, when] of settledHere) {
|
|
127
|
+
if (settledHere.size <= SETTLED_HERE_MAX && at - when < SETTLED_HERE_TTL_MS) break;
|
|
128
|
+
settledHere.delete(id);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const settledRecently = (turnId) => {
|
|
132
|
+
forgetStale(now());
|
|
133
|
+
return settledHere.has(turnId);
|
|
134
|
+
};
|
|
135
|
+
const settleTurn = (turnId, payload) => {
|
|
136
|
+
const at = now();
|
|
137
|
+
settledHere.delete(turnId); // re-inserted at the end: the map stays in time order
|
|
138
|
+
settledHere.set(turnId, at);
|
|
139
|
+
forgetStale(at);
|
|
140
|
+
return settleWorkTurn(turnId, payload);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A PLACE A TURN OF THIS PROCESS IS STARTING IN (2026-09-29) — the turn
|
|
145
|
+
* lock's in-process half, keyed by the lock's own path.
|
|
146
|
+
*
|
|
147
|
+
* `flowviant-turn.lock` is written at SPAWN, several awaits after it is
|
|
148
|
+
* read (a credential mint, the attachment fetch), so two turns of one
|
|
149
|
+
* session offered in one roster both read "unlocked" and both spawned: two
|
|
150
|
+
* CLIs resuming one conversation at once, the incoherence the lock exists
|
|
151
|
+
* to prevent. The claim is checked and taken synchronously beside the file
|
|
152
|
+
* check, so the second turn waits for a later poll like any locked one,
|
|
153
|
+
* and it is released in the turn's outer `finally`, after its settle.
|
|
154
|
+
*/
|
|
155
|
+
const startingIn = new Set();
|
|
156
|
+
|
|
98
157
|
const processWorkTurns = (jobs) => {
|
|
99
158
|
for (const job of jobs ?? []) {
|
|
100
159
|
if (!job || typeof job.id !== 'string' || !job.body || !job.sessionId) continue;
|
|
101
160
|
if (workAnswering.has(job.id)) continue;
|
|
161
|
+
// Settled by this lane already: a roster read from before the settle
|
|
162
|
+
// landed. See `settledHere`.
|
|
163
|
+
if (settledRecently(job.id)) continue;
|
|
102
164
|
// The turn already RAN and its answer sits in the delivery queue — never
|
|
103
165
|
// run it again while the report is merely undelivered.
|
|
104
166
|
if (pendingWorkReports.has(job.id)) continue;
|
|
@@ -140,7 +202,7 @@ export function createWorkSessionTurns({
|
|
|
140
202
|
* tab for the server's whole expiry window.
|
|
141
203
|
*/
|
|
142
204
|
if (place !== REPO_PLACE && !isSafePathSegment(place)) {
|
|
143
|
-
void
|
|
205
|
+
void settleTurn(job.id, {
|
|
144
206
|
ok: false,
|
|
145
207
|
answer:
|
|
146
208
|
'the server named a working directory this machine refuses to use — close and reopen the tab, then send the message again',
|
|
@@ -178,12 +240,15 @@ export function createWorkSessionTurns({
|
|
|
178
240
|
* out here beside `usage`, and spread wherever it is. Last write wins
|
|
179
241
|
* across a fresh retry: that spawn is the one that answered. */
|
|
180
242
|
let model = null;
|
|
243
|
+
/** The `startingIn` key this turn claimed — released in the outer
|
|
244
|
+
* `finally`, and only by the turn that took it. */
|
|
245
|
+
let startClaim = null;
|
|
181
246
|
try {
|
|
182
247
|
const tries = workAttempts.get(job.id) ?? 0;
|
|
183
248
|
if (tries >= MAX_WORK_TRIES) {
|
|
184
249
|
// Out of local tries: SETTLE, don't skip — a silently skipped turn
|
|
185
250
|
// strands the tab for the server's whole 24h expiry window.
|
|
186
|
-
await
|
|
251
|
+
await settleTurn(job.id, {
|
|
187
252
|
ok: false,
|
|
188
253
|
answer: `the turn failed ${tries} times on this machine — check the daemon log, then send the message again`,
|
|
189
254
|
});
|
|
@@ -209,7 +274,7 @@ export function createWorkSessionTurns({
|
|
|
209
274
|
typeof job.adopt.id !== 'string' ||
|
|
210
275
|
!/^[0-9a-f][0-9a-f-]{6,62}$/i.test(job.adopt.id)
|
|
211
276
|
) {
|
|
212
|
-
await
|
|
277
|
+
await settleTurn(job.id, {
|
|
213
278
|
ok: false,
|
|
214
279
|
answer: 'that terminal session id is not one this machine can resume',
|
|
215
280
|
});
|
|
@@ -223,7 +288,7 @@ export function createWorkSessionTurns({
|
|
|
223
288
|
srcCwd = null;
|
|
224
289
|
}
|
|
225
290
|
if (!srcCwd) {
|
|
226
|
-
await
|
|
291
|
+
await settleTurn(job.id, {
|
|
227
292
|
ok: false,
|
|
228
293
|
answer: "the terminal session's directory no longer exists on the machine",
|
|
229
294
|
});
|
|
@@ -247,14 +312,14 @@ export function createWorkSessionTurns({
|
|
|
247
312
|
/* keep the literal path */
|
|
248
313
|
}
|
|
249
314
|
if (!under(srcCwd, realRoot)) {
|
|
250
|
-
await
|
|
315
|
+
await settleTurn(job.id, {
|
|
251
316
|
ok: false,
|
|
252
317
|
answer: "the terminal session's directory is outside this project's repository",
|
|
253
318
|
});
|
|
254
319
|
return;
|
|
255
320
|
}
|
|
256
321
|
if (under(srcCwd, realBase)) {
|
|
257
|
-
await
|
|
322
|
+
await settleTurn(job.id, {
|
|
258
323
|
ok: false,
|
|
259
324
|
answer:
|
|
260
325
|
"that directory is one of the daemon's own worktrees — its session is already a tab, not something to adopt",
|
|
@@ -264,7 +329,7 @@ export function createWorkSessionTurns({
|
|
|
264
329
|
try {
|
|
265
330
|
srcHead = git(['rev-parse', 'HEAD'], srcCwd);
|
|
266
331
|
} catch {
|
|
267
|
-
await
|
|
332
|
+
await settleTurn(job.id, {
|
|
268
333
|
ok: false,
|
|
269
334
|
answer:
|
|
270
335
|
"the terminal session's directory is not a usable git checkout (no HEAD to branch from)",
|
|
@@ -281,7 +346,7 @@ export function createWorkSessionTurns({
|
|
|
281
346
|
? isAgyConversationLive(job.adopt.id)
|
|
282
347
|
: isTerminalSessionLive(job.adopt.id);
|
|
283
348
|
if (adoptLive) {
|
|
284
|
-
await
|
|
349
|
+
await settleTurn(job.id, {
|
|
285
350
|
ok: false,
|
|
286
351
|
answer:
|
|
287
352
|
'That terminal session is still open on the machine — close it there first, then adopt.',
|
|
@@ -297,7 +362,7 @@ export function createWorkSessionTurns({
|
|
|
297
362
|
// the session's own id, which is what every tab has always done.
|
|
298
363
|
const dir = placeWtFor(place, adopting ? srcHead : undefined);
|
|
299
364
|
if (!dir) {
|
|
300
|
-
await
|
|
365
|
+
await settleTurn(job.id, {
|
|
301
366
|
ok: false,
|
|
302
367
|
answer:
|
|
303
368
|
'the session worktree could not be opened on the machine — check the daemon log',
|
|
@@ -308,13 +373,20 @@ export function createWorkSessionTurns({
|
|
|
308
373
|
// life, most likely; the lock outlives a restart. Leave the job
|
|
309
374
|
// pending and look again next poll; spawning a second CLI would put
|
|
310
375
|
// two Claudes in one held context. Costs no attempt: nothing ran.
|
|
376
|
+
// …AND ONE OF OURS MAY BE ABOUT TO: `startingIn`, checked and
|
|
377
|
+
// claimed with no await between, because the file is only written
|
|
378
|
+
// at spawn. Keyed by the lock's path (the directory, when the git
|
|
379
|
+
// dir cannot be named) — the file lock's own scope.
|
|
311
380
|
const lockPath = sessionMetaPath(dir.wt, 'flowviant-turn.lock');
|
|
312
|
-
|
|
381
|
+
const startKey = lockPath || dir.wt;
|
|
382
|
+
if (startingIn.has(startKey) || turnLockedByLivePid(lockPath)) {
|
|
313
383
|
warn(
|
|
314
384
|
`a turn is already running in "${job.sessionName || job.sessionId}" — waiting for it to finish`
|
|
315
385
|
);
|
|
316
386
|
return;
|
|
317
387
|
}
|
|
388
|
+
startingIn.add(startKey);
|
|
389
|
+
startClaim = startKey;
|
|
318
390
|
// WHICH BRAIN the roster says this tab speaks (null/absent = Claude,
|
|
319
391
|
// which is what every tab ran on until now) — honored by
|
|
320
392
|
// sessionRuntime: on a first turn a named runtime IS the pick, and a
|
|
@@ -327,14 +399,14 @@ export function createWorkSessionTurns({
|
|
|
327
399
|
if (rt.mismatch) {
|
|
328
400
|
// Something upstream changed this tab's identity mid-life. A held
|
|
329
401
|
// context must never be answered by a different brain — say so.
|
|
330
|
-
await
|
|
402
|
+
await settleTurn(job.id, {
|
|
331
403
|
ok: false,
|
|
332
404
|
answer: `this tab is pinned to ${rt.mismatch.pin} but the server says it is a ${rt.mismatch.runtime} tab — reopen a new tab`,
|
|
333
405
|
});
|
|
334
406
|
return;
|
|
335
407
|
}
|
|
336
408
|
if (rt.missing) {
|
|
337
|
-
await
|
|
409
|
+
await settleTurn(job.id, {
|
|
338
410
|
ok: false,
|
|
339
411
|
answer: `this session runs on ${rt.missing}, which is no longer installed on the machine — reinstall it, or open a new tab`,
|
|
340
412
|
});
|
|
@@ -346,14 +418,14 @@ export function createWorkSessionTurns({
|
|
|
346
418
|
// tab can run on (Antigravity has no MCP config, and the session's
|
|
347
419
|
// whole control plane rides one). An honest sentence beats the
|
|
348
420
|
// mcpFor throw this used to crash into every turn.
|
|
349
|
-
await
|
|
421
|
+
await settleTurn(job.id, {
|
|
350
422
|
ok: false,
|
|
351
423
|
answer: `this session runs on ${rt.unsupported}, which cannot drive a Terminal tab on this machine — open a new tab`,
|
|
352
424
|
});
|
|
353
425
|
return;
|
|
354
426
|
}
|
|
355
427
|
if (!rt.id) {
|
|
356
|
-
await
|
|
428
|
+
await settleTurn(job.id, {
|
|
357
429
|
ok: false,
|
|
358
430
|
answer:
|
|
359
431
|
'No coding CLI is installed on the machine — install Claude Code (or another supported CLI), then send the message again',
|
|
@@ -366,7 +438,7 @@ export function createWorkSessionTurns({
|
|
|
366
438
|
// (--conversation). Codex has no adoptable store yet, and its
|
|
367
439
|
// args builder backstops this with a loud throw — but a sentence
|
|
368
440
|
// here beats a stack there.
|
|
369
|
-
await
|
|
441
|
+
await settleTurn(job.id, {
|
|
370
442
|
ok: false,
|
|
371
443
|
answer:
|
|
372
444
|
'adopting this terminal session needs its own CLI on the machine — install it, then try again',
|
|
@@ -383,7 +455,7 @@ export function createWorkSessionTurns({
|
|
|
383
455
|
*/
|
|
384
456
|
const planTurn = job.planMode === true && job.capture !== true;
|
|
385
457
|
if (planTurn && rt.id !== 'claude') {
|
|
386
|
-
await
|
|
458
|
+
await settleTurn(job.id, {
|
|
387
459
|
ok: false,
|
|
388
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`,
|
|
389
461
|
});
|
|
@@ -409,7 +481,7 @@ export function createWorkSessionTurns({
|
|
|
409
481
|
return;
|
|
410
482
|
}
|
|
411
483
|
if (mint?.gone) {
|
|
412
|
-
await
|
|
484
|
+
await settleTurn(job.id, {
|
|
413
485
|
ok: false,
|
|
414
486
|
answer:
|
|
415
487
|
'Flowviant no longer offers this session to this machine — the tab may have been closed or moved',
|
|
@@ -417,7 +489,7 @@ export function createWorkSessionTurns({
|
|
|
417
489
|
return;
|
|
418
490
|
}
|
|
419
491
|
if (!mint?.token) {
|
|
420
|
-
await
|
|
492
|
+
await settleTurn(job.id, {
|
|
421
493
|
ok: false,
|
|
422
494
|
answer:
|
|
423
495
|
'the machine could not mint a session credential from Flowviant — check its connection, then send the message again',
|
|
@@ -554,6 +626,11 @@ export function createWorkSessionTurns({
|
|
|
554
626
|
let out;
|
|
555
627
|
let seenThreadId = null; // codex's conversation id, off thread.started
|
|
556
628
|
let seenClaudeSession = null; // claude's own conversation id, off system.init
|
|
629
|
+
// What a Codex turn's own events say went wrong (codexRelay.mjs):
|
|
630
|
+
// whether the agent said anything at all, and each tool call Codex
|
|
631
|
+
// refused for want of an approval, by `server.tool`.
|
|
632
|
+
let codexSpoke = false;
|
|
633
|
+
const approvalRefused = [];
|
|
557
634
|
const spawned = []; // this turn's children, for the teardown registry
|
|
558
635
|
// THE TURN'S TOOL LOG — see workToolLog.mjs for its shape rules.
|
|
559
636
|
const turnLog = createToolLog(dir.wt);
|
|
@@ -662,6 +739,12 @@ export function createWorkSessionTurns({
|
|
|
662
739
|
onActivity: (a) => {
|
|
663
740
|
narrator.line(a?.label);
|
|
664
741
|
auditCommand(a);
|
|
742
|
+
if (a?.kind === 'tool' && APPROVAL_REFUSED.test(a.error ?? '')) approvalRefused.push(a.label);
|
|
743
|
+
},
|
|
744
|
+
// One Codex agent MESSAGE (runtimeEvents.mjs `answer`): the turn
|
|
745
|
+
// spoke, so a 401 in its words is its words, not its failure.
|
|
746
|
+
onAnswer: (a) => {
|
|
747
|
+
if (String(a ?? '').trim()) codexSpoke = true;
|
|
665
748
|
},
|
|
666
749
|
// The structured twin of the line above — see toolLog.
|
|
667
750
|
onToolEvent: pushToolEvent,
|
|
@@ -765,6 +848,11 @@ export function createWorkSessionTurns({
|
|
|
765
848
|
* that tab replied identically, with nothing on any surface able
|
|
766
849
|
* to clear it. A tab bricked forever by its own CLI pruning its
|
|
767
850
|
* history, which it does on its own schedule.
|
|
851
|
+
*
|
|
852
|
+
* An EMPTY Claude resume that emitted init is not retried
|
|
853
|
+
* (2026-09-29, resumeLost.mjs): its conversation was found, and a
|
|
854
|
+
* fresh run would answer with no memory of the chat and re-pin the
|
|
855
|
+
* tab to that new conversation.
|
|
768
856
|
*/
|
|
769
857
|
if (
|
|
770
858
|
!adopting &&
|
|
@@ -831,7 +919,7 @@ export function createWorkSessionTurns({
|
|
|
831
919
|
// transcript is most likely gone (cleaned, expired, deleted). Say
|
|
832
920
|
// exactly that; no sessionRef is recorded, so the server keeps
|
|
833
921
|
// offering the adoption and a retry after the user checks is cheap.
|
|
834
|
-
await
|
|
922
|
+
await settleTurn(job.id, {
|
|
835
923
|
ok: false,
|
|
836
924
|
answer: "Couldn't resume the terminal session — it may have been removed.",
|
|
837
925
|
// Whatever it DID before coming back empty is exactly the
|
|
@@ -862,17 +950,42 @@ export function createWorkSessionTurns({
|
|
|
862
950
|
}
|
|
863
951
|
}
|
|
864
952
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
953
|
+
/**
|
|
954
|
+
* NO WORDS, SAID AS MEASURED (2026-09-29). Claude's init event means
|
|
955
|
+
* it ran in this conversation — kept, and resumed by the next
|
|
956
|
+
* message, since an empty resume that found it no longer runs fresh
|
|
957
|
+
* (resumeLost.mjs) — and its result held no words. "Signed out" is
|
|
958
|
+
* a guess, left to the case where no init came.
|
|
959
|
+
*/
|
|
960
|
+
const noWords =
|
|
961
|
+
rt.id === 'claude' && seenClaudeSession
|
|
962
|
+
? 'Claude Code ran in this conversation and returned no words — send the message again'
|
|
963
|
+
: 'the turn produced no output on the machine — its CLI may be signed out; try again';
|
|
964
|
+
/**
|
|
965
|
+
* A CODEX TURN SAYS WHAT ITS OWN WORDS MEASURED (2026-09-29,
|
|
966
|
+
* codexRelay.mjs). A turn that never spoke and whose output is a
|
|
967
|
+
* 401 was not answered: it fails, its sentence first and Codex's
|
|
968
|
+
* retry log kept under it — it used to settle as the chat's REPLY.
|
|
969
|
+
* Tool calls Codex refused for want of an approval are named above
|
|
970
|
+
* whatever the agent said, so a chat that staged nothing says why.
|
|
971
|
+
*/
|
|
972
|
+
const loginFailure = rt.id === 'codex' && !codexSpoke ? codexLoginFailure(answer) : null;
|
|
973
|
+
const refusedLine = rt.id === 'codex' ? codexApprovalSentence(approvalRefused) : null;
|
|
974
|
+
const spoke = answer.length > 0 && !loginFailure;
|
|
975
|
+
await settleTurn(job.id, {
|
|
976
|
+
ok: spoke,
|
|
977
|
+
answer: loginFailure
|
|
978
|
+
? withRelay(codexLoginSentence(loginFailure), answer)
|
|
979
|
+
: answer.length > 0
|
|
980
|
+
? refusedLine
|
|
981
|
+
? withRelay(refusedLine, answer)
|
|
982
|
+
: // Scrub: a reply can quote config or env-adjacent code.
|
|
983
|
+
envScrub(answer).slice(0, 16000)
|
|
984
|
+
: (refusedLine ?? noWords),
|
|
872
985
|
// Only a turn that actually SPOKE proves a conversation lives
|
|
873
986
|
// here. Recording the path unconditionally is how a crashed first
|
|
874
987
|
// turn used to brick resume for the session's whole life.
|
|
875
|
-
...(
|
|
988
|
+
...(spoke ? { sessionRef: dir.wt } : {}),
|
|
876
989
|
// The turn's tool log, in final form — the durable copy that lands
|
|
877
990
|
// on the settled message (the live copy on the record is cleared
|
|
878
991
|
// at settle). Already scrubbed at collection.
|
|
@@ -880,10 +993,11 @@ export function createWorkSessionTurns({
|
|
|
880
993
|
...(usage ? { usage } : {}),
|
|
881
994
|
...(model ? { model } : {}),
|
|
882
995
|
});
|
|
883
|
-
if (
|
|
996
|
+
if (spoke) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
997
|
+
else if (loginFailure) warn('codex answered 401 for this session turn — settled as failed, in words');
|
|
884
998
|
else warn('session turn produced no output — settled as failed');
|
|
885
999
|
} catch (e) {
|
|
886
|
-
await
|
|
1000
|
+
await settleTurn(job.id, {
|
|
887
1001
|
ok: false,
|
|
888
1002
|
// Scrub, like every string that leaves this machine: an exception
|
|
889
1003
|
// routinely quotes command output, and command output can quote a
|
|
@@ -899,6 +1013,7 @@ export function createWorkSessionTurns({
|
|
|
899
1013
|
warn(`session turn failed: ${e?.message ?? e}`);
|
|
900
1014
|
} finally {
|
|
901
1015
|
workAnswering.delete(job.id);
|
|
1016
|
+
if (startClaim) startingIn.delete(startClaim);
|
|
902
1017
|
// Nothing spawned, or everything already has: releasing twice is the
|
|
903
1018
|
// normal case and costs nothing. A reservation that leaked would
|
|
904
1019
|
// shrink this machine's ceiling for the life of the process.
|
|
@@ -928,5 +1043,5 @@ export function createWorkSessionTurns({
|
|
|
928
1043
|
}
|
|
929
1044
|
};
|
|
930
1045
|
|
|
931
|
-
return { processWorkTurns, workAnswering };
|
|
1046
|
+
return { processWorkTurns, workAnswering, settledHere };
|
|
932
1047
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.0",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|