flowviant 0.125.6 → 0.125.9
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 +84 -6
- package/bin/cli.mjs +7 -1
- package/bin/lib/activityFeed.mjs +69 -0
- package/bin/lib/agentMergeReports.mjs +46 -0
- package/bin/lib/agentMergeResolution.mjs +44 -0
- package/bin/lib/agentPlan.mjs +6 -2
- package/bin/lib/claudeInput.mjs +61 -1
- package/bin/lib/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +54 -4
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexResearch.mjs +33 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/daemonJournal.mjs +102 -0
- package/bin/lib/daemonLogging.mjs +22 -18
- package/bin/lib/fleet.mjs +16 -6
- package/bin/lib/fleetReports.mjs +12 -5
- package/bin/lib/hooks/orchestrator.mjs +3 -1
- package/bin/lib/logReplay.mjs +133 -0
- package/bin/lib/mcpInventory.mjs +2 -1
- package/bin/lib/mcpReasons.mjs +6 -0
- package/bin/lib/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +49 -7
- package/bin/lib/orchestratorNative.mjs +133 -3
- package/bin/lib/orchestratorPrompt.mjs +50 -4
- package/bin/lib/postureTools.mjs +14 -6
- package/bin/lib/printable.mjs +1 -1
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectTools.mjs +3 -2
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryMerge.mjs +1 -1
- package/bin/lib/researchHandoff.mjs +144 -0
- package/bin/lib/researchHandoffText.mjs +3 -0
- package/bin/lib/researchKnowledge.mjs +60 -0
- package/bin/lib/researchRestoration.mjs +38 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/runTurn.mjs +98 -44
- package/bin/lib/runtimeCapabilities.mjs +7 -2
- package/bin/lib/runtimeClaude.mjs +30 -8
- package/bin/lib/runtimeCodex.mjs +54 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- package/bin/lib/runtimeOfferings.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/turnDiagnostics.mjs +2 -1
- package/bin/lib/turnOutput.mjs +63 -0
- package/bin/lib/turnRecord.mjs +11 -0
- package/bin/lib/turnSubagents.mjs +13 -3
- package/bin/lib/ui.mjs +2 -2
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/uplinkScrub.mjs +49 -0
- package/bin/lib/views.mjs +49 -58
- package/bin/lib/wikiRunner.mjs +2 -2
- package/bin/lib/work.mjs +5 -5
- package/bin/lib/workActivity.mjs +65 -0
- package/bin/lib/workAgentBriefRunner.mjs +2 -2
- package/bin/lib/workAgentMerges.mjs +57 -52
- package/bin/lib/workAgentPlans.mjs +76 -27
- package/bin/lib/workAgentPrecheck.mjs +1 -1
- package/bin/lib/workAgentTurnExecution.mjs +62 -8
- package/bin/lib/workAgentTurnReports.mjs +33 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -4
- package/bin/lib/workAgentTurns.mjs +12 -8
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionTurns.mjs +64 -22
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
* Daemon→server reports, every one of them: no version floor, and a permanent
|
|
15
15
|
* 4xx is the server saying it will never take the body (see each POST).
|
|
16
16
|
*/
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { createReportJournal } from './reportJournal.mjs';
|
|
17
19
|
import { FLEET_URL } from './config.mjs';
|
|
18
20
|
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
19
21
|
import { fleetHeaders, postFleetJson } from './fleetPost.mjs';
|
|
20
22
|
|
|
21
|
-
export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
23
|
+
export function createAgentTurnReports({ REJECT_RETRY_MS, baseDir = null }) {
|
|
22
24
|
const AGENT_TURN_DONE_URL = fleetEndpoint('agent-turn-done', FLEET_URL);
|
|
23
25
|
const AGENT_TRACE_URL = fleetEndpoint('agent-trace', FLEET_URL);
|
|
24
26
|
const AGENT_ACTIVITY_URL = fleetEndpoint('agent-activity', FLEET_URL);
|
|
@@ -40,8 +42,17 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
40
42
|
* BOUNDED by the roster itself: a held body's clock is refreshed while the
|
|
41
43
|
* server keeps offering its turn, and once offering stops — settled by the
|
|
42
44
|
* re-POST, or expired server-side — the grace below is all that keeps it.
|
|
45
|
+
*
|
|
46
|
+
* EVERY HELD BODY IS JOURNALED (2026-10-20), not only a merge recovery's.
|
|
47
|
+
* Any agent turn whose body was held in memory alone lost it to a restart
|
|
48
|
+
* between the answer and the server's 200, and the server's re-offer to the
|
|
49
|
+
* successor ran the turn again. The settle is bound to the machine
|
|
50
|
+
* credential, not the process, so the successor's re-POST lands it.
|
|
43
51
|
*/
|
|
44
|
-
const
|
|
52
|
+
const journal = baseDir ? createReportJournal(join(baseDir, 'agent-recovery-reports')) : null;
|
|
53
|
+
const agentReported = new Map((journal?.load().work ?? []).filter(([id, held]) =>
|
|
54
|
+
held?.body?.turnId === id && typeof held.at === 'number'));
|
|
55
|
+
const persistRecovery = () => journal?.save(new Map(agentReported), new Map()); // turnId -> { body, at }
|
|
45
56
|
const agentRejectedUntil = new Map(); // turnId -> earliest re-POST of a refused body
|
|
46
57
|
const AGENT_REPORT_GRACE_MS = 30 * 60_000;
|
|
47
58
|
|
|
@@ -53,6 +64,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
53
64
|
const postAgentTurn = async (body) => {
|
|
54
65
|
const turnId = String(body.turnId);
|
|
55
66
|
agentReported.set(turnId, { body, at: Date.now() });
|
|
67
|
+
persistRecovery();
|
|
56
68
|
try {
|
|
57
69
|
const res = await postFleetJson(AGENT_TURN_DONE_URL, body, { timeoutMs: 30_000 });
|
|
58
70
|
const j = await res.json().catch(() => null);
|
|
@@ -68,9 +80,10 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
68
80
|
* class. So a refused body stays HELD — it is the skip-guard — and its
|
|
69
81
|
* re-POST backs off instead of hammering a body the server just refused.
|
|
70
82
|
*/
|
|
71
|
-
if (res.ok) {
|
|
83
|
+
if (res.ok && (body.mergeResolved === undefined || typeof j?.data?.settled === 'boolean')) {
|
|
72
84
|
agentReported.delete(turnId);
|
|
73
85
|
agentRejectedUntil.delete(turnId);
|
|
86
|
+
persistRecovery();
|
|
74
87
|
} else if (isConsideredRefusal(res.status)) {
|
|
75
88
|
agentRejectedUntil.set(turnId, Date.now() + REJECT_RETRY_MS);
|
|
76
89
|
}
|
|
@@ -114,6 +127,21 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
114
127
|
}
|
|
115
128
|
};
|
|
116
129
|
|
|
130
|
+
/** The CLI's context measure for a running turn (2026-10-19); the server
|
|
131
|
+
* drops one whose turn already settled. Best effort, like the line. */
|
|
132
|
+
const postAgentContext = async (agentId, turnId, context) => {
|
|
133
|
+
try {
|
|
134
|
+
await fetch(AGENT_ACTIVITY_URL, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: fleetHeaders(),
|
|
137
|
+
signal: AbortSignal.timeout(15_000),
|
|
138
|
+
body: JSON.stringify({ agentId, turnId, context }),
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
/* a readout; the next measurement replaces it */
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
117
145
|
const postAgentParked = async (reason, runtime) => {
|
|
118
146
|
try {
|
|
119
147
|
await fetch(AGENT_PARKED_URL, {
|
|
@@ -145,6 +173,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
145
173
|
else if (now - held.at > AGENT_REPORT_GRACE_MS) {
|
|
146
174
|
agentReported.delete(id);
|
|
147
175
|
agentRejectedUntil.delete(id);
|
|
176
|
+
persistRecovery();
|
|
148
177
|
}
|
|
149
178
|
}
|
|
150
179
|
};
|
|
@@ -153,6 +182,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
153
182
|
postAgentTurn,
|
|
154
183
|
postAgentTrace,
|
|
155
184
|
postAgentActivity,
|
|
185
|
+
postAgentContext,
|
|
156
186
|
postAgentParked,
|
|
157
187
|
pruneHeldReports,
|
|
158
188
|
agentReported,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { AGENT_TASK_KICKOFF, AGENT_HUMAN_KICKOFF, SYSTEM_AGENT_FOR, withProjectContext } from './prompts.mjs';
|
|
26
26
|
import { INTERNAL_WAIT_CONTRACT } from './agentInternalWait.mjs';
|
|
27
|
-
import {
|
|
27
|
+
import { orchestratorContract } from './orchestratorPrompt.mjs';
|
|
28
28
|
|
|
29
29
|
/** What every kickoff of this job is handed: the agent, the card and its
|
|
30
30
|
* discussion, the job's context handoffs, the card's files, the place in
|
|
@@ -74,8 +74,10 @@ export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, o
|
|
|
74
74
|
messageFiles,
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
/** The turn's system prompt, in its fixed section order.
|
|
78
|
-
|
|
77
|
+
/** The turn's system prompt, in its fixed section order. `orchestration` is
|
|
78
|
+
* null on a solo run, else what the contract names: `{ posture, runtime,
|
|
79
|
+
* model, effort }` (orchestratorPrompt.mjs). */
|
|
80
|
+
export function agentTurnSystem({ taskKind, runtimeLabel, prerequisitesAccepted, knowledgeDir, artifactsAccepted, toolsNote, artifactInstructions, toolInstructions, repositoryRun, orchestration = null }) {
|
|
79
81
|
// The knowledge paragraph, when this box holds a library — the same
|
|
80
82
|
// composer the tabs use, so an agent and a tab can never be told two
|
|
81
83
|
// different things about the same directory.
|
|
@@ -86,5 +88,5 @@ export function agentTurnSystem({ taskKind, runtimeLabel, prerequisitesAccepted,
|
|
|
86
88
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
87
89
|
// land on its page, under the facts row.
|
|
88
90
|
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>"])}` : '') + (
|
|
91
|
+
}) + (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>"])}` : '') + (orchestration ? `\n\n${orchestratorContract(orchestration)}` : '');
|
|
90
92
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportAgentOutcome } from './workActivity.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* THE AGENT-TURN LANE: which offered turns run on this machine, and that each
|
|
3
4
|
* one is settled exactly once.
|
|
@@ -27,7 +28,7 @@ import { createAgentTurnReports } from './workAgentTurnReports.mjs';
|
|
|
27
28
|
import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
|
|
28
29
|
import { failedTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
29
30
|
import { turnCommits } from './turnReceipts.mjs';
|
|
30
|
-
import {
|
|
31
|
+
import { interruptThenKill } from './processGroup.mjs';
|
|
31
32
|
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
32
33
|
import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
|
|
33
34
|
|
|
@@ -64,11 +65,12 @@ export function createWorkAgentTurns({
|
|
|
64
65
|
postAgentTurn,
|
|
65
66
|
postAgentTrace,
|
|
66
67
|
postAgentActivity,
|
|
68
|
+
postAgentContext,
|
|
67
69
|
postAgentParked,
|
|
68
70
|
pruneHeldReports,
|
|
69
71
|
agentReported,
|
|
70
72
|
agentRejectedUntil,
|
|
71
|
-
} = createAgentTurnReports({ REJECT_RETRY_MS });
|
|
73
|
+
} = createAgentTurnReports({ REJECT_RETRY_MS, baseDir });
|
|
72
74
|
|
|
73
75
|
// ── AGENT TURNS: one task per prompt ───────────────────────────────────────
|
|
74
76
|
//
|
|
@@ -135,6 +137,7 @@ export function createWorkAgentTurns({
|
|
|
135
137
|
agentChildren,
|
|
136
138
|
postAgentTrace,
|
|
137
139
|
postAgentActivity,
|
|
140
|
+
postAgentContext,
|
|
138
141
|
postAgentParked,
|
|
139
142
|
fetchAgentFiles,
|
|
140
143
|
makeLiveMessages,
|
|
@@ -203,7 +206,8 @@ export function createWorkAgentTurns({
|
|
|
203
206
|
};
|
|
204
207
|
const settleOnce = (body) => {
|
|
205
208
|
settled = true;
|
|
206
|
-
|
|
209
|
+
reportAgentOutcome(job, body);
|
|
210
|
+
return postAgentTurn(withLiveReceipts(job.kind === 'merge_resolve' ? { ...body, mergeResolved: body.mergeResolved ?? false } : body));
|
|
207
211
|
};
|
|
208
212
|
|
|
209
213
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
@@ -219,11 +223,11 @@ export function createWorkAgentTurns({
|
|
|
219
223
|
if (settled) return;
|
|
220
224
|
const commits = turnCommits({ repositoryReceipts: measured.repositoryReceipts, wt: measured.wt, before: measured.before, baseRef });
|
|
221
225
|
if (run.interrupted) {
|
|
222
|
-
await
|
|
226
|
+
await settleOnce(interruptedTurnSettlement({ turnId, ran: measured.ranCli === true, commits, usage: measured.usage, model: measured.model, branch: measured.branch, wt: measured.wt }));
|
|
223
227
|
return;
|
|
224
228
|
}
|
|
225
|
-
await
|
|
226
|
-
|
|
229
|
+
await settleOnce(
|
|
230
|
+
failedTurnSettlement({
|
|
227
231
|
turnId,
|
|
228
232
|
error,
|
|
229
233
|
commits,
|
|
@@ -231,7 +235,7 @@ export function createWorkAgentTurns({
|
|
|
231
235
|
model: measured.model,
|
|
232
236
|
branch: measured.branch,
|
|
233
237
|
wt: measured.wt,
|
|
234
|
-
})
|
|
238
|
+
})
|
|
235
239
|
);
|
|
236
240
|
}
|
|
237
241
|
}, () => {
|
|
@@ -434,7 +438,7 @@ export function createWorkAgentTurns({
|
|
|
434
438
|
run.interrupted = true;
|
|
435
439
|
run.abortSetup?.();
|
|
436
440
|
const child = agentChildren.get(run.place);
|
|
437
|
-
if (child)
|
|
441
|
+
if (child) interruptThenKill(child);
|
|
438
442
|
continue;
|
|
439
443
|
}
|
|
440
444
|
if (agentReported.has(id) || notRunningSaid.has(id)) continue;
|
|
@@ -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
|
}
|
package/bin/lib/workIntake.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportWorkOutcome } from './workActivity.mjs';
|
|
1
2
|
/** One leased incoming ticket, drafted by this machine's read-only CLI. */
|
|
2
3
|
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
3
4
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
@@ -39,12 +40,17 @@ export function createWorkIntake({
|
|
|
39
40
|
const claim = (id) =>
|
|
40
41
|
claimFleetLease(claiming, { id }, { won: (r) => r.status === 200 && r.json?.ok === true });
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const sendReport = async (body) => {
|
|
43
44
|
await postBestEffort(done, { ...body, instance: DAEMON_INSTANCE });
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const run = async (job, releaseSlot) => {
|
|
47
48
|
const id = String(job.id);
|
|
49
|
+
const work = { kind: 'intake', id, name: job.title || 'Incoming ticket', turnId: id };
|
|
50
|
+
const report = body => {
|
|
51
|
+
reportWorkOutcome(work, { outcome: body.outcome === INTAKE_OUTCOMES.drafted ? 'drafted' : 'nothing', answer: body.error });
|
|
52
|
+
return sendReport(body);
|
|
53
|
+
};
|
|
48
54
|
// No stand-down check here, as in the plan lane: a standing-down loop stops
|
|
49
55
|
// processing the roster, and the one spawn gate is runTurn's own
|
|
50
56
|
// (standDownGate.mjs; its pin keeps the askers few).
|
|
@@ -80,7 +86,7 @@ export function createWorkIntake({
|
|
|
80
86
|
cwd: repoRoot,
|
|
81
87
|
runtime,
|
|
82
88
|
answerFromResult: true,
|
|
83
|
-
|
|
89
|
+
work,
|
|
84
90
|
// There is no MCP principal for a ticket, and no model or effort pin:
|
|
85
91
|
// this one-shot reader uses the machine's consult defaults.
|
|
86
92
|
onUsage: (u) => { usage = addUsage(usage, { ...u, runtime }); },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reportWorkOutcome } from './workActivity.mjs';
|
|
2
|
+
import { createTurnOutput } from './turnOutput.mjs';
|
|
1
3
|
/** The daemon holds every selected place while one personal CLI integrates it. */
|
|
2
4
|
import { trackedRepositorySetup } from './repositorySetup.mjs';
|
|
3
5
|
import { git } from './git.mjs';
|
|
@@ -25,6 +27,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
25
27
|
};
|
|
26
28
|
return async (job) => {
|
|
27
29
|
const spec = job.batch.spec;
|
|
30
|
+
const work = { kind: 'batch merge', id: spec.id, name: 'Selected merge', turnId: spec.id };
|
|
28
31
|
const hold = admit?.('churn');
|
|
29
32
|
if (hold) return;
|
|
30
33
|
const release = admit?.reserve?.() ?? (() => {});
|
|
@@ -33,6 +36,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
33
36
|
const claim = await request('agent-merge-batch-claim', { batchId: spec.id });
|
|
34
37
|
if (!claim?.claimed) return;
|
|
35
38
|
claimed = true;
|
|
39
|
+
createTurnOutput({ work }).event('Merge started', 'start');
|
|
36
40
|
if (spec.members.some((m) => !isSafePathSegment(m.placeId))) throw new Error('A selected agent has no valid worktree identity.');
|
|
37
41
|
// Renewal covers a long coordinator or validation command, not just Git beats.
|
|
38
42
|
timer = setInterval(() => { void request('agent-merge-batch-progress', { batchId: spec.id, progress: {} }).catch(() => {}); }, 60_000);
|
|
@@ -68,7 +72,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
68
72
|
turnArgs: { prompt: mergeBatchPrompt(context), system: MERGE_BATCH_SYSTEM,
|
|
69
73
|
runtime, profile: 'build', knowledgeDir: knowledgeDirFor(repoRoot), model: leader.model || undefined, effort: leader.effort || undefined,
|
|
70
74
|
cwd: context.repositories[0].integrationWorktree, writableDirs: context.repositories.filter((r) => !r.alreadyLanded).map((r) => r.integrationWorktree),
|
|
71
|
-
answerFromResult: true,
|
|
75
|
+
answerFromResult: true, work } });
|
|
72
76
|
if (result.wedged) throw new Error('The coordinating CLI ran out of time. Retry the batch to continue with its saved integration work.');
|
|
73
77
|
return candidateObjects(String(result.out ?? '')).find((r) => r.status === 'delivered' || r.status === 'blocked');
|
|
74
78
|
} finally { if (child) groupKillChildren?.delete(child); }
|
|
@@ -84,9 +88,11 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
|
|
|
84
88
|
reporting = true;
|
|
85
89
|
const done = await request('agent-merge-batch-done', { batchId: spec.id, ok: true });
|
|
86
90
|
if (!done?.settled) throw new Error('The combined merge result has not been acknowledged. Retry will remeasure base.');
|
|
91
|
+
reportWorkOutcome(work, { outcome: 'merged' });
|
|
87
92
|
onRepoChanged(); landed.observe();
|
|
88
93
|
});
|
|
89
94
|
} catch (e) {
|
|
95
|
+
if (claimed) reportWorkOutcome(work, { outcome: 'nothing', answer: relayedText([e?.stdout, e?.stderr, e?.message].filter(Boolean).join('\n')) });
|
|
90
96
|
// After a successful push/report attempt, leave the lease for replay: a transport
|
|
91
97
|
// failure says nothing about whether the server committed the final receipt.
|
|
92
98
|
if (claimed && !reporting) {
|
|
@@ -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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The person's own CLI diagnoses planning, under the ordinary consult fence. */
|
|
2
|
+
import { candidateObjects } from './modelJson.mjs';
|
|
3
|
+
import { scrub } from './uplinkScrub.mjs';
|
|
4
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
5
|
+
import { DAEMON_INSTANCE } from './config.mjs';
|
|
6
|
+
|
|
7
|
+
export const PLAN_RECOVERY_SYSTEM = `You are recovering the person's retained Plan press.
|
|
8
|
+
Read the original selection, failure, retained evidence, answers and repository.
|
|
9
|
+
Establish the cause before claiming a repair. Diagnose/debug the planning failure
|
|
10
|
+
with the available read-only tools. Repair the planning approach, context or output
|
|
11
|
+
instructions. Do not change repository files, start agents or approve work.
|
|
12
|
+
Retry count is never a reason to ask the person to click Plan again.
|
|
13
|
+
Only ask for an answer, credential action or changed-scope decision that reading
|
|
14
|
+
and diagnosis cannot supply. Ask one specific question and explain what it settles.
|
|
15
|
+
Return one JSON object: {"status":"repaired","summary":"cause and corrected approach"}
|
|
16
|
+
or {"status":"question","question":"specific question","context":"findings"}.
|
|
17
|
+
A repaired result is followed automatically by a fresh planning turn on the same
|
|
18
|
+
selection and chosen CLI/model/effort. The result is a proposal for human review. When recovery.start is present, the person
|
|
19
|
+
already approved only that Start group: diagnose its admission or pickup failure,
|
|
20
|
+
preserve its exact ordered selection, CLI and creation/attachment choice, and
|
|
21
|
+
repair only its planning context. A valid same-scope repair continues that approval
|
|
22
|
+
automatically; any genuine scope decision needs a specific question first.`;
|
|
23
|
+
|
|
24
|
+
export function parsePlanDiagnosis(out) {
|
|
25
|
+
for (const v of candidateObjects(String(out ?? ''))) {
|
|
26
|
+
if (v.status === 'repaired' && typeof v.summary === 'string' && v.summary.trim())
|
|
27
|
+
return { status: 'repaired', summary: scrub(v.summary) };
|
|
28
|
+
if (v.status === 'question' && typeof v.question === 'string' && v.question.trim())
|
|
29
|
+
return { status: 'question', question: scrub(v.question), ...(typeof v.context === 'string' ? { context: scrub(v.context) } : {}) };
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Unknown transport state never means Stop. Only an answered holder read does. */
|
|
35
|
+
export async function watchPlanAttempt(url, job) {
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
let busy = false;
|
|
38
|
+
const check = async () => {
|
|
39
|
+
if (busy) return;
|
|
40
|
+
busy = true;
|
|
41
|
+
try {
|
|
42
|
+
const r = await fleetRequest(url, { body: { id: job.id, attempt: job.attempt, instance: DAEMON_INSTANCE }, timeoutMs: 15_000 });
|
|
43
|
+
if (r.ok && r.json?.data?.active === false) controller.abort();
|
|
44
|
+
} finally { busy = false; }
|
|
45
|
+
};
|
|
46
|
+
await check();
|
|
47
|
+
const timer = setInterval(check, 5_000);
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
return { signal: controller.signal, check, stop: () => clearInterval(timer) };
|
|
50
|
+
}
|
|
@@ -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
|
}
|