@awak-app/simy-cli 0.6.10 → 0.6.12
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 +48 -0
- package/package.json +2 -2
- package/src/agent.js +93 -45
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +110 -30
- package/src/codex-plugins/simy/hooks/workspace-lease.mjs +32 -5
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +4 -2
- package/src/coding-run-recovery.js +49 -0
- package/src/coding-run-store.js +9 -2
- package/src/connection-diagnostics.js +77 -0
- package/src/connection-discovery.js +161 -0
- package/src/delivery-hooks/command.js +5 -5
- package/src/delivery-hooks/desktop.js +5 -2
- package/src/delivery-hooks/supervisor.js +4 -3
- package/src/index.js +21 -0
- package/src/launch-authorization.js +80 -0
- package/src/orchestrator/evidence.js +10 -2
- package/src/orchestrator/loop.js +99 -44
- package/src/orchestrator/risk.js +17 -2
- package/src/runner.js +63 -27
- package/src/session-supervision/codex-app-server.js +72 -8
- package/src/session-supervision/continuation-analysis.js +46 -9
- package/src/session-supervision/index.js +70 -17
- package/src/session-supervision/statistics.js +138 -60
- package/src/sqm/failure-circuit.js +105 -0
- package/src/sqm/review-worker.js +18 -4
- package/src/sqm/session.js +9 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { TERMINAL as TERMINAL_RUNS, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
-
import { listCodexSessions, openCodexAppServer, readCodexSession, CodexAppServerError } from './codex-app-server.js';
|
|
3
|
+
import { TERMINAL as TERMINAL_RUNS, canUpgradeDelivery, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
+
import { listCodexSessions, openCodexAppServer, readCodexSession, readCodexArchiveStates, CodexAppServerError } from './codex-app-server.js';
|
|
5
5
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
6
6
|
import { originalSessionTarget } from './original-session-target.js';
|
|
7
7
|
import { readCodexTitles, sessionTitle } from './session-titles.js';
|
|
@@ -55,25 +55,56 @@ async function requireClaudeChannel(record, metadata, inspectProcess) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function discoverSessions({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
58
|
-
inspectProcess = processIdentity, now = Date.now(), signal, listCodexTitles = readCodexTitles } = {}) {
|
|
58
|
+
inspectProcess = processIdentity, now = Date.now(), signal, listCodexTitles = readCodexTitles, codexHome } = {}) {
|
|
59
59
|
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
60
60
|
assertActive();
|
|
61
61
|
const records = await listRecords(root);
|
|
62
62
|
const priorities = records.filter(retainedSession);
|
|
63
63
|
let discovered = [], codexError = null, claude = [], claudeError = null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
let archiveStates = new Map();
|
|
65
|
+
try { discovered = await withClient(openClient, async client => {
|
|
66
|
+
const active = await listCodexSessions({ client, inspectState: true, maxSessions: RECENT_SESSION_LIMIT,
|
|
67
|
+
prioritySessions: priorities.filter(record => record.provider === 'codex').map(record => record.session) });
|
|
68
|
+
const ids = new Set(active.map(item => item.session));
|
|
69
|
+
archiveStates = await readCodexArchiveStates(client, records.filter(record => record.provider === 'codex' && !ids.has(record.session)), { codexHome });
|
|
70
|
+
return active;
|
|
71
|
+
}, signal); }
|
|
67
72
|
catch (error) { codexError = error?.code === 'unavailable' ? 'Codex executable unavailable.' : error?.code === 'unsupported' ? 'This Codex version lacks a verified app-server writer lock; update before enrollment.' : 'Codex conversation discovery unavailable.'; }
|
|
68
73
|
assertActive();
|
|
69
74
|
try { claude = await listClaude({ signal, limit: RECENT_SESSION_LIMIT,
|
|
70
75
|
prioritySessions: priorities.filter(record => record.provider === 'claude').map(record => record.session) }); }
|
|
71
76
|
catch { claudeError = 'Claude conversation discovery unavailable.'; }
|
|
72
77
|
assertActive();
|
|
78
|
+
const activeCodex = new Set(discovered.map(item => item.session));
|
|
79
|
+
for (let index = 0; index < records.length; index++) {
|
|
80
|
+
assertActive();
|
|
81
|
+
const record = records[index];
|
|
82
|
+
if (record.provider !== 'codex') continue;
|
|
83
|
+
const isArchived = archiveStates.get(record.session) === true;
|
|
84
|
+
if (isArchived && record.providerArchived === true) continue;
|
|
85
|
+
if (!isArchived && !(record.providerArchived === true && (activeCodex.has(record.session) || archiveStates.get(record.session) === false))) continue;
|
|
86
|
+
records[index] = await updateRecord(root, record.provider, record.session, current => {
|
|
87
|
+
if (!current) return current;
|
|
88
|
+
current.providerArchived = isArchived;
|
|
89
|
+
if (isArchived) {
|
|
90
|
+
current.tracked = false; current.monitor = false; current.autoContinue = false;
|
|
91
|
+
current.autoContinueEpoch = randomUUID();
|
|
92
|
+
if (current.run && !TERMINAL_RUNS.has(current.run.state)) {
|
|
93
|
+
current.run.state = 'paused'; current.run.reason = 'The original provider conversation was archived.';
|
|
94
|
+
current.run.updatedAt = now;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
assertActive();
|
|
73
101
|
const byKey = new Map();
|
|
74
102
|
const recordsByKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
75
|
-
for (const item of [...discovered, ...claude])
|
|
103
|
+
for (const item of [...discovered, ...claude]) {
|
|
104
|
+
if (recordsByKey.get(`${item.provider}:${item.session}`)?.providerArchived !== true) byKey.set(`${item.provider}:${item.session}`, item);
|
|
105
|
+
}
|
|
76
106
|
for (const record of records) {
|
|
107
|
+
if (record.providerArchived === true) continue;
|
|
77
108
|
const key = `${record.provider}:${record.session}`;
|
|
78
109
|
if (!byKey.has(key) && (record.observedAt || record.admission || retainedSession(record))) byKey.set(key, {
|
|
79
110
|
provider: record.provider, session: record.session, cwd: record.cwd, title: '', source: record.admission?.source || 'hook',
|
|
@@ -99,14 +130,17 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
99
130
|
? { ready: false, code: 'session_metadata_unavailable', reason: claudeError }
|
|
100
131
|
: await liveClaudeChannel(record, item, inspectProcess) : null;
|
|
101
132
|
const liveHook = claudeChannel?.ready === true;
|
|
102
|
-
const
|
|
133
|
+
const upgradeable = canUpgradeDelivery(run);
|
|
134
|
+
const continuationIssue = liveHook && run ? !run.managed && !upgradeable
|
|
103
135
|
? { code: 'existing_loop', reason: 'This conversation already has a Delivery Loop. Use its existing controls.' }
|
|
104
136
|
: TERMINAL_RUNS.has(run.state) ? { code: 'task_settled', reason: 'This supervised task has settled. Review its existing status before continuing.' } : null : null;
|
|
105
137
|
let isRunning = item.isRunning;
|
|
106
|
-
|
|
138
|
+
const hookIsNewer = Number.isFinite(record?.runtime?.observedAt) && Number.isFinite(item.lastActivityAt) && record.runtime.observedAt > item.lastActivityAt;
|
|
139
|
+
if ((isRunning === undefined || isRunning === false && hookIsNewer) && record?.runtime?.process?.identity && record.cwd === item.cwd) {
|
|
107
140
|
try {
|
|
108
|
-
//
|
|
109
|
-
//
|
|
141
|
+
// A hook newer than the persisted provider activity may describe a
|
|
142
|
+
// new turn. Require its same live process; older hooks cannot override
|
|
143
|
+
// a more recent completed provider turn.
|
|
110
144
|
if (record.runtime.isRunning && await inspectProcess(record.runtime.process.pid) === record.runtime.process.identity) isRunning = true;
|
|
111
145
|
else if (isRunning === undefined && !record.runtime.isRunning) isRunning = false;
|
|
112
146
|
} catch { /* Unverified process leaves runtime status unknown. */ }
|
|
@@ -130,7 +164,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
130
164
|
capabilities: { canMonitor: item.capabilities?.canMonitor ?? true,
|
|
131
165
|
canAnalyze: !run && (item.provider === 'codex' ? item.observation.source === 'codex_app_server' && !codexError : item.observation.source === 'claude_local_index' && !claudeError),
|
|
132
166
|
canEnroll: (supported || liveHook) && !run, canContinue: (supported || liveHook) && !run,
|
|
133
|
-
canAutoContinue: (supported || liveHook) && (!run || Boolean(run.managed) && !TERMINAL_RUNS.has(run.state)) },
|
|
167
|
+
canAutoContinue: (supported || liveHook) && (!run || upgradeable || Boolean(run.managed) && !TERMINAL_RUNS.has(run.state)) },
|
|
134
168
|
reason: item.provider === 'claude' ? continuationIssue?.reason || claudeChannel.reason :
|
|
135
169
|
codexError || (!supported ? 'Exact model or provider metadata is unavailable.' : null),
|
|
136
170
|
};
|
|
@@ -151,9 +185,10 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
151
185
|
export async function setSessionAutoContinue({ root, provider, session, enabled, continuationPolicy, ...options }) {
|
|
152
186
|
if (!['codex', 'claude'].includes(provider) || typeof enabled !== 'boolean') throw new Error('Unsupported automatic continuation request.');
|
|
153
187
|
if (enabled) await trackSession({ root, provider, session, tracked: true, ...options });
|
|
154
|
-
|
|
188
|
+
const updated = await updateRecord(root, provider, session, async previous => {
|
|
155
189
|
if (!previous) throw new Error('Refresh this conversation before changing automatic continuation.');
|
|
156
|
-
if (enabled && previous.
|
|
190
|
+
if (enabled && previous.providerArchived === true) throw new Error('Restore the original provider conversation before enabling automatic continuation.');
|
|
191
|
+
if (enabled && previous.run && !previous.run.managed && !canUpgradeDelivery(previous.run)) throw new Error('Resolve the existing loop blocker before enabling automatic continuation.');
|
|
157
192
|
if (enabled && TERMINAL_RUNS.has(previous.run?.state)) throw new Error('This conversation is settled. Resume an eligible existing task after refreshing its status.');
|
|
158
193
|
if (enabled && provider === 'codex' && !previous.admission?.model) throw new Error('Automatic continuation is unavailable for this conversation.');
|
|
159
194
|
if (enabled && provider === 'claude') await requireClaudeChannel(previous,
|
|
@@ -172,6 +207,14 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
172
207
|
if (next.run?.managed) next.run.managed.autoContinueEpoch = epoch;
|
|
173
208
|
return next;
|
|
174
209
|
});
|
|
210
|
+
if (enabled && updated.run && !updated.run.managed) {
|
|
211
|
+
// Enrollment checks this exact opt-in epoch under its state lock, so an
|
|
212
|
+
// overlapping Off action prevents the promotion from dispatching work.
|
|
213
|
+
await continueSession({ ...options, root, provider, session,
|
|
214
|
+
...updated.autoContinueSettings, autoContinueEpoch: updated.autoContinueEpoch });
|
|
215
|
+
return readRecord(root, provider, session);
|
|
216
|
+
}
|
|
217
|
+
return updated;
|
|
175
218
|
}
|
|
176
219
|
|
|
177
220
|
export async function readSessionAutoContinue({ root, provider, session }) {
|
|
@@ -195,6 +238,7 @@ export async function trackSession({ root, provider, session, tracked = true, op
|
|
|
195
238
|
if (!metadata) throw new Error('Exact provider session metadata is unavailable.');
|
|
196
239
|
const cwd = await realpath(metadata.cwd);
|
|
197
240
|
await updateRecord(root, provider, session, previous => {
|
|
241
|
+
if (previous?.providerArchived === true) throw new Error('Restore the original provider conversation before enabling automatic continuation or tracking.');
|
|
198
242
|
if (previous && previous.cwd !== cwd) throw new Error('Session workspace changed; refresh its metadata.');
|
|
199
243
|
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }), tracked: true, monitor: true,
|
|
200
244
|
admission: { ...(previous?.admission || {}), source: metadata.observation.source, provider, session, cwd, model: metadata.model, validatedAt: now } };
|
|
@@ -215,6 +259,7 @@ export async function monitorSession({ root, provider, session, monitor = true,
|
|
|
215
259
|
if (!metadata) throw new Error('Exact provider session metadata is unavailable.');
|
|
216
260
|
const cwd = await realpath(metadata.cwd);
|
|
217
261
|
await updateRecord(root, provider, session, previous => {
|
|
262
|
+
if (previous?.providerArchived === true) throw new Error('Restore the original provider conversation before enabling supervision.');
|
|
218
263
|
if (previous && previous.cwd !== cwd) throw new Error('Session workspace changed; refresh its metadata.');
|
|
219
264
|
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }), monitor,
|
|
220
265
|
admission: { source: metadata.observation.source, provider, session, cwd, model: metadata.model, validatedAt: now } };
|
|
@@ -223,7 +268,7 @@ export async function monitorSession({ root, provider, session, monitor = true,
|
|
|
223
268
|
}
|
|
224
269
|
|
|
225
270
|
export async function enrollSession({ root, provider, session, objective, acceptanceCriteria, contextBootstrap = false, autonomyPolicy = 'balanced',
|
|
226
|
-
maxFailures = 3, maxNoProgressMinutes, continuationPolicy, autoContinueEpoch, openClient = openCodexAppServer,
|
|
271
|
+
maxFailures = 3, maxNoProgressMinutes, continuationPolicy, autoContinueEpoch, executionCwd, upgradeRunId, openClient = openCodexAppServer,
|
|
227
272
|
readClaude = readClaudeSession, inspectProcess = processIdentity, now = Date.now() }) {
|
|
228
273
|
if (!['codex', 'claude'].includes(provider)) throw new Error('This provider does not have a verified exact-session enrollment channel.');
|
|
229
274
|
if (typeof objective !== 'string' || !objective.trim() || objective.length > 4000) throw new Error('A concrete objective is required.');
|
|
@@ -233,7 +278,7 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
233
278
|
metadata = await readClaude(session);
|
|
234
279
|
const record = await readRecord(root, provider, session);
|
|
235
280
|
await requireClaudeChannel(record, metadata, inspectProcess);
|
|
236
|
-
if (record.run) throw new Error('This session already has a Delivery Loop.');
|
|
281
|
+
if (record.run && !upgradeRunId) throw new Error('This session already has a Delivery Loop.');
|
|
237
282
|
await monitorSession({ root, provider, session, monitor: true, readClaude: async () => metadata, now });
|
|
238
283
|
// A second check narrows the time between metadata admission and the
|
|
239
284
|
// original provider's next Stop boundary. Process exit still remains a
|
|
@@ -247,7 +292,7 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
247
292
|
const initialPlan = initialDiagnosisPlan(objective.trim(), contextBootstrap);
|
|
248
293
|
const admission = provider === 'codex' ? { source: 'codex_app_server', provider, session, cwd, model: metadata.model } : undefined;
|
|
249
294
|
const liveRecord = provider === 'claude' ? await readRecord(root, provider, session) : null;
|
|
250
|
-
const run = await startDelivery({ root, provider, session, cwd, objective: objective.trim(), nextStep: 'Inspect current evidence and propose the next assessed action.',
|
|
295
|
+
const run = await startDelivery({ root, provider, session, cwd, executionCwd, upgradeRunId, objective: objective.trim(), nextStep: 'Inspect current evidence and propose the next assessed action.',
|
|
251
296
|
maxFailures, continuationPolicy, now, admission, inspectProcess, autoContinueEpoch,
|
|
252
297
|
liveHookAdmission: liveRecord ? { hookStartedAt: liveRecord.hookStartedAt, pid: liveRecord.process.pid, identity: liveRecord.process.identity } : undefined,
|
|
253
298
|
managed: { acceptanceCriteria, contextBootstrap, autonomyPolicy, maxNoProgressMinutes, approvalScopes: ['task'], approvalPolicy: 'wait',
|
|
@@ -258,6 +303,14 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
258
303
|
}
|
|
259
304
|
|
|
260
305
|
export async function continueSession(options) {
|
|
306
|
+
const record = await readRecord(options.root, options.provider, options.session);
|
|
307
|
+
if (record?.run && !record.run.managed) {
|
|
308
|
+
// The user asked to continue this task. Adopt its goal and failure budget,
|
|
309
|
+
// rather than enrolling the generic context-bootstrap goal as a new run.
|
|
310
|
+
return enrollSession({ ...options, objective: record.run.objective, upgradeRunId: record.run.id,
|
|
311
|
+
maxFailures: record.run.maxFailures, contextBootstrap: false,
|
|
312
|
+
acceptanceCriteria: [{ id: 'original-goal', description: record.run.objective.slice(0, 500), check: { type: 'manual' } }] });
|
|
313
|
+
}
|
|
261
314
|
return enrollSession({ ...options, objective: 'Understand and continue the selected conversation’s current user request.',
|
|
262
315
|
acceptanceCriteria: [], contextBootstrap: true });
|
|
263
316
|
}
|
|
@@ -6,81 +6,159 @@ import { join, relative, isAbsolute, sep } from 'node:path';
|
|
|
6
6
|
const HOUR = 3_600_000;
|
|
7
7
|
export const unavailableStatistics = () => ({ state: 'unavailable', hours: [], activity: [] });
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const READ_BYTES = 1024 * 1024;
|
|
10
|
+
const MAX_LINE_BYTES = 4 * READ_BYTES;
|
|
11
|
+
const WINDOW_MS = 7 * 24 * HOUR;
|
|
12
|
+
// One selected transcript only; no raw messages or on-disk cache. Identity and
|
|
13
|
+
// ctime prevent reuse after replacement/truncation even when size is unchanged.
|
|
14
|
+
let cachedTranscript;
|
|
15
|
+
const fingerprint = info => [info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs].join(':');
|
|
16
|
+
|
|
17
|
+
// Only usage envelopes and timestamps survive parsing, never message text.
|
|
18
|
+
// The reverse reducer attributes each cumulative difference to the newer row,
|
|
19
|
+
// retaining an older baseline even when it lies outside the displayed week.
|
|
20
|
+
class StatisticsAccumulator {
|
|
21
|
+
constructor(provider, session, now, reverse = false) {
|
|
22
|
+
Object.assign(this, { provider, session, scanNow: now, reverse, events: [], usage: new Map(), cumulative: 0,
|
|
23
|
+
pending: null, earliest: Infinity, missing: false, matched: false, wrongSession: false });
|
|
24
|
+
}
|
|
25
|
+
recordUsage(at, total, delta) {
|
|
26
|
+
if (at < this.scanNow - WINDOW_MS) return;
|
|
27
|
+
const key = `codex:${at}:${total}`;
|
|
28
|
+
if (delta > 0 || !this.usage.has(key)) this.usage.set(key, { at, tokens: delta });
|
|
29
|
+
}
|
|
30
|
+
add(line) {
|
|
31
|
+
if (!line.trim()) return;
|
|
16
32
|
let row;
|
|
17
|
-
try { row = JSON.parse(line); } catch { missing = true;
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
try { row = JSON.parse(line); } catch { this.missing = true; return; }
|
|
34
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) { this.missing = true; return; }
|
|
35
|
+
if (this.provider === 'codex' && row.type === 'session_meta') {
|
|
36
|
+
if (row.payload?.id !== this.session) this.wrongSession = true;
|
|
37
|
+
else this.matched = true;
|
|
21
38
|
}
|
|
22
|
-
if (provider === 'claude') {
|
|
23
|
-
if (row.sessionId !== session || row.isSidechain === true)
|
|
24
|
-
matched = true;
|
|
39
|
+
if (this.provider === 'claude') {
|
|
40
|
+
if (row.sessionId !== this.session || row.isSidechain === true) return;
|
|
41
|
+
this.matched = true;
|
|
25
42
|
}
|
|
26
43
|
const at = Date.parse(row.timestamp);
|
|
27
|
-
if (!Number.isFinite(at)
|
|
28
|
-
const
|
|
44
|
+
if (!Number.isFinite(at)) return;
|
|
45
|
+
const inWindow = at >= this.scanNow - WINDOW_MS;
|
|
46
|
+
const active = this.provider === 'claude' ? ['user', 'assistant'].includes(row.type) && !row.isMeta
|
|
29
47
|
: ['response_item', 'event_msg'].includes(row.type) && row.payload?.type !== 'token_count';
|
|
30
|
-
if (active)
|
|
31
|
-
|
|
48
|
+
if (active) {
|
|
49
|
+
this.earliest = Math.min(this.earliest, at);
|
|
50
|
+
if (inWindow) this.events.push(at);
|
|
51
|
+
}
|
|
52
|
+
if (this.provider === 'codex' && row.type === 'event_msg' && row.payload?.type === 'token_count') {
|
|
32
53
|
const total = row.payload.info?.total_token_usage?.total_tokens;
|
|
33
|
-
if (!Number.isSafeInteger(total) || total < 0)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
if (!Number.isSafeInteger(total) || total < 0) return;
|
|
55
|
+
this.earliest = Math.min(this.earliest, at);
|
|
56
|
+
if (this.reverse) {
|
|
57
|
+
if (this.pending) {
|
|
58
|
+
const delta = this.pending.total - total;
|
|
59
|
+
if (delta < 0) this.missing = true;
|
|
60
|
+
else this.recordUsage(this.pending.at, this.pending.total, delta);
|
|
61
|
+
}
|
|
62
|
+
this.pending = { at, total };
|
|
63
|
+
} else {
|
|
64
|
+
if (at > this.scanNow) return;
|
|
65
|
+
if (total < this.cumulative) { this.missing = true; this.cumulative = total; return; }
|
|
66
|
+
this.recordUsage(at, total, total - this.cumulative);
|
|
67
|
+
this.cumulative = total;
|
|
68
|
+
}
|
|
69
|
+
} else if (this.provider === 'claude' && row.type === 'assistant' && row.message?.id && (inWindow || !this.reverse || this.usage.has(row.message.id))) {
|
|
39
70
|
const u = row.message.usage;
|
|
40
|
-
if (!u || ![u.input_tokens, u.output_tokens
|
|
41
|
-
|
|
42
|
-
if (![u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0].every(x => Number.isSafeInteger(x) && x >= 0)) continue;
|
|
71
|
+
if (!u || ![u.input_tokens, u.output_tokens, u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0]
|
|
72
|
+
.every(x => Number.isSafeInteger(x) && x >= 0)) return;
|
|
43
73
|
const tokens = u.input_tokens + u.output_tokens + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
44
|
-
if (!Number.isSafeInteger(tokens)
|
|
45
|
-
const previous = usage.get(row.message.id);
|
|
46
|
-
if (!previous || tokens > previous.tokens) usage.set(row.message.id, { at, tokens });
|
|
74
|
+
if (!Number.isSafeInteger(tokens)) return;
|
|
75
|
+
const previous = this.usage.get(row.message.id);
|
|
76
|
+
if (!previous || tokens > previous.tokens || tokens === previous.tokens && at < previous.at) this.usage.set(row.message.id, { at, tokens });
|
|
47
77
|
}
|
|
48
78
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
finish(now, incomplete = false, coverage = {}) {
|
|
80
|
+
if (!this.matched || this.wrongSession || this.earliest > now) return unavailableStatistics();
|
|
81
|
+
const missing = this.missing || incomplete;
|
|
82
|
+
const start = Math.max(this.earliest, now - WINDOW_MS), end = now;
|
|
83
|
+
const hours = new Map();
|
|
84
|
+
for (let at = Math.floor(start / HOUR) * HOUR; at <= end; at += HOUR) hours.set(at, { at, tokens: null });
|
|
85
|
+
const usage = [...this.usage.values()];
|
|
86
|
+
if (this.reverse && this.pending) usage.push({ at: this.pending.at, tokens: this.pending.total });
|
|
87
|
+
for (const item of usage) {
|
|
88
|
+
if (item.at < start || item.at > end) continue;
|
|
89
|
+
const bucket = hours.get(Math.floor(item.at / HOUR) * HOUR);
|
|
90
|
+
bucket.tokens = (bucket.tokens ?? 0) + item.tokens;
|
|
91
|
+
}
|
|
92
|
+
const activity = [];
|
|
93
|
+
for (const at of this.events.filter(x => x >= start && x <= end).sort((a, b) => a - b)) {
|
|
94
|
+
const previous = activity.at(-1);
|
|
95
|
+
if (previous && at - previous.end <= 5 * 60_000) previous.end = at;
|
|
96
|
+
else activity.push({ start: at, end: at });
|
|
97
|
+
}
|
|
98
|
+
const activeMs = activity.reduce((total, x) => total + x.end - x.start, 0);
|
|
99
|
+
const elapsedMs = end - start;
|
|
100
|
+
return { state: missing ? 'partial' : 'ready', start, end, observedAt: now, hours: [...hours.values()], activity,
|
|
101
|
+
tokens: [...hours.values()].some(h => h.tokens !== null) ? [...hours.values()].reduce((sum, h) => sum + (h.tokens || 0), 0) : null,
|
|
102
|
+
activeMs, idleMs: missing ? null : Math.max(0, elapsedMs - activeMs),
|
|
103
|
+
utilization: missing || !elapsedMs ? null : activeMs / elapsedMs,
|
|
104
|
+
coverage: { ...coverage, complete: !missing, scope: 'rolling_7_days' },
|
|
105
|
+
activityMethod: 'recorded_events_5m_gap', includesCachedInput: true };
|
|
60
106
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function transcriptStatistics(text, provider, session, { now = Date.now(), truncated = false } = {}) {
|
|
110
|
+
const accumulator = new StatisticsAccumulator(provider, session, now);
|
|
111
|
+
for (const line of text.split('\n')) accumulator.add(line);
|
|
112
|
+
return accumulator.finish(now, truncated);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readReverse(handle, size, accumulator) {
|
|
116
|
+
let position = size, carry = Buffer.alloc(0), skipping = false, bytesRead = 0;
|
|
117
|
+
while (position > 0) {
|
|
118
|
+
const length = Math.min(position, READ_BYTES); position -= length;
|
|
119
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
120
|
+
let filled = 0;
|
|
121
|
+
while (filled < length) {
|
|
122
|
+
const read = await handle.read(buffer, filled, length - filled, position + filled);
|
|
123
|
+
if (!read.bytesRead) break;
|
|
124
|
+
filled += read.bytesRead;
|
|
125
|
+
}
|
|
126
|
+
bytesRead += filled;
|
|
127
|
+
if (filled !== length) accumulator.missing = true;
|
|
128
|
+
const data = Buffer.concat([buffer.subarray(0, filled), carry]);
|
|
129
|
+
let end = data.length;
|
|
130
|
+
for (let index = data.lastIndexOf(10); index >= 0; index = data.lastIndexOf(10, index - 1)) {
|
|
131
|
+
const line = data.subarray(index + 1, end);
|
|
132
|
+
if (!skipping && line.length <= MAX_LINE_BYTES) accumulator.add(line.toString('utf8'));
|
|
133
|
+
else accumulator.missing = true;
|
|
134
|
+
skipping = false; end = index;
|
|
135
|
+
if (index === 0) break;
|
|
136
|
+
}
|
|
137
|
+
if (end > MAX_LINE_BYTES || skipping) { carry = Buffer.alloc(0); skipping = true; accumulator.missing = true; }
|
|
138
|
+
else carry = Buffer.from(data.subarray(0, end));
|
|
66
139
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return { state: missing ? 'partial' : 'ready', start, end, observedAt: now, hours: [...hours.values()], activity,
|
|
70
|
-
tokens: [...hours.values()].some(h => h.tokens !== null) ? [...hours.values()].reduce((sum, h) => sum + (h.tokens || 0), 0) : null,
|
|
71
|
-
activeMs, idleMs: Math.max(0, elapsedMs - activeMs), utilization: elapsedMs ? activeMs / elapsedMs : null,
|
|
72
|
-
activityMethod: 'recorded_events_5m_gap', includesCachedInput: true };
|
|
140
|
+
if (!skipping) accumulator.add(carry.toString('utf8'));
|
|
141
|
+
return bytesRead;
|
|
73
142
|
}
|
|
74
143
|
|
|
75
|
-
export async function statisticsFromHandle(handle, info, provider, session, options) {
|
|
76
|
-
const
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
144
|
+
export async function statisticsFromHandle(handle, info, provider, session, { now = Date.now(), ...options } = {}) {
|
|
145
|
+
const key = `${provider}:${session}:${fingerprint(info)}`;
|
|
146
|
+
// Rebuild time-dependent buckets from cached envelopes, never cache wall-clock
|
|
147
|
+
// idle time. A backwards clock or a new file fingerprint requires a new scan.
|
|
148
|
+
if (cachedTranscript?.key === key && now >= cachedTranscript.accumulator.scanNow) {
|
|
149
|
+
const cached = cachedTranscript;
|
|
150
|
+
const changed = fingerprint(await handle.stat()) !== fingerprint(info);
|
|
151
|
+
if (changed) cachedTranscript = undefined;
|
|
152
|
+
return cached.accumulator.finish(now, changed || options.truncated, cached.coverage);
|
|
153
|
+
}
|
|
154
|
+
const accumulator = new StatisticsAccumulator(provider, session, now, true);
|
|
155
|
+
const bytesRead = await readReverse(handle, info.size, accumulator);
|
|
81
156
|
const after = await handle.stat();
|
|
82
|
-
|
|
83
|
-
|
|
157
|
+
const changed = fingerprint(after) !== fingerprint(info);
|
|
158
|
+
const coverage = { bytesRead, totalBytes: info.size };
|
|
159
|
+
if (!changed && !accumulator.missing && !options.truncated) cachedTranscript = { key, accumulator, coverage };
|
|
160
|
+
else cachedTranscript = undefined;
|
|
161
|
+
return accumulator.finish(now, changed || options.truncated, coverage);
|
|
84
162
|
}
|
|
85
163
|
|
|
86
164
|
export async function readCodexStatistics(client, candidate, { home = process.env.CODEX_HOME || join(homedir(), '.codex'), ...options } = {}) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { compareVersions } from "../backend-executable.js";
|
|
5
|
+
import { CLI_VERSION } from "../cli-contract.js";
|
|
6
|
+
import { defaultSessionRoot } from "../session-store.js";
|
|
7
|
+
|
|
8
|
+
export const SQM_FAILURE_PAUSE_THRESHOLD = 6;
|
|
9
|
+
const scopeKey = scope => JSON.stringify([scope?.webOrigin, scope?.accountId, scope?.organizationId]);
|
|
10
|
+
const validVersion = value => typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value);
|
|
11
|
+
|
|
12
|
+
export function createSqmFailureCircuitStore({ root = defaultSessionRoot() } = {}) {
|
|
13
|
+
const directory = join(root, "sqm-failure-circuit");
|
|
14
|
+
const path = scope => {
|
|
15
|
+
if (!scope?.webOrigin || !scope?.accountId || !scope?.organizationId) throw new Error("SQM failure state requires an organization scope.");
|
|
16
|
+
return join(directory, `${createHash("sha256").update(scopeKey(scope)).digest("hex")}.json`);
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
async read(scope) {
|
|
20
|
+
try {
|
|
21
|
+
const state = JSON.parse(await readFile(path(scope), "utf8"));
|
|
22
|
+
if (!validVersion(state.version) || !Number.isInteger(state.failures) || state.failures < 0 || state.failures > SQM_FAILURE_PAUSE_THRESHOLD || (state.inFlight !== undefined && typeof state.inFlight !== "boolean"))
|
|
23
|
+
throw new Error("Invalid saved SQM failure state.");
|
|
24
|
+
return state;
|
|
25
|
+
} catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
26
|
+
},
|
|
27
|
+
async write(scope, state) {
|
|
28
|
+
const target = path(scope), temporary = `${target}.${randomUUID()}.tmp`;
|
|
29
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
32
|
+
await rename(temporary, target);
|
|
33
|
+
} finally { await rm(temporary, { force: true }); }
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// One counter per organization on this machine, shared by both local engines.
|
|
39
|
+
// Neither reconnecting nor an empty claim demonstrates successful analysis.
|
|
40
|
+
export function createSqmFailureCircuit({ version = CLI_VERSION, store = null } = {}) {
|
|
41
|
+
if (!validVersion(version)) throw new Error("SQM failure circuit requires a SIMY version.");
|
|
42
|
+
const states = new Map();
|
|
43
|
+
const unreadableScopes = new Set();
|
|
44
|
+
const save = async (scope, state) => {
|
|
45
|
+
try { await store?.write(scope, { version: state.version, failures: state.failures, inFlight: state.inFlight === true }); state.storageError = false; }
|
|
46
|
+
catch (error) { state.storageError = true; throw error; }
|
|
47
|
+
};
|
|
48
|
+
const load = async scope => {
|
|
49
|
+
const key = scopeKey(scope);
|
|
50
|
+
let state = states.get(key);
|
|
51
|
+
if (!state) {
|
|
52
|
+
let saved;
|
|
53
|
+
try { saved = await store?.read(scope); unreadableScopes.delete(key); }
|
|
54
|
+
catch (error) { unreadableScopes.add(key); throw error; }
|
|
55
|
+
state = saved ? { version: saved.version, failures: saved.failures, inFlight: saved.inFlight === true } : { version, failures: 0, inFlight: false };
|
|
56
|
+
states.set(key, state);
|
|
57
|
+
// An unfinished review from this same version means the last host exited
|
|
58
|
+
// without a result or graceful cancellation. Count it once on restart.
|
|
59
|
+
if (state.inFlight && compareVersions(version, state.version) <= 0) {
|
|
60
|
+
state.failures = Math.min(SQM_FAILURE_PAUSE_THRESHOLD, state.failures + 1);
|
|
61
|
+
state.inFlight = false;
|
|
62
|
+
await save(scope, state);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (compareVersions(version, state.version) > 0) {
|
|
66
|
+
state.version = version;
|
|
67
|
+
state.failures = 0;
|
|
68
|
+
state.inFlight = false;
|
|
69
|
+
await save(scope, state);
|
|
70
|
+
} else if (state.storageError) await save(scope, state);
|
|
71
|
+
return state;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
async canClaim(scope) { return (await load(scope)).failures < SQM_FAILURE_PAUSE_THRESHOLD; },
|
|
75
|
+
async begin(scope) {
|
|
76
|
+
const state = await load(scope);
|
|
77
|
+
state.inFlight = true;
|
|
78
|
+
await save(scope, state);
|
|
79
|
+
},
|
|
80
|
+
async cancel(scope) {
|
|
81
|
+
const state = await load(scope);
|
|
82
|
+
state.inFlight = false;
|
|
83
|
+
await save(scope, state);
|
|
84
|
+
},
|
|
85
|
+
async failure(scope) {
|
|
86
|
+
const state = await load(scope);
|
|
87
|
+
state.failures = Math.min(SQM_FAILURE_PAUSE_THRESHOLD, state.failures + 1);
|
|
88
|
+
state.inFlight = false;
|
|
89
|
+
await save(scope, state);
|
|
90
|
+
},
|
|
91
|
+
async success(scope) {
|
|
92
|
+
const state = await load(scope);
|
|
93
|
+
state.failures = 0;
|
|
94
|
+
state.inFlight = false;
|
|
95
|
+
await save(scope, state);
|
|
96
|
+
},
|
|
97
|
+
snapshot(scope) {
|
|
98
|
+
const state = states.get(scopeKey(scope));
|
|
99
|
+
return { consecutive_failures: state?.failures || 0,
|
|
100
|
+
paused_until_update: (state?.failures || 0) >= SQM_FAILURE_PAUSE_THRESHOLD,
|
|
101
|
+
paused_at_version: (state?.failures || 0) >= SQM_FAILURE_PAUSE_THRESHOLD ? state.version : null,
|
|
102
|
+
storage_unavailable: state?.storageError === true || unreadableScopes.has(scopeKey(scope)) };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
7
|
import { safeSqmDiagnostic, formatSqmDiagnostic } from "./diagnostics.js";
|
|
8
|
+
import { createSqmFailureCircuit } from "./failure-circuit.js";
|
|
8
9
|
import { inspectProviderStatus } from "../desktop-status.js";
|
|
9
10
|
import { inspectDesktopExecutor } from "../desktop-executor.js";
|
|
10
11
|
import { safeProviderDiagnostic, PROVIDER_FAILURE_HINTS } from "./provider-diagnostics.js";
|
|
@@ -109,6 +110,7 @@ export function createSqmLocalReviewWorker({
|
|
|
109
110
|
now = () => Date.now(),
|
|
110
111
|
providerFailureCooldownMs = 5 * 60_000,
|
|
111
112
|
getRecoveryKey = () => null,
|
|
113
|
+
failureCircuit = createSqmFailureCircuit(),
|
|
112
114
|
} = {}) {
|
|
113
115
|
if (!api || typeof api.request !== "function") throw new TypeError("api.request is required");
|
|
114
116
|
let activeJobId = null;
|
|
@@ -187,7 +189,6 @@ export function createSqmLocalReviewWorker({
|
|
|
187
189
|
kind: "analysis", state: "running", provider, startedAt: new Date().toISOString(), webUrl: analysisTaskUrl(workspaceUrl, claim.job_id) });
|
|
188
190
|
historyScopes.set(history[0], JSON.stringify(historyScope));
|
|
189
191
|
history.splice(50);
|
|
190
|
-
await persist(historyScope, history[0]);
|
|
191
192
|
onChange();
|
|
192
193
|
let heartbeatTimer = null;
|
|
193
194
|
const heartbeat = () => {
|
|
@@ -203,6 +204,8 @@ export function createSqmLocalReviewWorker({
|
|
|
203
204
|
const providerReport = (review, selectedProvider = provider) => claim.capabilities?.reported_executor_provider === true
|
|
204
205
|
? { reported_executor_provider: review.provider || selectedProvider } : {};
|
|
205
206
|
try {
|
|
207
|
+
await failureCircuit.begin(historyScope);
|
|
208
|
+
await persist(historyScope, history[0]);
|
|
206
209
|
const { review: analysis, response: advanced } = await validatedReview(claim.request, provider, "invalid_sqm_analysis_output", analysis => request({
|
|
207
210
|
action: "advance",
|
|
208
211
|
...leaseBody(claim, provider),
|
|
@@ -250,7 +253,11 @@ export function createSqmLocalReviewWorker({
|
|
|
250
253
|
return {
|
|
251
254
|
snapshot() {
|
|
252
255
|
const pause = currentPause();
|
|
253
|
-
|
|
256
|
+
const circuit = failureCircuit.snapshot(getHistoryScope());
|
|
257
|
+
return { active_job_id: activeJobId, polling: Boolean(pollPromise), closed, suspended,
|
|
258
|
+
paused: pausedEnvironment !== null || circuit.paused_until_update || circuit.storage_unavailable,
|
|
259
|
+
pause_reason: circuit.paused_until_update ? "consecutive_failures_until_update" : circuit.storage_unavailable ? "failure_state_unavailable" : pauseReason,
|
|
260
|
+
...circuit, issue: issueContext === contextKey(getProvider(), getHistoryScope()) ? issue : null,
|
|
254
261
|
...(pause ? { provider_pause: { code: pause.code, status: pause.status, retry_at: new Date(pause.retryAt).toISOString(), message: pause.message } } : {}) };
|
|
255
262
|
},
|
|
256
263
|
desktopTasks() { return { activeCount: activeJobId ? 1 : 0, items: visibleHistory().map(item => ({ ...item, canStop: item.id === activeJobId })) }; },
|
|
@@ -258,7 +265,7 @@ export function createSqmLocalReviewWorker({
|
|
|
258
265
|
const item = visibleHistory().find(item => item.key === key);
|
|
259
266
|
return item ? { key, instruction: `SQM local review · ${item.provider}`, result: item.state === "completed" ? "SQM review submitted." : "", error: item.error || "", links: item.webUrl ? [{ label: "Analysis details", url: item.webUrl }] : [] } : null;
|
|
260
267
|
},
|
|
261
|
-
retry() { if (closed || suspended || pollPromise) return false; pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; onChange(); return true; },
|
|
268
|
+
retry() { if (closed || suspended || pollPromise || failureCircuit.snapshot(getHistoryScope()).paused_until_update) return false; pausedEnvironment = null; pauseReason = null; pauses.delete(contextKey(getProvider(), getHistoryScope())); issue = null; onChange(); return true; },
|
|
262
269
|
resetHistory() { if (!pollPromise) { history.splice(0); issue = null; pausedEnvironment = null; pauseReason = null; } },
|
|
263
270
|
poll() {
|
|
264
271
|
if (closed || suspended || pollPromise) return pollPromise || Promise.resolve(null);
|
|
@@ -279,6 +286,8 @@ export function createSqmLocalReviewWorker({
|
|
|
279
286
|
const workspaceUrl = getWorkspaceUrl();
|
|
280
287
|
reviewController = new AbortController();
|
|
281
288
|
pollPromise = (async () => {
|
|
289
|
+
if (!await failureCircuit.canClaim(historyScope)) return null;
|
|
290
|
+
if (reviewController.signal.aborted) throw providerFailure("analysis_cancelled");
|
|
282
291
|
const claim = await api.request({
|
|
283
292
|
action: "claim",
|
|
284
293
|
executor_provider: provider,
|
|
@@ -293,11 +302,16 @@ export function createSqmLocalReviewWorker({
|
|
|
293
302
|
return null;
|
|
294
303
|
}
|
|
295
304
|
const result = await processClaim(claim, provider, historyScope, workspaceUrl);
|
|
305
|
+
try { await failureCircuit.success(historyScope); } catch (storageError) { onError(storageError); }
|
|
296
306
|
pauses.delete(key);
|
|
297
307
|
return result;
|
|
298
308
|
})()
|
|
299
309
|
.catch(async (error) => {
|
|
300
|
-
if (error.code === "analysis_cancelled")
|
|
310
|
+
if (error.code === "analysis_cancelled") {
|
|
311
|
+
try { await failureCircuit.cancel(historyScope); } catch (storageError) { onError(storageError); }
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
try { await failureCircuit.failure(historyScope); } catch (storageError) { onError(storageError); }
|
|
301
315
|
pauseForFailure(error, provider, historyScope, recoveryKey);
|
|
302
316
|
if (error.jobId && /^[a-f0-9-]{36}$/i.test(error.jobId) && !history.some(item => item.id === error.jobId)) {
|
|
303
317
|
const scope = historyScope;
|
package/src/sqm/session.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { findReusableConnection } from "../connection-discovery.js";
|
|
2
|
+
import { connectionScope } from "../connection-diagnostics.js";
|
|
3
|
+
import { CLI_VERSION, CLI_API_CONTRACT_VERSION } from "../cli-contract.js";
|
|
1
4
|
import { startAgent } from "../agent.js";
|
|
2
5
|
import { openAuthorizationUrl } from "../browser.js";
|
|
3
6
|
import { isSessionValid, readSession } from "../session-store.js";
|
|
@@ -16,6 +19,7 @@ export async function ensureSqmSession(
|
|
|
16
19
|
readSessionImpl = readSession,
|
|
17
20
|
isSessionValidImpl = isSessionValid,
|
|
18
21
|
startAgentImpl = startAgent,
|
|
22
|
+
findConnectionImpl = findReusableConnection,
|
|
19
23
|
openAuthorizationUrlImpl = openAuthorizationUrl,
|
|
20
24
|
createAgentShutdownImpl = createAgentShutdown,
|
|
21
25
|
expectedScope,
|
|
@@ -31,8 +35,11 @@ export async function ensureSqmSession(
|
|
|
31
35
|
return session;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
const existing = await findConnectionImpl({ webOrigin, cliVersion: CLI_VERSION,
|
|
39
|
+
apiContractVersion: CLI_API_CONTRACT_VERSION, expectedScope: connectionScope(session, webOrigin),
|
|
40
|
+
authorize: true, reauthorize: session?.token === rejectedToken && Boolean(rejectedToken) });
|
|
41
|
+
const agent = existing || await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
|
|
42
|
+
const shutdown = existing ? async () => {} : createAgentShutdownImpl(agent);
|
|
36
43
|
try {
|
|
37
44
|
const loginUrl = agent.loginUrl;
|
|
38
45
|
if (!loginUrl) throw new Error("SIMY could not create an SQM authorization session.");
|