flowviant 0.125.5 → 0.125.7
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 +4 -5
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +11 -6
- package/bin/lib/mcpReasons.mjs +26 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +221 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AN AGENT TURN'S PROMPT PAIR — its kickoff, a live message's follow-up and
|
|
3
|
+
* its system prompt (pure, 2026-10-18, the SOLID pass). The twin of
|
|
4
|
+
* workTurnShape.mjs, which is the same thing for a tab.
|
|
5
|
+
*
|
|
6
|
+
* Split out of workAgentTurnExecution.mjs, which composed all three inline in
|
|
7
|
+
* the turn: two AGENT_HUMAN_KICKOFF argument bags spelling the same ten
|
|
8
|
+
* fields, the task kickoff beside them, and a system prompt of eight sections
|
|
9
|
+
* on one line. The copies had already drifted: investigationContext reached
|
|
10
|
+
* the task and human kickoffs on 2026-10-12, the follow-up written two days
|
|
11
|
+
* later went without it, and live messages carried less context than the
|
|
12
|
+
* first prompt until 2026-10-15 added it beside researchContext at all three
|
|
13
|
+
* sites. So the fields every kickoff shares are spelled once (`shared`), and
|
|
14
|
+
* the next job context key is one edit here.
|
|
15
|
+
*
|
|
16
|
+
* ONE SPEC BUILDER (`AGENT_TASK_SPEC`) is shared by the stash the run writes
|
|
17
|
+
* before the CLI and the kickoff here, so what the reviewer reads is
|
|
18
|
+
* byte-identical to what the agent read — the card's discussion included
|
|
19
|
+
* (0.106.0): both are handed the same `job.cardThread`, and the builder
|
|
20
|
+
* prints it inside the spec — and the card's files after it (0.112.0).
|
|
21
|
+
*
|
|
22
|
+
* Pure: the run measures (the files, the roster's answers, the tools note)
|
|
23
|
+
* and hands the values in; nothing here reads a disk or a clock.
|
|
24
|
+
*/
|
|
25
|
+
import { AGENT_TASK_KICKOFF, AGENT_HUMAN_KICKOFF, SYSTEM_AGENT_FOR, withProjectContext } from './prompts.mjs';
|
|
26
|
+
import { INTERNAL_WAIT_CONTRACT } from './agentInternalWait.mjs';
|
|
27
|
+
import { ORCHESTRATOR_CONTRACT } from './orchestratorPrompt.mjs';
|
|
28
|
+
|
|
29
|
+
/** What every kickoff of this job is handed: the agent, the card and its
|
|
30
|
+
* discussion, the job's context handoffs, the card's files, the place in
|
|
31
|
+
* the run. */
|
|
32
|
+
const shared = (job, cardFiles) => ({
|
|
33
|
+
agentName: job.agentName,
|
|
34
|
+
task: job.task,
|
|
35
|
+
cardThread: job.cardThread,
|
|
36
|
+
investigationContext: job.investigationContext,
|
|
37
|
+
researchContext: job.researchContext,
|
|
38
|
+
cardFiles,
|
|
39
|
+
position: job.position ?? 1,
|
|
40
|
+
total: job.total ?? 1,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** The turn's first prompt: the card, on a task turn; the person's words
|
|
44
|
+
* otherwise. `cardFiles` and `messageFiles` are the files already on disk. */
|
|
45
|
+
export const agentKickoff = (job, { cardFiles, messageFiles }) =>
|
|
46
|
+
job.kind === 'task' && job.task
|
|
47
|
+
? AGENT_TASK_KICKOFF({
|
|
48
|
+
// The card's discussion (0.106.0), printed inside THE CARD by
|
|
49
|
+
// the same builder the run's stash wrote down — and the files
|
|
50
|
+
// on it (0.112.0), after it, the same way.
|
|
51
|
+
...shared(job, cardFiles),
|
|
52
|
+
resumeBody: job.body,
|
|
53
|
+
})
|
|
54
|
+
: AGENT_HUMAN_KICKOFF({
|
|
55
|
+
// The discussion on the one card this message is about, in a
|
|
56
|
+
// fence of its own after the person's words (0.106.0).
|
|
57
|
+
...shared(job, cardFiles),
|
|
58
|
+
// INTERRUPT NOW: the previous run was stopped to send this.
|
|
59
|
+
interrupted: job.interrupted === true,
|
|
60
|
+
message: job.body ?? '',
|
|
61
|
+
askedByName: job.askedByName,
|
|
62
|
+
// The files (0.112.0): the person's inside their words, the
|
|
63
|
+
// card's at the foot of its discussion.
|
|
64
|
+
messageFiles,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** A live message sent while the turn runs, as the CLI is handed it — on an
|
|
68
|
+
* orchestrated card with its id, so the route it takes is reported. */
|
|
69
|
+
export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, orchestrated }) =>
|
|
70
|
+
AGENT_HUMAN_KICKOFF({
|
|
71
|
+
...shared(job, cardFiles),
|
|
72
|
+
message: followup.body + (followup.libraryPrompt ?? '') + (orchestrated ? `\nMessage id: ${followup.id}. Choose and report its route with card_orchestrator.route_message.` : ''),
|
|
73
|
+
askedByName: followup.askedByName,
|
|
74
|
+
messageFiles,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** The turn's system prompt, in its fixed section order. */
|
|
78
|
+
export function agentTurnSystem({ taskKind, runtimeLabel, prerequisitesAccepted, knowledgeDir, artifactsAccepted, toolsNote, artifactInstructions, toolInstructions, repositoryRun, orchestrated }) {
|
|
79
|
+
// The knowledge paragraph, when this box holds a library — the same
|
|
80
|
+
// composer the tabs use, so an agent and a tab can never be told two
|
|
81
|
+
// different things about the same directory.
|
|
82
|
+
// Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
|
|
83
|
+
// told it is Codex, never Claude.
|
|
84
|
+
return withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeLabel, { prerequisitesAccepted }), {
|
|
85
|
+
knowledgeDir,
|
|
86
|
+
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
87
|
+
// land on its page, under the facts row.
|
|
88
|
+
artifacts: artifactsAccepted,
|
|
89
|
+
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (toolInstructions ? `\n\n${toolInstructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : '') + (orchestrated ? `\n\n${ORCHESTRATOR_CONTRACT}` : '');
|
|
90
|
+
}
|
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync } from 'node:fs';
|
|
23
23
|
import { isSafePathSegment } from './git.mjs';
|
|
24
|
+
import { AGENT_PLACE_PREFIX } from './workPlaces.mjs';
|
|
24
25
|
import { c, note } from './ui.mjs';
|
|
25
26
|
import { createAgentTurnReports } from './workAgentTurnReports.mjs';
|
|
26
27
|
import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
|
|
27
|
-
import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import { failedTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
29
|
+
import { turnCommits } from './turnReceipts.mjs';
|
|
30
|
+
import { interruptThenKill } from './processGroup.mjs';
|
|
29
31
|
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
30
32
|
import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
|
|
31
33
|
|
|
@@ -54,12 +56,15 @@ export function createWorkAgentTurns({
|
|
|
54
56
|
/** The turn's files, fetched into its worktree (workAttachments.mjs, 0.112.0). */
|
|
55
57
|
fetchAgentFiles,
|
|
56
58
|
makeBrief = createAgentBriefRunner({ admit, workChildren }),
|
|
59
|
+
/** The run's live-message relay (workLiveMessages.mjs); a test's stand-in. */
|
|
60
|
+
makeLiveMessages,
|
|
57
61
|
}) {
|
|
58
62
|
const reportDelivery = createDeliveryReporter();
|
|
59
63
|
const {
|
|
60
64
|
postAgentTurn,
|
|
61
65
|
postAgentTrace,
|
|
62
66
|
postAgentActivity,
|
|
67
|
+
postAgentContext,
|
|
63
68
|
postAgentParked,
|
|
64
69
|
pruneHeldReports,
|
|
65
70
|
agentReported,
|
|
@@ -111,7 +116,7 @@ export function createWorkAgentTurns({
|
|
|
111
116
|
* read before the stop's settle landed. Taken once, for a little while. */
|
|
112
117
|
const stoppedFor = new Map(); // agentId -> when
|
|
113
118
|
|
|
114
|
-
const { runAgentTurnInPlace
|
|
119
|
+
const { runAgentTurnInPlace } = createAgentTurnExecution({
|
|
115
120
|
admit,
|
|
116
121
|
agentRepositories,
|
|
117
122
|
makeBrief,
|
|
@@ -131,8 +136,10 @@ export function createWorkAgentTurns({
|
|
|
131
136
|
agentChildren,
|
|
132
137
|
postAgentTrace,
|
|
133
138
|
postAgentActivity,
|
|
139
|
+
postAgentContext,
|
|
134
140
|
postAgentParked,
|
|
135
141
|
fetchAgentFiles,
|
|
142
|
+
makeLiveMessages,
|
|
136
143
|
});
|
|
137
144
|
|
|
138
145
|
/** `releaseSlot` hands back the admission reservation the caller took on this
|
|
@@ -183,33 +190,42 @@ export function createWorkAgentTurns({
|
|
|
183
190
|
* branch, the spend, the branch and worktree. Through the same held-body
|
|
184
191
|
* wire, so a failed POST is re-sent on the next offer and the CLI is not
|
|
185
192
|
* run again.
|
|
193
|
+
*
|
|
194
|
+
* The catch carries what the run's own settles carry, the live receipts
|
|
195
|
+
* included (2026-10-18): a note the CLI already took is folded into this
|
|
196
|
+
* run only when a settle names it, and the relay's own flush is bounded,
|
|
197
|
+
* so a catch that posted without them left the note pending — offered
|
|
198
|
+
* later as its own turn, a second paid run.
|
|
186
199
|
*/
|
|
187
200
|
const measured = { wt: null, before: null, branch: null, usage: null, model: null };
|
|
188
201
|
let settled = false;
|
|
202
|
+
const withLiveReceipts = (body) => {
|
|
203
|
+
const liveMessages = agentRuns.get(turnId)?.live?.receipts();
|
|
204
|
+
return { ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) };
|
|
205
|
+
};
|
|
189
206
|
const settleOnce = (body) => {
|
|
190
207
|
settled = true;
|
|
191
|
-
|
|
192
|
-
return postAgentTurn({ ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) });
|
|
208
|
+
return postAgentTurn(withLiveReceipts(body));
|
|
193
209
|
};
|
|
194
210
|
|
|
195
211
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
196
212
|
// places: those are agents' by construction, and anything else here would
|
|
197
213
|
// be a tab's directory, where a turn is a reader by the product's own law.
|
|
198
214
|
const run = agentRuns.get(turnId) ?? { place, interrupted: false };
|
|
199
|
-
await inPlace(place, place.startsWith(
|
|
215
|
+
await inPlace(place, place.startsWith(AGENT_PLACE_PREFIX), async () => {
|
|
200
216
|
run.waitingReason = null;
|
|
201
217
|
try {
|
|
202
218
|
await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
|
|
203
219
|
if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
|
|
204
220
|
} catch (error) {
|
|
205
221
|
if (settled) return;
|
|
206
|
-
const commits =
|
|
222
|
+
const commits = turnCommits({ repositoryReceipts: measured.repositoryReceipts, wt: measured.wt, before: measured.before, baseRef });
|
|
207
223
|
if (run.interrupted) {
|
|
208
|
-
await postAgentTurn({ turnId,
|
|
224
|
+
await postAgentTurn(withLiveReceipts(interruptedTurnSettlement({ turnId, ran: measured.ranCli === true, commits, usage: measured.usage, model: measured.model, branch: measured.branch, wt: measured.wt })));
|
|
209
225
|
return;
|
|
210
226
|
}
|
|
211
227
|
await postAgentTurn(
|
|
212
|
-
failedTurnSettlement({
|
|
228
|
+
withLiveReceipts(failedTurnSettlement({
|
|
213
229
|
turnId,
|
|
214
230
|
error,
|
|
215
231
|
commits,
|
|
@@ -217,7 +233,7 @@ export function createWorkAgentTurns({
|
|
|
217
233
|
model: measured.model,
|
|
218
234
|
branch: measured.branch,
|
|
219
235
|
wt: measured.wt,
|
|
220
|
-
})
|
|
236
|
+
}))
|
|
221
237
|
);
|
|
222
238
|
}
|
|
223
239
|
}, () => {
|
|
@@ -299,7 +315,7 @@ export function createWorkAgentTurns({
|
|
|
299
315
|
const place = String(job.placeId || '');
|
|
300
316
|
const wt = typeof held.body.worktree === 'string' ? held.body.worktree : null;
|
|
301
317
|
if (reply?.review === true && isSafePathSegment(place) && wt && existsSync(wt)) {
|
|
302
|
-
await inPlace(place, place.startsWith(
|
|
318
|
+
await inPlace(place, place.startsWith(AGENT_PLACE_PREFIX), () =>
|
|
303
319
|
runReviewEntry(String(job.agentId || ''), wt, job.agentName)
|
|
304
320
|
);
|
|
305
321
|
}
|
|
@@ -420,7 +436,7 @@ export function createWorkAgentTurns({
|
|
|
420
436
|
run.interrupted = true;
|
|
421
437
|
run.abortSetup?.();
|
|
422
438
|
const child = agentChildren.get(run.place);
|
|
423
|
-
if (child)
|
|
439
|
+
if (child) interruptThenKill(child);
|
|
424
440
|
continue;
|
|
425
441
|
}
|
|
426
442
|
if (agentReported.has(id) || notRunningSaid.has(id)) continue;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* number of times; the stored files stay listed, which is the truth.
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
|
+
import { agentIdFromPlace } from './workPlaces.mjs';
|
|
18
19
|
|
|
19
20
|
/** Attempts per agent per process, and the wait between two of them. */
|
|
20
21
|
const MAX_TRIES = 4;
|
|
@@ -37,7 +38,7 @@ export function createWorkArtifactReconcile({
|
|
|
37
38
|
const reconcileOne = async (place) => {
|
|
38
39
|
const wt = placeDir(place);
|
|
39
40
|
if (!wt || !existsSync(wt)) return;
|
|
40
|
-
const agentId = place
|
|
41
|
+
const agentId = agentIdFromPlace(place);
|
|
41
42
|
try {
|
|
42
43
|
await inPlace(place, true, () => artifacts.reconcile({ placeDir: wt, before: beforeArtifacts(wt), agentId }));
|
|
43
44
|
done.add(place);
|
|
@@ -58,7 +59,7 @@ export function createWorkArtifactReconcile({
|
|
|
58
59
|
for (const id of done) if (!live.has(id)) done.delete(id);
|
|
59
60
|
for (const id of tries.keys()) if (!live.has(id)) tries.delete(id);
|
|
60
61
|
const due = activeIds.filter((place) => {
|
|
61
|
-
if (
|
|
62
|
+
if (agentIdFromPlace(place) === null) return false;
|
|
62
63
|
if (done.has(place) || placeLocks.has(place)) return false; // a turn or a merge is there
|
|
63
64
|
const t = tries.get(place);
|
|
64
65
|
return !t || (t.n < MAX_TRIES && now() - t.at >= RETRY_MS);
|
|
@@ -54,6 +54,8 @@ export function createWorkAttachments() {
|
|
|
54
54
|
* doing that check is one deploy away from being zero places.
|
|
55
55
|
*/
|
|
56
56
|
const UPLOAD_DIR = '.flowviant/uploads';
|
|
57
|
+
/** The pause before a file fetch's one retry. */
|
|
58
|
+
const RETRY_FETCH_MS = 1_000;
|
|
57
59
|
// `safeFileName` (bin/lib/safeFileName.mjs — the server's rule, one daemon
|
|
58
60
|
// home): THE EXTENSION SURVIVES A CUT, and it is idempotent over its own
|
|
59
61
|
// output, so re-applying it to what the server already sanitized is a no-op
|
|
@@ -151,10 +153,17 @@ export function createWorkAttachments() {
|
|
|
151
153
|
if (!a?.id || typeof a.id !== 'string' || !/^[0-9a-f-]{8,64}$/i.test(a.id)) continue;
|
|
152
154
|
if (Number(a.size) > ATTACHMENT_MAX_BYTES) continue;
|
|
153
155
|
try {
|
|
154
|
-
|
|
156
|
+
// ONE MORE TRY for a blip (2026-10-19): a dropped connection or a 5xx
|
|
157
|
+
// cost the file outright. A 4xx is the server's answer and stands.
|
|
158
|
+
const get = () => fetch(`${FILE_URL[endpoint]}/${a.id}`, {
|
|
155
159
|
headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT },
|
|
156
160
|
signal: AbortSignal.timeout(60_000),
|
|
157
161
|
});
|
|
162
|
+
let res = await get().catch(() => null);
|
|
163
|
+
if (!res || res.status >= 500) {
|
|
164
|
+
await new Promise((r) => setTimeout(r, RETRY_FETCH_MS));
|
|
165
|
+
res = await get();
|
|
166
|
+
}
|
|
158
167
|
if (!res.ok) continue;
|
|
159
168
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
160
169
|
if (buf.byteLength === 0 || buf.byteLength > ATTACHMENT_MAX_BYTES) continue;
|
|
@@ -187,12 +196,25 @@ export function createWorkAttachments() {
|
|
|
187
196
|
return landed;
|
|
188
197
|
};
|
|
189
198
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
199
|
+
/**
|
|
200
|
+
* A TAB MESSAGE'S FILES, each `{ path }` landed or `{ missed: name }`
|
|
201
|
+
* (2026-10-19). The Terminal listed only what arrived, on the argument that
|
|
202
|
+
* its person is watching and re-attaches — but the person sees the file's
|
|
203
|
+
* chip on their message whether it arrived or not, and a CLI told nothing
|
|
204
|
+
* answered as if it had read it. Named, a miss is said in the reply, as the
|
|
205
|
+
* agent lane's always were (`fetchAgentFiles`).
|
|
206
|
+
*/
|
|
207
|
+
const fetchTabFiles = async (wt, attachments) => {
|
|
192
208
|
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
193
|
-
|
|
209
|
+
const list = attachments.slice(0, 8);
|
|
210
|
+
const landed = await landFiles(wt, list, 'attachment', false);
|
|
211
|
+
return list.map((a, i) => (landed[i] ? { path: landed[i] } : { missed: safeUploadName(a?.name) }));
|
|
194
212
|
};
|
|
195
213
|
|
|
214
|
+
/** @returns relative paths written, in the order the human attached them. */
|
|
215
|
+
const fetchAttachments = async (wt, attachments) =>
|
|
216
|
+
(await fetchTabFiles(wt, attachments)).flatMap((f) => (f.path ? [f.path] : []));
|
|
217
|
+
|
|
196
218
|
/**
|
|
197
219
|
* AN AGENT TURN'S FILES (0.112.0), read off the wire (agentFiles.mjs: each
|
|
198
220
|
* origin at the server's cap, message files first) and fetched from
|
|
@@ -218,5 +240,5 @@ export function createWorkAttachments() {
|
|
|
218
240
|
return out;
|
|
219
241
|
};
|
|
220
242
|
|
|
221
|
-
return { fetchAttachments, fetchAgentFiles };
|
|
243
|
+
return { fetchAttachments, fetchTabFiles, fetchAgentFiles };
|
|
222
244
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
/** The daemon holds every selected place while one personal CLI integrates it. */
|
|
1
2
|
import { trackedRepositorySetup } from './repositorySetup.mjs';
|
|
2
3
|
import { git } from './git.mjs';
|
|
3
4
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
4
|
-
|
|
5
|
-
import {
|
|
5
|
+
import { integrateMergeBatch } from './mergeBatch.mjs';
|
|
6
|
+
import { mergeBatchPrompt, MERGE_BATCH_SYSTEM } from './repositoryPrompts.mjs';
|
|
6
7
|
import { runTurn } from './runTurn.mjs';
|
|
7
8
|
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
8
|
-
import { candidateObjects } from './
|
|
9
|
+
import { candidateObjects } from './modelJson.mjs';
|
|
9
10
|
import { pickRuntimeFor } from './runtimeDetection.mjs';
|
|
10
11
|
import { fleetRequest } from './fleetPost.mjs';
|
|
11
12
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
12
13
|
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
13
14
|
import { worktreePath } from './workPlaces.mjs';
|
|
14
15
|
import { scrub } from './uplinkScrub.mjs';
|
|
16
|
+
import { relayedText } from './prWorkflow.mjs';
|
|
15
17
|
import { isSafePathSegment, stripUrlUserinfo } from './git.mjs';
|
|
16
18
|
import { killGroup } from './processGroup.mjs';
|
|
17
19
|
|
|
@@ -63,7 +65,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
63
65
|
const result = await runCappedTurn({ runTurn: runCli, workChildren, releaseSlot: release,
|
|
64
66
|
transcriptCwd: context.repositories[0].integrationWorktree, capMs: 20 * 60_000, killChild: killGroup,
|
|
65
67
|
onSpawn: (ch) => { child = ch; groupKillChildren?.add(ch); },
|
|
66
|
-
turnArgs: { prompt: mergeBatchPrompt(context), system:
|
|
68
|
+
turnArgs: { prompt: mergeBatchPrompt(context), system: MERGE_BATCH_SYSTEM,
|
|
67
69
|
runtime, profile: 'build', knowledgeDir: knowledgeDirFor(repoRoot), model: leader.model || undefined, effort: leader.effort || undefined,
|
|
68
70
|
cwd: context.repositories[0].integrationWorktree, writableDirs: context.repositories.filter((r) => !r.alreadyLanded).map((r) => r.integrationWorktree),
|
|
69
71
|
answerFromResult: true, label: '[batch merge]' } });
|
|
@@ -88,7 +90,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
88
90
|
// After a successful push/report attempt, leave the lease for replay: a transport
|
|
89
91
|
// failure says nothing about whether the server committed the final receipt.
|
|
90
92
|
if (claimed && !reporting) {
|
|
91
|
-
const detail =
|
|
93
|
+
const detail = relayedText([e?.stdout, e?.stderr, e?.message].filter(Boolean).join('\n')).slice(0, 2000);
|
|
92
94
|
await request('agent-merge-batch-done', { batchId: spec.id, ok: false, detail }).catch(() => {});
|
|
93
95
|
}
|
|
94
96
|
} finally {
|
|
@@ -82,7 +82,39 @@ export function createWorkNarration() {
|
|
|
82
82
|
}, wait);
|
|
83
83
|
timer.unref?.(); // never hold the process open for a spinner
|
|
84
84
|
};
|
|
85
|
+
// THE CONTEXT READOUT rides the same door with its turn (2026-10-19):
|
|
86
|
+
// the latest measurement only, one POST at a time, never a turn's failure.
|
|
87
|
+
// The server writes no narration line for a context post.
|
|
88
|
+
let ctxPending = null;
|
|
89
|
+
let ctxSending = false;
|
|
90
|
+
const sendContext = async () => {
|
|
91
|
+
ctxSending = true;
|
|
92
|
+
while (ctxPending && !stopped) {
|
|
93
|
+
const context = ctxPending;
|
|
94
|
+
ctxPending = null;
|
|
95
|
+
try {
|
|
96
|
+
await fetch(ACTIVITY_URL, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
100
|
+
'User-Agent': USER_AGENT,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
signal: AbortSignal.timeout(10_000),
|
|
104
|
+
body: JSON.stringify({ sessionId, turnId, lines: [], context }),
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
/* a readout is decoration, like the line */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
ctxSending = false;
|
|
111
|
+
};
|
|
85
112
|
return {
|
|
113
|
+
context(measured) {
|
|
114
|
+
if (!turnId || stopped || !measured) return;
|
|
115
|
+
ctxPending = measured;
|
|
116
|
+
if (!ctxSending) void sendContext();
|
|
117
|
+
},
|
|
86
118
|
line(label) {
|
|
87
119
|
// Scrub, like every string that leaves this machine: a narration line
|
|
88
120
|
// is the CLI's own stdout — a command echoing an env var, a read of a
|
|
@@ -20,7 +20,7 @@ import { RESUME_MARKERS } from './conversationIds.mjs';
|
|
|
20
20
|
import { worktreeDiff } from './worktreeDiff.mjs';
|
|
21
21
|
import { myPubB64 } from './boxIdentity.mjs';
|
|
22
22
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
23
|
-
import { isValidPlace } from './workPlaces.mjs';
|
|
23
|
+
import { isValidPlace, AGENT_PLACE_PREFIX, agentIdFromPlace } from './workPlaces.mjs';
|
|
24
24
|
|
|
25
25
|
export function createWorkPlaceReports({
|
|
26
26
|
repositoryPlaces,
|
|
@@ -176,7 +176,7 @@ export function createWorkPlaceReports({
|
|
|
176
176
|
* TABS GET NOTHING. A tab's place is shared by design and its work is a
|
|
177
177
|
* human's own directory; attributing one would be a fact with no reader.
|
|
178
178
|
*/
|
|
179
|
-
const pub = sessionId
|
|
179
|
+
const pub = agentIdFromPlace(sessionId) !== null ? myPubB64() : null;
|
|
180
180
|
/**
|
|
181
181
|
* …AND WHAT THIS MACHINE PUSHED OF IT (0.86.0).
|
|
182
182
|
*
|
|
@@ -193,7 +193,7 @@ export function createWorkPlaceReports({
|
|
|
193
193
|
* Mutually exclusive without a rule of its own, because the state it reads
|
|
194
194
|
* holds a sha or an error and never both.
|
|
195
195
|
*/
|
|
196
|
-
const pushed = sessionId.startsWith(
|
|
196
|
+
const pushed = sessionId.startsWith(AGENT_PLACE_PREFIX) ? agentPublished.get(sessionId) : null;
|
|
197
197
|
return {
|
|
198
198
|
sessionId,
|
|
199
199
|
...d,
|
package/bin/lib/workPlaces.mjs
CHANGED
|
@@ -29,6 +29,20 @@ export const REPO_PLACE = 'repo';
|
|
|
29
29
|
* audit F163). */
|
|
30
30
|
export const PERSON_PLACE_PREFIX = 'u-';
|
|
31
31
|
|
|
32
|
+
/** An AGENT's place is `a-<agentId>` — the shared AGENT_PLACE_PREFIX /
|
|
33
|
+
* agentPlaceId; a wire value held by the release parity gate (2026-10-18,
|
|
34
|
+
* the SOLID pass, as F163 holds the other two). Its writer lock, its envpub
|
|
35
|
+
* and the artifact reconcile all key on it. */
|
|
36
|
+
export const AGENT_PLACE_PREFIX = 'a-';
|
|
37
|
+
|
|
38
|
+
/** The agent a place names, or null for every other place shape — the shared
|
|
39
|
+
* `agentIdFromPlace`, mirrored: the prefix alone names nothing. */
|
|
40
|
+
export function agentIdFromPlace(place) {
|
|
41
|
+
return typeof place === 'string' && place.startsWith(AGENT_PLACE_PREFIX) && place.length > AGENT_PLACE_PREFIX.length
|
|
42
|
+
? place.slice(AGENT_PLACE_PREFIX.length)
|
|
43
|
+
: null;
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
/**
|
|
33
47
|
* A PLACE THIS MACHINE WILL RESOLVE: the checkout, or a safe path segment.
|
|
34
48
|
*
|
|
@@ -19,6 +19,7 @@ import { sweepMergedBranch } from './shipSweep.mjs';
|
|
|
19
19
|
import { note } from './ui.mjs';
|
|
20
20
|
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
21
21
|
import { fleetRequest } from './fleetPost.mjs';
|
|
22
|
+
import { createReportJournal } from './reportJournal.mjs';
|
|
22
23
|
|
|
23
24
|
export function createWorkReportQueue({
|
|
24
25
|
repoRoot,
|
|
@@ -29,6 +30,9 @@ export function createWorkReportQueue({
|
|
|
29
30
|
onRepoChanged,
|
|
30
31
|
reportPlaceWorktrees,
|
|
31
32
|
burstListeners,
|
|
33
|
+
/** Where the queue survives a restart (`reportJournal.mjs`); absent, the
|
|
34
|
+
* queue lives in memory only, as it did before 2026-10-19. */
|
|
35
|
+
journalDir = null,
|
|
32
36
|
}) {
|
|
33
37
|
const WORK_DONE_URL = fleetEndpoint('work-turn-done', FLEET_URL);
|
|
34
38
|
const SHIP_DONE_URL = fleetEndpoint('ship-done', FLEET_URL);
|
|
@@ -43,6 +47,16 @@ export function createWorkReportQueue({
|
|
|
43
47
|
*/
|
|
44
48
|
const pendingWorkReports = new Map(); // turnId -> work-turn-done body
|
|
45
49
|
const pendingShipReports = new Map(); // sessionId -> ship-done body
|
|
50
|
+
// …AND ON DISK, so a restart between the answer and the 200 delivers the
|
|
51
|
+
// answer rather than re-running the turn (reportJournal.mjs). Seeded once;
|
|
52
|
+
// the first poll's flush delivers what a previous process left.
|
|
53
|
+
const journal = journalDir ? createReportJournal(journalDir) : null;
|
|
54
|
+
if (journal) {
|
|
55
|
+
const kept = journal.load();
|
|
56
|
+
for (const [id, body] of kept.work) pendingWorkReports.set(id, body);
|
|
57
|
+
for (const [id, body] of kept.ship) pendingShipReports.set(id, body);
|
|
58
|
+
}
|
|
59
|
+
const persist = () => journal?.save(pendingWorkReports, pendingShipReports);
|
|
46
60
|
/** POST a settle body. Four outcomes, and the split between the last two is
|
|
47
61
|
* load-bearing:
|
|
48
62
|
* - 'ok' — delivered.
|
|
@@ -142,10 +156,13 @@ export function createWorkReportQueue({
|
|
|
142
156
|
const deliver = async (q, id, body) => {
|
|
143
157
|
const r = await postSettle(q.url, body, q.terminal);
|
|
144
158
|
if (r === 'retry' || r === 'reject') {
|
|
159
|
+
const fresh = !q.pending.has(id);
|
|
145
160
|
q.pending.set(id, body);
|
|
161
|
+
if (fresh) persist();
|
|
146
162
|
if (r === 'reject') reportBackoff.set(backoffKey(q, id), Date.now() + REJECT_RETRY_MS);
|
|
147
163
|
} else {
|
|
148
|
-
q.pending.delete(id);
|
|
164
|
+
const held = q.pending.delete(id);
|
|
165
|
+
if (held) persist();
|
|
149
166
|
reportBackoff.delete(backoffKey(q, id));
|
|
150
167
|
q.delivered(id);
|
|
151
168
|
}
|
|
@@ -8,17 +8,14 @@
|
|
|
8
8
|
* THE ID SHAPES MOVED to a leaf (audit 2026-10-10, F157): conversationIds.mjs
|
|
9
9
|
* owns every CLI's conversation-id guard and marker name, because the
|
|
10
10
|
* machine readers below the lanes (runtimeLimits, the session scanners) need
|
|
11
|
-
* them too and a lane module is not theirs to import.
|
|
12
|
-
* agent lane imports from here are re-exported unchanged.
|
|
11
|
+
* them too and a lane module is not theirs to import.
|
|
13
12
|
*/
|
|
14
13
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
15
14
|
import { join } from 'node:path';
|
|
16
15
|
import { homedir } from 'node:os';
|
|
17
16
|
import { canRun, RUNTIMES, runtimeById, runtimeLabel } from './runtimes.mjs';
|
|
18
17
|
import { detectRuntimes } from './runtimeDetection.mjs';
|
|
19
|
-
import { AGY_CONV_RE
|
|
20
|
-
|
|
21
|
-
export { AGY_CONV_RE, CODEX_THREAD_RE };
|
|
18
|
+
import { AGY_CONV_RE } from './conversationIds.mjs';
|
|
22
19
|
|
|
23
20
|
export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
24
21
|
/**
|