flowviant 0.125.7 → 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 +80 -1
- 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/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +25 -4
- package/bin/lib/codexResearch.mjs +33 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- 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/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +40 -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/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/runTurn.mjs +76 -43
- package/bin/lib/runtimeClaude.mjs +9 -4
- package/bin/lib/runtimeCodex.mjs +51 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- 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/uplinkScrub.mjs +49 -0
- package/bin/lib/views.mjs +49 -58
- package/bin/lib/wikiRunner.mjs +2 -2
- package/bin/lib/work.mjs +2 -2
- 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 +42 -8
- package/bin/lib/workAgentTurnReports.mjs +17 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -4
- package/bin/lib/workAgentTurns.mjs +8 -6
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workSessionTurns.mjs +5 -2
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { planWork, reportPlanOutcome } from './workActivity.mjs';
|
|
1
2
|
/** Agent plan claims, read-only planning turns, and proposal reports. */
|
|
3
|
+
import { PLAN_RECOVERY_SYSTEM, parsePlanDiagnosis, watchPlanAttempt } from './workPlanRecovery.mjs';
|
|
4
|
+
import { readClaudeAuthStatus } from './runtimeAuth.mjs';
|
|
2
5
|
import { discoveryGuide } from './discoveryContract.mjs';
|
|
3
6
|
import { compareRepositoryBaselines, measureRepositoryBaseline, repositoryBaselineOf } from './repositoryBaseline.mjs';
|
|
4
7
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
@@ -33,10 +36,12 @@ export function createWorkAgentPlans({
|
|
|
33
36
|
runTurn = defaultRunTurn,
|
|
34
37
|
pickRuntimeFor = defaultPickRuntimeFor,
|
|
35
38
|
detectRuntimes = defaultDetectRuntimes,
|
|
39
|
+
authStatus = readClaudeAuthStatus,
|
|
36
40
|
}) {
|
|
37
41
|
const AGENT_PLAN_DONE_URL = fleetEndpoint('agent-plan-done', FLEET_URL);
|
|
38
42
|
const AGENT_PLAN_ACTIVITY_URL = fleetEndpoint('agent-plan-activity', FLEET_URL);
|
|
39
43
|
const AGENT_PLAN_CLAIM_URL = fleetEndpoint('agent-plan-claim', FLEET_URL);
|
|
44
|
+
const AGENT_PLAN_STATE_URL = fleetEndpoint('agent-plan-state', FLEET_URL);
|
|
40
45
|
|
|
41
46
|
// ── AGENT PLAN JOBS: the Deploy press ──────────────────────────────────────
|
|
42
47
|
//
|
|
@@ -60,13 +65,13 @@ export function createWorkAgentPlans({
|
|
|
60
65
|
// tabs. It writes nothing, so a writer lock would only starve real work.
|
|
61
66
|
const planning = new Set(); // press ids in flight on this tick
|
|
62
67
|
|
|
63
|
-
const
|
|
68
|
+
const sendAgentPlan = async (body) => {
|
|
64
69
|
// Bounded retry on 408/429/5xx, an other-4xx counted as delivered — see
|
|
65
70
|
// postBestEffort's own docblock. Previously this swallowed every
|
|
66
71
|
// response including ordinary 5xx and rate limits, so a transient
|
|
67
72
|
// failure looked identical to a hostile refusal: nothing retried, and
|
|
68
73
|
// the proposal sat unsettled until the server's own expiry.
|
|
69
|
-
|
|
74
|
+
return postBestEffort(AGENT_PLAN_DONE_URL, { ...body, instance: DAEMON_INSTANCE });
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
/**
|
|
@@ -90,7 +95,7 @@ export function createWorkAgentPlans({
|
|
|
90
95
|
* turn it describes. A daemon that never calls this is a machine that has
|
|
91
96
|
* not narrated, which is the only thing an absent line can mean.
|
|
92
97
|
*/
|
|
93
|
-
const postAgentPlanActivity = async (planId, line) => {
|
|
98
|
+
const postAgentPlanActivity = async (planId, line, attempt, phase, diagnosis) => {
|
|
94
99
|
const text = envScrub(String(line ?? '')).slice(0, 400);
|
|
95
100
|
if (!text) return;
|
|
96
101
|
try {
|
|
@@ -98,7 +103,7 @@ export function createWorkAgentPlans({
|
|
|
98
103
|
method: 'POST',
|
|
99
104
|
headers: fleetHeaders(),
|
|
100
105
|
signal: AbortSignal.timeout(15_000),
|
|
101
|
-
body: JSON.stringify({ planId, line: text }),
|
|
106
|
+
body: JSON.stringify({ planId, line: text, instance: DAEMON_INSTANCE, ...(attempt !== undefined ? { attempt } : {}), ...(phase ? { phase, diagnosis } : {}) }),
|
|
102
107
|
});
|
|
103
108
|
} catch {
|
|
104
109
|
/* a readout — losing a line costs nothing */
|
|
@@ -134,7 +139,7 @@ export function createWorkAgentPlans({
|
|
|
134
139
|
const PLAN_TURN_TIMEOUT_MS = 15 * 60_000;
|
|
135
140
|
|
|
136
141
|
// The peer may hold it; doing nothing is the safe answer.
|
|
137
|
-
const claimAgentPlan = (id) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id });
|
|
142
|
+
const claimAgentPlan = (id, attempt) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id, ...(attempt !== undefined ? { attempt } : {}) });
|
|
138
143
|
|
|
139
144
|
/** See `runAgentTurn` — the admission reservation, released the moment the
|
|
140
145
|
* planner's CLI exists. */
|
|
@@ -149,11 +154,27 @@ export function createWorkAgentPlans({
|
|
|
149
154
|
// kill keeps (refuse cheaply, then claim) and it is because these two
|
|
150
155
|
// lanes settle differently: a kill's settle does not check a lease for the
|
|
151
156
|
// not-found case, and a plan's always does.
|
|
152
|
-
if (!(await claimAgentPlan(id))) return;
|
|
157
|
+
if (!(await claimAgentPlan(id, job.attempt))) return;
|
|
158
|
+
const control = job.recoveryEnabled ? await watchPlanAttempt(AGENT_PLAN_STATE_URL, job) : null;
|
|
159
|
+
const report = async (body) => {
|
|
160
|
+
const result = { ...body, ...(job.attempt !== undefined ? { attempt: job.attempt } : {}) };
|
|
161
|
+
reportPlanOutcome(job, result);
|
|
162
|
+
// Keep the measured result while its acknowledgement is unknown. A
|
|
163
|
+
// network retry delivers this report, never another paid planning turn.
|
|
164
|
+
while (!control?.signal.aborted) {
|
|
165
|
+
const sent = await sendAgentPlan(result);
|
|
166
|
+
if (sent !== false || !job.recoveryEnabled) return;
|
|
167
|
+
await postAgentPlanActivity(id, 'The Plan result has not been acknowledged. Retrying delivery.', job.attempt);
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const unavailable = (reason) => report({ id, ...(job.recoveryEnabled ? { question: { question: `${reason}. Can you make that CLI available on the machine and tell me when it is ready?`, context: reason } } : { error: reason }) });
|
|
172
|
+
try {
|
|
173
|
+
if (control?.signal.aborted) return;
|
|
153
174
|
if (tasks.length === 0) {
|
|
154
175
|
// The press named cards the doc no longer has. A real answer, and one the
|
|
155
176
|
// board can explain, rather than a turn that plans nothing.
|
|
156
|
-
await
|
|
177
|
+
await report({ id, error: 'those cards are no longer on the board' });
|
|
157
178
|
return;
|
|
158
179
|
}
|
|
159
180
|
|
|
@@ -172,23 +193,27 @@ export function createWorkAgentPlans({
|
|
|
172
193
|
rt = pickedRuntime;
|
|
173
194
|
const runtime = runtimeById(rt);
|
|
174
195
|
if (!runtime) {
|
|
175
|
-
await
|
|
196
|
+
await unavailable(`${rt} is not a CLI this machine knows`);
|
|
176
197
|
return;
|
|
177
198
|
}
|
|
178
199
|
const label = runtime.label;
|
|
179
200
|
if (!detectRuntimes().find((r) => r.id === rt)?.installed) {
|
|
180
|
-
await
|
|
201
|
+
await unavailable(`${label} is not installed on this machine`);
|
|
181
202
|
return;
|
|
182
203
|
}
|
|
183
204
|
if (!canRun(runtime, 'consult')) {
|
|
184
|
-
await
|
|
205
|
+
await unavailable(`${label} can't run a read-only turn on this machine`);
|
|
185
206
|
return;
|
|
186
207
|
}
|
|
187
208
|
} else {
|
|
188
209
|
rt = pickRuntimeFor('consult');
|
|
189
210
|
}
|
|
190
211
|
if (!rt) {
|
|
191
|
-
await
|
|
212
|
+
await unavailable('No CLI on this machine can run a read-only turn');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (job.recoveryEnabled && job.recovery && rt === 'claude' && authStatus({ refresh: true }).signedIn === false) {
|
|
216
|
+
await report({ id, question: { question: 'Claude Code reports that it is signed out. Can you sign in on this machine and tell me when it is ready?', context: 'The CLI’s native auth status reported signed out.' } });
|
|
192
217
|
return;
|
|
193
218
|
}
|
|
194
219
|
|
|
@@ -204,7 +229,7 @@ export function createWorkAgentPlans({
|
|
|
204
229
|
const now = Date.now();
|
|
205
230
|
if (now - lastPlanBeat < 2_000) return;
|
|
206
231
|
lastPlanBeat = now;
|
|
207
|
-
void postAgentPlanActivity(id, line);
|
|
232
|
+
void postAgentPlanActivity(id, line, job.attempt);
|
|
208
233
|
};
|
|
209
234
|
/**
|
|
210
235
|
* THE MACHINE'S OWN VOICE, for the moments only this side can see — the
|
|
@@ -219,10 +244,11 @@ export function createWorkAgentPlans({
|
|
|
219
244
|
*/
|
|
220
245
|
const say = (line) => {
|
|
221
246
|
lastPlanBeat = Date.now();
|
|
222
|
-
void postAgentPlanActivity(id, line);
|
|
247
|
+
void postAgentPlanActivity(id, line, job.attempt);
|
|
223
248
|
};
|
|
224
249
|
|
|
225
250
|
let out = '';
|
|
251
|
+
let diagnosis = null;
|
|
226
252
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
227
253
|
let wedged = false;
|
|
228
254
|
/**
|
|
@@ -234,7 +260,7 @@ export function createWorkAgentPlans({
|
|
|
234
260
|
*/
|
|
235
261
|
let usage = null;
|
|
236
262
|
const discovery = createToolLog(repoRoot);
|
|
237
|
-
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog });
|
|
263
|
+
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog, ...(diagnosis ? { diagnosis: diagnosis.summary } : {}) });
|
|
238
264
|
// Said BEFORE the lock is asked for, because a reader only waits when a
|
|
239
265
|
// writer holds the checkout or is queued ahead of it — which is exactly
|
|
240
266
|
// this condition, read one line before we join the queue.
|
|
@@ -261,20 +287,17 @@ export function createWorkAgentPlans({
|
|
|
261
287
|
discovery.toolLog.baseline = measureRepositoryBaseline({ cwd: repoRoot, baseRef, knowledgeDir });
|
|
262
288
|
discovery.toolLog.captureBaseline = repositoryBaselineOf(job.captureBaseline);
|
|
263
289
|
discovery.toolLog.reconciliation = compareRepositoryBaselines(discovery.toolLog.baseline, discovery.toolLog.captureBaseline);
|
|
264
|
-
(
|
|
290
|
+
const consult = async (prompt, system) => await runCappedTurn({
|
|
265
291
|
runTurn,
|
|
292
|
+
signal: control?.signal,
|
|
266
293
|
capMs: PLAN_TURN_TIMEOUT_MS,
|
|
267
294
|
workChildren,
|
|
268
295
|
releaseSlot,
|
|
269
296
|
transcriptCwd: repoRoot,
|
|
270
297
|
turnArgs: {
|
|
271
298
|
diagnosticPlanId: id,
|
|
272
|
-
prompt
|
|
273
|
-
|
|
274
|
-
liveAgents,
|
|
275
|
-
agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3,
|
|
276
|
-
}),
|
|
277
|
-
system: withProjectContext(SYSTEM_PLAN, { knowledgeDir }) + '\n\n' + discoveryGuide(rt, 'planning') + '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(discovery.toolLog.baseline, discovery.toolLog.captureBaseline, discovery.toolLog.reconciliation),
|
|
299
|
+
prompt,
|
|
300
|
+
system,
|
|
278
301
|
knowledgeDir,
|
|
279
302
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
280
303
|
// passed empty, so there is no control plane on this turn at all.
|
|
@@ -283,7 +306,7 @@ export function createWorkAgentPlans({
|
|
|
283
306
|
runtime: rt,
|
|
284
307
|
...brainFor(job, rt),
|
|
285
308
|
answerFromResult: true,
|
|
286
|
-
|
|
309
|
+
work: planWork(job),
|
|
287
310
|
// The CLI's own tail, forwarded to the press. Same rule as an agent
|
|
288
311
|
// turn's: throttled, overwritten rather than appended, and never
|
|
289
312
|
// awaited by the turn.
|
|
@@ -300,14 +323,39 @@ export function createWorkAgentPlans({
|
|
|
300
323
|
onSpawn: () => {
|
|
301
324
|
say(`${runtimeLabel(rt)} started on this machine`);
|
|
302
325
|
},
|
|
303
|
-
})
|
|
326
|
+
});
|
|
327
|
+
let context = AGENT_PLAN_KICKOFF({ tasks, liveAgents, agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3 });
|
|
328
|
+
if (job.recovery?.start) context += '\nSTART REPAIR: This is a supporting diagnosis for an already approved Start. Return exactly ONE group with the same ordered taskIds, attachment target (or absence) and CLI. Preserve the approval; do not start an unapproved sibling. Diagnose admission/pickup errors and repair planning evidence/context. Ask only a genuine person-only question in the diagnostic JSON. Approval and source revision (data): ' + JSON.stringify(job.recovery.start);
|
|
329
|
+
const system = withProjectContext(SYSTEM_PLAN, { knowledgeDir }) + '\n\n' + discoveryGuide(rt, 'planning') + '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(discovery.toolLog.baseline, discovery.toolLog.captureBaseline, discovery.toolLog.reconciliation);
|
|
330
|
+
if (job.recovery) {
|
|
331
|
+
say(`Diagnosing the failed Plan: ${job.recovery.reason}`);
|
|
332
|
+
const diagnostic = await consult(context + '\nRetained recovery context (data): ' + JSON.stringify(job.recovery), withProjectContext(PLAN_RECOVERY_SYSTEM, { knowledgeDir }) + '\n' + discoveryGuide(rt, 'planning'));
|
|
333
|
+
if (control?.signal.aborted) return;
|
|
334
|
+
if (diagnostic.wedged) { wedged = true; return; }
|
|
335
|
+
const result = parsePlanDiagnosis(diagnostic.out);
|
|
336
|
+
if (result?.status === 'question') {
|
|
337
|
+
diagnosis = result;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!result) throw new Error(`The diagnostic turn did not return a diagnosis: ${envScrub(String(diagnostic.out ?? '')).slice(0, 300) || 'no output'}`);
|
|
341
|
+
diagnosis = result;
|
|
342
|
+
await postAgentPlanActivity(id, `Retrying Plan: ${result.summary}`, job.attempt, 'retrying', result.summary);
|
|
343
|
+
await control?.check();
|
|
344
|
+
if (control?.signal.aborted) return;
|
|
345
|
+
}
|
|
346
|
+
({ out, wedged } = await consult(context + (diagnosis ? '\nDiagnosis and corrected approach (data): ' + JSON.stringify(diagnosis) : ''), system));
|
|
304
347
|
});
|
|
305
348
|
} catch (e) {
|
|
306
349
|
discovery.toolLog.state = 'partial';
|
|
307
|
-
await
|
|
350
|
+
await report({ id, error: envScrub(String(e?.message || e)).slice(0, 500), ...spent() });
|
|
308
351
|
return;
|
|
309
352
|
}
|
|
310
353
|
|
|
354
|
+
if (control?.signal.aborted) return;
|
|
355
|
+
if (diagnosis?.status === 'question') {
|
|
356
|
+
await report({ id, question: { question: diagnosis.question, context: diagnosis.context }, ...spent() });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
311
359
|
if (wedged) {
|
|
312
360
|
discovery.toolLog.state = 'partial';
|
|
313
361
|
// NEVER LEAVE A CLAIMED PRESS UNREPORTED — the belt the merge lane wears
|
|
@@ -315,7 +363,7 @@ export function createWorkAgentPlans({
|
|
|
315
363
|
// only side that knows: the server would have expired this press in
|
|
316
364
|
// another fifteen minutes with a sentence blaming a daemon that had in
|
|
317
365
|
// fact answered.
|
|
318
|
-
await
|
|
366
|
+
await report({
|
|
319
367
|
id,
|
|
320
368
|
error: 'the planning turn ran past fifteen minutes on this machine and was stopped',
|
|
321
369
|
...spent(),
|
|
@@ -325,7 +373,7 @@ export function createWorkAgentPlans({
|
|
|
325
373
|
|
|
326
374
|
const proposal = parseProposal(out);
|
|
327
375
|
if (!proposal) {
|
|
328
|
-
await
|
|
376
|
+
await report({
|
|
329
377
|
id,
|
|
330
378
|
// The CLI's own words when it has any — a turn that explained why it
|
|
331
379
|
// could not plan is far more use than "planning failed".
|
|
@@ -336,7 +384,8 @@ export function createWorkAgentPlans({
|
|
|
336
384
|
});
|
|
337
385
|
return;
|
|
338
386
|
}
|
|
339
|
-
await
|
|
387
|
+
await report({ id, proposal, sessionRef: repoRoot, ...spent() });
|
|
388
|
+
} finally { control?.stop(); }
|
|
340
389
|
};
|
|
341
390
|
|
|
342
391
|
const processAgentPlanJobs = (jobs) => {
|
|
@@ -239,7 +239,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
239
239
|
// agent grading its own homework out of its own context; this is a
|
|
240
240
|
// stranger reading a diff.
|
|
241
241
|
answerFromResult: true,
|
|
242
|
-
|
|
242
|
+
work: { kind: 'pre-review', id: agentId, name: agentName },
|
|
243
243
|
// The CLI's own count for this reading. Set, never accumulated: one
|
|
244
244
|
// turn is one `result` event, and the adding-up is the server's.
|
|
245
245
|
// Tagged with the CLI, so a Claude pre-review of a Codex agent is
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { agentWork } from './workActivity.mjs';
|
|
2
|
+
import { relayedText } from './prWorkflow.mjs';
|
|
3
|
+
import { measuredMergeResolution } from './agentMergeResolution.mjs';
|
|
1
4
|
import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryDelivery.mjs';
|
|
2
5
|
/**
|
|
3
6
|
* ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
|
|
@@ -18,7 +21,7 @@ import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryD
|
|
|
18
21
|
* answered. `measured` is the same bargain the other way: the branch facts and
|
|
19
22
|
* the spend, written here as they are learned, read by that catch.
|
|
20
23
|
*/
|
|
21
|
-
import { PROJECT_ID } from './config.mjs';
|
|
24
|
+
import { MODEL, PROJECT_ID } from './config.mjs';
|
|
22
25
|
import { setRuntimeLimit } from './daemonState.mjs';
|
|
23
26
|
import { gitOrNull } from './git.mjs';
|
|
24
27
|
import { turnCommits } from './turnReceipts.mjs';
|
|
@@ -49,6 +52,8 @@ import { createDeliveryReporter } from './workDelivery.mjs';
|
|
|
49
52
|
import { createLiveMessages } from './workLiveMessages.mjs';
|
|
50
53
|
import { createOrchestrator } from './orchestrator.mjs';
|
|
51
54
|
import { claudeOrchestrationSafe } from './orchestratorHooks.mjs';
|
|
55
|
+
import { ORCHESTRATED_POSTURES, orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
56
|
+
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
52
57
|
import { codexSubagentsSafe } from './runtimeCodex.mjs';
|
|
53
58
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
54
59
|
|
|
@@ -304,10 +309,12 @@ export function createAgentTurnExecution({
|
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
// ORCHESTRATED ONLY WHERE THE CLI CAN BE GATED (2026-10-17). v2 reaches every
|
|
307
|
-
// job at
|
|
308
|
-
// included) has no controller, so
|
|
312
|
+
// job at its kind's floor; an artifact card (design, model, image, deck) or
|
|
313
|
+
// a CLI below its measured floor (unknown included) has no controller, so
|
|
314
|
+
// no marker: its notes fold on receipt. Research is orchestrated too since
|
|
315
|
+
// 2026-10-19, with read-only children (orchestratorPrompt.mjs).
|
|
309
316
|
const codexVersion = rt === 'codex' ? measuredRuntimeVersion('codex')?.version : undefined;
|
|
310
|
-
const orchestrating = job.orchestrationVersion === 2 && posture
|
|
317
|
+
const orchestrating = job.orchestrationVersion === 2 && orchestratesPosture(posture) &&
|
|
311
318
|
(rt === 'claude' ? claudeOrchestrationSafe(measuredRuntimeVersion('claude')?.version) : rt === 'codex' && codexSubagentsSafe(codexVersion));
|
|
312
319
|
/**
|
|
313
320
|
* THE WHOLE STREAM, not just its latest line — see trace.mjs.
|
|
@@ -351,6 +358,10 @@ export function createAgentTurnExecution({
|
|
|
351
358
|
* neither key.
|
|
352
359
|
*/
|
|
353
360
|
const brain = brainFor(job, rt);
|
|
361
|
+
// The card's model and effort as the parent runs them, for an orchestrated
|
|
362
|
+
// run's children and contract: Claude always names --model (the daemon's
|
|
363
|
+
// pin when the card has none); Codex's unpinned default is its own.
|
|
364
|
+
const cardBrain = { model: brain.model ?? (rt === 'claude' ? MODEL : null), effort: brain.effort ?? null };
|
|
354
365
|
|
|
355
366
|
let out = '';
|
|
356
367
|
let child = null;
|
|
@@ -384,7 +395,7 @@ export function createAgentTurnExecution({
|
|
|
384
395
|
/** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
|
|
385
396
|
* every settle `usage` rides, for the same reason; null sends no key. */
|
|
386
397
|
let model = null;
|
|
387
|
-
const brief = makeBrief?.({ agentId, turnId, runtime: rt, model: () => model ?? job.model });
|
|
398
|
+
const brief = makeBrief?.({ agentId, turnId, workName: job.task?.title || job.agentName, runtime: rt, model: () => model ?? job.model });
|
|
388
399
|
brief?.append('prompt', job.task ? { title: job.task.title, message: job.body, discussion: job.cardThread, brief: job.task.brief, criteria: job.task.criteria } : job.body);
|
|
389
400
|
/**
|
|
390
401
|
* THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
|
|
@@ -404,6 +415,9 @@ export function createAgentTurnExecution({
|
|
|
404
415
|
try {
|
|
405
416
|
if (orchestrating) {
|
|
406
417
|
orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
|
|
418
|
+
// Codex's spawn takes only a catalog model (knownCodexModels, the
|
|
419
|
+
// measured models_cache.json rows the roster already reports).
|
|
420
|
+
children: { access: ORCHESTRATED_POSTURES[posture], ...cardBrain, ...(rt === 'codex' ? { catalog: knownCodexModels() } : {}) },
|
|
407
421
|
// Whether the words reached the CLI; Codex reads its inbox from hooks.
|
|
408
422
|
notify: message => liveInput ? run.live?.inject(message) === true : false,
|
|
409
423
|
taken: id => run.live?.onTaken(id) });
|
|
@@ -424,8 +438,10 @@ export function createAgentTurnExecution({
|
|
|
424
438
|
})) } : {}),
|
|
425
439
|
// The kickoff and the system prompt, composed by the agent lane's
|
|
426
440
|
// shape (workAgentTurnShape.mjs) from what this run measured.
|
|
441
|
+
researchHandoff: job.researchHandoff,
|
|
442
|
+
researchIdentity: { projectId: PROJECT_ID, agentId, turnId, taskId: job.task?.id ?? job.repositoryTaskId },
|
|
427
443
|
prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message }),
|
|
428
|
-
system: agentTurnSystem({ taskKind, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun,
|
|
444
|
+
system: agentTurnSystem({ taskKind, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun, orchestration: orchestrator ? { posture, runtime: rt, ...cardBrain } : null }),
|
|
429
445
|
agentTools: projectTools,
|
|
430
446
|
writableDirs: repositoryRun?.writableDirs,
|
|
431
447
|
...((tools.envNeeded || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...tools.env, ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
|
|
@@ -465,7 +481,7 @@ export function createAgentTurnExecution({
|
|
|
465
481
|
// Present only when the container named one — see brainFor.
|
|
466
482
|
...brain,
|
|
467
483
|
answerFromResult: true,
|
|
468
|
-
|
|
484
|
+
work: agentWork(job),
|
|
469
485
|
// The CLI's own context measure while the turn runs (claudeInput.mjs),
|
|
470
486
|
// the latest one at a time; the server clears it at turn end.
|
|
471
487
|
onContext: (() => {
|
|
@@ -752,7 +768,25 @@ export function createAgentTurnExecution({
|
|
|
752
768
|
// reported as `nothing` — it did not deliver and it did not ask.
|
|
753
769
|
await postAgentParked(limit, rt);
|
|
754
770
|
}
|
|
755
|
-
if (job.kind === 'merge_resolve'
|
|
771
|
+
if (job.kind === 'merge_resolve') {
|
|
772
|
+
const resolution = () => repositoryRun
|
|
773
|
+
? allResolutionsCommitted(repositoryRun.sourcesNow())
|
|
774
|
+
: measuredMergeResolution(wt, before, baseRef());
|
|
775
|
+
body.mergeResolved = resolution();
|
|
776
|
+
if (body.outcome === 'delivered' && body.mergeResolved) {
|
|
777
|
+
const rows = repositoryRun?.sourcesNow() ?? [{ wt }];
|
|
778
|
+
for (const r of rows) {
|
|
779
|
+
const head = gitOrNull(['rev-parse', 'HEAD'], r.wt);
|
|
780
|
+
const check = await runReviewEntry(agentId, r.wt, job.agentName, repositoryRun ? r : undefined);
|
|
781
|
+
if (check?.status === 'failed') {
|
|
782
|
+
body.mergeResolved = false;
|
|
783
|
+
body.mergeDetail = relayedText(check.text || check.output || 'The repository check failed during merge recovery.').slice(0, 2000);
|
|
784
|
+
}
|
|
785
|
+
if (!head || head !== gitOrNull(['rev-parse', 'HEAD'], r.wt)) body.mergeResolved = false;
|
|
786
|
+
}
|
|
787
|
+
body.mergeResolved = body.mergeResolved && resolution();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
756
790
|
const reply = await postAgentTurn(body);
|
|
757
791
|
// The queue just emptied. Run the project's own check and the AI
|
|
758
792
|
// pre-review HERE, in the worktree we are already standing in and still
|
|
@@ -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
|
}
|
|
@@ -160,6 +173,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
|
|
|
160
173
|
else if (now - held.at > AGENT_REPORT_GRACE_MS) {
|
|
161
174
|
agentReported.delete(id);
|
|
162
175
|
agentRejectedUntil.delete(id);
|
|
176
|
+
persistRecovery();
|
|
163
177
|
}
|
|
164
178
|
}
|
|
165
179
|
};
|
|
@@ -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.
|
|
@@ -69,7 +70,7 @@ export function createWorkAgentTurns({
|
|
|
69
70
|
pruneHeldReports,
|
|
70
71
|
agentReported,
|
|
71
72
|
agentRejectedUntil,
|
|
72
|
-
} = createAgentTurnReports({ REJECT_RETRY_MS });
|
|
73
|
+
} = createAgentTurnReports({ REJECT_RETRY_MS, baseDir });
|
|
73
74
|
|
|
74
75
|
// ── AGENT TURNS: one task per prompt ───────────────────────────────────────
|
|
75
76
|
//
|
|
@@ -205,7 +206,8 @@ export function createWorkAgentTurns({
|
|
|
205
206
|
};
|
|
206
207
|
const settleOnce = (body) => {
|
|
207
208
|
settled = true;
|
|
208
|
-
|
|
209
|
+
reportAgentOutcome(job, body);
|
|
210
|
+
return postAgentTurn(withLiveReceipts(job.kind === 'merge_resolve' ? { ...body, mergeResolved: body.mergeResolved ?? false } : body));
|
|
209
211
|
};
|
|
210
212
|
|
|
211
213
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
@@ -221,11 +223,11 @@ export function createWorkAgentTurns({
|
|
|
221
223
|
if (settled) return;
|
|
222
224
|
const commits = turnCommits({ repositoryReceipts: measured.repositoryReceipts, wt: measured.wt, before: measured.before, baseRef });
|
|
223
225
|
if (run.interrupted) {
|
|
224
|
-
await
|
|
226
|
+
await settleOnce(interruptedTurnSettlement({ turnId, ran: measured.ranCli === true, commits, usage: measured.usage, model: measured.model, branch: measured.branch, wt: measured.wt }));
|
|
225
227
|
return;
|
|
226
228
|
}
|
|
227
|
-
await
|
|
228
|
-
|
|
229
|
+
await settleOnce(
|
|
230
|
+
failedTurnSettlement({
|
|
229
231
|
turnId,
|
|
230
232
|
error,
|
|
231
233
|
commits,
|
|
@@ -233,7 +235,7 @@ export function createWorkAgentTurns({
|
|
|
233
235
|
model: measured.model,
|
|
234
236
|
branch: measured.branch,
|
|
235
237
|
wt: measured.wt,
|
|
236
|
-
})
|
|
238
|
+
})
|
|
237
239
|
);
|
|
238
240
|
}
|
|
239
241
|
}, () => {
|
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) {
|