@awak-app/simy-cli 0.7.32 → 0.7.34
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 +22 -3
- package/package.json +3 -2
- package/src/auto-update.js +10 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +44 -12
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +13 -4
- package/src/delivery-hooks/desktop.js +1 -1
- package/src/desktop-access.js +1 -1
- package/src/desktop-frame-writer.js +29 -0
- package/src/desktop-host.js +252 -77
- package/src/orchestrator/loop.js +2 -0
- package/src/session-supervision/analysis-manager.js +83 -18
- package/src/session-supervision/claude-desktop-lineage.js +1 -1
- package/src/session-supervision/claude-sessions.js +16 -4
- package/src/session-supervision/codex-app-server.js +45 -30
- package/src/session-supervision/continuation-readiness.js +58 -0
- package/src/session-supervision/decision-presentation.js +11 -3
- package/src/session-supervision/index.js +126 -28
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +79 -0
- package/src/session-supervision/remote-entry.js +7 -4
- package/src/session-supervision/remote-host-recovery.js +21 -0
- package/src/session-supervision/remote-hosts.js +28 -8
- package/src/session-supervision/remote-owner-entry.js +20 -0
- package/src/session-supervision/remote-service.js +36 -0
- package/src/session-supervision/statistics.js +19 -3
- package/src/session-supervision/work-state.js +7 -1
- package/src/sqm/local-tests.js +2 -1
- package/src/update-connection.js +16 -0
|
@@ -7,6 +7,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { compareSessionWork } from './work-state.js';
|
|
9
9
|
import { manualAcceptanceCommand, matchesManualAcceptanceControls, manualAcceptanceContinuation } from './manual-acceptance-command.js';
|
|
10
|
+
import { remoteHostRecovery } from './remote-host-recovery.js';
|
|
10
11
|
|
|
11
12
|
const MAX_FRAME = 8 * 1024 * 1024;
|
|
12
13
|
const MAX_HOSTS = 8;
|
|
@@ -83,9 +84,10 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
83
84
|
let preferenceScope = null;
|
|
84
85
|
let scope = null, local = null, closed = false, settingsError = null, writes = Promise.resolve(), generation = 0;
|
|
85
86
|
const changed = () => { if (!closed) onChange(); };
|
|
86
|
-
function disconnect(connection, reason = 'Remote connection closed.') {
|
|
87
|
+
function disconnect(connection, reason = 'Remote connection closed.', errorCode = 'transport_unavailable') {
|
|
87
88
|
const child = connection.child;
|
|
88
89
|
connection.child = null; connection.ready = false; connection.reason = reason; connection.state = 'unavailable'; connection.retryAt = now() + retryMs;
|
|
90
|
+
connection.errorCode = errorCode;
|
|
89
91
|
clearTimeout(connection.handshake);
|
|
90
92
|
for (const job of connection.pending.values()) { clearTimeout(job.timer); job.reject(error('Remote operation result is unknown. Check the original conversation before retrying.', 'remote_operation_unknown')); }
|
|
91
93
|
connection.pending.clear();
|
|
@@ -106,7 +108,8 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
106
108
|
const id = `${connection.id}:${frame.id}`;
|
|
107
109
|
return preferences.change(expectedScope, state => {
|
|
108
110
|
if (state.events[id] || closed || preferenceScope !== expectedScope || !connection.ready) return;
|
|
109
|
-
onNotify({ ...stamp(event, connection), hostId: connection.id, id
|
|
111
|
+
onNotify({ ...stamp(event, connection), hostId: connection.id, id,
|
|
112
|
+
...(event.continuationWaitId ? { sessionIdentity: `${event.provider}|${event.session}|${connection.id}` } : {}) });
|
|
110
113
|
state.events[id] = now();
|
|
111
114
|
});
|
|
112
115
|
}
|
|
@@ -139,7 +142,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
139
142
|
}
|
|
140
143
|
function connect(connection) {
|
|
141
144
|
if (closed || connection.enabled === false || connection.child || now() < connection.retryAt) return;
|
|
142
|
-
connection.state = 'connecting'; connection.reason = null;
|
|
145
|
+
connection.state = 'connecting'; connection.reason = null; connection.errorCode = null;
|
|
143
146
|
let child;
|
|
144
147
|
try { child = spawnImpl('ssh', remoteSshArgs(connection.alias, REMOTE_HOST_COMMAND), { stdio: ['pipe', 'pipe', 'pipe'], shell: false }); }
|
|
145
148
|
catch { disconnect(connection, 'Could not start SSH.'); return; }
|
|
@@ -175,10 +178,11 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
175
178
|
connection.capabilities = Array.isArray(status?.remoteControl?.actions)
|
|
176
179
|
? status.remoteControl.actions.filter(action => typeof action === 'string' && allowedActions.has(action))
|
|
177
180
|
: status?.remoteControl === undefined && status?.remoteSessionProtocol === 1 ? [...legacyActions] : [];
|
|
178
|
-
if (status?.remoteSessionProtocol !== 1) { disconnect(connection, 'Update SIMY on the remote host to use remote sessions.'); changed(); return; }
|
|
181
|
+
if (status?.remoteSessionProtocol !== 1) { disconnect(connection, 'Update SIMY on the remote host to use remote sessions.', 'update_required'); changed(); return; }
|
|
179
182
|
if (!usable(status) || status.auth.userId !== local?.auth?.userId || status.access.organizationId !== local?.access?.organizationId) {
|
|
180
183
|
clearTimeout(connection.handshake);
|
|
181
184
|
connection.ready = false; connection.state = 'access_required'; connection.reason = 'Use Remote connections in this CLI to sign in to this host.';
|
|
185
|
+
connection.errorCode = status?.auth?.state !== 'signed_in' ? 'simy_auth_required' : 'access_required';
|
|
182
186
|
connection.status = null;
|
|
183
187
|
for (const [id, job] of connection.pending) if (!job.setup) { clearTimeout(job.timer); job.reject(error(connection.reason, 'remote_operation_unknown')); connection.pending.delete(id); }
|
|
184
188
|
} else if (!Array.isArray(status.sessionSupervision?.sessions) || !Array.isArray(status.deliveryHooks?.runs)) {
|
|
@@ -186,6 +190,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
186
190
|
} else {
|
|
187
191
|
clearTimeout(connection.handshake);
|
|
188
192
|
connection.status = status; connection.ready = true; connection.state = 'ready'; connection.reason = null; connection.receivedAt = now();
|
|
193
|
+
connection.errorCode = null;
|
|
189
194
|
if (status.controllerEventsVersion === 1) void pullEvents(connection).catch(() => {});
|
|
190
195
|
else for (const event of status.controllerEvents || []) void receiveNotification(connection, event)?.catch(() => {});
|
|
191
196
|
const expectedScope = preferenceScope;
|
|
@@ -210,7 +215,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
210
215
|
} else if (frame.type === 'notify' && connection.ready && local && typeof frame.id === 'string' && ['finished','failed','paused','needs_decision'].includes(frame.kind)) {
|
|
211
216
|
void receiveNotification(connection, frame)?.catch(() => {});
|
|
212
217
|
} else if (frame.type === 'error') {
|
|
213
|
-
disconnect(connection, remoteOperationFailure(frame.code).message); changed(); return;
|
|
218
|
+
disconnect(connection, remoteOperationFailure(frame.code).message, frame.code === 'remote_owner_origin_mismatch' ? 'access_required' : 'transport_unavailable'); changed(); return;
|
|
214
219
|
} else if (frame.type === 'task_detail') {
|
|
215
220
|
const pending = connection.pending.get(frame.id);
|
|
216
221
|
pending?.onDetail?.({ ...frame, id: pending.id, taskKey: pending.taskKey });
|
|
@@ -252,7 +257,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
252
257
|
c.label = host.label; c.enabled = host.enabled !== false;
|
|
253
258
|
if (!c.enabled) {
|
|
254
259
|
if (c.child) disconnect(c);
|
|
255
|
-
c.ready = false; c.status = null; c.state = 'disconnected'; c.reason = null;
|
|
260
|
+
c.ready = false; c.status = null; c.state = 'disconnected'; c.reason = null; c.errorCode = null;
|
|
256
261
|
continue;
|
|
257
262
|
}
|
|
258
263
|
if (c.state === 'disconnected') c.retryAt = 0;
|
|
@@ -339,8 +344,22 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
339
344
|
if (!c) return;
|
|
340
345
|
c.enabled = enabled;
|
|
341
346
|
if (!enabled) { disconnect(c); c.status = null; c.state = 'disconnected'; c.reason = null; }
|
|
342
|
-
else {
|
|
347
|
+
else {
|
|
348
|
+
// Explicit Connect also retries a stale or unauthorized live transport.
|
|
349
|
+
// Resetting backoff alone cannot replace a child that is still present.
|
|
350
|
+
// Keep healthy connections and an in-flight handshake intact.
|
|
351
|
+
if (c.child && c.state !== 'connecting' && (!c.ready || now() - c.receivedAt > 45_000)) disconnect(c);
|
|
352
|
+
c.retryAt = 0;
|
|
353
|
+
if (!c.child) c.state = 'connecting';
|
|
354
|
+
}
|
|
343
355
|
});
|
|
356
|
+
const c = connections.get(host.id);
|
|
357
|
+
if (enabled && c?.child && c.capabilities?.includes('refresh')) {
|
|
358
|
+
// Explicit Connect/Check again must request a new host observation even
|
|
359
|
+
// when SSH is healthy. Keep its owner and in-flight mutation receipts:
|
|
360
|
+
// refreshing providers is read-only and must never replay execution.
|
|
361
|
+
c.child.stdin.write(JSON.stringify({ id: randomUUID(), action: 'refresh' }) + '\n');
|
|
362
|
+
}
|
|
344
363
|
}
|
|
345
364
|
async function forward(command, status) {
|
|
346
365
|
if (closed || !usable(status) || scopeOf(status) !== scope) throw error('SIMY account changed. Refresh before using the remote host.', 'remote_host_access');
|
|
@@ -478,7 +497,8 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
478
497
|
const remoteActiveCount = hosts.filter(c => c.ready).reduce((n,c) => n + (c.status?.[key]?.activeCount || 0), 0);
|
|
479
498
|
return { ...status[key], items: [...localItems, ...rows], localActiveCount: Math.max(0, (status[key]?.activeCount || 0) - priorRemote), activeCount: Math.max(0, (status[key]?.activeCount || 0) - priorRemote) + remoteActiveCount, remoteActiveCount, unknownHosts: hosts.filter(c => !c.ready).map(c => c.id) };
|
|
480
499
|
}
|
|
481
|
-
return { ...status, remoteHosts: { error: settingsError, items: hosts.map(c => ({ id: c.id, alias: c.alias, label: c.label, enabled: c.enabled !== false, state: c.ready && now() - c.receivedAt > 45_000 ? 'unavailable' : c.state, transportId: c.ready ? c.transportId : null, reason: c.reason
|
|
500
|
+
return { ...status, remoteHosts: { error: settingsError, items: hosts.map(c => ({ id: c.id, alias: c.alias, label: c.label, enabled: c.enabled !== false, state: c.ready && now() - c.receivedAt > 45_000 ? 'unavailable' : c.state, transportId: c.ready ? c.transportId : null, reason: c.reason,
|
|
501
|
+
recovery: remoteHostRecovery({ state: c.state, enabled: c.enabled, status: c.status, ready: c.ready, fresh: now() - c.receivedAt <= 45_000, errorCode: c.errorCode }) })) },
|
|
482
502
|
tasks: aggregate('tasks', tasks), sqmTasks: aggregate('sqmTasks', sqmTasks),
|
|
483
503
|
sessionSupervision: { ...status.sessionSupervision, selectionCandidates: [...(status.sessionSupervision?.selectionCandidates || []).filter(row => !row.hostId || row.hostId === 'local'), ...selectionCandidates], sessions: [...(status.sessionSupervision?.sessions || []).filter(row => !row.hostId || row.hostId === 'local'), ...sessions].sort(compareSessionWork) },
|
|
484
504
|
deliveryHooks: { ...status.deliveryHooks, runs: [...(status.deliveryHooks?.runs || []).filter(row => !row.hostId || row.hostId === 'local'), ...runs] } };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Separate from the SSH entry so reconnects only attach and never replace work.
|
|
2
|
+
import { PassThrough, Writable } from 'node:stream';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { adoptInstalledRuntime, pinnedWorkflowSource } from '../delivery-hooks/local-runtime.js';
|
|
7
|
+
|
|
8
|
+
process.env.SIMY_REMOTE_CHILD = '1';
|
|
9
|
+
process.env.SIMY_AUTO_UPDATE = '0';
|
|
10
|
+
const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
|
|
11
|
+
try {
|
|
12
|
+
await adoptInstalledRuntime({ root });
|
|
13
|
+
const selected = await pinnedWorkflowSource({ root });
|
|
14
|
+
const host = await import(selected ? pathToFileURL(join(selected, 'src/desktop-host.js')).href : '../desktop-host.js');
|
|
15
|
+
if (host.remoteSessionProtocol !== 1) throw new Error('Update the remote SIMY runtime.');
|
|
16
|
+
host.startDesktopEntry({ input: new PassThrough(), output: new Writable({ write(_chunk, _encoding, done) { done(); } }) });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
process.stderr.write(`Remote SIMY owner could not start: ${error.message}\n`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, open } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { relayToOwner } from './owner-transport.js';
|
|
7
|
+
|
|
8
|
+
// The SSH viewer is disposable; the ordinary locked desktop owner is not.
|
|
9
|
+
// Startup contenders use the existing desktop lock, never remove it or stop an
|
|
10
|
+
// owner. A lost viewer must not turn into a repeated execution command.
|
|
11
|
+
export async function attachRemoteService({ root, webOrigin, input = process.stdin, output = process.stdout,
|
|
12
|
+
relay = relayToOwner, spawnImpl = spawn, timeoutMs = 30_000, retryMs = 200 } = {}) {
|
|
13
|
+
if (await relay({ root, webOrigin, input, output })) return;
|
|
14
|
+
await mkdir(join(root, 'logs'), { recursive: true, mode: 0o700 });
|
|
15
|
+
const log = await open(join(root, 'logs', 'remote-service.log'),
|
|
16
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW || 0), 0o600);
|
|
17
|
+
try {
|
|
18
|
+
const env = { ...process.env, SIMY_HOME: root, SIMY_WEB_ORIGIN: webOrigin,
|
|
19
|
+
SIMY_REMOTE_CHILD: '1', SIMY_AUTO_UPDATE: '0' };
|
|
20
|
+
// A viewer cannot hand its update-candidate exemption to a new owner.
|
|
21
|
+
delete env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
|
|
22
|
+
delete env.SIMY_UPDATE_TRANSFER_TOKEN;
|
|
23
|
+
const child = spawnImpl(process.execPath, [fileURLToPath(new URL('./remote-owner-entry.js', import.meta.url))], {
|
|
24
|
+
detached: true, stdio: ['ignore', log.fd, log.fd],
|
|
25
|
+
env,
|
|
26
|
+
});
|
|
27
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
28
|
+
child.unref();
|
|
29
|
+
} finally { await log.close(); }
|
|
30
|
+
const until = Date.now() + timeoutMs;
|
|
31
|
+
while (Date.now() < until) {
|
|
32
|
+
if (await relay({ root, webOrigin, input, output })) return;
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, retryMs));
|
|
34
|
+
}
|
|
35
|
+
throw Object.assign(new Error('Remote SIMY did not start. Inspect logs/remote-service.log; existing work was retained.'), { code: 'remote_owner_unavailable' });
|
|
36
|
+
}
|
|
@@ -10,6 +10,8 @@ export const unavailableStatistics = () => ({ state: 'unavailable', hours: [], a
|
|
|
10
10
|
const READ_BYTES = 1024 * 1024;
|
|
11
11
|
const MAX_LINE_BYTES = 4 * READ_BYTES;
|
|
12
12
|
const WINDOW_MS = 7 * 24 * HOUR;
|
|
13
|
+
const CODEX_WORK_EVENTS = new Set(['task_started', 'task_complete', 'task_aborted', 'turn_aborted',
|
|
14
|
+
'item_completed', 'user_message', 'agent_message', 'agent_reasoning', 'agent_reasoning_raw_content']);
|
|
13
15
|
// One selected transcript only; no raw messages or on-disk cache. Identity and
|
|
14
16
|
// ctime prevent reuse after replacement/truncation even when size is unchanged.
|
|
15
17
|
let cachedTranscript;
|
|
@@ -54,6 +56,15 @@ class StatisticsAccumulator {
|
|
|
54
56
|
const key = `codex:${at}:${total}`;
|
|
55
57
|
if (delta > 0 || !this.usage.has(key)) this.usage.set(key, { at, tokens: delta });
|
|
56
58
|
}
|
|
59
|
+
addOversized(prefix) {
|
|
60
|
+
// Only the canonical Codex response envelope is safe to dismiss here:
|
|
61
|
+
// response items contain activity, not cumulative usage or session identity.
|
|
62
|
+
// Keep unknown headers and oversized counters/metadata incomplete.
|
|
63
|
+
const header = /^\s*\{\s*"timestamp"\s*:\s*"([^"\\]+)"\s*,\s*(?:"ordinal"\s*:\s*\d+\s*,\s*)?"type"\s*:\s*"response_item"\s*,/.exec(prefix.subarray(0, 1024).toString('utf8'));
|
|
64
|
+
const at = header ? Date.parse(header[1]) : NaN;
|
|
65
|
+
if (this.provider === 'codex' && Number.isFinite(at) && at < this.scanNow - WINDOW_MS) return;
|
|
66
|
+
this.missing = true;
|
|
67
|
+
}
|
|
57
68
|
add(line) {
|
|
58
69
|
if (!line.trim()) return;
|
|
59
70
|
let row;
|
|
@@ -77,7 +88,7 @@ class StatisticsAccumulator {
|
|
|
77
88
|
if (!Number.isFinite(at)) return;
|
|
78
89
|
const inWindow = at >= this.scanNow - WINDOW_MS;
|
|
79
90
|
const active = this.provider === 'claude' ? ['user', 'assistant'].includes(row.type) && !row.isMeta
|
|
80
|
-
:
|
|
91
|
+
: row.type === 'response_item' || row.type === 'event_msg' && CODEX_WORK_EVENTS.has(row.payload?.type);
|
|
81
92
|
if (active) {
|
|
82
93
|
this.earliest = Math.min(this.earliest, at);
|
|
83
94
|
if (inWindow) this.events.push(at);
|
|
@@ -189,7 +200,7 @@ async function readReverse(handle, size, accumulator, signal) {
|
|
|
189
200
|
for (let index = data.lastIndexOf(10); index >= 0; index = data.lastIndexOf(10, index - 1)) {
|
|
190
201
|
const line = data.subarray(index + 1, end);
|
|
191
202
|
if (!skipping && line.length <= MAX_LINE_BYTES) accumulator.add(line.toString('utf8'));
|
|
192
|
-
else accumulator.
|
|
203
|
+
else accumulator.addOversized(line);
|
|
193
204
|
skipping = false; end = index;
|
|
194
205
|
// Disk I/O alone does not yield while parsing a dense page. Keep status,
|
|
195
206
|
// selection and cancellation responsive even for huge cached transcripts.
|
|
@@ -200,10 +211,15 @@ async function readReverse(handle, size, accumulator, signal) {
|
|
|
200
211
|
}
|
|
201
212
|
if (index === 0) break;
|
|
202
213
|
}
|
|
203
|
-
if (end > MAX_LINE_BYTES || skipping) {
|
|
214
|
+
if (end > MAX_LINE_BYTES || skipping) {
|
|
215
|
+
// Retain a bounded prefix until the start of this line is reached. The
|
|
216
|
+
// prefix may straddle a page, so keep the adjacent bytes when skipping.
|
|
217
|
+
carry = Buffer.from(data.subarray(0, Math.min(end, 1024))); skipping = true;
|
|
218
|
+
}
|
|
204
219
|
else carry = Buffer.from(data.subarray(0, end));
|
|
205
220
|
}
|
|
206
221
|
if (!skipping) accumulator.add(carry.toString('utf8'));
|
|
222
|
+
else accumulator.addOversized(carry);
|
|
207
223
|
return bytesRead;
|
|
208
224
|
}
|
|
209
225
|
|
|
@@ -85,7 +85,13 @@ export function sessionWorkState(candidate, run, { now = Date.now(), provider }
|
|
|
85
85
|
const phase = run?.phase || run?.managed?.phase;
|
|
86
86
|
const stopped = run?.state === 'stopped' || run?.stopReason?.actor === 'user' ||
|
|
87
87
|
run?.state === 'paused' && /^(Paused by user\.|Automatic continuation paused by user\.|User interrupted the provider\.|Stopped by user\.)$/.test(run.reason || '');
|
|
88
|
-
if (stopped)
|
|
88
|
+
if (stopped) {
|
|
89
|
+
if (run?.reason === 'User interrupted the provider.' && run?.stopReason?.actor !== 'user')
|
|
90
|
+
return result('stopped', '中断を検出', '元のAIから中断が通知されました。本人による停止操作かは確認できません。', 'resume', true);
|
|
91
|
+
if (run?.stopReason?.actor === 'user' || /^(Stopped by user\.|Paused by user\.|Automatic continuation paused by user\.)$/.test(run?.reason || ''))
|
|
92
|
+
return result('stopped', '自分で停止', '停止の指示を維持しています。', 'resume');
|
|
93
|
+
return result('stopped', '停止中', '停止理由を確認してから再開してください。', 'resume', true);
|
|
94
|
+
}
|
|
89
95
|
if (approval && !approval.denied || candidate.decisionRequest?.kind === 'approval')
|
|
90
96
|
return result('approval', '承認待ち', candidate.decisionRequest?.context || '実行する操作の確認が必要です。', 'review_approval', true);
|
|
91
97
|
if (question || phase === 'needs_clarification' || candidate.analysis?.state === 'needs_user' || candidate.decisionRequest?.kind === 'question')
|
package/src/sqm/local-tests.js
CHANGED
|
@@ -16,7 +16,8 @@ const profiles = {
|
|
|
16
16
|
// finite catalog-owned deadline while allowing slow local process I/O.
|
|
17
17
|
{ id: 'repair-and-stop-boundaries', timeoutMs: 1_200_000, files: ['test/purpose-search.test.js', 'test/delivery-explicit-resume.test.js', 'test/purpose-continuation.test.js'] },
|
|
18
18
|
{ id: 'sqm-runner', files: ['test/sqm-local-tests.test.js'] },
|
|
19
|
-
{ id: 'provider-startup', files: ['test/codex-app-server-discovery.test.js', 'test/session-supervision.test.js'
|
|
19
|
+
{ id: 'provider-startup', files: ['test/codex-app-server-discovery.test.js', 'test/session-supervision.test.js',
|
|
20
|
+
'test/progressive-discovery.test.js', 'test/desktop-record-index.test.js', 'test/desktop-background-performance.test.js'] },
|
|
20
21
|
{ id: 'handoff-and-audit-recovery', files: ['test/consistency-handoff.test.js', 'test/consistency-monitor.test.js', 'test/consistency-auditor.test.js', 'test/desktop-host.test.js'] },
|
|
21
22
|
{ id: 'migration-and-current-outcomes', files: ['test/autonomous-upgrade.test.js', 'test/local-delivery-runtime.test.js', 'test/delivery-migration-crash.test.js', 'test/purpose-inspection.test.js', 'test/purpose-search-scenarios.test.js', 'test/sqm-large-diff.test.js'] },
|
|
22
23
|
],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizeWebOrigin } from './web-origin.js';
|
|
3
|
+
|
|
4
|
+
// Pin the resolved settings directory and origin, not a parent shell's defaults.
|
|
5
|
+
// Session/remote-host files stay in that directory across runtime replacements.
|
|
6
|
+
export function updateConnectionEnvironment({ env = process.env, root, webOrigin }) {
|
|
7
|
+
if (typeof root !== 'string' || !root.trim()) throw new Error('SIMY state directory is required for an update.');
|
|
8
|
+
return { ...env, SIMY_HOME: path.resolve(root), SIMY_WEB_ORIGIN: normalizeWebOrigin(webOrigin) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sameUpdateIdentity(expected, actual) {
|
|
12
|
+
if (expected?.auth?.state !== 'signed_in') return actual?.auth?.state !== 'signed_in';
|
|
13
|
+
return actual?.auth?.state === 'signed_in' && Boolean(expected.auth.userId) &&
|
|
14
|
+
actual.auth.userId === expected.auth.userId &&
|
|
15
|
+
(expected.access?.organizationId || null) === (actual.access?.organizationId || null);
|
|
16
|
+
}
|