@awak-app/simy-cli 0.5.59 → 0.5.61
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/package.json +1 -1
- package/src/backend-executable.js +36 -21
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +105 -16
- package/src/delivery-hooks/desktop.js +4 -2
- package/src/delivery-hooks/supervisor.js +28 -19
- package/src/desktop-host.js +68 -24
- package/src/session-supervision/action-association-api.js +1 -1
- package/src/session-supervision/action-association-host.js +65 -35
- package/src/session-supervision/action-association.js +17 -10
- package/src/session-supervision/action-deadline.js +9 -1
- package/src/session-supervision/association-store.js +78 -21
- package/src/session-supervision/automatic-continuation.js +18 -1
- package/src/session-supervision/claude-sessions.js +48 -17
- package/src/session-supervision/codex-app-server.js +35 -14
- package/src/session-supervision/index.js +34 -10
- package/src/sqm/review-worker.js +5 -1
|
@@ -10,6 +10,24 @@ const MAX_TRANSCRIPTS = 4096;
|
|
|
10
10
|
const MAX_PREFIX_BYTES = 256 * 1024;
|
|
11
11
|
const MAX_LINES = 256;
|
|
12
12
|
const MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
// Metadata is observation only. Exact-session/context readers always open the
|
|
14
|
+
// transcript again; cached values contain no conversation message bodies.
|
|
15
|
+
const metadataCache = new Map();
|
|
16
|
+
const fingerprint = info => [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':');
|
|
17
|
+
function rememberMetadata(path, value) {
|
|
18
|
+
metadataCache.delete(path); metadataCache.set(path, value);
|
|
19
|
+
while (metadataCache.size > MAX_TRANSCRIPTS) metadataCache.delete(metadataCache.keys().next().value);
|
|
20
|
+
}
|
|
21
|
+
function candidateMetadata(id, metadata, lastActivityAt, observedAt) {
|
|
22
|
+
return {
|
|
23
|
+
provider: 'claude', session: id, cwd: metadata.cwd, title: metadata.title,
|
|
24
|
+
source: 'claude_cli', lastActivityAt,
|
|
25
|
+
observation: { source: 'claude_local_index', observedAt },
|
|
26
|
+
availability: 'unsupported', model: null,
|
|
27
|
+
capabilities: { canMonitor: true, canEnroll: false, canAutoContinue: false },
|
|
28
|
+
reason: 'Claude session ownership and safe exact-session takeover are unverified; monitoring only.',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
13
31
|
|
|
14
32
|
function within(parent, child) {
|
|
15
33
|
const path = relative(parent, child);
|
|
@@ -53,7 +71,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
53
71
|
return found ? { cwd, title } : null;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
async function inspectTranscript(root, projectName, fileName, observedAt, includeContext = false) {
|
|
74
|
+
async function inspectTranscript(root, projectName, fileName, observedAt, includeContext = false, cacheMetadata = false) {
|
|
57
75
|
const id = fileName.slice(0, -'.jsonl'.length);
|
|
58
76
|
if (!UUID.test(id)) return null;
|
|
59
77
|
const project = join(root, projectName);
|
|
@@ -62,8 +80,19 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
62
80
|
let handle;
|
|
63
81
|
try {
|
|
64
82
|
const before = await lstat(path);
|
|
65
|
-
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_FILE_BYTES) return null;
|
|
66
|
-
if (!within(root, await realpath(path))) return null;
|
|
83
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_FILE_BYTES) { metadataCache.delete(path); return null; }
|
|
84
|
+
if (!within(root, await realpath(path))) { metadataCache.delete(path); return null; }
|
|
85
|
+
let key;
|
|
86
|
+
if (cacheMetadata) {
|
|
87
|
+
const info = await lstat(path, { bigint: true });
|
|
88
|
+
if (!info.isFile() || info.isSymbolicLink() || info.dev !== BigInt(before.dev) || info.ino !== BigInt(before.ino)) return null;
|
|
89
|
+
key = fingerprint(info);
|
|
90
|
+
const cached = metadataCache.get(path);
|
|
91
|
+
if (cached?.key === key) {
|
|
92
|
+
rememberMetadata(path, cached);
|
|
93
|
+
return cached.metadata ? candidateMetadata(id, cached.metadata, cached.lastActivityAt, observedAt) : null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
67
96
|
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
68
97
|
const opened = await handle.stat();
|
|
69
98
|
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) return null;
|
|
@@ -75,22 +104,22 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
75
104
|
const buffer = Buffer.allocUnsafe(Math.min(opened.size, MAX_PREFIX_BYTES));
|
|
76
105
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
77
106
|
const metadata = metadataFromPrefix(buffer.subarray(0, bytesRead), id);
|
|
107
|
+
if (cacheMetadata) {
|
|
108
|
+
const [after, pathAfter] = await Promise.all([handle.stat({ bigint: true }), lstat(path, { bigint: true })]);
|
|
109
|
+
if (fingerprint(after) === key && fingerprint(pathAfter) === key && pathAfter.isFile() && !pathAfter.isSymbolicLink())
|
|
110
|
+
rememberMetadata(path, { key, metadata, lastActivityAt: opened.mtimeMs });
|
|
111
|
+
else metadataCache.delete(path);
|
|
112
|
+
}
|
|
78
113
|
if (!metadata) return null;
|
|
79
|
-
const candidate =
|
|
80
|
-
provider: 'claude', session: id, cwd: metadata.cwd, title: metadata.title,
|
|
81
|
-
source: 'claude_cli', lastActivityAt: opened.mtimeMs,
|
|
82
|
-
observation: { source: 'claude_local_index', observedAt },
|
|
83
|
-
availability: 'unsupported', model: null,
|
|
84
|
-
capabilities: { canMonitor: true, canEnroll: false, canAutoContinue: false },
|
|
85
|
-
reason: 'Claude session ownership and safe exact-session takeover are unverified; monitoring only.',
|
|
86
|
-
};
|
|
114
|
+
const candidate = candidateMetadata(id, metadata, opened.mtimeMs, observedAt);
|
|
87
115
|
if (includeContext) candidate.context = await conversationFromHandle(handle, opened, id, metadata.cwd);
|
|
88
116
|
return candidate;
|
|
89
|
-
} catch { return null; }
|
|
117
|
+
} catch { metadataCache.delete(path); return null; }
|
|
90
118
|
finally { await handle?.close(); }
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
async function collect({ home = homedir(), limit = 100, session = null, includeContext = false } = {}) {
|
|
121
|
+
async function collect({ home = homedir(), limit = 100, session = null, includeContext = false, cacheMetadata = false, signal } = {}) {
|
|
122
|
+
if (signal?.aborted) return [];
|
|
94
123
|
const root = await projectsRoot(home);
|
|
95
124
|
if (!root) return [];
|
|
96
125
|
const maximum = Number.isInteger(limit) ? Math.max(1, Math.min(limit, 200)) : 100;
|
|
@@ -100,16 +129,18 @@ async function collect({ home = homedir(), limit = 100, session = null, includeC
|
|
|
100
129
|
let projects;
|
|
101
130
|
try { projects = await readdir(root, { withFileTypes: true }); } catch { return []; }
|
|
102
131
|
for (const entry of projects.filter(item => item.isDirectory() && !item.isSymbolicLink()).slice(0, MAX_PROJECTS)) {
|
|
132
|
+
if (signal?.aborted) return [];
|
|
103
133
|
const project = join(root, entry.name);
|
|
104
134
|
let files;
|
|
105
135
|
try { files = await readdir(project, { withFileTypes: true }); } catch { continue; }
|
|
106
136
|
for (const file of files) {
|
|
137
|
+
if (signal?.aborted) return [];
|
|
107
138
|
if (inspected >= MAX_TRANSCRIPTS) break;
|
|
108
139
|
if (!file.isFile() || file.isSymbolicLink() || !UUID.test(file.name.replace(/\.jsonl$/, '')) || !file.name.endsWith('.jsonl')) continue;
|
|
109
140
|
const id = file.name.slice(0, -'.jsonl'.length);
|
|
110
141
|
if (session && id !== session) continue;
|
|
111
142
|
inspected++;
|
|
112
|
-
const candidate = await inspectTranscript(root, entry.name, file.name, observedAt, includeContext);
|
|
143
|
+
const candidate = await inspectTranscript(root, entry.name, file.name, observedAt, includeContext, cacheMetadata);
|
|
113
144
|
if (!candidate) continue;
|
|
114
145
|
const key = `${candidate.session}\0${candidate.cwd}${includeContext ? `\0${entry.name}\0${file.name}` : ''}`;
|
|
115
146
|
if (!result.has(key) || candidate.lastActivityAt > result.get(key).lastActivityAt) result.set(key, candidate);
|
|
@@ -120,8 +151,8 @@ async function collect({ home = homedir(), limit = 100, session = null, includeC
|
|
|
120
151
|
return session ? sorted : sorted.slice(0, maximum);
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
export async function listClaudeSessions({ home, limit } = {}) {
|
|
124
|
-
return collect({ home, limit });
|
|
154
|
+
export async function listClaudeSessions({ home, limit, signal } = {}) {
|
|
155
|
+
return collect({ home, limit, signal, cacheMetadata: true });
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
export async function readClaudeSession(session, { home } = {}) {
|
|
@@ -163,7 +194,7 @@ async function conversationFromHandle(handle, opened, session, cwd) {
|
|
|
163
194
|
export async function readClaudeConversation(candidate, { home, signal } = {}) {
|
|
164
195
|
if (signal?.aborted) return boundConversation(null);
|
|
165
196
|
if (!UUID.test(candidate?.session || '') || typeof candidate?.cwd !== 'string') return boundConversation(null);
|
|
166
|
-
const matches = await collect({ home, session: candidate.session, includeContext: true });
|
|
197
|
+
const matches = await collect({ home, session: candidate.session, includeContext: true, signal });
|
|
167
198
|
const selected = matches.filter(item => item.cwd === candidate.cwd);
|
|
168
199
|
if (signal?.aborted || selected.length !== 1) return boundConversation(null);
|
|
169
200
|
if (Number.isFinite(candidate.lastActivityAt) && selected[0].lastActivityAt !== candidate.lastActivityAt) {
|
|
@@ -23,7 +23,7 @@ export class CodexAppServerError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export class CodexAppServerClient {
|
|
26
|
-
constructor(child, { maxLineBytes = MAX_LINE, omitInlineAttachments = false } = {}) {
|
|
26
|
+
constructor(child, { maxLineBytes = MAX_LINE, omitInlineAttachments = false, signal } = {}) {
|
|
27
27
|
this.maxLineBytes = Number.isInteger(maxLineBytes) ? Math.max(MAX_LINE, Math.min(maxLineBytes, 8 * 1024 * 1024)) : MAX_LINE;
|
|
28
28
|
this.child = child;
|
|
29
29
|
this.nextId = 0;
|
|
@@ -61,7 +61,12 @@ export class CodexAppServerClient {
|
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
child.on('error', error => this.fail(error));
|
|
64
|
-
child.on('close', () => this.fail(new CodexAppServerError('closed', 'Codex app-server closed.')));
|
|
64
|
+
child.on('close', () => { this.childClosed = true; this.detachAbort(); this.fail(new CodexAppServerError('closed', 'Codex app-server closed.')); });
|
|
65
|
+
child.stdin?.on?.('error', error => this.fail(error));
|
|
66
|
+
this.abortSignal = signal;
|
|
67
|
+
this.cancelled = () => { this.fail(new CodexAppServerError('cancelled', 'Codex app-server was cancelled.')); void this.close(); };
|
|
68
|
+
signal?.addEventListener('abort', this.cancelled, { once: true });
|
|
69
|
+
if (signal?.aborted) this.cancelled();
|
|
65
70
|
// Diagnostics can contain private data. Drain, but never retain or expose them.
|
|
66
71
|
child.stderr?.resume();
|
|
67
72
|
}
|
|
@@ -116,14 +121,28 @@ export class CodexAppServerClient {
|
|
|
116
121
|
|
|
117
122
|
onNotification(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
detachAbort() { this.abortSignal?.removeEventListener('abort', this.cancelled); }
|
|
125
|
+
|
|
126
|
+
close() {
|
|
127
|
+
if (this.closing) return this.closing;
|
|
128
|
+
this.detachAbort();
|
|
129
|
+
this.fail(new CodexAppServerError('closed', 'Codex app-server closed.'));
|
|
130
|
+
if (this.childClosed || this.child.exitCode != null || this.child.signalCode != null) return this.closing = Promise.resolve();
|
|
131
|
+
this.closing = new Promise(resolve => {
|
|
132
|
+
const finished = () => {
|
|
133
|
+
clearTimeout(force); clearTimeout(limit);
|
|
134
|
+
this.child.removeListener('close', finished);
|
|
135
|
+
resolve();
|
|
136
|
+
};
|
|
137
|
+
// Subscribe before ending/killing: a fast or fake child can close during
|
|
138
|
+
// kill itself. One close owns both timers and drains rejected RPCs.
|
|
139
|
+
this.child.once('close', finished);
|
|
140
|
+
const force = setTimeout(() => { this.child.kill('SIGKILL'); }, 2_000);
|
|
141
|
+
const limit = setTimeout(finished, 3_000);
|
|
142
|
+
this.child.stdin.end();
|
|
143
|
+
this.child.kill('SIGTERM');
|
|
126
144
|
});
|
|
145
|
+
return this.closing;
|
|
127
146
|
}
|
|
128
147
|
}
|
|
129
148
|
|
|
@@ -134,9 +153,9 @@ export function supportsCodexWriterLock(version) {
|
|
|
134
153
|
return parts[0] > 0 || parts[1] > 158 || parts[1] === 158 && parts[2] >= 0;
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
export async function openCodexAppServer({ inspectExecutable = inspectBackendExecutable, spawnImpl = spawn, signal, maxLineBytes, omitInlineAttachments = false } = {}) {
|
|
156
|
+
export async function openCodexAppServer({ inspectExecutable = inspectBackendExecutable, spawnImpl = spawn, signal, maxLineBytes, omitInlineAttachments = false, cancelOnAbort = false } = {}) {
|
|
138
157
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Codex app-server was cancelled.');
|
|
139
|
-
const inspection = await inspectExecutable('codex');
|
|
158
|
+
const inspection = await inspectExecutable('codex', cancelOnAbort ? { signal } : undefined);
|
|
140
159
|
if (!inspection?.available || !inspection.executable) throw new CodexAppServerError('unavailable', 'Codex executable is unavailable.');
|
|
141
160
|
if (!supportsCodexWriterLock(inspection.installed_version)) throw new CodexAppServerError('unsupported', 'Codex version does not have a verified app-server writer lock.');
|
|
142
161
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Codex app-server was cancelled.');
|
|
@@ -144,12 +163,13 @@ export async function openCodexAppServer({ inspectExecutable = inspectBackendExe
|
|
|
144
163
|
// restore a temporary read-only override. The caller's signal is checked
|
|
145
164
|
// before spawn and by the turn watcher below.
|
|
146
165
|
const child = spawnImpl(inspection.executable, ['app-server', '--stdio'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
147
|
-
const client = new CodexAppServerClient(child, { maxLineBytes, omitInlineAttachments });
|
|
166
|
+
const client = new CodexAppServerClient(child, { maxLineBytes, omitInlineAttachments, signal: cancelOnAbort ? signal : undefined });
|
|
148
167
|
try {
|
|
149
168
|
await client.request('initialize', { clientInfo: { name: 'simy_session_supervision', title: 'SIMY Session Supervision', version: '1.0.0' } });
|
|
169
|
+
if (cancelOnAbort && signal?.aborted) throw new CodexAppServerError('cancelled', 'Codex app-server was cancelled.');
|
|
150
170
|
client.notify('initialized');
|
|
151
171
|
return client;
|
|
152
|
-
} catch (error) { await client.close(); throw error; }
|
|
172
|
+
} catch (error) { await client.close(); if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Codex app-server was cancelled.'); throw error; }
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
export function codexThreadMetadata(thread) {
|
|
@@ -266,10 +286,11 @@ export async function readCodexSession(client, session) {
|
|
|
266
286
|
|
|
267
287
|
export async function readCodexConversation(candidate, { openClient = openCodexAppServer, signal } = {}) {
|
|
268
288
|
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation analysis was cancelled.');
|
|
269
|
-
const client = await openClient({ signal, maxLineBytes: ANALYSIS_MAX_LINE, omitInlineAttachments: true });
|
|
289
|
+
const client = await openClient({ signal, maxLineBytes: ANALYSIS_MAX_LINE, omitInlineAttachments: true, cancelOnAbort: true });
|
|
270
290
|
const cancelled = () => { void client.close(); };
|
|
271
291
|
signal?.addEventListener('abort', cancelled, { once: true });
|
|
272
292
|
try {
|
|
293
|
+
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation analysis was cancelled.');
|
|
273
294
|
// Full thread history can contain tens of MiB of tool output. Analysis
|
|
274
295
|
// reads only metadata and a fixed number of recent display summaries.
|
|
275
296
|
const metadata = await readCodexSession(client, candidate.session);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { TERMINAL as TERMINAL_RUNS, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
-
import { listCodexSessions, openCodexAppServer, readCodexSession } from './codex-app-server.js';
|
|
4
|
+
import { listCodexSessions, openCodexAppServer, readCodexSession, 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
|
|
|
@@ -32,9 +32,15 @@ function initialDiagnosisPlan(objective, contextBootstrap = false) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function withClient(openClient, work) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
async function withClient(openClient, work, signal) {
|
|
36
|
+
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.');
|
|
37
|
+
const client = await openClient(signal ? { signal, cancelOnAbort: true } : undefined);
|
|
38
|
+
const cancelled = () => { void client.close(); };
|
|
39
|
+
signal?.addEventListener('abort', cancelled, { once: true });
|
|
40
|
+
try {
|
|
41
|
+
if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.');
|
|
42
|
+
return await work(client);
|
|
43
|
+
} finally { signal?.removeEventListener('abort', cancelled); await client.close(); }
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
async function liveClaudeHook(record, metadata, inspectProcess) {
|
|
@@ -49,14 +55,19 @@ async function liveClaudeHook(record, metadata, inspectProcess) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export async function discoverSessions({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
52
|
-
inspectProcess = processIdentity, now = Date.now() } = {}) {
|
|
58
|
+
inspectProcess = processIdentity, now = Date.now(), signal } = {}) {
|
|
59
|
+
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
60
|
+
assertActive();
|
|
53
61
|
const records = await listRecords(root);
|
|
54
62
|
let discovered = [], codexError = null, claude = [], claudeError = null;
|
|
55
63
|
try { discovered = await withClient(openClient, client => listCodexSessions({ client, inspectState: true,
|
|
56
|
-
prioritySessions: records.filter(record => record.provider === 'codex' && (record.autoContinue || record.monitor || record.tracked || record.run)).map(record => record.session) })); }
|
|
64
|
+
prioritySessions: records.filter(record => record.provider === 'codex' && (record.autoContinue || record.monitor || record.tracked || record.run)).map(record => record.session) }), signal); }
|
|
57
65
|
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.'; }
|
|
58
|
-
|
|
66
|
+
assertActive();
|
|
67
|
+
try { claude = await listClaude({ signal }); } catch { claudeError = 'Claude conversation discovery unavailable.'; }
|
|
68
|
+
assertActive();
|
|
59
69
|
const byKey = new Map();
|
|
70
|
+
const recordsByKey = new Map(records.map(record => [`${record.provider}:${record.session}`, record]));
|
|
60
71
|
for (const item of [...discovered, ...claude]) byKey.set(`${item.provider}:${item.session}`, item);
|
|
61
72
|
for (const record of records) {
|
|
62
73
|
const key = `${record.provider}:${record.session}`;
|
|
@@ -67,8 +78,12 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
67
78
|
availability: 'unknown', model: record.admission?.model || null,
|
|
68
79
|
});
|
|
69
80
|
}
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
// Historical sessions must not fan out hundreds of process-inspection
|
|
82
|
+
// subprocesses. Preserve input ordering while limiting live metadata probes.
|
|
83
|
+
const items = [...byKey.values()], projected = new Array(items.length);
|
|
84
|
+
let nextItem = 0;
|
|
85
|
+
const project = async item => {
|
|
86
|
+
const record = recordsByKey.get(`${item.provider}:${item.session}`);
|
|
72
87
|
const run = record?.run;
|
|
73
88
|
const mode = run?.managed ? run.state === 'recovery_required' && run.recoveryCause === 'session_supervision' ? 'pending_takeover' : 'managed' : run ? 'standard_loop' : record?.monitor ? 'monitor' : 'unmanaged';
|
|
74
89
|
const supported = item.provider === 'codex' && item.observation.source === 'codex_app_server' && Boolean(item.model) && !codexError;
|
|
@@ -104,7 +119,16 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
104
119
|
'Claude enrollment needs a live matching SIMY SessionStart hook and provider process; cold CLI takeover is unavailable.' :
|
|
105
120
|
codexError || (!supported ? 'Exact model or provider metadata is unavailable.' : null),
|
|
106
121
|
};
|
|
107
|
-
}
|
|
122
|
+
};
|
|
123
|
+
await Promise.all(Array.from({ length: Math.min(8, items.length) }, async () => {
|
|
124
|
+
while (nextItem < items.length) {
|
|
125
|
+
assertActive();
|
|
126
|
+
const index = nextItem++;
|
|
127
|
+
projected[index] = await project(items[index]);
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
assertActive();
|
|
131
|
+
projected.sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0));
|
|
108
132
|
// Keep existing bindings visible during a truncated discovery or provider
|
|
109
133
|
// outage, even when newer unbound conversations fill the candidate window.
|
|
110
134
|
const bound = projected.filter(item => item.autoContinue || item.binding.mode !== 'unmanaged');
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -14,6 +14,8 @@ const DEFAULT_LEASE_SECONDS = 300;
|
|
|
14
14
|
// Claim can retrieve and freeze organization-scoped Slack evidence (45s budget).
|
|
15
15
|
const DEFAULT_REQUEST_TIMEOUT_MS = 75_000;
|
|
16
16
|
const DEFAULT_EXECUTION_TIMEOUT_MS = 240_000;
|
|
17
|
+
// Pin SQM independently of the user's interactive model and CLI defaults.
|
|
18
|
+
export const SQM_CODEX_MODEL = "gpt-6.1-sol";
|
|
17
19
|
|
|
18
20
|
export class SqmLocalReviewError extends Error {
|
|
19
21
|
constructor(message, { status = null, code = "sqm_local_review_error" } = {}) {
|
|
@@ -244,6 +246,8 @@ export async function runStructuredProviderReview(
|
|
|
244
246
|
...(readOnlyAnalysis ? ["--system-prompt", request.system, "--max-turns", "1"] : []),
|
|
245
247
|
] : [
|
|
246
248
|
"exec",
|
|
249
|
+
"--model",
|
|
250
|
+
SQM_CODEX_MODEL,
|
|
247
251
|
"--skip-git-repo-check",
|
|
248
252
|
"--sandbox",
|
|
249
253
|
"read-only",
|
|
@@ -276,7 +280,7 @@ export async function runStructuredProviderReview(
|
|
|
276
280
|
}
|
|
277
281
|
return {
|
|
278
282
|
output,
|
|
279
|
-
model: `${provider}-local/${inspection.installed_version || "unknown"}`,
|
|
283
|
+
model: provider === "codex" ? SQM_CODEX_MODEL : `${provider}-local/${inspection.installed_version || "unknown"}`,
|
|
280
284
|
};
|
|
281
285
|
} catch (error) {
|
|
282
286
|
if (error instanceof SqmLocalReviewError || error?.code === 'desktop_access_required') throw error;
|