@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/daemon-lifecycle.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { sessionRenewing } from './supabase.js';
|
|
1
2
|
import { createServer } from 'node:http';
|
|
2
|
-
import { randomUUID, timingSafeEqual
|
|
3
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
4
|
import { existsSync, readFileSync, writeFileSync, openSync, closeSync, mkdirSync } from 'node:fs';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { spawn } from 'node:child_process';
|
|
7
8
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
9
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
9
|
-
import { configDir, getMachineIdentity, lifecycleToken, readLifecycleToken, readMcpToken,
|
|
10
|
+
import { configDir, getMachineIdentity, lifecycleToken, readLifecycleToken, readMcpToken, readSessionRecord, machineHostname } from './config.js';
|
|
10
11
|
import { entryPath, ensureAutostart, prepareAutostart, autostartDisabled, autostartEnabled, loadedStartupJob, unloadStoppedStartupJob } from './autostart.js';
|
|
11
12
|
import { CLI_VERSION } from './package-version.js';
|
|
12
13
|
import { TOOLS_SERVER_PORT } from './env.js';
|
|
13
14
|
import { claimLifecycleOperation, publishRuntime, readMigration, readOperation, readRuntime, removeOperation, removeRuntime, updateOperation, updateRuntime, writeMigration } from './daemon-lock.js';
|
|
14
15
|
import { inspectProcess, processIdentityMatches, osBootIdentity, loopbackListenerPid, terminateOwnedRoot } from './win-shell.js';
|
|
15
16
|
import { initializeOwnedWork, snapshotOwnedWork, interruptOwnedWork, markOwnedWorkInterrupted, ownedWorkInstanceNonces } from './daemon-processes.js';
|
|
16
|
-
import { startPresence, stopPresence, liveClient, suspendPresenceWork, resumePresenceWork, presenceCloudState } from './presence.js';
|
|
17
|
+
import { startPresence, stopPresence, liveClient, suspendPresenceWork, resumePresenceWork, presenceCloudState, observePresenceSession } from './presence.js';
|
|
17
18
|
import { NotLoggedIn } from './supabase.js';
|
|
18
19
|
import { launchWindowsService, windowsJobExecutable } from './windows-job.js';
|
|
19
20
|
const MAX_COMMAND_MS = 30_000;
|
|
@@ -42,8 +43,7 @@ async function bounded(promise, deadline) {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
function sessionFingerprint() {
|
|
45
|
-
|
|
46
|
-
return session ? createHash('sha256').update(JSON.stringify(session)).digest('hex') : 'none';
|
|
46
|
+
return readSessionRecord()?.generation ?? 'none';
|
|
47
47
|
}
|
|
48
48
|
function pendingExecution(work) {
|
|
49
49
|
const claims = new Set((work.claimPending ?? []).map((item) => item.id));
|
|
@@ -82,6 +82,20 @@ export async function inspectLocalRuntime(deadline = Date.now() + 10_000) {
|
|
|
82
82
|
return { record, status: null };
|
|
83
83
|
} // Verified local owner is alive; readiness remains unknown.
|
|
84
84
|
}
|
|
85
|
+
/** Acknowledgment means admission is closed, not merely that credentials were deleted. */
|
|
86
|
+
export async function notifySessionChanged() {
|
|
87
|
+
const deadline = Date.now() + 5000;
|
|
88
|
+
if (runtime) {
|
|
89
|
+
await bounded(observePresenceSession(), deadline);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const current = await inspectLocalRuntime(deadline);
|
|
93
|
+
if (!current)
|
|
94
|
+
return;
|
|
95
|
+
if (!current.status)
|
|
96
|
+
throw new Error('Sign-in changed locally, but the running service did not confirm it. Check this computer in Companion.');
|
|
97
|
+
await control(current.record, 'session-changed', deadline);
|
|
98
|
+
}
|
|
85
99
|
async function verifiedLegacyOwner(deadline) {
|
|
86
100
|
const token = readMcpToken();
|
|
87
101
|
if (!token)
|
|
@@ -188,22 +202,29 @@ let signalController = null;
|
|
|
188
202
|
async function runtimeStatus(deadline = Date.now() + 8000) {
|
|
189
203
|
if (!runtime)
|
|
190
204
|
throw new Error('This process does not own the local service.');
|
|
191
|
-
return { nonce: runtime.record.nonce, pid: process.pid, version: CLI_VERSION, local: runtime.record.state, cloud: presenceCloudState(), work: await snapshotOwnedWork(deadline) };
|
|
205
|
+
return { nonce: runtime.record.nonce, pid: process.pid, version: CLI_VERSION, local: runtime.record.state, cloud: presenceCloudState(), sessionRenewing: sessionRenewing(), work: await snapshotOwnedWork(deadline) };
|
|
192
206
|
}
|
|
193
207
|
async function connectCloud() {
|
|
194
|
-
if (!runtime || runtime.record.state === 'stopping' || connecting
|
|
195
|
-
return;
|
|
196
|
-
if (rejectedSession === sessionFingerprint())
|
|
208
|
+
if (!runtime || runtime.record.state === 'stopping' || connecting)
|
|
197
209
|
return;
|
|
198
210
|
connecting = (async () => {
|
|
199
211
|
try {
|
|
212
|
+
await observePresenceSession();
|
|
213
|
+
if (liveClient())
|
|
214
|
+
return;
|
|
215
|
+
if (rejectedSession === sessionFingerprint())
|
|
216
|
+
return;
|
|
200
217
|
await startPresence();
|
|
201
218
|
rejectedSession = null;
|
|
202
219
|
}
|
|
203
220
|
catch (error) {
|
|
204
|
-
if (error instanceof NotLoggedIn)
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
if (error instanceof NotLoggedIn) {
|
|
222
|
+
try {
|
|
223
|
+
rejectedSession = sessionFingerprint();
|
|
224
|
+
}
|
|
225
|
+
catch { /* Storage failures remain unavailable. */ }
|
|
226
|
+
}
|
|
227
|
+
console.warn(error instanceof NotLoggedIn ? 'Cloud sign-in is required. Local service controls remain available.' : 'Cloud connection or local sign-in storage is unavailable. Local service controls remain available.');
|
|
207
228
|
}
|
|
208
229
|
})();
|
|
209
230
|
try {
|
|
@@ -354,6 +375,12 @@ export async function startLocalRuntime({ onStop } = {}) {
|
|
|
354
375
|
res.end(JSON.stringify(await runtimeStatus()));
|
|
355
376
|
return;
|
|
356
377
|
}
|
|
378
|
+
if (req.url === '/session-changed' && req.method === 'POST') {
|
|
379
|
+
await observePresenceSession();
|
|
380
|
+
res.end(JSON.stringify(await runtimeStatus(Date.now() + 3000)));
|
|
381
|
+
void connectCloud().catch(() => { }); // The resident reconnect loop retains failed startup.
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
357
384
|
if (req.url !== '/stop' || req.method !== 'POST' || req.headers['content-type'] !== 'application/json') {
|
|
358
385
|
res.writeHead(405);
|
|
359
386
|
res.end(JSON.stringify({ error: 'Unsupported local control action.' }));
|
package/dist/daemon-processes.js
CHANGED
|
@@ -13,6 +13,7 @@ const claimWaiters = new Set();
|
|
|
13
13
|
const admissionWaiters = new Set();
|
|
14
14
|
const handles = new Map();
|
|
15
15
|
const creating = new Set();
|
|
16
|
+
const preparing = new Set();
|
|
16
17
|
function path() { return join(lifecycleDir(), 'owned-work.json'); }
|
|
17
18
|
function record(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
18
19
|
function identity(value) {
|
|
@@ -32,6 +33,8 @@ function reference(value) {
|
|
|
32
33
|
return typeof value.runId === 'string' && typeof value.cardId === 'string'
|
|
33
34
|
&& (value.processToken === null || typeof value.processToken === 'string') && typeof value.startedAt === 'string'
|
|
34
35
|
&& (value.resumedAt === null || typeof value.resumedAt === 'string') && ids(value.observedPendingTurnIds)
|
|
36
|
+
&& (value.authRecoveryRunId === undefined || value.authRecoveryRunId === null || typeof value.authRecoveryRunId === 'string')
|
|
37
|
+
&& (value.harness === undefined || value.harness === null || ['codex', 'claude'].includes(String(value.harness)))
|
|
35
38
|
&& (value.pid === undefined || value.pid === null || Number.isInteger(value.pid) && value.pid > 0);
|
|
36
39
|
if (value.surface === 'worker')
|
|
37
40
|
return typeof value.todoId === 'string' && (value.workerId === null || typeof value.workerId === 'string')
|
|
@@ -46,7 +49,7 @@ function readStore() {
|
|
|
46
49
|
if (!existsSync(path()))
|
|
47
50
|
return { version: 1, entries: [], legacy: [] };
|
|
48
51
|
const value = JSON.parse(readFileSync(path(), 'utf8'));
|
|
49
|
-
if (!record(value) || (value.legacyBootstrap !== undefined && typeof value.legacyBootstrap !== 'boolean') || (value.rpcClaims !== undefined && (!Array.isArray(value.rpcClaims) || !value.rpcClaims.every((row) => record(row) && typeof row.id === 'string' && ['panel', 'cli'].includes(String(row.surface)) && typeof row.action === 'string' && typeof row.accountId === 'string' && typeof row.machineId === 'string' && typeof row.instanceNonce === 'string'))) || value.version !== 1 || !Array.isArray(value.entries) || !Array.isArray(value.legacy)
|
|
52
|
+
if (!record(value) || (value.mcpCleanup !== undefined && (!Array.isArray(value.mcpCleanup) || !value.mcpCleanup.every(row => record(row) && typeof row.accountId === 'string' && typeof row.sessionId === 'string'))) || (value.legacyBootstrap !== undefined && typeof value.legacyBootstrap !== 'boolean') || (value.rpcClaims !== undefined && (!Array.isArray(value.rpcClaims) || !value.rpcClaims.every((row) => record(row) && typeof row.id === 'string' && ['panel', 'cli'].includes(String(row.surface)) && typeof row.action === 'string' && typeof row.accountId === 'string' && typeof row.machineId === 'string' && typeof row.instanceNonce === 'string' && (row.args === undefined || record(row.args)) && (row.result === undefined || Array.isArray(row.result))))) || value.version !== 1 || !Array.isArray(value.entries) || !Array.isArray(value.legacy)
|
|
50
53
|
|| !value.entries.every((entry) => record(entry) && typeof entry.id === 'string'
|
|
51
54
|
&& typeof entry.accountId === 'string' && typeof entry.machineId === 'string' && typeof entry.instanceNonce === 'string'
|
|
52
55
|
&& (entry.harness === null || typeof entry.harness === 'string') && (entry.reference === null || reference(entry.reference))
|
|
@@ -58,6 +61,16 @@ function readStore() {
|
|
|
58
61
|
&& (entry.gatedCreation === undefined || typeof entry.gatedCreation === 'boolean')
|
|
59
62
|
&& (entry.windowsJob === undefined || typeof entry.windowsJob === 'string')
|
|
60
63
|
&& (entry.rawPid === undefined || Number.isInteger(entry.rawPid) && entry.rawPid > 0)
|
|
64
|
+
&& (entry.terminalOutcome === undefined || record(entry.terminalOutcome) && ['panel', 'worker', 'reply'].includes(String(entry.terminalOutcome.surface)) && typeof entry.terminalOutcome.ok === 'boolean' && (entry.terminalOutcome.text === null || typeof entry.terminalOutcome.text === 'string') && typeof entry.terminalOutcome.reason === 'string'
|
|
65
|
+
&& (entry.terminalOutcome.failureKind === undefined || ['authentication', 'preparation-unavailable', 'missing-binary', 'timeout', 'usage-limit', 'invalid-model', 'transient', 'unknown'].includes(String(entry.terminalOutcome.failureKind)))
|
|
66
|
+
&& (entry.terminalOutcome.completedAt === undefined || typeof entry.terminalOutcome.completedAt === 'number' && Number.isFinite(entry.terminalOutcome.completedAt) && entry.terminalOutcome.completedAt > 0)
|
|
67
|
+
&& (entry.terminalOutcome.harnessIdentity === undefined || typeof entry.terminalOutcome.harnessIdentity === 'string')
|
|
68
|
+
&& (entry.terminalOutcome.stopped === undefined || typeof entry.terminalOutcome.stopped === 'boolean')
|
|
69
|
+
&& (entry.terminalOutcome.workerIsLive === undefined || typeof entry.terminalOutcome.workerIsLive === 'boolean')
|
|
70
|
+
&& (entry.terminalOutcome.replyingToClaim === undefined || entry.terminalOutcome.replyingToClaim === null || typeof entry.terminalOutcome.replyingToClaim === 'string')
|
|
71
|
+
&& (entry.terminalOutcome.disposition === undefined || record(entry.terminalOutcome.disposition) && ['done', 'died'].includes(String(entry.terminalOutcome.disposition.worker)) && typeof entry.terminalOutcome.disposition.reason === 'string' && ['done', 'needs-input', 'retry', 'failed'].includes(String(entry.terminalOutcome.disposition.todo)))
|
|
72
|
+
&& (entry.terminalOutcome.level === undefined || [1, 2, 3].includes(Number(entry.terminalOutcome.level)))
|
|
73
|
+
&& (entry.terminalOutcome.workItemId === undefined || entry.terminalOutcome.workItemId === null || typeof entry.terminalOutcome.workItemId === 'string'))
|
|
61
74
|
&& (entry.preparationFailure === undefined || record(entry.preparationFailure)
|
|
62
75
|
&& typeof entry.preparationFailure.reason === 'string' && [1, 2, 3].includes(Number(entry.preparationFailure.level))
|
|
63
76
|
&& record(entry.reference) && entry.reference.surface === 'panel')
|
|
@@ -73,7 +86,7 @@ function readStore() {
|
|
|
73
86
|
const store = value;
|
|
74
87
|
const intents = readIntents();
|
|
75
88
|
for (const entry of store.entries) {
|
|
76
|
-
if (!entry.preparationFailure)
|
|
89
|
+
if (!entry.preparationFailure && !entry.terminalOutcome)
|
|
77
90
|
entry.interruption ??= intents[entry.id] ?? intents[`instance:${entry.instanceNonce}`] ?? null;
|
|
78
91
|
}
|
|
79
92
|
return store;
|
|
@@ -192,18 +205,19 @@ export function resumeOwnedWorkAdmission() {
|
|
|
192
205
|
export function reserveOwnedWork(ref, harness = null, workId) {
|
|
193
206
|
const owner = requireContext();
|
|
194
207
|
const store = readStore();
|
|
195
|
-
const prior = store.entries.find((row) => row.unstarted && row.
|
|
208
|
+
const prior = store.entries.find((row) => row.unstarted && !row.spawning && !row.interruption && !preparing.has(row.id) && row.accountId === owner.accountId && row.machineId === owner.machineId
|
|
196
209
|
&& row.reference && (ref ? sameAttempt(row.reference, ref) : workId ? row.reference.surface === 'panel' && row.reference.runId === workId : false));
|
|
197
210
|
const entry = prior ?? {
|
|
198
211
|
id: randomUUID(), accountId: owner.accountId, machineId: owner.machineId, instanceNonce: owner.instanceNonce,
|
|
199
212
|
harness, reference: ref, process: null, descendants: [], state: 'pending', interruption: null,
|
|
200
213
|
};
|
|
201
|
-
entry.
|
|
214
|
+
preparing.add(entry.id);
|
|
215
|
+
entry.instanceNonce = owner.instanceNonce;
|
|
202
216
|
if (harness)
|
|
203
217
|
entry.harness = harness;
|
|
204
218
|
if (ref) {
|
|
205
219
|
entry.reference = ref;
|
|
206
|
-
store.entries = store.entries.filter((row) => row.id === entry.id || !row.unstarted || !row.reference || !sameAttempt(row.reference, ref));
|
|
220
|
+
store.entries = store.entries.filter((row) => row.id === entry.id || row.accountId !== owner.accountId || row.machineId !== owner.machineId || !row.unstarted || !row.reference || !sameAttempt(row.reference, ref));
|
|
207
221
|
}
|
|
208
222
|
if (!prior)
|
|
209
223
|
store.entries.push(entry);
|
|
@@ -241,6 +255,7 @@ export function reserveOwnedWork(ref, harness = null, workId) {
|
|
|
241
255
|
throw new Error('Work was interrupted before its harness could start.');
|
|
242
256
|
}
|
|
243
257
|
changeEntry(entry.id, (row) => {
|
|
258
|
+
row.unstarted = false;
|
|
244
259
|
row.spawning = true;
|
|
245
260
|
row.gatedCreation = true;
|
|
246
261
|
if (process.platform === 'win32')
|
|
@@ -248,11 +263,18 @@ export function reserveOwnedWork(ref, harness = null, workId) {
|
|
|
248
263
|
});
|
|
249
264
|
creating.add(entry.id);
|
|
250
265
|
},
|
|
266
|
+
deferOutcome: (outcome) => {
|
|
267
|
+
changeEntry(entry.id, row => { if (!row.interruption)
|
|
268
|
+
row.terminalOutcome = outcome; });
|
|
269
|
+
},
|
|
270
|
+
acknowledgeOutcome: () => { if (readStore().entries.some(row => row.id === entry.id))
|
|
271
|
+
changeEntry(entry.id, row => { delete row.terminalOutcome; }); },
|
|
251
272
|
complete: () => {
|
|
273
|
+
preparing.delete(entry.id);
|
|
252
274
|
const current = readStore().entries.find((row) => row.id === entry.id);
|
|
253
275
|
if (current?.preparationFailure && current.state !== 'exited')
|
|
254
276
|
throw new Error('The unprompted harness must exit before its preparation failure is released.');
|
|
255
|
-
if (current && !current.interruption)
|
|
277
|
+
if (current && !current.interruption && !current.terminalOutcome)
|
|
256
278
|
removeEntry(entry.id);
|
|
257
279
|
},
|
|
258
280
|
deferFailure: (reason, level) => {
|
|
@@ -268,12 +290,13 @@ export function reserveOwnedWork(ref, harness = null, workId) {
|
|
|
268
290
|
},
|
|
269
291
|
exited: () => confirmOwnedExecutionExited(entry.id),
|
|
270
292
|
finishPreparation: () => {
|
|
293
|
+
preparing.delete(entry.id);
|
|
271
294
|
if (registering)
|
|
272
295
|
return;
|
|
273
296
|
const current = readStore().entries.find((row) => row.id === entry.id);
|
|
274
297
|
if (!current)
|
|
275
298
|
return;
|
|
276
|
-
if (current.preparationFailure)
|
|
299
|
+
if (current.preparationFailure || current.terminalOutcome || current.unstarted && current.rpcId && !current.interruption)
|
|
277
300
|
return;
|
|
278
301
|
if (current.interruption && current.reference)
|
|
279
302
|
changeEntry(entry.id, (row) => { row.state = 'exited'; });
|
|
@@ -288,14 +311,14 @@ export function reserveOwnedWork(ref, harness = null, workId) {
|
|
|
288
311
|
row.reference = value;
|
|
289
312
|
if (agent)
|
|
290
313
|
row.harness = agent;
|
|
291
|
-
current.entries = current.entries.filter((item) => item.id === entry.id || !item.unstarted || !item.reference || !sameAttempt(item.reference, value));
|
|
314
|
+
current.entries = current.entries.filter((item) => item.id === entry.id || item.accountId !== owner.accountId || item.machineId !== owner.machineId || !item.unstarted || !item.reference || !sameAttempt(item.reference, value));
|
|
292
315
|
writeStore(current);
|
|
293
316
|
},
|
|
294
317
|
abandon: () => {
|
|
295
318
|
const current = readStore().entries.find((candidate) => candidate.id === entry.id);
|
|
296
319
|
if (!current)
|
|
297
320
|
return;
|
|
298
|
-
if (current.preparationFailure)
|
|
321
|
+
if (current.preparationFailure || current.terminalOutcome || current.unstarted && current.rpcId && !current.interruption)
|
|
299
322
|
return;
|
|
300
323
|
if (current.process)
|
|
301
324
|
throw new Error('A registered process must be observed exited before its record is released.');
|
|
@@ -397,6 +420,14 @@ export async function snapshotOwnedWork(deadline) {
|
|
|
397
420
|
const store = readStore();
|
|
398
421
|
const result = { active: [], pending: [], claimPending: [], unknown: [], receipts: 0, legacyHeldTodoIds: [] };
|
|
399
422
|
for (const entry of store.entries) {
|
|
423
|
+
if (entry.terminalOutcome) {
|
|
424
|
+
result.receipts++;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (entry.unstarted && !entry.spawning && !entry.interruption && !entry.preparationFailure && !preparing.has(entry.id)) {
|
|
428
|
+
result.claimPending.push(summary(entry));
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
400
431
|
if (entry.preparationFailure) {
|
|
401
432
|
result.receipts++;
|
|
402
433
|
if (entry.state === 'exited')
|
|
@@ -574,6 +605,29 @@ export function acknowledgeInterruption(id) {
|
|
|
574
605
|
}
|
|
575
606
|
removeEntry(id);
|
|
576
607
|
}
|
|
608
|
+
export function updateTerminalOutcome(id, outcome) {
|
|
609
|
+
const owner = requireContext();
|
|
610
|
+
const row = readStore().entries.find(entry => entry.id === id && entry.accountId === owner.accountId && entry.machineId === owner.machineId);
|
|
611
|
+
if (!row?.terminalOutcome)
|
|
612
|
+
throw new Error('The completed outcome no longer belongs to this account.');
|
|
613
|
+
changeEntry(id, entry => { entry.terminalOutcome = outcome; });
|
|
614
|
+
}
|
|
615
|
+
export function pendingTerminalOutcomes() {
|
|
616
|
+
const owner = requireContext();
|
|
617
|
+
return readStore().entries.flatMap(entry => entry.accountId === owner.accountId && entry.machineId === owner.machineId
|
|
618
|
+
&& entry.reference && entry.terminalOutcome && entry.state === 'exited' && !entry.interruption
|
|
619
|
+
? [{ id: entry.id, harness: entry.harness, reference: entry.reference, outcome: entry.terminalOutcome }] : []);
|
|
620
|
+
}
|
|
621
|
+
export function acknowledgeTerminalOutcome(id) {
|
|
622
|
+
const owner = requireContext();
|
|
623
|
+
const entry = readStore().entries.find(row => row.id === id);
|
|
624
|
+
if (!entry)
|
|
625
|
+
return;
|
|
626
|
+
if (entry.accountId !== owner.accountId || entry.machineId !== owner.machineId || entry.state !== 'exited' || !entry.terminalOutcome) {
|
|
627
|
+
throw new Error('Outcome acknowledgement does not match this account and ended execution.');
|
|
628
|
+
}
|
|
629
|
+
removeEntry(id);
|
|
630
|
+
}
|
|
577
631
|
export function failedPanelPreparations() {
|
|
578
632
|
const owner = requireContext();
|
|
579
633
|
return readStore().entries.flatMap((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
|
|
@@ -703,23 +757,23 @@ function sameAttempt(a, b) {
|
|
|
703
757
|
&& a.grantLeases.every((source) => b.grantLeases.some((other) => other.id === source.id && other.resume_taken_at === source.resume_taken_at));
|
|
704
758
|
return false;
|
|
705
759
|
}
|
|
706
|
-
export function beginRpcClaim(surface, action) {
|
|
760
|
+
export function beginRpcClaim(surface, action, args) {
|
|
707
761
|
if (!ownedWorkAllowed())
|
|
708
762
|
throw new Error('This machine is not accepting new work.');
|
|
709
763
|
const owner = requireContext();
|
|
710
764
|
const store = readStore();
|
|
711
765
|
const id = randomUUID();
|
|
712
766
|
liveRpcClaims.add(id);
|
|
713
|
-
(store.rpcClaims ??= []).push({ id, surface, action, accountId: owner.accountId, machineId: owner.machineId, instanceNonce: owner.instanceNonce });
|
|
767
|
+
(store.rpcClaims ??= []).push({ id, surface, action, args, accountId: owner.accountId, machineId: owner.machineId, instanceNonce: owner.instanceNonce });
|
|
714
768
|
writeStore(store);
|
|
715
769
|
return id;
|
|
716
770
|
}
|
|
717
771
|
export function pendingRpcClaims(surface) {
|
|
718
772
|
const owner = requireContext();
|
|
719
|
-
return (readStore().rpcClaims ?? []).filter((row) => row.surface === surface && row.accountId === owner.accountId && row.machineId === owner.machineId && !liveRpcClaims.has(row.id));
|
|
773
|
+
return (readStore().rpcClaims ?? []).filter((row) => row.surface === surface && row.accountId === owner.accountId && row.machineId === owner.machineId && row.result === undefined && !liveRpcClaims.has(row.id));
|
|
720
774
|
}
|
|
721
775
|
export function deferRpcClaim(id) { liveRpcClaims.delete(id); }
|
|
722
|
-
export function recordRpcClaims(id, refs, interrupted = false) {
|
|
776
|
+
export function recordRpcClaims(id, refs, interrupted = false, result) {
|
|
723
777
|
const owner = requireContext();
|
|
724
778
|
interrupted ||= forcedOperationId !== null;
|
|
725
779
|
const store = readStore();
|
|
@@ -730,27 +784,77 @@ export function recordRpcClaims(id, refs, interrupted = false) {
|
|
|
730
784
|
return;
|
|
731
785
|
}
|
|
732
786
|
for (const ref of refs) {
|
|
733
|
-
|
|
787
|
+
const prior = store.entries.find(row => row.reference && row.accountId === owner.accountId && sameAttempt(row.reference, ref));
|
|
788
|
+
if (prior) {
|
|
789
|
+
if (prior.unstarted && !prior.spawning && !prior.interruption)
|
|
790
|
+
prior.instanceNonce = owner.instanceNonce;
|
|
734
791
|
continue;
|
|
792
|
+
}
|
|
735
793
|
store.entries.push({ id: randomUUID(), accountId: owner.accountId, machineId: owner.machineId,
|
|
736
794
|
instanceNonce: owner.instanceNonce, harness: null, reference: ref, process: null, descendants: [],
|
|
737
795
|
state: interrupted ? 'exited' : 'pending', interruption: interrupted ? { operationId: id, interruptedAt: new Date().toISOString() } : null,
|
|
738
796
|
rpcId: id, unstarted: true });
|
|
739
797
|
}
|
|
740
|
-
|
|
798
|
+
if (result && !interrupted) {
|
|
799
|
+
claim.result = result;
|
|
800
|
+
claim.instanceNonce = owner.instanceNonce;
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
store.rpcClaims = store.rpcClaims?.filter((row) => row.id !== id);
|
|
804
|
+
writeStore(store);
|
|
805
|
+
if (!result || interrupted)
|
|
806
|
+
liveRpcClaims.delete(id);
|
|
807
|
+
}
|
|
808
|
+
export function pendingRpcReference(ref) {
|
|
809
|
+
const owner = requireContext();
|
|
810
|
+
return readStore().entries.some(row => row.unstarted && !row.spawning && !row.interruption && !preparing.has(row.id) && row.accountId === owner.accountId && row.machineId === owner.machineId && row.reference && sameAttempt(row.reference, ref));
|
|
811
|
+
}
|
|
812
|
+
export function retireRpcReference(ref) {
|
|
813
|
+
const owner = requireContext(), store = readStore();
|
|
814
|
+
store.entries = store.entries.filter(row => !row.unstarted || row.spawning || row.interruption || row.accountId !== owner.accountId || row.machineId !== owner.machineId || !row.reference || !sameAttempt(row.reference, ref));
|
|
741
815
|
writeStore(store);
|
|
742
|
-
liveRpcClaims.delete(id);
|
|
743
816
|
}
|
|
744
|
-
export function
|
|
817
|
+
export function recoveredPanelClaims() { return recoveredRpcClaims('panel'); }
|
|
818
|
+
export function recoveredRpcClaims(surface) {
|
|
819
|
+
const owner = requireContext();
|
|
820
|
+
return (readStore().rpcClaims ?? []).filter(row => row.surface === surface && row.result !== undefined && row.accountId === owner.accountId && row.machineId === owner.machineId && !liveRpcClaims.has(row.id));
|
|
821
|
+
}
|
|
822
|
+
export function finishRpcClaims(ids, retainUnstarted = false) {
|
|
745
823
|
const owner = requireContext();
|
|
746
824
|
const store = readStore();
|
|
747
|
-
|
|
825
|
+
for (const id of ids)
|
|
826
|
+
liveRpcClaims.delete(id);
|
|
827
|
+
if (retainUnstarted)
|
|
828
|
+
ids = ids.filter(id => !store.rpcClaims?.some(row => row.id === id && row.result === undefined) && !store.entries.some(row => row.rpcId === id && row.unstarted && !row.interruption && !row.preparationFailure && row.accountId === owner.accountId && row.machineId === owner.machineId));
|
|
829
|
+
store.rpcClaims = store.rpcClaims?.filter(row => !ids.includes(row.id) || row.accountId !== owner.accountId || row.machineId !== owner.machineId);
|
|
830
|
+
for (const id of ids)
|
|
831
|
+
liveRpcClaims.delete(id);
|
|
832
|
+
store.entries = store.entries.filter((row) => !row.unstarted || !row.rpcId || !ids.includes(row.rpcId) || preparing.has(row.id)
|
|
748
833
|
|| row.accountId !== owner.accountId || row.machineId !== owner.machineId || row.interruption || row.preparationFailure);
|
|
749
834
|
writeStore(store);
|
|
750
835
|
}
|
|
751
836
|
export function pendingPanelAttempt(runId) {
|
|
752
837
|
const owner = requireContext();
|
|
753
|
-
const row = readStore().entries.find((entry) => entry.
|
|
838
|
+
const row = readStore().entries.find((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId && entry.state === 'pending' && !entry.interruption && !entry.spawning
|
|
754
839
|
&& entry.reference?.surface === 'panel' && entry.reference.runId === runId);
|
|
755
840
|
return row?.reference?.surface === 'panel' ? row.reference : null;
|
|
756
841
|
}
|
|
842
|
+
/** Failed cleanup stays with the account that opened the MCP session. */
|
|
843
|
+
export function deferMcpCleanup(rows) {
|
|
844
|
+
if (!rows.length)
|
|
845
|
+
return;
|
|
846
|
+
const store = readStore();
|
|
847
|
+
for (const row of rows) {
|
|
848
|
+
if (!(store.mcpCleanup ??= []).some(existing => existing.accountId === row.accountId && existing.sessionId === row.sessionId))
|
|
849
|
+
store.mcpCleanup.push(row);
|
|
850
|
+
}
|
|
851
|
+
writeStore(store);
|
|
852
|
+
}
|
|
853
|
+
export function pendingMcpCleanup(accountId) {
|
|
854
|
+
return (readStore().mcpCleanup ?? []).filter(row => row.accountId === accountId).map(row => row.sessionId);
|
|
855
|
+
}
|
|
856
|
+
export function acknowledgeMcpCleanup(accountId, sessionIds) {
|
|
857
|
+
const store = readStore();
|
|
858
|
+
store.mcpCleanup = store.mcpCleanup?.filter(row => row.accountId !== accountId || !sessionIds.includes(row.sessionId));
|
|
859
|
+
writeStore(store);
|
|
860
|
+
}
|
package/dist/failure-reason.js
CHANGED
|
@@ -64,22 +64,8 @@ export function plainFailureReason(agent, error, stderr = '') {
|
|
|
64
64
|
|| haystack.includes('429') || haystack.includes('overloaded')) {
|
|
65
65
|
return `${name}'s usage limit is in effect on this account, so the run could not start.`;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
18a SLICE 7 ADDED THE OAUTH AND 401 SIGNATURES, from the real thing rather
|
|
71
|
-
than from imagination. The Windows machine's Claude Code had a stale token
|
|
72
|
-
and said, on STDOUT as a stream-json result: `"error":
|
|
73
|
-
"authentication_failed"` and `API Error: 401 OAuth access token has
|
|
74
|
-
expired.` The word "authentication" above would have matched that, but only
|
|
75
|
-
once stdout was being read at all, which is the other half of the fix. `401`
|
|
76
|
-
and `oauth` are here so the same failure is caught when it arrives in
|
|
77
|
-
shorter words. */
|
|
78
|
-
if (haystack.includes('unauthorized') || haystack.includes('not logged in')
|
|
79
|
-
|| haystack.includes('authentication') || haystack.includes('oauth')
|
|
80
|
-
|| haystack.includes('401')) {
|
|
81
|
-
return `${name} is not signed in on this machine.`;
|
|
82
|
-
}
|
|
67
|
+
if (nativeFailureKind(stderr, error) === 'authentication')
|
|
68
|
+
return failureMessage(agent, 'authentication');
|
|
83
69
|
/* THE RUN WAS KILLED, by the timeout or by a signal from outside. Not the
|
|
84
70
|
user's Stop, which never reaches here: a stopped run is settled with its own
|
|
85
71
|
flag well before this. */
|
|
@@ -96,3 +82,77 @@ export function plainFailureReason(agent, error, stderr = '') {
|
|
|
96
82
|
}
|
|
97
83
|
return `${name} stopped without finishing.`;
|
|
98
84
|
}
|
|
85
|
+
/** Accept only native error records and verified diagnostic lines. Assistant
|
|
86
|
+
* prose, a quoted status code and credential-file presence prove nothing. */
|
|
87
|
+
export function nativeFailureKind(output, diagnostics = '') {
|
|
88
|
+
const classify = (value) => {
|
|
89
|
+
if (!value || typeof value !== 'object')
|
|
90
|
+
return 'unknown';
|
|
91
|
+
const record = value;
|
|
92
|
+
const nested = record.error;
|
|
93
|
+
const error = nested && typeof nested === 'object' ? nested : record;
|
|
94
|
+
const code = typeof error.code === 'string' ? error.code : typeof nested === 'string' ? nested : typeof error.type === 'string' ? error.type : '';
|
|
95
|
+
if (['authentication_error', 'authentication_failed', 'invalid_api_key', 'invalid_authentication', 'token_expired', 'refresh_token_expired', 'refresh_token_reused', 'refresh_token_invalidated'].includes(code))
|
|
96
|
+
return 'authentication';
|
|
97
|
+
if (['rate_limit_error', 'rate_limit_exceeded', 'usage_limit_reached', 'insufficient_quota'].includes(code))
|
|
98
|
+
return 'usage-limit';
|
|
99
|
+
if (code === 'invalid_request_error' && record.status === 400 && typeof error.message === 'string' && /^The requested model is not supported for this account\.?$/.test(error.message))
|
|
100
|
+
return 'invalid-model';
|
|
101
|
+
if (['model_not_found', 'unsupported_model', 'unrecognized_model'].includes(code))
|
|
102
|
+
return 'invalid-model';
|
|
103
|
+
if (['overloaded_error', 'server_error', 'temporarily_unavailable', 'connection_error'].includes(code))
|
|
104
|
+
return 'transient';
|
|
105
|
+
if (typeof error.message === 'string') {
|
|
106
|
+
if (error.message === 'Your access token could not be refreshed. Please log out and sign in again.')
|
|
107
|
+
return 'authentication';
|
|
108
|
+
try {
|
|
109
|
+
return classify(JSON.parse(error.message));
|
|
110
|
+
}
|
|
111
|
+
catch { /* Not a nested native record. */ }
|
|
112
|
+
}
|
|
113
|
+
return 'unknown';
|
|
114
|
+
};
|
|
115
|
+
for (const line of output.split('\n')) {
|
|
116
|
+
const text = line.trim();
|
|
117
|
+
try {
|
|
118
|
+
const event = JSON.parse(text);
|
|
119
|
+
// Ignore native assistant messages, tool results and their quoted prose.
|
|
120
|
+
if (['turn.failed', 'error'].includes(event?.type) || (event?.type === 'result' && event.is_error === true)) {
|
|
121
|
+
if (event.type === 'result' && (event.api_error_status === undefined || event.api_error_status === 401)
|
|
122
|
+
&& event.result === 'Failed to authenticate. API Error: 401 Invalid bearer token')
|
|
123
|
+
return 'authentication';
|
|
124
|
+
if (event.type === 'result' && event.api_error_status === 401 && typeof event.result === 'string'
|
|
125
|
+
&& /^Failed to authenticate\. API Error: 401 OAuth access token (?:has (?:expired|been revoked)|is invalid)\./.test(event.result))
|
|
126
|
+
return 'authentication';
|
|
127
|
+
const kind = classify(event);
|
|
128
|
+
if (kind !== 'unknown')
|
|
129
|
+
return kind;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { /* Native diagnostics may be non-JSON. */ }
|
|
133
|
+
}
|
|
134
|
+
for (const text of diagnostics.split('\n').map(line => line.trim())) {
|
|
135
|
+
if (/^API Error: (529|503) (Overloaded|Service unavailable)$/.test(text))
|
|
136
|
+
return 'transient';
|
|
137
|
+
if (/^API Error: 401 (?:OAuth access token (?:has (?:expired|been revoked)|is invalid)\.|.*"type"\s*:\s*"authentication_error")/.test(text)
|
|
138
|
+
|| /^\[claude-code:authentication_failed\]/.test(text)
|
|
139
|
+
|| /^ERROR: Your access token could not be refreshed because your refresh token was already used\./.test(text))
|
|
140
|
+
return 'authentication';
|
|
141
|
+
if (/^\[claude-code:unrecognized_model\]/.test(text))
|
|
142
|
+
return 'invalid-model';
|
|
143
|
+
}
|
|
144
|
+
return 'unknown';
|
|
145
|
+
}
|
|
146
|
+
export function failureMessage(agent, kind) {
|
|
147
|
+
const name = agentDisplayName(agent);
|
|
148
|
+
switch (kind) {
|
|
149
|
+
case 'authentication': return `${name} needs sign-in on its assigned computer. Saved work is waiting for you to restart this card.`;
|
|
150
|
+
case 'preparation-unavailable': return `${name}'s local credentials or execution configuration could not be checked.`;
|
|
151
|
+
case 'missing-binary': return `${name} is not installed on this machine, or is not on its PATH.`;
|
|
152
|
+
case 'timeout': return 'The run was stopped for taking too long.';
|
|
153
|
+
case 'usage-limit': return `${name}'s usage limit is in effect on this account.`;
|
|
154
|
+
case 'invalid-model': return `${name} rejected the selected model. Choose an available model and retry.`;
|
|
155
|
+
case 'transient': return `${name}'s service is temporarily unavailable.`;
|
|
156
|
+
default: return `${name} stopped without finishing.`;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { autostartOn, autostartOff } from './autostart.js';
|
|
|
6
6
|
import { detectAgents } from './agents.js';
|
|
7
7
|
import { getMachineIdentity, clearSession, readSession, machineHostname } from './config.js';
|
|
8
8
|
import { CLI_VERSION } from './package-version.js';
|
|
9
|
-
import { inspectLocalRuntime, runLifecycleCommand } from './daemon-lifecycle.js';
|
|
9
|
+
import { inspectLocalRuntime, runLifecycleCommand, notifySessionChanged } from './daemon-lifecycle.js';
|
|
10
10
|
import { readMigration, readRuntime } from './daemon-lock.js';
|
|
11
11
|
import { claudeRegisteredOnDisk, codexRegisteredOnDisk } from './mcp.js';
|
|
12
12
|
import { panelCommand } from './panel3/cli.js';
|
|
@@ -110,9 +110,12 @@ async function main() {
|
|
|
110
110
|
throw new Error('Usage: cs ' + cmd + ' [--force]');
|
|
111
111
|
return runLifecycleCommand(cmd, arg === '--force');
|
|
112
112
|
case 'status': return status();
|
|
113
|
-
case 'logout':
|
|
114
|
-
|
|
113
|
+
case 'logout': {
|
|
114
|
+
const signedIn = await clearSession();
|
|
115
|
+
await notifySessionChanged();
|
|
116
|
+
console.log(signedIn ? 'Signed out.' : 'Was not signed in.');
|
|
115
117
|
return;
|
|
118
|
+
}
|
|
116
119
|
/* THE PANEL'S OWN COMMANDS, ROUTED WHOLE. The person types one CLI, so the
|
|
117
120
|
card commands are `cs` subcommands; the argument handling stays inside
|
|
118
121
|
`panel3/`, which is the one import this file makes into it (named in
|
package/dist/login.js
CHANGED
|
@@ -2,8 +2,9 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { SUPABASE_URL, SUPABASE_KEY } from './env.js';
|
|
4
4
|
import { openBrowser } from './browser.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { getMachineIdentity } from './config.js';
|
|
6
|
+
import { acceptLogin } from './supabase.js';
|
|
7
|
+
import { notifySessionChanged } from './daemon-lifecycle.js';
|
|
7
8
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
9
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
9
10
|
export async function login() {
|
|
@@ -80,15 +81,12 @@ async function handleCallback(req, res, state, finish) {
|
|
|
80
81
|
res.end(JSON.stringify({ ok: false, error: 'Invalid or missing state' }));
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
|
-
writeSession({ access_token, refresh_token });
|
|
84
84
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (error || !data.user?.email)
|
|
88
|
-
throw new Error(error?.message ?? 'No user for this session.');
|
|
85
|
+
const email = await acceptLogin({ access_token, refresh_token });
|
|
86
|
+
await notifySessionChanged();
|
|
89
87
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
90
88
|
res.end(JSON.stringify({ ok: true }));
|
|
91
|
-
finish({ ok: true, email
|
|
89
|
+
finish({ ok: true, email });
|
|
92
90
|
}
|
|
93
91
|
catch (err) {
|
|
94
92
|
const message = `Session could not be verified: ${err.message}`;
|