@awak-app/simy-cli 0.7.31 → 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.
Files changed (32) hide show
  1. package/README.md +22 -3
  2. package/package.json +3 -2
  3. package/src/agent.js +7 -2
  4. package/src/auto-update.js +10 -1
  5. package/src/cli-contract.js +1 -1
  6. package/src/cli-observability.js +1 -1
  7. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +27 -3
  8. package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +13 -4
  9. package/src/delivery-hooks/desktop.js +1 -1
  10. package/src/desktop-access.js +1 -1
  11. package/src/desktop-frame-writer.js +29 -0
  12. package/src/desktop-host.js +176 -74
  13. package/src/orchestrator/loop.js +2 -0
  14. package/src/session-supervision/analysis-manager.js +83 -18
  15. package/src/session-supervision/claude-desktop-lineage.js +1 -1
  16. package/src/session-supervision/claude-sessions.js +16 -4
  17. package/src/session-supervision/codex-app-server.js +39 -30
  18. package/src/session-supervision/continuation-readiness.js +58 -0
  19. package/src/session-supervision/decision-presentation.js +11 -3
  20. package/src/session-supervision/index.js +101 -24
  21. package/src/session-supervision/owner-transport.js +23 -8
  22. package/src/session-supervision/progressive-discovery.js +66 -0
  23. package/src/session-supervision/remote-entry.js +7 -4
  24. package/src/session-supervision/remote-host-recovery.js +21 -0
  25. package/src/session-supervision/remote-hosts.js +28 -8
  26. package/src/session-supervision/remote-owner-entry.js +20 -0
  27. package/src/session-supervision/remote-service.js +36 -0
  28. package/src/session-supervision/statistics.js +19 -3
  29. package/src/session-supervision/work-state.js +7 -1
  30. package/src/team-threads.js +3 -6
  31. package/src/update-connection.js +16 -0
  32. package/src/web-api.js +9 -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
- : ['response_item', 'event_msg'].includes(row.type) && row.payload?.type !== 'token_count';
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.missing = true;
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) { carry = Buffer.alloc(0); skipping = true; accumulator.missing = true; }
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) return result('stopped', '自分で停止', '停止の指示を維持しています。', 'resume');
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')
@@ -1,10 +1,7 @@
1
+ import { publicImageUrl } from "./web-api.js";
1
2
  function profileAvatar(value) {
2
- if (typeof value !== "string" || value.length > 2048) return {};
3
- try {
4
- const url = new URL(value);
5
- if (url.protocol !== "https:" || !url.hostname || url.username || url.password) return {};
6
- return { avatarUrl: url.toString() };
7
- } catch { return {}; }
3
+ const url = publicImageUrl(value);
4
+ return url ? { avatarUrl: url } : {};
8
5
  }
9
6
  import { createHash } from "node:crypto";
10
7
  import { sanitizedText } from "./session-supervision/conversation-context.js";
@@ -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
+ }
package/src/web-api.js CHANGED
@@ -6,6 +6,15 @@ export function webApiHeaders({ token } = {}, extraHeaders = {}) {
6
6
  return headers;
7
7
  }
8
8
 
9
+ // Public profile images never carry the CLI's application credentials.
10
+ export function publicImageUrl(value) {
11
+ if (typeof value !== "string" || value.length > 2048) return null;
12
+ try {
13
+ const url = new URL(value);
14
+ return url.protocol === "https:" && url.hostname && !url.username && !url.password ? url.toString() : null;
15
+ } catch { return null; }
16
+ }
17
+
9
18
  const APP_DEV_ORIGIN = "https://app-dev.simy.one";
10
19
  const APP_DEV_GATEWAY_BASE_URL = "https://app.simy.one/api/local-cli-gateway/";
11
20