@ctrl-spc/cs 0.7.15 → 0.7.16
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/dist/agents.js +175 -1
- package/dist/codex-home.js +14 -11
- package/dist/companion-ui.js +24 -4
- package/dist/companion.js +33 -25
- package/dist/config.js +147 -16
- package/dist/daemon-lifecycle.js +39 -12
- package/dist/daemon-processes.js +123 -19
- package/dist/failure-reason.js +76 -16
- package/dist/index.js +6 -3
- package/dist/login.js +6 -8
- package/dist/mcp.js +55 -56
- package/dist/orchestrator.js +322 -197
- package/dist/panel3/coordinator.js +3 -1
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/run.js +220 -84
- package/dist/panel3/spawn.js +26 -12
- package/dist/panel3/tools.js +5 -0
- package/dist/presence-heartbeat.js +3 -0
- package/dist/presence.js +122 -145
- package/dist/supabase.js +141 -39
- package/package.json +1 -1
package/dist/panel3/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { failureMessage } from '../failure-reason.js';
|
|
1
2
|
/**
|
|
2
3
|
* ═══ AGENT PANEL v3: the poll loop that answers a card. ═══
|
|
3
4
|
*
|
|
@@ -137,7 +138,7 @@ import { answerPrompt, escalationPrompt, levelOnePrompt, ownerActivationPrompt,
|
|
|
137
138
|
import { ASK_CONTENT_COLUMNS, attachmentLine, gitRulesFor, loadAttachments, loadOutputNames, outputOf, recordBaseProtection, standingRulesFor, withAskContent, } from './show.js';
|
|
138
139
|
import { forgetSecrets, redactSecrets } from './secrets.js';
|
|
139
140
|
import { sayListening, sayPollingProblem, stopListening } from './presence.js';
|
|
140
|
-
import { selectedHarness } from './coordinator.js';
|
|
141
|
+
import { selectedHarness, recoveryHarnesses } from './coordinator.js';
|
|
141
142
|
import { baseBranchState, checkoutForCodebase, commitCardWork, detectBaseProtection, folderIsBranch, hasCheckoutForCodebase, mergeIntoBase, releaseBaseBranch, settleCardWorktree, worktreeForCard, ownsReconciliation, prepareCardReconciliation, finishCardReconciliation, worktreesOnThisMachine, } from './checkout.js';
|
|
142
143
|
import { harness, startAgent } from './spawn.js';
|
|
143
144
|
import { establishOwnerSession, listOwnerSessionIds, OWNER_SESSION_GRACE_MS, readOwnerSession, removeOwnerSession, validSessionUuid, writeOwnerSession, } from './session.js';
|
|
@@ -155,10 +156,10 @@ import { join } from 'node:path';
|
|
|
155
156
|
function recoveryAttempt(run) {
|
|
156
157
|
return { runId: run.id, processToken: run.process_token, startedAt: run.started_at, resumedAt: run.resumed_at };
|
|
157
158
|
}
|
|
158
|
-
async function startTrackedAgent(client, runId, preparation, work, ...args) {
|
|
159
|
+
async function startTrackedAgent(client, runId, preparation, work, captured, ...args) {
|
|
159
160
|
if (!preparation)
|
|
160
|
-
return startAgent(...args);
|
|
161
|
-
const claimed = work?.claimAttempt(runId);
|
|
161
|
+
return { ...startAgent(...args), attempt: captured };
|
|
162
|
+
const claimed = captured ?? work?.claimAttempt(runId);
|
|
162
163
|
if (!claimed)
|
|
163
164
|
throw new Error('This run no longer belongs to the claimed attempt.');
|
|
164
165
|
preparation.setAttempt(claimed);
|
|
@@ -207,7 +208,7 @@ async function startTrackedAgent(client, runId, preparation, work, ...args) {
|
|
|
207
208
|
return answer;
|
|
208
209
|
});
|
|
209
210
|
void answered.catch(() => { }); // The caller records the PID before awaiting settlement.
|
|
210
|
-
return { ...started, answered, attempt: claimed, preparationFailed: () => preparationFailed };
|
|
211
|
+
return { ...started, answered, attempt: claimed, deferOutcome: preparation.deferOutcome, acknowledgeOutcome: preparation.acknowledgeOutcome, preparationFailed: () => preparationFailed };
|
|
211
212
|
}
|
|
212
213
|
function panelClaimAttempts(result) {
|
|
213
214
|
if (!Array.isArray(result))
|
|
@@ -220,14 +221,15 @@ function panelClaimAttempts(result) {
|
|
|
220
221
|
}
|
|
221
222
|
unique.set(attempt.run_id, { runId: attempt.run_id, cardId: attempt.card_id, processToken: attempt.process_token ?? null,
|
|
222
223
|
pid: attempt.pid ?? null, startedAt: attempt.started_at, resumedAt: attempt.resumed_at ?? null,
|
|
223
|
-
observedPendingTurnIds: attempt.observed_pending_turn_ids ?? null });
|
|
224
|
+
observedPendingTurnIds: attempt.observed_pending_turn_ids ?? null, authRecoveryRunId: attempt.auth_recovery_run_id ?? null, harness: attempt.harness ?? null });
|
|
224
225
|
}
|
|
225
226
|
return [...unique.values()];
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
function capturedPanelAttempt(rows, runId, work) {
|
|
229
|
+
return work?.claimAttempt(runId) ?? panelClaimAttempts(rows).find(attempt => attempt.runId === runId);
|
|
230
|
+
}
|
|
231
|
+
async function panelClaim(client, work, action, args, nativeContext) {
|
|
232
|
+
const operationId = work?.beginRpc(action, { ...args, ...(nativeContext ? { nativeContext } : {}) }) ?? randomUUID();
|
|
231
233
|
try {
|
|
232
234
|
const response = await client.rpc(action, { ...args, p_operation_id: operationId });
|
|
233
235
|
if (!response.error) {
|
|
@@ -237,14 +239,16 @@ async function panelClaim(client, work, action, args) {
|
|
|
237
239
|
const row = journal.data?.[0];
|
|
238
240
|
if (row?.outcome !== 'completed' || row.action !== action)
|
|
239
241
|
throw new Error('The claim was cancelled before it could start.');
|
|
240
|
-
|
|
242
|
+
const attempts = panelClaimAttempts(row.result);
|
|
243
|
+
work?.recordRpc(operationId, attempts, false, row.result);
|
|
244
|
+
return { ...response, data: row.result, operationId };
|
|
241
245
|
}
|
|
242
246
|
if (response.error)
|
|
243
|
-
work
|
|
247
|
+
work?.deferRpc(operationId);
|
|
244
248
|
return { ...response, operationId };
|
|
245
249
|
}
|
|
246
250
|
catch (error) {
|
|
247
|
-
work
|
|
251
|
+
work?.deferRpc(operationId);
|
|
248
252
|
throw error;
|
|
249
253
|
}
|
|
250
254
|
}
|
|
@@ -252,7 +256,23 @@ async function reconcilePanelInterruptions(client, machineId, work) {
|
|
|
252
256
|
const capability = await client.rpc('panel3_interrupt_machine_runs', { p_machine_id: machineId,
|
|
253
257
|
p_operation_id: randomUUID(), p_interrupted_at: new Date().toISOString(), p_attempts: [] });
|
|
254
258
|
if (capability.error)
|
|
255
|
-
throw capability.error;
|
|
259
|
+
throw new Error(`could not verify interruption recovery: ${capability.error.message}`);
|
|
260
|
+
for (const pending of work.pendingOutcomes()) {
|
|
261
|
+
const { attempt, outcome } = pending;
|
|
262
|
+
const { data, error } = await client.from('panel3_runs').select('started_at,resumed_at,process_token,state,ended_at,level,card:panel3_cards!panel3_runs_card_id_fkey(conversation_run_id)').eq('id', attempt.runId).maybeSingle();
|
|
263
|
+
if (error)
|
|
264
|
+
throw new Error(`could not read the completed attempt: ${error.message}`);
|
|
265
|
+
if (data && data.started_at === attempt.startedAt && (data.resumed_at ?? null) === attempt.resumedAt
|
|
266
|
+
&& (data.process_token ?? null) === attempt.processToken && data.ended_at === null) {
|
|
267
|
+
const card = (Array.isArray(data.card) ? data.card[0] : data.card);
|
|
268
|
+
const successfulToolEnding = !outcome.ok && outcome.failureKind !== 'authentication' && (data.state === 'asked' || (data.level === 1 && !!card?.conversation_run_id));
|
|
269
|
+
if (outcome.ok || successfulToolEnding)
|
|
270
|
+
await writeAnswer(client, attempt.runId, attempt.cardId, successfulToolEnding ? null : outcome.text, attempt.processToken ?? undefined, attempt);
|
|
271
|
+
else
|
|
272
|
+
await endRun(client, outcome.level ?? 1, attempt.runId, attempt.cardId, outcome.reason, attempt, outcome.failureKind);
|
|
273
|
+
}
|
|
274
|
+
work.acknowledgeOutcome(pending.id);
|
|
275
|
+
}
|
|
256
276
|
for (const failure of work.failedPreparations()) {
|
|
257
277
|
await endRun(client, failure.level, failure.attempt.runId, failure.attempt.cardId, failure.reason, failure.attempt);
|
|
258
278
|
work.acknowledgePreparationFailure(failure.id);
|
|
@@ -260,12 +280,12 @@ async function reconcilePanelInterruptions(client, machineId, work) {
|
|
|
260
280
|
for (const claim of work.pendingRpcs()) {
|
|
261
281
|
const journal = await client.rpc('panel3_reconcile_claim', { p_machine_id: machineId, p_operation_id: claim.id });
|
|
262
282
|
if (journal.error)
|
|
263
|
-
throw journal.error;
|
|
283
|
+
throw new Error(`could not read the claim receipt: ${journal.error.message}`);
|
|
264
284
|
const row = journal.data?.[0];
|
|
265
285
|
if (row?.outcome === 'cancelled')
|
|
266
286
|
work.recordRpc(claim.id, [], true);
|
|
267
287
|
else if (row?.outcome === 'completed' && row.action === claim.action)
|
|
268
|
-
work.recordRpc(claim.id, panelClaimAttempts(row.result),
|
|
288
|
+
work.recordRpc(claim.id, panelClaimAttempts(row.result), false, row.result);
|
|
269
289
|
else
|
|
270
290
|
throw new Error('The pending claim could not be reconciled.');
|
|
271
291
|
}
|
|
@@ -742,11 +762,14 @@ async function recordProcess(client, runId, pid, brief, processToken, attempt) {
|
|
|
742
762
|
* there first. It is a fact about the record rather than a failure, so it does
|
|
743
763
|
* not go through `returned()`, exactly as the answer's own null does not.
|
|
744
764
|
*/
|
|
745
|
-
async function giveUp(client, runId, reason, processToken, attempt) {
|
|
765
|
+
async function giveUp(client, runId, reason, processToken, attempt, failureKind) {
|
|
746
766
|
const { data, error } = await client
|
|
747
767
|
.rpc('panel3_give_up', {
|
|
748
768
|
p_run_id: runId,
|
|
749
769
|
p_reason: reason,
|
|
770
|
+
p_failure_kind: failureKind ?? null,
|
|
771
|
+
p_process_token: processToken ?? null,
|
|
772
|
+
p_expected_attempt: null,
|
|
750
773
|
...(processToken === undefined ? {} : { p_process_token: processToken }),
|
|
751
774
|
...(attempt ? { p_process_token: attempt.processToken, p_expected_attempt: {
|
|
752
775
|
started_at: attempt.startedAt, resumed_at: attempt.resumedAt,
|
|
@@ -799,7 +822,7 @@ async function giveUp(client, runId, reason, processToken, attempt) {
|
|
|
799
822
|
* a run can be the last thing live on it — but no turn is written, exactly as
|
|
800
823
|
* none is written for a run that stopped to ask.
|
|
801
824
|
*/
|
|
802
|
-
async function writeAnswer(client, runId, cardId, text, processToken) {
|
|
825
|
+
async function writeAnswer(client, runId, cardId, text, processToken, attempt) {
|
|
803
826
|
/* ═══ THE SECOND OF THE TWO CHOKEPOINTS THE CREDENTIAL RULE RESTS ON. ═══
|
|
804
827
|
This is the ONE path an agent's own words take to a hosted row — a turn on
|
|
805
828
|
the card at levels 1 and 2, and a level 3's `panel3_runs.report`, which
|
|
@@ -809,6 +832,9 @@ async function writeAnswer(client, runId, cardId, text, processToken) {
|
|
|
809
832
|
const { data: turnId, error } = await client
|
|
810
833
|
.rpc('panel3_answer', {
|
|
811
834
|
p_run_id: runId,
|
|
835
|
+
p_process_token: processToken ?? attempt?.processToken ?? null,
|
|
836
|
+
p_expected_attempt: attempt ? { started_at: attempt.startedAt, resumed_at: attempt.resumedAt } : null,
|
|
837
|
+
p_auth_recovery_run_id: attempt?.authRecoveryRunId ?? null,
|
|
812
838
|
p_body: text === null ? null : redactSecrets(processToken === undefined ? runId : `${runId}:${processToken}`, text),
|
|
813
839
|
...(processToken === undefined ? {} : { p_process_token: processToken }),
|
|
814
840
|
});
|
|
@@ -876,14 +902,15 @@ async function writeAnswer(client, runId, cardId, text, processToken) {
|
|
|
876
902
|
away. */
|
|
877
903
|
said(`run ${runId} had already ended, so its answer was not written to card ${cardId}`);
|
|
878
904
|
}
|
|
879
|
-
return ownerSettlementAccepted(run, processToken);
|
|
905
|
+
return ownerSettlementAccepted(run, processToken, attempt);
|
|
880
906
|
}
|
|
881
907
|
out(`answered card ${cardId} run ${runId} ${text?.length ?? 0} characters`);
|
|
882
908
|
return true;
|
|
883
909
|
}
|
|
884
|
-
export function ownerSettlementAccepted(run, processToken) {
|
|
910
|
+
export function ownerSettlementAccepted(run, processToken, attempt) {
|
|
885
911
|
return (run.state === 'asked' || run.state === 'finished')
|
|
886
|
-
&& (processToken === undefined || run.processToken === processToken)
|
|
912
|
+
&& (processToken === undefined || run.processToken === processToken)
|
|
913
|
+
&& (!attempt || run.processToken === attempt.processToken && run.startedAt === attempt.startedAt && (run.resumedAt ?? null) === attempt.resumedAt);
|
|
887
914
|
}
|
|
888
915
|
/** What the record says a run is now, and at what level. Read only to say the
|
|
889
916
|
* right sentence about something that has already happened; nothing branches on
|
|
@@ -891,7 +918,7 @@ export function ownerSettlementAccepted(run, processToken) {
|
|
|
891
918
|
async function runNow(client, runId) {
|
|
892
919
|
const runs = await returned(client
|
|
893
920
|
.from('panel3_runs')
|
|
894
|
-
.select('state, level, process_token, card:panel3_cards!panel3_runs_card_id_fkey(conversation_run_id)')
|
|
921
|
+
.select('state, level, process_token, started_at, resumed_at, card:panel3_cards!panel3_runs_card_id_fkey(conversation_run_id)')
|
|
895
922
|
.eq('id', runId), 'read', `the state of run ${runId}`);
|
|
896
923
|
const run = runs[0];
|
|
897
924
|
return run
|
|
@@ -899,9 +926,9 @@ async function runNow(client, runId) {
|
|
|
899
926
|
state: run.state,
|
|
900
927
|
level: run.level,
|
|
901
928
|
conversationOwnerId: run.card?.conversation_run_id ?? null,
|
|
902
|
-
processToken: run.process_token,
|
|
929
|
+
processToken: run.process_token, startedAt: run.started_at, resumedAt: run.resumed_at,
|
|
903
930
|
}
|
|
904
|
-
: { state: 'no longer on the record', level: null, conversationOwnerId: null, processToken: null };
|
|
931
|
+
: { state: 'no longer on the record', level: null, conversationOwnerId: null, processToken: null, startedAt: null, resumedAt: null };
|
|
905
932
|
}
|
|
906
933
|
/**
|
|
907
934
|
* A run whose process failed, ended with the reason ON ITS OWN COLUMN, and its
|
|
@@ -1000,6 +1027,7 @@ async function answerCard(client, tools, machineId, cardId, turns) {
|
|
|
1000
1027
|
const preparation = tools.work?.prepare(turns[0].run_id);
|
|
1001
1028
|
try {
|
|
1002
1029
|
const runId = turns[0].run_id;
|
|
1030
|
+
const attempt = capturedPanelAttempt(turns, runId, tools.work);
|
|
1003
1031
|
/* BEFORE THE SPAWN, AND ITS FAILURE IS THE SPAWN'S FAILURE. The receipts are
|
|
1004
1032
|
part of what the agent is sent, so a read that fails must not be papered
|
|
1005
1033
|
over with an empty list: that reads as a card that has made nothing, which
|
|
@@ -1043,7 +1071,7 @@ async function answerCard(client, tools, machineId, cardId, turns) {
|
|
|
1043
1071
|
}
|
|
1044
1072
|
catch (error) {
|
|
1045
1073
|
const why = error instanceof Error ? error.message : String(error);
|
|
1046
|
-
await endRun(client, LEVEL, runId, cardId, why);
|
|
1074
|
+
await endRun(client, LEVEL, runId, cardId, why, attempt);
|
|
1047
1075
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
1048
1076
|
}
|
|
1049
1077
|
/* ═══ THE RUN ID IS ON THE URL, AND THAT IS THE WHOLE OF WHAT THE AGENT IS
|
|
@@ -1055,7 +1083,7 @@ async function answerCard(client, tools, machineId, cardId, turns) {
|
|
|
1055
1083
|
/* AN EMPTY DIRECTORY OF THE USER'S OWN, because level 1 has no code tool to
|
|
1056
1084
|
use a real one with, and the daemon's inherited cwd under a launchd login
|
|
1057
1085
|
item is the filesystem root. */
|
|
1058
|
-
const started = await startTrackedAgent(client, runId, preparation, tools.work, withStandingRules(rules, brief, where.block), LEVEL, tools.urlFor(runId), where.cwd, undefined, settings);
|
|
1086
|
+
const started = await startTrackedAgent(client, runId, preparation, tools.work, attempt, withStandingRules(rules, brief, where.block), LEVEL, tools.urlFor(runId), where.cwd, undefined, settings);
|
|
1059
1087
|
out(`run ${runId} level ${LEVEL} ${started.pid ? `pid ${started.pid}` : 'no process'}`);
|
|
1060
1088
|
try {
|
|
1061
1089
|
/* THE BRIEF, NOT WHAT THE PROCESS WAS HANDED. The rules are current at the
|
|
@@ -1399,11 +1427,11 @@ async function workingDirectory(client, runId, level, isOwner, knownCodebase) {
|
|
|
1399
1427
|
* Then the row, then the process, then the pid — constraint 8, in the only order
|
|
1400
1428
|
* that satisfies it.
|
|
1401
1429
|
*/
|
|
1402
|
-
async function startChild(client, tools, machineId, parentRunId, brief, codebase, parentProcessToken, choice = {}) {
|
|
1430
|
+
async function startChild(client, tools, machineId, parentRunId, brief, codebase, parentProcessToken, choice = {}, recovered) {
|
|
1403
1431
|
let claimOperationId;
|
|
1404
1432
|
const preparation = tools.work?.prepare();
|
|
1405
1433
|
try {
|
|
1406
|
-
const response = await panelClaim(client, tools.work, 'panel3_dispatch', {
|
|
1434
|
+
const response = recovered ?? await panelClaim(client, tools.work, 'panel3_dispatch', {
|
|
1407
1435
|
p_parent_run_id: parentRunId,
|
|
1408
1436
|
p_brief: brief,
|
|
1409
1437
|
p_machine_id: machineId,
|
|
@@ -1425,6 +1453,7 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1425
1453
|
throw new Error(`NO AGENT WAS STARTED and nothing was written: run ${parentRunId} has ended, is already as `
|
|
1426
1454
|
+ 'deep as anything may be sent from, or this conversation already has its owner. Exit now.');
|
|
1427
1455
|
}
|
|
1456
|
+
const attempt = capturedPanelAttempt(data ?? [], row.run_id, tools.work);
|
|
1428
1457
|
if (row.run_level !== 2 && row.run_level !== 3) {
|
|
1429
1458
|
/* UNREACHABLE, AND STILL SETTLED. `panel3_dispatch` writes `parent.level + 1`
|
|
1430
1459
|
from a parent it has just checked is below 3, so there is no level here
|
|
@@ -1433,7 +1462,7 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1433
1462
|
recovery sweep wait out the pid grace window to conclude what is already
|
|
1434
1463
|
known. */
|
|
1435
1464
|
const why = `run ${row.run_id} was written at level ${row.run_level}, which cannot be spawned`;
|
|
1436
|
-
await giveUp(client, row.run_id, why, row.process_token ?? undefined);
|
|
1465
|
+
await giveUp(client, row.run_id, why, row.process_token ?? undefined, attempt);
|
|
1437
1466
|
throw new Error(why);
|
|
1438
1467
|
}
|
|
1439
1468
|
const level = row.run_level;
|
|
@@ -1459,12 +1488,12 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1459
1488
|
}
|
|
1460
1489
|
catch (error) {
|
|
1461
1490
|
const why = error instanceof Error ? error.message : String(error);
|
|
1462
|
-
await giveUp(client, row.run_id, why, row.process_token ?? undefined);
|
|
1491
|
+
await giveUp(client, row.run_id, why, row.process_token ?? undefined, attempt);
|
|
1463
1492
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
1464
1493
|
}
|
|
1465
1494
|
const processToken = row.process_token ?? undefined;
|
|
1466
1495
|
const isOwner = level === 2;
|
|
1467
|
-
const started = await startTrackedAgent(client, row.run_id, preparation, tools.work, withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id, processToken), where.cwd, isOwner ? { ownerId: row.run_id } : undefined, settings);
|
|
1496
|
+
const started = await startTrackedAgent(client, row.run_id, preparation, tools.work, attempt, withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id, processToken), where.cwd, isOwner ? { ownerId: row.run_id } : undefined, settings);
|
|
1468
1497
|
if (started.pid === null) {
|
|
1469
1498
|
if (started.interrupted?.())
|
|
1470
1499
|
throw new Error('Work was interrupted by the service command.');
|
|
@@ -1474,7 +1503,7 @@ async function startChild(client, tools, machineId, parentRunId, brief, codebase
|
|
|
1474
1503
|
and the tool call fails saying no agent was started. */
|
|
1475
1504
|
const answer = await started.answered;
|
|
1476
1505
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
1477
|
-
await giveUp(client, row.run_id, reason, processToken);
|
|
1506
|
+
await giveUp(client, row.run_id, reason, processToken, attempt);
|
|
1478
1507
|
throw new Error(`NO AGENT IS RUNNING: ${reason}`);
|
|
1479
1508
|
}
|
|
1480
1509
|
out(`dispatch run ${row.run_id} level ${level} under ${parentRunId} pid ${started.pid}`);
|
|
@@ -1565,12 +1594,13 @@ async function attemptsSoFar(client, runId) {
|
|
|
1565
1594
|
* Returns whether THIS call is what ended it. False is a fact about the record,
|
|
1566
1595
|
* not a failure: the run had already ended, and the caller says so.
|
|
1567
1596
|
*/
|
|
1568
|
-
async function endRun(client, level, runId, cardId, why, attempt) {
|
|
1597
|
+
async function endRun(client, level, runId, cardId, why, attempt, failureKind) {
|
|
1569
1598
|
if (level !== 1)
|
|
1570
|
-
return (await giveUp(client, runId, why, undefined, attempt)) !== null;
|
|
1599
|
+
return (await giveUp(client, runId, why, undefined, attempt, failureKind)) !== null;
|
|
1571
1600
|
if (attempt) {
|
|
1572
1601
|
const { data, error } = await client.rpc('panel3_end_run', {
|
|
1573
1602
|
p_run_id: runId, p_reason: why, p_process_token: attempt.processToken,
|
|
1603
|
+
p_failure_kind: failureKind ?? null,
|
|
1574
1604
|
p_expected_attempt: { started_at: attempt.startedAt, resumed_at: attempt.resumedAt },
|
|
1575
1605
|
});
|
|
1576
1606
|
if (error)
|
|
@@ -1666,6 +1696,11 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1666
1696
|
return started.answered.then(async (answer) => {
|
|
1667
1697
|
if (started.interrupted?.() || started.preparationFailed?.())
|
|
1668
1698
|
return;
|
|
1699
|
+
if (!answer.ok)
|
|
1700
|
+
answer = { ...answer, reason: failureMessage(started.attempt?.harness ?? harness(), answer.failureKind ?? 'unknown') };
|
|
1701
|
+
started.deferOutcome?.({ surface: 'panel', ok: answer.ok,
|
|
1702
|
+
text: answer.ok && speaksToTheCard ? redactSecrets(processToken === undefined ? runId : `${runId}:${processToken}`, answer.text) : null,
|
|
1703
|
+
reason: answer.ok ? '' : answer.reason, failureKind: answer.ok ? undefined : answer.failureKind, level });
|
|
1669
1704
|
/* ═══ A RUN THAT STOPPED TO ASK DID NOT DIE, WHATEVER THE HARNESS PRINTED
|
|
1670
1705
|
ON ITS WAY OUT. ═══
|
|
1671
1706
|
|
|
@@ -1690,12 +1725,20 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1690
1725
|
into an ordinary ending, and then handed to `writeAnswer` — which already
|
|
1691
1726
|
owns this case and already prints its sentence — rather than to a second
|
|
1692
1727
|
ending written here beside it. */
|
|
1693
|
-
|
|
1728
|
+
const observe = (authenticated) => {
|
|
1729
|
+
const selected = started.attempt?.harness;
|
|
1730
|
+
if (selected)
|
|
1731
|
+
tools.work?.observeHarness(selected, authenticated ? 'authenticated' : 'sign-in-required', authenticated ? 'dispatch-success' : 'provider-rejected');
|
|
1732
|
+
};
|
|
1733
|
+
if (!answer.ok && answer.failureKind !== 'authentication') {
|
|
1694
1734
|
const run = await runNow(client, runId);
|
|
1695
1735
|
if (run.state === 'asked') {
|
|
1696
|
-
|
|
1697
|
-
|
|
1736
|
+
started.deferOutcome?.({ surface: 'panel', ok: true, text: null, reason: '', level });
|
|
1737
|
+
const accepted = await writeAnswer(client, runId, cardId, null, processToken, started.attempt);
|
|
1738
|
+
if (accepted) {
|
|
1739
|
+
observe(true);
|
|
1698
1740
|
await ownerSession?.established();
|
|
1741
|
+
}
|
|
1699
1742
|
else
|
|
1700
1743
|
await ownerSession?.failed();
|
|
1701
1744
|
return;
|
|
@@ -1705,7 +1748,9 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1705
1748
|
exit cleanly without an agent message after the tool succeeds. The
|
|
1706
1749
|
record, not prose the launcher was told not to write, decides success. */
|
|
1707
1750
|
if (level === 1 && run.conversationOwnerId !== null) {
|
|
1708
|
-
|
|
1751
|
+
started.deferOutcome?.({ surface: 'panel', ok: true, text: null, reason: '', level });
|
|
1752
|
+
if (await writeAnswer(client, runId, cardId, null, processToken, started.attempt))
|
|
1753
|
+
observe(true);
|
|
1709
1754
|
return;
|
|
1710
1755
|
}
|
|
1711
1756
|
}
|
|
@@ -1716,7 +1761,7 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1716
1761
|
Repeating an invalid model/effort request cannot repair it. Unknown
|
|
1717
1762
|
process deaths retain the existing recovery policy. */
|
|
1718
1763
|
const attempts = await attemptsSoFar(client, runId);
|
|
1719
|
-
if (answer.retryable
|
|
1764
|
+
if (answer.retryable === true && started.pid !== null && attempts < MAX_ATTEMPTS) {
|
|
1720
1765
|
out(`retry run ${runId} attempt ${attempts} of ${MAX_ATTEMPTS} died: ${answer.reason}`);
|
|
1721
1766
|
const again = processToken === undefined
|
|
1722
1767
|
? await resumeRun(client, tools, machineId, runId, started.pid)
|
|
@@ -1754,8 +1799,10 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1754
1799
|
than two. */
|
|
1755
1800
|
const why = attempts > 1 ? `${answer.reason} (after ${attempts} attempts)` : answer.reason;
|
|
1756
1801
|
const ended = processToken === undefined
|
|
1757
|
-
? await endRun(client, level, runId, cardId, why)
|
|
1758
|
-
: (await giveUp(client, runId, why, processToken)) !== null;
|
|
1802
|
+
? await endRun(client, level, runId, cardId, why, started.attempt, answer.failureKind)
|
|
1803
|
+
: (await giveUp(client, runId, why, processToken, started.attempt, answer.failureKind)) !== null;
|
|
1804
|
+
if (ended && answer.failureKind === 'authentication')
|
|
1805
|
+
observe(false);
|
|
1759
1806
|
if (!ended) {
|
|
1760
1807
|
/* IT WAS ALREADY SETTLED, by recovery, which decided this process was
|
|
1761
1808
|
gone before it said so itself, or by the person's Stop. Nothing was
|
|
@@ -1772,12 +1819,14 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1772
1819
|
/* ═══ ITS OWN WORDS, ONTO THE CARD, THROUGH THE SAME STATEMENT EVERY LEVEL
|
|
1773
1820
|
USES. ═══ Nothing reads them, nothing shortens them and nothing waits to
|
|
1774
1821
|
approve them: ux.md's "whoever did the work writes the answer". */
|
|
1775
|
-
const accepted = await writeAnswer(client, runId, cardId, speaksToTheCard ? answer.text : null, processToken);
|
|
1776
|
-
if (accepted)
|
|
1822
|
+
const accepted = await writeAnswer(client, runId, cardId, speaksToTheCard ? answer.text : null, processToken, started.attempt);
|
|
1823
|
+
if (accepted) {
|
|
1824
|
+
observe(true);
|
|
1777
1825
|
await ownerSession?.established();
|
|
1826
|
+
}
|
|
1778
1827
|
else
|
|
1779
1828
|
await ownerSession?.failed();
|
|
1780
|
-
}).then(() => { started.completed?.(); });
|
|
1829
|
+
}).then(() => { started.acknowledgeOutcome?.(); started.completed?.(); });
|
|
1781
1830
|
}
|
|
1782
1831
|
/**
|
|
1783
1832
|
* ═══ ONE RUN, STARTED AGAIN AS ITSELF, WITH WHAT IT WAS SENT AND WHAT IT HAD
|
|
@@ -1835,7 +1884,7 @@ function settle(client, tools, machineId, level, runId, cardId, started, speaksT
|
|
|
1835
1884
|
* the record refuses a fourth, and the caller is the one that knows what to say
|
|
1836
1885
|
* about the failure it was holding.
|
|
1837
1886
|
*/
|
|
1838
|
-
async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
1887
|
+
async function resumeRun(client, tools, machineId, runId, afterPid, recovered) {
|
|
1839
1888
|
/* ═══ THE SWEEP CHECKS BEFORE THE CLAIM AND THE RETRY CANNOT, AND `afterPid`
|
|
1840
1889
|
IS THE WHOLE OF WHAT DECIDES IT. ═══ Said once, here. See the header for
|
|
1841
1890
|
both halves of the argument: a machine with no checkout must not take a run
|
|
@@ -1853,7 +1902,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1853
1902
|
let claimOperationId;
|
|
1854
1903
|
const preparation = tools.work?.prepare();
|
|
1855
1904
|
try {
|
|
1856
|
-
const response = await panelClaim(client, tools.work, 'panel3_resume', {
|
|
1905
|
+
const response = recovered ?? await panelClaim(client, tools.work, 'panel3_resume', {
|
|
1857
1906
|
p_run_id: runId,
|
|
1858
1907
|
p_machine_id: machineId,
|
|
1859
1908
|
p_after_pid: afterPid,
|
|
@@ -1865,6 +1914,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1865
1914
|
const claimed = data?.[0];
|
|
1866
1915
|
if (!claimed)
|
|
1867
1916
|
return null;
|
|
1917
|
+
const attempt = capturedPanelAttempt(data ?? [], runId, tools.work);
|
|
1868
1918
|
const level = claimed.run_level === 1 ? 1 : claimed.run_level === 2 ? 2 : 3;
|
|
1869
1919
|
if (claimed.run_level !== level) {
|
|
1870
1920
|
/* UNREACHABLE, AND STILL SETTLED, exactly as in `startChild`: the level
|
|
@@ -1872,7 +1922,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1872
1922
|
The claim has already happened, so leaving it would strand the run for a
|
|
1873
1923
|
whole grace window before anything looked at it again. */
|
|
1874
1924
|
const why = `run ${runId} is at level ${claimed.run_level}, which cannot be spawned`;
|
|
1875
|
-
await giveUp(client, runId, why);
|
|
1925
|
+
await giveUp(client, runId, why, undefined, attempt);
|
|
1876
1926
|
throw new Error(why);
|
|
1877
1927
|
}
|
|
1878
1928
|
/* ONE RESOLUTION FOR BOTH PATHS, AFTER THE CLAIM. It used to fork on whether
|
|
@@ -1901,7 +1951,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1901
1951
|
1 here: handing the person's message back mints a new run with its
|
|
1902
1952
|
attempts at one, which is the bound the retry is under, undone by the
|
|
1903
1953
|
one path that could not start. See `endRun`. */
|
|
1904
|
-
await endRun(client, level, runId, claimed.run_card_id, why);
|
|
1954
|
+
await endRun(client, level, runId, claimed.run_card_id, why, attempt);
|
|
1905
1955
|
throw new Error(`NO AGENT IS RUNNING: ${level === 1 ? stderrOnly : why}`);
|
|
1906
1956
|
}
|
|
1907
1957
|
/* WHAT IT SENT OTHERS TO DO, FROM THE RECORD. Level 3 has no `dispatch`, so it
|
|
@@ -1942,14 +1992,14 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1942
1992
|
}
|
|
1943
1993
|
catch (error) {
|
|
1944
1994
|
const why = error instanceof Error ? error.message : String(error);
|
|
1945
|
-
await endRun(client, level, runId, claimed.run_card_id, why);
|
|
1995
|
+
await endRun(client, level, runId, claimed.run_card_id, why, attempt);
|
|
1946
1996
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
1947
1997
|
}
|
|
1948
1998
|
/* ═══ WHY IT DIED IS WHAT DIFFERS, AND IT IS TOLD THE TRUTH ABOUT IT. ═══
|
|
1949
1999
|
`resumePrompt` opens by saying the machine went down, which is true of the
|
|
1950
2000
|
sweep and false of a retry: the daemon that watched this harness exit is
|
|
1951
2001
|
still running. See `retryPrompt`. */
|
|
1952
|
-
const started = await startTrackedAgent(client, runId, preparation, tools.work, withStandingRules(rules, afterPid === null
|
|
2002
|
+
const started = await startTrackedAgent(client, runId, preparation, tools.work, attempt, withStandingRules(rules, afterPid === null
|
|
1953
2003
|
? resumePrompt(claimed.run_brief, claimed.run_report, children)
|
|
1954
2004
|
: retryPrompt(claimed.run_brief, claimed.run_report, children), where.block, [], pictures), level, tools.urlFor(runId), where.cwd, undefined, settings);
|
|
1955
2005
|
if (started.pid === null) {
|
|
@@ -1962,7 +2012,7 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
1962
2012
|
const answer = await started.answered;
|
|
1963
2013
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
1964
2014
|
// The same level fork, for the same reason. See `endRun`.
|
|
1965
|
-
await endRun(client, level, runId, claimed.run_card_id, reason);
|
|
2015
|
+
await endRun(client, level, runId, claimed.run_card_id, reason, attempt);
|
|
1966
2016
|
throw new Error(`NO AGENT IS RUNNING: ${reason}`);
|
|
1967
2017
|
}
|
|
1968
2018
|
out(`resume run ${runId} level ${level} pid ${started.pid} `
|
|
@@ -2012,10 +2062,11 @@ async function resumeRun(client, tools, machineId, runId, afterPid) {
|
|
|
2012
2062
|
async function startRearmed(client, tools, machineId, row) {
|
|
2013
2063
|
const preparation = tools.work?.prepare(row.run_id);
|
|
2014
2064
|
try {
|
|
2065
|
+
const attempt = capturedPanelAttempt([row], row.run_id, tools.work);
|
|
2015
2066
|
const level = row.run_level === 1 ? 1 : row.run_level === 2 ? 2 : 3;
|
|
2016
2067
|
if (row.run_level !== level) {
|
|
2017
2068
|
const why = `run ${row.run_id} is at level ${row.run_level}, which cannot be spawned`;
|
|
2018
|
-
await giveUp(client, row.run_id, why);
|
|
2069
|
+
await giveUp(client, row.run_id, why, undefined, attempt);
|
|
2019
2070
|
throw new Error(why);
|
|
2020
2071
|
}
|
|
2021
2072
|
let where;
|
|
@@ -2036,7 +2087,7 @@ async function startRearmed(client, tools, machineId, row) {
|
|
|
2036
2087
|
// The level fork, which this path needs for the same reason `resumeRun`'s
|
|
2037
2088
|
// two do: a re-arm serves level 1, and a level 1 run holds the person's
|
|
2038
2089
|
// message. See `endRun`.
|
|
2039
|
-
await endRun(client, level, row.run_id, row.run_card_id, why);
|
|
2090
|
+
await endRun(client, level, row.run_id, row.run_card_id, why, attempt);
|
|
2040
2091
|
throw new Error(`NO AGENT IS RUNNING: ${level === 1 ? said : why}`);
|
|
2041
2092
|
}
|
|
2042
2093
|
/* WHAT IT SENT OTHERS TO DO AND WHAT THEY WROTE, FROM THE RECORD, AS LATE AS
|
|
@@ -2077,17 +2128,17 @@ async function startRearmed(client, tools, machineId, row) {
|
|
|
2077
2128
|
}
|
|
2078
2129
|
catch (error) {
|
|
2079
2130
|
const why = error instanceof Error ? error.message : String(error);
|
|
2080
|
-
await endRun(client, level, row.run_id, row.run_card_id, why);
|
|
2131
|
+
await endRun(client, level, row.run_id, row.run_card_id, why, attempt);
|
|
2081
2132
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
2082
2133
|
}
|
|
2083
|
-
const started = await startTrackedAgent(client, row.run_id, preparation, tools.work, withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id), where.cwd, undefined, settings);
|
|
2134
|
+
const started = await startTrackedAgent(client, row.run_id, preparation, tools.work, attempt, withStandingRules(rules, prompt, where.block, [], pictures), level, tools.urlFor(row.run_id), where.cwd, undefined, settings);
|
|
2084
2135
|
if (started.pid === null) {
|
|
2085
2136
|
if (started.interrupted?.())
|
|
2086
2137
|
throw new Error('Work was interrupted by the service command.');
|
|
2087
2138
|
const answer = await started.answered;
|
|
2088
2139
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
2089
2140
|
// The same level fork, for the same reason. See `endRun`.
|
|
2090
|
-
await endRun(client, level, row.run_id, row.run_card_id, reason);
|
|
2141
|
+
await endRun(client, level, row.run_id, row.run_card_id, reason, attempt);
|
|
2091
2142
|
throw new Error(`NO AGENT IS RUNNING: ${reason}`);
|
|
2092
2143
|
}
|
|
2093
2144
|
out(`rearm run ${row.run_id} level ${level} pid ${started.pid} `
|
|
@@ -2269,7 +2320,7 @@ export function resumableOwnerSessionId(candidate, machineId, machineHarness) {
|
|
|
2269
2320
|
? local.nativeSessionId
|
|
2270
2321
|
: undefined;
|
|
2271
2322
|
}
|
|
2272
|
-
async function activateOwner(client, tools, machineId, runId, afterProcessToken = null, afterPid = null) {
|
|
2323
|
+
async function activateOwner(client, tools, machineId, runId, afterProcessToken = null, afterPid = null, recovered) {
|
|
2273
2324
|
let claimOperationId;
|
|
2274
2325
|
const preparation = tools.work?.prepare();
|
|
2275
2326
|
try {
|
|
@@ -2283,7 +2334,13 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2283
2334
|
const machineHarness = candidate.handed_back_at === null && candidate.harness !== null
|
|
2284
2335
|
? harness({ CTRL_SPC_V3_AGENT: candidate.harness })
|
|
2285
2336
|
: await selectedHarness(client, machineId);
|
|
2286
|
-
|
|
2337
|
+
if (recovered?.nativeContext && (recovered.nativeContext.harness !== machineHarness || recovered.nativeContext.processToken !== null && typeof recovered.nativeContext.processToken !== 'string' || recovered.nativeContext.nativeSessionId !== undefined && typeof recovered.nativeContext.nativeSessionId !== 'string'))
|
|
2338
|
+
throw new Error('The saved native conversation context is invalid.');
|
|
2339
|
+
const resumeSessionId = recovered?.nativeContext
|
|
2340
|
+
? resumableOwnerSessionId({ ...candidate, process_token: recovered.nativeContext.processToken }, machineId, recovered.nativeContext.harness)
|
|
2341
|
+
: resumableOwnerSessionId(candidate, machineId, machineHarness);
|
|
2342
|
+
if (recovered?.nativeContext?.nativeSessionId && resumeSessionId !== recovered.nativeContext.nativeSessionId)
|
|
2343
|
+
throw new Error('The saved native conversation is no longer available for this attempt.');
|
|
2287
2344
|
/* ═══ THE EXISTENCE CHECK BEFORE THE CLAIM, AND THE COPY AFTER IT. ═══ This
|
|
2288
2345
|
was the whole resolution, which was right while resolving meant reading a
|
|
2289
2346
|
folder out of a file. Since worktrees-8 it also CREATES one, and a daemon
|
|
@@ -2294,13 +2351,13 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2294
2351
|
if (candidate.codebase_id !== null) {
|
|
2295
2352
|
checkoutForCodebase(await codebaseOfRun(client, candidate.id), hostname());
|
|
2296
2353
|
}
|
|
2297
|
-
const response = await panelClaim(client, tools.work, 'panel3_take_owner_activation', {
|
|
2354
|
+
const response = recovered ?? await panelClaim(client, tools.work, 'panel3_take_owner_activation', {
|
|
2298
2355
|
p_run_id: runId,
|
|
2299
2356
|
p_machine_id: machineId,
|
|
2300
2357
|
p_agent: machineHarness,
|
|
2301
2358
|
p_after_process_token: afterProcessToken,
|
|
2302
2359
|
p_after_pid: afterPid,
|
|
2303
|
-
});
|
|
2360
|
+
}, { nativeSessionId: resumeSessionId, processToken: candidate.process_token, harness: machineHarness });
|
|
2304
2361
|
const { data, error, operationId } = response;
|
|
2305
2362
|
claimOperationId = operationId;
|
|
2306
2363
|
if (error)
|
|
@@ -2308,6 +2365,7 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2308
2365
|
const claimed = data?.[0];
|
|
2309
2366
|
if (!claimed)
|
|
2310
2367
|
return null;
|
|
2368
|
+
const attempt = capturedPanelAttempt(data ?? [], runId, tools.work);
|
|
2311
2369
|
const [events, children] = await Promise.all([
|
|
2312
2370
|
ownerConversation(client, claimed.run_card_id, new Set(claimed.turn_ids ?? [])),
|
|
2313
2371
|
ownerChildren(client, runId),
|
|
@@ -2370,7 +2428,7 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2370
2428
|
}
|
|
2371
2429
|
catch (error) {
|
|
2372
2430
|
const why = error instanceof Error ? error.message : String(error);
|
|
2373
|
-
await giveUp(client, runId, why, claimed.process_token);
|
|
2431
|
+
await giveUp(client, runId, why, claimed.process_token, attempt);
|
|
2374
2432
|
throw new Error(`NO AGENT IS RUNNING: ${why}`);
|
|
2375
2433
|
}
|
|
2376
2434
|
/* THE MERGE, OFF THE MARK ON THE ASK, BEFORE THE AGENT THAT WILL SPEAK ABOUT
|
|
@@ -2391,13 +2449,13 @@ async function activateOwner(client, tools, machineId, runId, afterProcessToken
|
|
|
2391
2449
|
: ownerActivationPrompt(claimed.run_brief, claimed.run_report, events, children, delivered !== null && !delivered.mine
|
|
2392
2450
|
? { id: delivered.id, question: delivered.question }
|
|
2393
2451
|
: null, currentArtifactAnswer, afterPid !== null, landing);
|
|
2394
|
-
const started = await startTrackedAgent(client, runId, preparation, tools.work, withStandingRules(rules, prompt, where.block, attached, pictures), 2, tools.urlFor(runId, claimed.process_token), where.cwd, { ownerId: runId, ...(resumeSessionId ? { resumeSessionId } : {}) }, settings);
|
|
2452
|
+
const started = await startTrackedAgent(client, runId, preparation, tools.work, attempt, withStandingRules(rules, prompt, where.block, attached, pictures), 2, tools.urlFor(runId, claimed.process_token), where.cwd, { ownerId: runId, ...(resumeSessionId ? { resumeSessionId } : {}) }, settings);
|
|
2395
2453
|
if (started.pid === null) {
|
|
2396
2454
|
if (started.interrupted?.())
|
|
2397
2455
|
throw new Error('Work was interrupted by the service command.');
|
|
2398
2456
|
const answer = await started.answered;
|
|
2399
2457
|
const reason = answer.ok ? 'the process ended before it could be identified' : answer.reason;
|
|
2400
|
-
await giveUp(client, runId, reason, claimed.process_token);
|
|
2458
|
+
await giveUp(client, runId, reason, claimed.process_token, attempt);
|
|
2401
2459
|
throw new Error(`NO AGENT IS RUNNING: ${reason}`);
|
|
2402
2460
|
}
|
|
2403
2461
|
try {
|
|
@@ -3324,6 +3382,7 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3324
3382
|
const machineId = getMachineIdentity().id;
|
|
3325
3383
|
const machineName = hostname();
|
|
3326
3384
|
let listeningHarness = null;
|
|
3385
|
+
const listeningRoutes = new Set();
|
|
3327
3386
|
/* THE RUNS THIS PROCESS IS HOLDING RIGHT NOW, so recovery cannot declare its
|
|
3328
3387
|
own live work dead in the moment before a pid is recorded. It covers THIS
|
|
3329
3388
|
daemon only, which is why `PID_GRACE_MS` exists for the other ones. Keyed by
|
|
@@ -3349,9 +3408,10 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3349
3408
|
`tools` is referenced inside the callback it is being given, which is safe
|
|
3350
3409
|
for the plain reason that the callback can only run once a request has
|
|
3351
3410
|
arrived at a server that by then exists. */
|
|
3411
|
+
if (work && !work.cloudAllowed())
|
|
3412
|
+
throw new Error('Cloud operations are suspended for this sign-in.');
|
|
3352
3413
|
if (work)
|
|
3353
3414
|
await reconcilePanelInterruptions(current(), machineId, work);
|
|
3354
|
-
lifecycle?.reconciled?.();
|
|
3355
3415
|
const tools = await startToolsServer(current(), async (parentRunId, brief, codebase, processToken, choice) => {
|
|
3356
3416
|
const endClaim = work?.beginClaim();
|
|
3357
3417
|
try {
|
|
@@ -3418,9 +3478,18 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3418
3478
|
`--once` still fails loudly, because the acceptance harness reads the exit
|
|
3419
3479
|
code and a swallowed failure there would make a broken suite look green. */
|
|
3420
3480
|
try {
|
|
3481
|
+
if (work && !work.cloudAllowed()) {
|
|
3482
|
+
await sleep(POLL_INTERVAL_MS);
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3421
3485
|
if (work)
|
|
3422
3486
|
await reconcilePanelInterruptions(current(), machineId, work);
|
|
3423
3487
|
if (work && !work.allowed()) {
|
|
3488
|
+
// A successful read proves polling before recovery opens admission.
|
|
3489
|
+
const { error } = await current().from('panel3_cards').select('id').limit(1);
|
|
3490
|
+
if (error)
|
|
3491
|
+
throw error;
|
|
3492
|
+
lifecycle?.ready();
|
|
3424
3493
|
await sleep(POLL_INTERVAL_MS);
|
|
3425
3494
|
continue;
|
|
3426
3495
|
}
|
|
@@ -3431,30 +3500,96 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3431
3500
|
to run first: `panel3_stop_card` has already ended the runs, so recovery
|
|
3432
3501
|
cannot see them and neither take can start them. */
|
|
3433
3502
|
await killStopped(current(), machineId, work);
|
|
3503
|
+
for (const claim of work?.recoveredClaims() ?? []) {
|
|
3504
|
+
const attempts = panelClaimAttempts(claim.result);
|
|
3505
|
+
const eligible = new Set();
|
|
3506
|
+
for (const attempt of attempts) {
|
|
3507
|
+
if (inFlight.has(attempt.runId))
|
|
3508
|
+
continue;
|
|
3509
|
+
const pending = work?.claimAttempt(attempt.runId);
|
|
3510
|
+
if (!pending)
|
|
3511
|
+
continue; // Preparation or an actual stop already owns it.
|
|
3512
|
+
const { data, error } = await current().from('panel3_runs')
|
|
3513
|
+
.select('started_at,resumed_at,process_token,state,ended_at').eq('id', attempt.runId).maybeSingle();
|
|
3514
|
+
if (error)
|
|
3515
|
+
throw error;
|
|
3516
|
+
if (data?.state === 'running' && data.ended_at === null && data.started_at === attempt.startedAt
|
|
3517
|
+
&& (data.resumed_at ?? null) === attempt.resumedAt && (data.process_token ?? null) === attempt.processToken)
|
|
3518
|
+
eligible.add(attempt.runId);
|
|
3519
|
+
}
|
|
3520
|
+
const rows = (claim.result ?? []).filter((row) => eligible.has(row._attempt?.run_id ?? row.run_id));
|
|
3521
|
+
const response = { data: rows, error: null, operationId: claim.id, nativeContext: claim.args?.nativeContext };
|
|
3522
|
+
const args = claim.args ?? {};
|
|
3523
|
+
if (rows.length) {
|
|
3524
|
+
if (claim.action === 'panel3_take_turns') {
|
|
3525
|
+
for (const [cardId, turns] of byCard(rows))
|
|
3526
|
+
hold(turns[0].run_id, answerCard(current(), tools, machineId, cardId, turns));
|
|
3527
|
+
}
|
|
3528
|
+
else if (claim.action === 'panel3_take_rearms') {
|
|
3529
|
+
for (const row of rows) {
|
|
3530
|
+
const started = await startRearmed(current(), tools, machineId, row);
|
|
3531
|
+
hold(row.run_id, started.settled);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
else if (claim.action === 'panel3_dispatch') {
|
|
3535
|
+
const row = rows[0];
|
|
3536
|
+
const codebase = args.p_codebase_id ? await codebaseOfRun(current(), row.run_id) : null;
|
|
3537
|
+
const started = await startChild(current(), tools, machineId, String(args.p_parent_run_id), String(args.p_brief), codebase, typeof args.p_process_token === 'string' ? args.p_process_token : undefined, {}, response);
|
|
3538
|
+
hold(started.runId, started.settled);
|
|
3539
|
+
}
|
|
3540
|
+
else if (claim.action === 'panel3_resume') {
|
|
3541
|
+
const runId = String(args.p_run_id);
|
|
3542
|
+
const started = await resumeRun(current(), tools, machineId, runId, typeof args.p_after_pid === 'number' ? args.p_after_pid : null, response);
|
|
3543
|
+
if (started)
|
|
3544
|
+
hold(runId, started.settled);
|
|
3545
|
+
}
|
|
3546
|
+
else if (claim.action === 'panel3_take_owner_activation') {
|
|
3547
|
+
const runId = String(args.p_run_id);
|
|
3548
|
+
const started = await activateOwner(current(), tools, machineId, runId, typeof args.p_after_process_token === 'string' ? args.p_after_process_token : null, typeof args.p_after_pid === 'number' ? args.p_after_pid : null, response);
|
|
3549
|
+
if (started)
|
|
3550
|
+
hold(runId, started.settled);
|
|
3551
|
+
}
|
|
3552
|
+
else
|
|
3553
|
+
throw new Error('The saved panel claim has an unsupported action.');
|
|
3554
|
+
}
|
|
3555
|
+
work?.finishRpcs([claim.id]);
|
|
3556
|
+
}
|
|
3434
3557
|
/* Publish readiness before claiming work. A card with an untaken turn reads the same whether a daemon
|
|
3435
3558
|
is two seconds away or nobody has one running; this row is the only place
|
|
3436
3559
|
the difference exists. It is written before the takes rather than after
|
|
3437
3560
|
so that a machine which is up but busy still reads as up. */
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3561
|
+
const installed = recoveryHarnesses();
|
|
3562
|
+
for (const route of [...listeningRoutes]) {
|
|
3563
|
+
if (!installed.includes(route)) {
|
|
3564
|
+
await stopListening(current(), machineId, route);
|
|
3565
|
+
listeningRoutes.delete(route);
|
|
3566
|
+
}
|
|
3443
3567
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3568
|
+
// A primary selection failure cannot skip an installed recovery route.
|
|
3569
|
+
for (const route of installed) {
|
|
3570
|
+
try {
|
|
3571
|
+
const response = await panelClaim(current(), work, 'panel3_take_turns', { p_machine_id: machineId, p_agent: route, p_recovery_only: true });
|
|
3572
|
+
if (response.operationId)
|
|
3573
|
+
claimOperations.push(response.operationId);
|
|
3574
|
+
const taken = await returned(Promise.resolve(response), 'take', 'explicit authentication recovery');
|
|
3575
|
+
await sayListening(current(), machineId, machineName, route);
|
|
3576
|
+
await sayPollingProblem(current(), machineId, route, null);
|
|
3577
|
+
listeningRoutes.add(route);
|
|
3578
|
+
for (const [cardId, turns] of byCard(taken))
|
|
3579
|
+
hold(turns[0].run_id, answerCard(current(), tools, machineId, cardId, turns));
|
|
3580
|
+
}
|
|
3581
|
+
catch (error) {
|
|
3582
|
+
try {
|
|
3583
|
+
await sayPollingProblem(current(), machineId, route, 'poll_failed');
|
|
3584
|
+
}
|
|
3585
|
+
catch { /* The last successful observation expires. */ }
|
|
3586
|
+
said(`Authentication recovery polling failed for ${route}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3448
3587
|
}
|
|
3449
|
-
throw error;
|
|
3450
3588
|
}
|
|
3589
|
+
const machineHarness = await selectedHarness(current(), machineId);
|
|
3590
|
+
listeningHarness = machineHarness;
|
|
3451
3591
|
if (signal?.aborted)
|
|
3452
3592
|
break;
|
|
3453
|
-
if (listeningHarness !== null && listeningHarness !== machineHarness) {
|
|
3454
|
-
await stopListening(current(), machineId, listeningHarness);
|
|
3455
|
-
}
|
|
3456
|
-
listeningHarness = machineHarness;
|
|
3457
|
-
await sayListening(current(), machineId, machineName, machineHarness);
|
|
3458
3593
|
await reconcileOwnerSessions(current(), machineId, machineHarness, new Set(inFlight.keys()), work);
|
|
3459
3594
|
await recoverStranded(current(), tools, machineId, new Set(inFlight.keys()), hold);
|
|
3460
3595
|
/* ═══ AND THE COPIES OF CARDS THAT ARE OVER. ═══ After recovery,
|
|
@@ -3481,7 +3616,7 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3481
3616
|
/* THE MACHINE ID GOES IN because the take writes the run row, and a run has
|
|
3482
3617
|
to say where it is running: the exclusion is cross-machine and recovery is
|
|
3483
3618
|
per-machine, so a row with nobody's machine on it could be neither. */
|
|
3484
|
-
const takenResponse = await panelClaim(current(), work, 'panel3_take_turns', { p_machine_id: machineId, p_agent: machineHarness });
|
|
3619
|
+
const takenResponse = await panelClaim(current(), work, 'panel3_take_turns', { p_machine_id: machineId, p_agent: machineHarness, p_recovery_only: false });
|
|
3485
3620
|
if (takenResponse.operationId)
|
|
3486
3621
|
claimOperations.push(takenResponse.operationId);
|
|
3487
3622
|
const taken = await returned(Promise.resolve(takenResponse), 'take', 'turns');
|
|
@@ -3529,6 +3664,8 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3529
3664
|
// rather than becoming an unhandled rejection.
|
|
3530
3665
|
hold(runId, answerCard(current(), tools, machineId, cardId, turns));
|
|
3531
3666
|
}
|
|
3667
|
+
await sayListening(current(), machineId, machineName, machineHarness);
|
|
3668
|
+
listeningRoutes.add(machineHarness);
|
|
3532
3669
|
await sayPollingProblem(current(), machineId, machineHarness, null);
|
|
3533
3670
|
lifecycle?.ready();
|
|
3534
3671
|
if (once) {
|
|
@@ -3565,8 +3702,8 @@ export async function run(args, injected, signal, lifecycle, work) {
|
|
|
3565
3702
|
}
|
|
3566
3703
|
finally {
|
|
3567
3704
|
try {
|
|
3568
|
-
|
|
3569
|
-
await stopListening(current(), machineId,
|
|
3705
|
+
for (const route of listeningRoutes)
|
|
3706
|
+
await stopListening(current(), machineId, route);
|
|
3570
3707
|
}
|
|
3571
3708
|
finally {
|
|
3572
3709
|
await tools.close();
|
|
@@ -3602,8 +3739,7 @@ export function startPanel(injected, work) {
|
|
|
3602
3739
|
}
|
|
3603
3740
|
return false;
|
|
3604
3741
|
},
|
|
3605
|
-
|
|
3606
|
-
ready: () => { if (!requested)
|
|
3742
|
+
ready: () => { resolveReady(); if (!requested)
|
|
3607
3743
|
resolveRestart?.(); },
|
|
3608
3744
|
}, work);
|
|
3609
3745
|
}
|