flowviant 0.113.0 → 0.114.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +29 -3
- package/bin/lib/agentTaskTypes.mjs +10 -1
- package/bin/lib/artifactContracts.mjs +97 -2
- package/bin/lib/artifactPolicy.mjs +1 -1
- package/bin/lib/artifacts.mjs +34 -1
- package/bin/lib/claudePosture.mjs +7 -0
- package/bin/lib/claudeStream.mjs +14 -1
- package/bin/lib/credentials.mjs +9 -9
- package/bin/lib/fleetRoster.mjs +1 -1
- package/bin/lib/git.mjs +5 -5
- package/bin/lib/knowledgeLibrary.mjs +14 -8
- package/bin/lib/machineListing.mjs +4 -4
- package/bin/lib/noWindowEnv.mjs +104 -0
- package/bin/lib/projectToolRuntimes.mjs +78 -20
- package/bin/lib/projectTools.mjs +11 -3
- package/bin/lib/prompts.mjs +23 -12
- package/bin/lib/resumeLost.mjs +6 -0
- package/bin/lib/runTurn.mjs +78 -11
- package/bin/lib/runtimeCodex.mjs +91 -8
- package/bin/lib/runtimeLimits.mjs +22 -7
- package/bin/lib/runtimes.mjs +3 -0
- package/bin/lib/startProject.mjs +6 -6
- package/bin/lib/turnModel.mjs +120 -0
- package/bin/lib/turnProfile.mjs +24 -5
- package/bin/lib/work.mjs +2 -0
- package/bin/lib/workAgentCheck.mjs +10 -4
- package/bin/lib/workAgentPrecheck.mjs +4 -0
- package/bin/lib/workAgentTurnExecution.mjs +64 -2
- package/bin/lib/workAgentTurnOutcome.mjs +7 -4
- package/bin/lib/workAgentTurns.mjs +2 -1
- package/bin/lib/workSessionTurns.mjs +15 -0
- package/package.json +1 -1
|
@@ -235,7 +235,25 @@ async function lastRateLimits(file) {
|
|
|
235
235
|
*/
|
|
236
236
|
export async function learnCodexLimits(threadId, { env = process.env, now } = {}) {
|
|
237
237
|
try {
|
|
238
|
-
|
|
238
|
+
const file = await codexRolloutFile(threadId, { env });
|
|
239
|
+
if (!file) return false;
|
|
240
|
+
const rl = await lastRateLimits(file);
|
|
241
|
+
if (!rl) return false;
|
|
242
|
+
recordCodexRateLimits(rl, now ?? Date.now());
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* WHERE A CODEX THREAD'S ROLLOUT IS, or null — the one lookup (and its cache)
|
|
251
|
+
* every reader of that file shares: the plan windows above, and the model a
|
|
252
|
+
* turn ran on (turnModel.mjs, 2026-09-29). Never throws.
|
|
253
|
+
*/
|
|
254
|
+
export async function codexRolloutFile(threadId, { env = process.env } = {}) {
|
|
255
|
+
try {
|
|
256
|
+
if (typeof threadId !== 'string' || !CODEX_THREAD_RE.test(threadId)) return null;
|
|
239
257
|
const root = join(env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
240
258
|
const key = `${root}\0${threadId}`;
|
|
241
259
|
let file = rolloutPaths.get(key) ?? null;
|
|
@@ -249,16 +267,13 @@ export async function learnCodexLimits(threadId, { env = process.env, now } = {}
|
|
|
249
267
|
}
|
|
250
268
|
if (!file) {
|
|
251
269
|
file = await findRollout(root, threadId);
|
|
252
|
-
if (!file) return
|
|
270
|
+
if (!file) return null;
|
|
253
271
|
rolloutPaths.set(key, file);
|
|
254
272
|
if (rolloutPaths.size > ROLLOUT_PATHS_MAX) rolloutPaths.delete(rolloutPaths.keys().next().value);
|
|
255
273
|
}
|
|
256
|
-
|
|
257
|
-
if (!rl) return false;
|
|
258
|
-
recordCodexRateLimits(rl, now ?? Date.now());
|
|
259
|
-
return true;
|
|
274
|
+
return file;
|
|
260
275
|
} catch {
|
|
261
|
-
return
|
|
276
|
+
return null;
|
|
262
277
|
}
|
|
263
278
|
}
|
|
264
279
|
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -156,6 +156,9 @@ const PROFILE_NEEDS_MCP = {
|
|
|
156
156
|
// says rides the final JSON — so neither non-code posture needs one.
|
|
157
157
|
design: false,
|
|
158
158
|
research: false,
|
|
159
|
+
// …and the image posture's (0.114.0), for the same reason: no MCP on an
|
|
160
|
+
// agent turn.
|
|
161
|
+
image: false,
|
|
159
162
|
};
|
|
160
163
|
|
|
161
164
|
/**
|
package/bin/lib/startProject.mjs
CHANGED
|
@@ -33,7 +33,7 @@ async function reexecAfterLogin() {
|
|
|
33
33
|
|
|
34
34
|
// TWO ROWS THAT READ ALIKE ARE NOT A CHOICE. `projectRowLabel` adds the id and
|
|
35
35
|
// the connected date to the COLLIDING rows only — production held two projects
|
|
36
|
-
//
|
|
36
|
+
// with one name, both bound to the same checkout, so this picker
|
|
37
37
|
// offered two identical lines and the only way to answer was to guess. See
|
|
38
38
|
// credentials.mjs for why the suffix is not on every row. (That exact case is
|
|
39
39
|
// a refusal now rather than a picker — a directory serves one project — but
|
|
@@ -62,7 +62,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
62
62
|
// binds it, and a headless start with no unambiguous answer refuses in words
|
|
63
63
|
// that name every stored project and every way out. The one thing this block
|
|
64
64
|
// must never do is serve a project the resolution did not name — "it said
|
|
65
|
-
//
|
|
65
|
+
// contoso in my calendar repo" is the confusion this exists to end.
|
|
66
66
|
// A RESTART IS NOT A PERSON. `reexec` (update.mjs) inherits stdio, so an
|
|
67
67
|
// auto-updated daemon's child sees two TTYs; without this it would stop on the
|
|
68
68
|
// binding confirm below and the machine would stay dark until somebody typed a
|
|
@@ -122,7 +122,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
122
122
|
|
|
123
123
|
const loginLabel = `connect ${repoRoot ? 'this repo' : 'a repo'} to a different project (${terminalCommand('login')})`;
|
|
124
124
|
// WHICH ONE LOOKS RIGHT — a pre-selection, never an auto-serve. The resolver
|
|
125
|
-
// refuses to serve a project the repo PATH did not name (the
|
|
125
|
+
// refuses to serve a project the repo PATH did not name (the contoso law);
|
|
126
126
|
// this only decides which row the cursor starts on, using the repo's folder
|
|
127
127
|
// name and its github repo-name against the stored project names. A unique
|
|
128
128
|
// match becomes ONE keypress; a wrong guess costs nothing, because the human
|
|
@@ -136,7 +136,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
136
136
|
|
|
137
137
|
// Bounded like the confirm below, and for the same reason — but silence
|
|
138
138
|
// means something DIFFERENT here and the difference is load-bearing. There
|
|
139
|
-
// is a real ambiguity to resolve; serving a guess is the
|
|
139
|
+
// is a real ambiguity to resolve; serving a guess is the contoso bug.
|
|
140
140
|
// So no answer REFUSES, which is exactly what this branch already does
|
|
141
141
|
// headless, and the message says how to answer without being present.
|
|
142
142
|
let chosen = null; // 0-based into [...choices, login]
|
|
@@ -220,7 +220,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
220
220
|
} else if (!externalToken && CREDENTIAL.needsConfirm && interactive) {
|
|
221
221
|
// ONE stored project, never tied to a repo — the pre-0.55.0 world. Ask once;
|
|
222
222
|
// yes binds and every later start is silent. This is the exact question
|
|
223
|
-
// whose absence had a calendar checkout serving
|
|
223
|
+
// whose absence had a calendar checkout serving contoso.
|
|
224
224
|
//
|
|
225
225
|
// AND IT TIMES OUT, because a prompt on a start path is a way for a machine
|
|
226
226
|
// to go dark. `FLOWVIANT_REEXEC` above covers the restart THIS version
|
|
@@ -240,7 +240,7 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
240
240
|
// not, which is the whole reason this product has exactly one refusal.
|
|
241
241
|
// · DO NOT BIND, because binding is the thing the question was FOR. Nobody
|
|
242
242
|
// answered, so nothing is cemented; the next human start asks again. That
|
|
243
|
-
// keeps the
|
|
243
|
+
// keeps the contoso case fixed for the person who is actually looking,
|
|
244
244
|
// which is the only person it could ever have been fixed for.
|
|
245
245
|
const creds = await import('./credentials.mjs');
|
|
246
246
|
const label = creds.projectLabel(CREDENTIAL.entry);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH MODEL A TURN ACTUALLY RAN ON, IN THE CLI'S OWN WORDS (2026-09-29).
|
|
3
|
+
*
|
|
4
|
+
* The owner: "how do users know which model of opus are they using on
|
|
5
|
+
* flowviant? like opus 5, or opus 5.5". A pin reads as typed ("opus") or as
|
|
6
|
+
* the machine default, and `opus` is an alias the CLI resolves on its own
|
|
7
|
+
* cadence — so the only true answer is the one the CLI gives about the turn it
|
|
8
|
+
* just ran. This file reads it; the settle bodies carry it as `model`; the app
|
|
9
|
+
* shows it by name. A RELAY: nothing here maps an alias to a model or guesses.
|
|
10
|
+
*
|
|
11
|
+
* TWO SOURCES, ONE SHAPE:
|
|
12
|
+
*
|
|
13
|
+
* · CLAUDE CODE says it on the stream. MEASURED on Claude Code 2.1.284,
|
|
14
|
+
* 2026-09-29 (`claude -p "say hi" --model haiku --output-format
|
|
15
|
+
* stream-json --verbose`): the `system`/`init` event carries
|
|
16
|
+
* `model: "claude-haiku-4-5-20251001"`, and every top-level `assistant`
|
|
17
|
+
* event carries the same id as `message.model`. claudeStream.mjs hands both
|
|
18
|
+
* over; runTurn.mjs keeps the first valid one per spawn, so the init wins
|
|
19
|
+
* and the first reply is the fallback. A subagent's messages
|
|
20
|
+
* (`parent_tool_use_id` set) are not the turn's model and are not read; a
|
|
21
|
+
* synthetic error message says `<synthetic>`, which the shape refuses.
|
|
22
|
+
* · CODEX says it in a file. `codex exec --json` (0.156.1) puts no model on
|
|
23
|
+
* stdout — its ThreadEvent vocabulary is thread/turn/item lifecycle and
|
|
24
|
+
* token usage only — but the thread's rollout writes one `turn_context`
|
|
25
|
+
* line per turn whose `payload.model` is the model that turn resolved
|
|
26
|
+
* (`"model":"gpt-6-sol"`, beside `collaboration_mode.settings.model` and
|
|
27
|
+
* `effort`). After the child closes, runTurn.mjs asks
|
|
28
|
+
* `codexTurnModel` for the NEWEST `turn_context` — and only one written
|
|
29
|
+
* since the spawn, so a resumed thread whose turn died before its context
|
|
30
|
+
* was written never reports the previous turn's model as this one's.
|
|
31
|
+
*
|
|
32
|
+
* THREE STATES: a valid id is a measurement; null is "not measured" and the
|
|
33
|
+
* settle carries no key at all. There is no empty value.
|
|
34
|
+
*
|
|
35
|
+
* THE SHAPE is the app's (`packages/shared/src/schemas/turnModel.ts` exports
|
|
36
|
+
* `TURN_MODEL_MAX` and `TURN_MODEL_RE`; `scripts/check-app-parity.mjs` holds
|
|
37
|
+
* them equal): the server re-applies it, so a looser copy here would only
|
|
38
|
+
* have a model dropped at the boundary.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { open } from 'node:fs/promises';
|
|
42
|
+
import { codexRolloutFile } from './runtimeLimits.mjs';
|
|
43
|
+
|
|
44
|
+
/** The longest model id a settle carries. */
|
|
45
|
+
export const TURN_MODEL_MAX = 80;
|
|
46
|
+
/** One model id — the CLIs' own alphabet (`claude-opus-5-5`, `gpt-6-sol`,
|
|
47
|
+
* `us.anthropic.claude-…:0`, `org/model`). Anything else is not relayed. */
|
|
48
|
+
export const TURN_MODEL_RE = /^[A-Za-z0-9._:\-/]{1,80}$/;
|
|
49
|
+
|
|
50
|
+
/** A model id this daemon will relay, or null. */
|
|
51
|
+
export function turnModelOf(v) {
|
|
52
|
+
return typeof v === 'string' && TURN_MODEL_RE.test(v) ? v : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read backwards this much at a time… */
|
|
56
|
+
const ROLLOUT_CHUNK_BYTES = 256 * 1024;
|
|
57
|
+
/** …and no further than this: a turn's own lines past it are not searched. */
|
|
58
|
+
const ROLLOUT_MODEL_SCAN_MAX = 32 * 1024 * 1024;
|
|
59
|
+
/** A rollout stamp a little before the spawn still counts (clock rounding). */
|
|
60
|
+
const SINCE_SLACK_MS = 2_000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* THE MODEL OF THE NEWEST `turn_context` IN A CODEX THREAD'S ROLLOUT, when it
|
|
64
|
+
* was written at or after `since` (epoch ms); else null.
|
|
65
|
+
*
|
|
66
|
+
* Read BACKWARDS from the end in chunks, because the line sits at the START
|
|
67
|
+
* of its turn and a single turn's rollout runs to megabytes (2.8 MB measured
|
|
68
|
+
* for one agent turn). Only lines naming `"turn_context"` are parsed.
|
|
69
|
+
* Best-effort from end to end: it never throws.
|
|
70
|
+
*/
|
|
71
|
+
export async function codexTurnModel(threadId, { since = 0, env = process.env } = {}) {
|
|
72
|
+
let fh = null;
|
|
73
|
+
try {
|
|
74
|
+
const file = await codexRolloutFile(threadId, { env });
|
|
75
|
+
if (!file) return null;
|
|
76
|
+
fh = await open(file, 'r');
|
|
77
|
+
const { size } = await fh.stat();
|
|
78
|
+
let pos = size;
|
|
79
|
+
// The bytes of a line whose start is in a chunk not read yet.
|
|
80
|
+
let rest = Buffer.alloc(0);
|
|
81
|
+
while (pos > 0 && size - pos < ROLLOUT_MODEL_SCAN_MAX) {
|
|
82
|
+
const len = Math.min(ROLLOUT_CHUNK_BYTES, pos);
|
|
83
|
+
pos -= len;
|
|
84
|
+
const chunk = Buffer.alloc(len);
|
|
85
|
+
await fh.read(chunk, 0, len, pos);
|
|
86
|
+
let data = Buffer.concat([chunk, rest]);
|
|
87
|
+
if (pos > 0) {
|
|
88
|
+
const nl = data.indexOf(0x0a);
|
|
89
|
+
if (nl < 0) {
|
|
90
|
+
rest = data;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
rest = data.subarray(0, nl);
|
|
94
|
+
data = data.subarray(nl + 1);
|
|
95
|
+
}
|
|
96
|
+
if (!data.includes('"turn_context"')) continue;
|
|
97
|
+
const lines = data.toString('utf8').split('\n');
|
|
98
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
99
|
+
if (!lines[i].includes('"turn_context"')) continue;
|
|
100
|
+
let ev;
|
|
101
|
+
try {
|
|
102
|
+
ev = JSON.parse(lines[i]);
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ev?.type !== 'turn_context') continue;
|
|
107
|
+
// The NEWEST context decides, whatever it says: an older one is a
|
|
108
|
+
// previous turn's.
|
|
109
|
+
const at = Date.parse(ev.timestamp ?? '');
|
|
110
|
+
if (!Number.isFinite(at) || at < since - SINCE_SLACK_MS) return null;
|
|
111
|
+
return turnModelOf(ev.payload?.model);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
} finally {
|
|
118
|
+
await fh?.close().catch(() => {});
|
|
119
|
+
}
|
|
120
|
+
}
|
package/bin/lib/turnProfile.mjs
CHANGED
|
@@ -19,15 +19,29 @@
|
|
|
19
19
|
* - plan-mode — a Workbench plan turn: Claude Code's own
|
|
20
20
|
* `--permission-mode plan`, REPLACING the build posture, no MCP.
|
|
21
21
|
* Claude only. The adapter sees `build` (argv is `perm`);
|
|
22
|
-
* - design / research — a non-code card's posture
|
|
23
|
-
* refused on any runtime that does not
|
|
24
|
-
* other adapter's fallback is its
|
|
22
|
+
* - design / research / image — a non-code card's posture
|
|
23
|
+
* (agentTaskKinds.mjs), refused on any runtime that does not
|
|
24
|
+
* DECLARE it, because every other adapter's fallback is its
|
|
25
|
+
* build branch. design and research are Claude Code's; image
|
|
26
|
+
* (0.114.0) is Codex's.
|
|
25
27
|
*
|
|
26
28
|
* Claude's permission list for each name lives beside the lists themselves
|
|
27
29
|
* (`claudePermFor`, claudePosture.mjs); this module owns the vocabulary and the
|
|
28
|
-
* refusals, and imports
|
|
30
|
+
* refusals, and imports the card-kind table and the runtime registry (whose
|
|
31
|
+
* declared `profiles` say which CLI a kind posture runs on).
|
|
29
32
|
*/
|
|
30
33
|
import { KIND_POSTURES } from './agentTaskKinds.mjs';
|
|
34
|
+
import { RUNTIMES } from './runtimes.mjs';
|
|
35
|
+
|
|
36
|
+
/** The CLIs that DECLARE a posture, by label — "Claude Code", "Codex". Read
|
|
37
|
+
* from the registry, so the refusal names whoever can run it, never a CLI
|
|
38
|
+
* written into the sentence by hand. */
|
|
39
|
+
const declaredBy = (name) =>
|
|
40
|
+
Object.values(RUNTIMES)
|
|
41
|
+
.filter((rt) => (rt.profiles ?? []).includes(name))
|
|
42
|
+
.map((rt) => rt.label)
|
|
43
|
+
.join(' or ') || 'no CLI this daemon knows';
|
|
44
|
+
const article = (word) => (/^[aeiou]/i.test(word) ? 'an' : 'a');
|
|
31
45
|
|
|
32
46
|
const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false });
|
|
33
47
|
|
|
@@ -46,12 +60,17 @@ export const TURN_PROFILES = Object.freeze({
|
|
|
46
60
|
}),
|
|
47
61
|
// The kind postures: fenced to "write only the artifacts directory", and a
|
|
48
62
|
// runtime that does not declare one would run its build branch instead.
|
|
63
|
+
// The sentence names the CLI(s) that do declare it: "a design card runs on
|
|
64
|
+
// Claude Code only", "an image card runs on Codex only".
|
|
49
65
|
...Object.fromEntries(
|
|
50
66
|
KIND_POSTURES.map((name) => [
|
|
51
67
|
name,
|
|
52
68
|
Object.freeze({
|
|
53
69
|
adapterProfile: name,
|
|
54
|
-
onlyOn: (rt) =>
|
|
70
|
+
onlyOn: (rt) =>
|
|
71
|
+
(rt.profiles ?? []).includes(name)
|
|
72
|
+
? null
|
|
73
|
+
: `${article(name)} ${name} card runs on ${declaredBy(name)} only — not '${rt.label}'`,
|
|
55
74
|
strictMcp: false,
|
|
56
75
|
}),
|
|
57
76
|
])
|
package/bin/lib/work.mjs
CHANGED
|
@@ -330,6 +330,8 @@ export function createWorkManager({
|
|
|
330
330
|
postBestEffort,
|
|
331
331
|
workChildren,
|
|
332
332
|
groupKillChildren,
|
|
333
|
+
// Only to find the agent's browser profile (noWindowEnv.mjs).
|
|
334
|
+
sessionMetaPath,
|
|
333
335
|
});
|
|
334
336
|
const { runPrecheck } = createWorkAgentPrecheck({
|
|
335
337
|
baseRef,
|
|
@@ -17,6 +17,7 @@ import { warn } from './ui.mjs';
|
|
|
17
17
|
import { ARTIFACT_DIR, artifactTypeFor } from './artifacts.mjs';
|
|
18
18
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
19
19
|
import { withoutMachineCredentials } from './machineEnv.mjs';
|
|
20
|
+
import { agentBrowserHome, openBrowserHome, withoutWindows } from './noWindowEnv.mjs';
|
|
20
21
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
21
22
|
import { workLanesStopped } from './standDownGate.mjs';
|
|
22
23
|
|
|
@@ -85,11 +86,14 @@ export function clearNonArtifacts(wt) {
|
|
|
85
86
|
* authenticates with; everything else is what the agent's own turn saw when it
|
|
86
87
|
* ran the same tests.
|
|
87
88
|
*/
|
|
88
|
-
export function checkEnv(env = process.env) {
|
|
89
|
-
|
|
89
|
+
export function checkEnv(env = process.env, browserHome = null) {
|
|
90
|
+
// …and, like that turn since 2026-09-29, no way to a window: a test run
|
|
91
|
+
// nobody is watching that opens a headed browser or an HTML report opens it
|
|
92
|
+
// on nobody's behalf (noWindowEnv.mjs). The agent's own browser profile.
|
|
93
|
+
return withoutWindows(withoutMachineCredentials(env), browserHome);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, groupKillChildren }) {
|
|
96
|
+
export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, groupKillChildren, sessionMetaPath }) {
|
|
93
97
|
const AGENT_CHECK_DONE_URL = fleetEndpoint('agent-check-done', FLEET_URL);
|
|
94
98
|
|
|
95
99
|
// ── THE PROJECT'S OWN CHECK, and the MERGE ─────────────────────────────────
|
|
@@ -142,6 +146,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
142
146
|
warn(
|
|
143
147
|
`agent ${agentId}: removed ${cleared.length} non-artifact file(s) from ${ARTIFACT_DIR} before the check — ${cleared.slice(0, 5).join(', ')}`
|
|
144
148
|
);
|
|
149
|
+
const browser = openBrowserHome(agentBrowserHome(sessionMetaPath, wt, agentId));
|
|
145
150
|
const out = await new Promise((resolve) => {
|
|
146
151
|
let text = '';
|
|
147
152
|
let done = false;
|
|
@@ -178,7 +183,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
178
183
|
shell: true,
|
|
179
184
|
detached: true,
|
|
180
185
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
-
env: checkEnv(),
|
|
186
|
+
env: checkEnv(process.env, browser.dir),
|
|
182
187
|
});
|
|
183
188
|
/**
|
|
184
189
|
* …AND IT IS TRACKED, so a stop or a takeover takes it with them.
|
|
@@ -266,6 +271,7 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
266
271
|
finish(code === 0 ? 'passed' : 'failed');
|
|
267
272
|
});
|
|
268
273
|
});
|
|
274
|
+
browser.cleanup();
|
|
269
275
|
await postCheck({
|
|
270
276
|
agentId,
|
|
271
277
|
status: out.status,
|
|
@@ -20,6 +20,7 @@ import { removeProbeTranscript } from './runtimeCapabilities.mjs';
|
|
|
20
20
|
import { taskIdsFromMessage } from './worktreeDiff.mjs';
|
|
21
21
|
import { limitLine } from './cliLimit.mjs';
|
|
22
22
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
23
|
+
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
23
24
|
|
|
24
25
|
export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workChildren }) {
|
|
25
26
|
const AGENT_PRECHECK_URL = fleetEndpoint('agent-precheck', FLEET_URL);
|
|
@@ -257,6 +258,9 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
257
258
|
profile: 'consult',
|
|
258
259
|
cwd: wt,
|
|
259
260
|
runtime: rt,
|
|
261
|
+
// The agent's own browser profile, in the worktree this reader stands
|
|
262
|
+
// in; no display either way (noWindowEnv.mjs, 2026-09-29).
|
|
263
|
+
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
260
264
|
// NO `resume`, AND THAT IS THE FEATURE. A resumed turn would be the
|
|
261
265
|
// agent grading its own homework out of its own context; this is a
|
|
262
266
|
// stranger reading a diff.
|
|
@@ -37,6 +37,8 @@ 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';
|
|
41
|
+
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
40
42
|
|
|
41
43
|
export function createAgentTurnExecution({
|
|
42
44
|
baseRef,
|
|
@@ -227,6 +229,25 @@ export function createAgentTurnExecution({
|
|
|
227
229
|
});
|
|
228
230
|
return;
|
|
229
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* A NON-CODE CARD WRITES ONE DIRECTORY, SO IT STANDS FIRST (0.114.0).
|
|
234
|
+
* Codex's image fence makes `.flowviant/artifacts` its only writable root
|
|
235
|
+
* and cannot create it from inside; every kind posture writes nowhere
|
|
236
|
+
* else. Made only as a real directory under a real `.flowviant`
|
|
237
|
+
* (`ensureArtifactDir`): a committed `.flowviant` symlink would otherwise
|
|
238
|
+
* point the fence's one write grant at somewhere outside the worktree. A
|
|
239
|
+
* worktree where it cannot stand is settled in words, before any CLI runs.
|
|
240
|
+
*/
|
|
241
|
+
if (kind.artifact && !ensureArtifactDir(wt)) {
|
|
242
|
+
await postAgentTurn({
|
|
243
|
+
turnId,
|
|
244
|
+
outcome: 'nothing',
|
|
245
|
+
answer: "this worktree's .flowviant/artifacts is not a real directory, so there is nowhere to write what the card hands back",
|
|
246
|
+
branch,
|
|
247
|
+
worktree: wt,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
230
251
|
|
|
231
252
|
// ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly
|
|
232
253
|
// right here — the ambiguity that forced per-tab session pinning in the
|
|
@@ -266,6 +287,23 @@ export function createAgentTurnExecution({
|
|
|
266
287
|
rt === 'codex'
|
|
267
288
|
? Boolean(codexResumeId)
|
|
268
289
|
: rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
290
|
+
/**
|
|
291
|
+
* …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
|
|
292
|
+
*
|
|
293
|
+
* Codex's thread index records each rollout under the CODEX_HOME the turn
|
|
294
|
+
* ran with, as that path — not the personal store the link leads to — so
|
|
295
|
+
* the per-turn home in /tmp every turn used to get died with its turn and
|
|
296
|
+
* took the pinned thread with it: the next card's `resume` failed "no
|
|
297
|
+
* rollout found" and the agent went to Stuck. Beside the thread marker,
|
|
298
|
+
* in the worktree's private git dir: outside the working tree (never
|
|
299
|
+
* committed, never seen by `git add -A`), refreshed by every turn and
|
|
300
|
+
* removed only with the worktree, when the agent retires and its threads
|
|
301
|
+
* stop mattering. One agent runs one turn at a time (its place's writer
|
|
302
|
+
* lock), so no two turns refresh it at once. Null (no git dir to hold it)
|
|
303
|
+
* is the per-turn home again, and a lost resume then runs once fresh.
|
|
304
|
+
*/
|
|
305
|
+
const codexAgentHome =
|
|
306
|
+
rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-home', agentId) : null;
|
|
269
307
|
|
|
270
308
|
/**
|
|
271
309
|
* THE TURN'S FILES, ON DISK BEFORE THE PROMPT NAMES THEM (0.112.0) — the
|
|
@@ -356,6 +394,15 @@ export function createAgentTurnExecution({
|
|
|
356
394
|
|
|
357
395
|
let out = '';
|
|
358
396
|
let child = null;
|
|
397
|
+
/**
|
|
398
|
+
* EVERY CHILD THIS TURN SPAWNED, not only the last (2026-09-29). A lost
|
|
399
|
+
* resume runs once more fresh (runTurnResumingOnce), so a turn can own two
|
|
400
|
+
* CLIs, and `child` names only the second: the first stayed in
|
|
401
|
+
* `workChildren` for the life of the daemon — one phantom live turn per
|
|
402
|
+
* retry, held against the machine's ceiling, and `workBusy()` true for
|
|
403
|
+
* ever, which holds the self-update and the tray's install for ever.
|
|
404
|
+
*/
|
|
405
|
+
const spawnedChildren = new Set();
|
|
359
406
|
/**
|
|
360
407
|
* WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
|
|
361
408
|
*
|
|
@@ -371,6 +418,9 @@ export function createAgentTurnExecution({
|
|
|
371
418
|
* what makes `...(usage ? … : {})` the whole guard.
|
|
372
419
|
*/
|
|
373
420
|
let usage = null;
|
|
421
|
+
/** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
|
|
422
|
+
* every settle `usage` rides, for the same reason; null sends no key. */
|
|
423
|
+
let model = null;
|
|
374
424
|
/**
|
|
375
425
|
* THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
|
|
376
426
|
* Codex's `out` is every message plus stderr plus error text, and
|
|
@@ -386,7 +436,7 @@ export function createAgentTurnExecution({
|
|
|
386
436
|
try {
|
|
387
437
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
388
438
|
// own worktree cannot silently change the next turn's tools.
|
|
389
|
-
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt);
|
|
439
|
+
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt, { codexHome: codexAgentHome });
|
|
390
440
|
const agentTurnArgs = {
|
|
391
441
|
prompt:
|
|
392
442
|
job.kind === 'task' && job.task
|
|
@@ -427,6 +477,11 @@ export function createAgentTurnExecution({
|
|
|
427
477
|
}) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
428
478
|
agentTools: projectTools,
|
|
429
479
|
...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
|
|
480
|
+
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
481
|
+
// a browser profile of the agent's own, kept beside its Codex home —
|
|
482
|
+
// so a Chrome it starts to look at its page never reaches the
|
|
483
|
+
// person's (noWindowEnv.mjs).
|
|
484
|
+
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
430
485
|
...(posture === 'build' && rt === 'claude'
|
|
431
486
|
? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
|
|
432
487
|
: posture === 'build' && rt === 'codex'
|
|
@@ -493,6 +548,9 @@ export function createAgentTurnExecution({
|
|
|
493
548
|
onUsage: (u) => {
|
|
494
549
|
usage = { ...u, runtime: rt };
|
|
495
550
|
},
|
|
551
|
+
onModel: (m) => {
|
|
552
|
+
model = m;
|
|
553
|
+
},
|
|
496
554
|
// WHAT THE CLI SAYS IT CAN BE ASKED FOR, AND WHICH CONNECTORS NEED A
|
|
497
555
|
// LOGIN — the same free fact off the same init event the tab lane
|
|
498
556
|
// records (2026-09-23). Without it a box that only ever runs agents
|
|
@@ -505,6 +563,7 @@ export function createAgentTurnExecution({
|
|
|
505
563
|
},
|
|
506
564
|
onSpawn: (ch) => {
|
|
507
565
|
child = ch;
|
|
566
|
+
spawnedChildren.add(ch);
|
|
508
567
|
// The AGENT it serves — what the machine snapshot charges this
|
|
509
568
|
// child's memory to.
|
|
510
569
|
workChildren.set(ch, agentId);
|
|
@@ -539,11 +598,13 @@ export function createAgentTurnExecution({
|
|
|
539
598
|
beforeFresh: () => {
|
|
540
599
|
seenThreadId = null;
|
|
541
600
|
lastAnswer = null;
|
|
601
|
+
model = null;
|
|
542
602
|
},
|
|
543
603
|
});
|
|
544
604
|
} finally {
|
|
545
605
|
// What the CLI counted, for the lane's catch if anything below throws.
|
|
546
606
|
measured.usage = usage;
|
|
607
|
+
measured.model = model;
|
|
547
608
|
projectTools?.cleanup();
|
|
548
609
|
/**
|
|
549
610
|
* THE TAIL, BEFORE THE SETTLE — so the last thing the agent did is on
|
|
@@ -557,7 +618,7 @@ export function createAgentTurnExecution({
|
|
|
557
618
|
*/
|
|
558
619
|
trace.stop();
|
|
559
620
|
await trace.flush(TRACE_FINAL_FLUSH_MS);
|
|
560
|
-
|
|
621
|
+
for (const ch of spawnedChildren) workChildren.delete(ch);
|
|
561
622
|
if (agentChildren.get(place) === child) agentChildren.delete(place);
|
|
562
623
|
if (ranMarker && rt === 'claude') {
|
|
563
624
|
try {
|
|
@@ -610,6 +671,7 @@ export function createAgentTurnExecution({
|
|
|
610
671
|
out,
|
|
611
672
|
lastAnswer,
|
|
612
673
|
usage,
|
|
674
|
+
model,
|
|
613
675
|
commits,
|
|
614
676
|
artifactsBefore,
|
|
615
677
|
branch,
|
|
@@ -26,7 +26,7 @@ import { parseTurnResult } from './agentPlan.mjs';
|
|
|
26
26
|
import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
|
|
27
27
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
28
28
|
|
|
29
|
-
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, commits, artifactsBefore, branch, wt }) {
|
|
29
|
+
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
|
|
30
30
|
const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
|
|
31
31
|
/**
|
|
32
32
|
* A LIMIT IS ONLY A LIMIT WHEN THE TURN PRODUCED NOTHING.
|
|
@@ -40,8 +40,10 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
40
40
|
*/
|
|
41
41
|
const limit = res ? null : limitLine(out);
|
|
42
42
|
/** What the caller posts, and whether it is the one settle that may say
|
|
43
|
-
* delivered (the only one whose reply can open the review-entry beat).
|
|
44
|
-
|
|
43
|
+
* delivered (the only one whose reply can open the review-entry beat).
|
|
44
|
+
* Every one carries the model the CLI said it ran on, when it said
|
|
45
|
+
* (turnModel.mjs) — a turn that stopped still ran on something. */
|
|
46
|
+
const report = (body, final = false) => ({ res, limit, body: model ? { ...body, model } : body, final });
|
|
45
47
|
if (limit) {
|
|
46
48
|
// The turn itself is reported as `nothing` — it did not deliver and it
|
|
47
49
|
// did not ask. The park that goes with it is the caller's side effect
|
|
@@ -193,7 +195,7 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
193
195
|
* never lies by omission), the spend if the CLI counted one, and the branch
|
|
194
196
|
* and worktree when a worktree existed. Nothing measured, nothing claimed.
|
|
195
197
|
*/
|
|
196
|
-
export function failedTurnSettlement({ turnId, error, commits = [], usage = null, branch = null, wt = null }) {
|
|
198
|
+
export function failedTurnSettlement({ turnId, error, commits = [], usage = null, model = null, branch = null, wt = null }) {
|
|
197
199
|
// An Error's message, or a thrown string; anything else says nothing rather
|
|
198
200
|
// than "[object Object]".
|
|
199
201
|
const why = (typeof error === 'string' ? error : typeof error?.message === 'string' ? error.message : '').trim();
|
|
@@ -205,6 +207,7 @@ export function failedTurnSettlement({ turnId, error, commits = [], usage = null
|
|
|
205
207
|
).slice(0, 1500),
|
|
206
208
|
...(commits.length ? { commits } : {}),
|
|
207
209
|
...(usage ? { usage } : {}),
|
|
210
|
+
...(model ? { model } : {}),
|
|
208
211
|
...(wt ? { branch, worktree: wt } : {}),
|
|
209
212
|
};
|
|
210
213
|
}
|
|
@@ -162,7 +162,7 @@ export function createWorkAgentTurns({
|
|
|
162
162
|
* wire, so a failed POST is re-sent on the next offer and the CLI is not
|
|
163
163
|
* run again.
|
|
164
164
|
*/
|
|
165
|
-
const measured = { wt: null, before: null, branch: null, usage: null };
|
|
165
|
+
const measured = { wt: null, before: null, branch: null, usage: null, model: null };
|
|
166
166
|
let settled = false;
|
|
167
167
|
const settleOnce = (body) => {
|
|
168
168
|
settled = true;
|
|
@@ -184,6 +184,7 @@ export function createWorkAgentTurns({
|
|
|
184
184
|
error,
|
|
185
185
|
commits,
|
|
186
186
|
usage: measured.usage,
|
|
187
|
+
model: measured.model,
|
|
187
188
|
branch: measured.branch,
|
|
188
189
|
wt: measured.wt,
|
|
189
190
|
})
|
|
@@ -174,6 +174,10 @@ export function createWorkSessionTurns({
|
|
|
174
174
|
* sits in the retry queue and is posted again adds nothing.
|
|
175
175
|
*/
|
|
176
176
|
let usage = null;
|
|
177
|
+
/** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) —
|
|
178
|
+
* out here beside `usage`, and spread wherever it is. Last write wins
|
|
179
|
+
* across a fresh retry: that spawn is the one that answered. */
|
|
180
|
+
let model = null;
|
|
177
181
|
try {
|
|
178
182
|
const tries = workAttempts.get(job.id) ?? 0;
|
|
179
183
|
if (tries >= MAX_WORK_TRIES) {
|
|
@@ -666,6 +670,9 @@ export function createWorkSessionTurns({
|
|
|
666
670
|
onUsage: (u) => {
|
|
667
671
|
usage = addUsage(usage, { ...u, runtime: rt.id });
|
|
668
672
|
},
|
|
673
|
+
onModel: (m) => {
|
|
674
|
+
model = m;
|
|
675
|
+
},
|
|
669
676
|
// What this CLI says it can be asked for by name. Harvested off
|
|
670
677
|
// the init event the stream already carries — no probe, no scan,
|
|
671
678
|
// no extra spawn — and reported on the next roster poll so the
|
|
@@ -687,6 +694,11 @@ export function createWorkSessionTurns({
|
|
|
687
694
|
cwd: dir.wt,
|
|
688
695
|
mcpArgs: mcp.args,
|
|
689
696
|
mcpEnv: mcp.env,
|
|
697
|
+
// A PERSON AT THE KEYBOARD MAY ASK FOR A WINDOW (2026-09-29): a
|
|
698
|
+
// Terminal tab keeps its environment's display. The capture chat
|
|
699
|
+
// does not — it only reads and stages cards, and the person
|
|
700
|
+
// talking to it may be on a phone (noWindowEnv.mjs).
|
|
701
|
+
display: !captureTab,
|
|
690
702
|
runtime: rt.id,
|
|
691
703
|
label: c.cyan('[tab]'),
|
|
692
704
|
// Only codex announces one (thread.started); held here so the id
|
|
@@ -827,6 +839,7 @@ export function createWorkSessionTurns({
|
|
|
827
839
|
...(toolLog.ev.length > 0 ? { tools: toolLog } : {}),
|
|
828
840
|
// …and an empty fork still spent what it spent.
|
|
829
841
|
...(usage ? { usage } : {}),
|
|
842
|
+
...(model ? { model } : {}),
|
|
830
843
|
});
|
|
831
844
|
warn('adopt turn produced no output — settled as failed');
|
|
832
845
|
return;
|
|
@@ -865,6 +878,7 @@ export function createWorkSessionTurns({
|
|
|
865
878
|
// at settle). Already scrubbed at collection.
|
|
866
879
|
...(toolLog.ev.length > 0 ? { tools: toolLog } : {}),
|
|
867
880
|
...(usage ? { usage } : {}),
|
|
881
|
+
...(model ? { model } : {}),
|
|
868
882
|
});
|
|
869
883
|
if (answer.length > 0) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
870
884
|
else warn('session turn produced no output — settled as failed');
|
|
@@ -880,6 +894,7 @@ export function createWorkSessionTurns({
|
|
|
880
894
|
...(toolLog?.ev.length > 0 ? { tools: toolLog } : {}),
|
|
881
895
|
// A turn that threw after its CLI ran still spent what it spent.
|
|
882
896
|
...(usage ? { usage } : {}),
|
|
897
|
+
...(model ? { model } : {}),
|
|
883
898
|
});
|
|
884
899
|
warn(`session turn failed: ${e?.message ?? e}`);
|
|
885
900
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.114.1",
|
|
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": {
|