@awak-app/simy-cli 0.7.32 → 0.7.33
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 +27 -3
- 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 +176 -74
- 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 +39 -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 +101 -24
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +66 -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/update-connection.js +16 -0
|
@@ -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')
|
|
@@ -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
|
+
}
|