@awak-app/simy-cli 0.7.25 → 0.7.27
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 +8 -0
- package/package.json +2 -2
- package/src/agent.js +22 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +262 -0
- package/src/codex-plugins/simy/hooks/consistency-handoff.mjs +45 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +76 -11
- package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +7 -5
- package/src/codex-plugins/simy/hooks/update-dispatch-gate.mjs +39 -1
- package/src/connection-health.js +253 -0
- package/src/connection-presentation.js +85 -0
- package/src/connection-recovery.js +102 -0
- package/src/console/app.js +60 -6
- package/src/console/commands.js +3 -0
- package/src/console/index.js +1 -0
- package/src/delivery-hooks/command.js +8 -0
- package/src/delivery-hooks/legacy-hook-routing.js +24 -0
- package/src/delivery-hooks/supervisor.js +14 -14
- package/src/desktop-host.js +49 -10
- package/src/direct-executor.js +8 -2
- package/src/doctor.js +1 -0
- package/src/index.js +12 -2
- package/src/remote-connection-guide.js +222 -0
- package/src/session-supervision/analysis-manager.js +24 -4
- package/src/session-supervision/automatic-continuation.js +69 -18
- package/src/session-supervision/codex-app-server.js +52 -7
- package/src/session-supervision/codex-peer-opinion.js +27 -5
- package/src/session-supervision/consistency-auditor.js +1 -1
- package/src/session-supervision/continuation-analysis.js +7 -3
- package/src/session-supervision/decision-presentation.js +1 -1
- package/src/session-supervision/index.js +23 -15
- package/src/session-supervision/outcome-plan.js +76 -0
- package/src/session-supervision/purpose-instruction.js +28 -0
- package/src/session-supervision/recovery-frontier.js +14 -4
- package/src/session-supervision/remote-command-queue.js +15 -0
- package/src/session-supervision/remote-control.js +3 -0
- package/src/session-supervision/remote-hosts.js +70 -19
- package/src/session-supervision/remote-managed-delivery.js +94 -0
- package/src/session-supervision/remote-session-list.js +1 -1
- package/src/session-supervision/statistics.js +10 -3
- package/src/session-supervision/stop-notifications.js +26 -10
- package/src/session-supervision/work-state.js +5 -0
- package/src/sqm/claude-plugin.js +8 -1
package/src/console/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Box, Spacer, Text, useApp, useInput, useStdout } from "ink";
|
|
|
8
8
|
import { consoleHelpLines, parseConsoleCommand } from "./commands.js";
|
|
9
9
|
import { summarizeCodingLoopEvent } from "../orchestrator/presentation.js";
|
|
10
10
|
import { isLocalRepositoryApprovalPending } from "../runner.js";
|
|
11
|
+
import { connectionStartupHint, connectionGuideLines } from '../remote-connection-guide.js';
|
|
11
12
|
|
|
12
13
|
const TERMINAL_STATES = new Set([
|
|
13
14
|
"merge_ready",
|
|
@@ -19,7 +20,7 @@ const TERMINAL_STATES = new Set([
|
|
|
19
20
|
]);
|
|
20
21
|
const NEW_TASK = "__new_task__";
|
|
21
22
|
|
|
22
|
-
export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
23
|
+
export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections = { hosts: [], warnings: [] } }) {
|
|
23
24
|
const { exit } = useApp();
|
|
24
25
|
const { stdout } = useStdout();
|
|
25
26
|
const [terminal, setTerminal] = useState(() => terminalSize(stdout));
|
|
@@ -29,7 +30,10 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
29
30
|
const [inputMode, setInputMode] = useState(() => runs.length === 0);
|
|
30
31
|
const [input, setInput] = useState("");
|
|
31
32
|
const [busy, setBusy] = useState(false);
|
|
32
|
-
const [notice, setNotice] = useState(
|
|
33
|
+
const [notice, setNotice] = useState(() => connectionStartupHint(remoteConnections));
|
|
34
|
+
const [connectionsOpen, setConnectionsOpen] = useState(false);
|
|
35
|
+
const [connectionIndex, setConnectionIndex] = useState(0);
|
|
36
|
+
const [connectionScroll, setConnectionScroll] = useState(0);
|
|
33
37
|
const [showHelp, setShowHelp] = useState(false);
|
|
34
38
|
const [confirmStop, setConfirmStop] = useState(false);
|
|
35
39
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
@@ -71,12 +75,16 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
71
75
|
const hasNextAction = Boolean(
|
|
72
76
|
selectedRun && nextActionForRun(selectedRun, isRepositoryMissing(selectedRun)),
|
|
73
77
|
);
|
|
78
|
+
const effectiveNotice = connectionsOpen ? '' : notice;
|
|
74
79
|
const actionRows =
|
|
75
80
|
1 +
|
|
76
81
|
(hasNextAction ? 1 : 0) +
|
|
77
|
-
(
|
|
82
|
+
(effectiveNotice || busy ? wrapTerminalText(`STATUS ${effectiveNotice || 'Please wait…'}`, terminal.columns).length : 0) +
|
|
78
83
|
(visibleUpdateNotice(cliUpdate) ? 1 : 0);
|
|
79
84
|
const mainHeight = Math.max(8, terminal.rows - 5 - actionRows);
|
|
85
|
+
const connectionRows = remoteConnectionRows(remoteConnections, connectionIndex, terminal.columns);
|
|
86
|
+
const connectionPageSize = Math.max(1, mainHeight - 4);
|
|
87
|
+
const connectionMaxScroll = Math.max(0, connectionRows.length - connectionPageSize);
|
|
80
88
|
const detailInnerWidth = Math.max(20, terminal.columns - 4);
|
|
81
89
|
const detailRows = useMemo(
|
|
82
90
|
() => buildRunDetailRows(selectedRun, detailInnerWidth),
|
|
@@ -117,6 +125,12 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
117
125
|
setShowHelp(true);
|
|
118
126
|
return;
|
|
119
127
|
}
|
|
128
|
+
if (command.type === 'connections') {
|
|
129
|
+
setConnectionsOpen(true);
|
|
130
|
+
setConnectionScroll(0);
|
|
131
|
+
setShowHelp(false);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
120
134
|
if (command.type === "new") {
|
|
121
135
|
setSelectedId(NEW_TASK);
|
|
122
136
|
setDetailOpen(false);
|
|
@@ -336,6 +350,17 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
336
350
|
|
|
337
351
|
const inputHandlerRef = useRef(() => {});
|
|
338
352
|
inputHandlerRef.current = (character, key) => {
|
|
353
|
+
if (connectionsOpen) {
|
|
354
|
+
if (key.escape || (key.ctrl && character === 'c')) {
|
|
355
|
+
setConnectionsOpen(false);
|
|
356
|
+
if (!selectedRun) setInputMode(true);
|
|
357
|
+
} else if (key.upArrow || key.downArrow) {
|
|
358
|
+
setConnectionIndex(current => Math.max(0, Math.min(Math.max(0, remoteConnections.hosts.length - 1), current + (key.upArrow ? -1 : 1))));
|
|
359
|
+
setConnectionScroll(0);
|
|
360
|
+
} else if (key.pageDown) setConnectionScroll(current => Math.min(connectionMaxScroll, current + connectionPageSize));
|
|
361
|
+
else if (key.pageUp) setConnectionScroll(current => Math.max(0, current - connectionPageSize));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
339
364
|
if (scanPrompt) {
|
|
340
365
|
if (key.escape || String(character).toLowerCase() === "n") {
|
|
341
366
|
setScanPrompt(null);
|
|
@@ -505,7 +530,9 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
505
530
|
jsx(Box, {
|
|
506
531
|
marginTop: 1,
|
|
507
532
|
height: mainHeight,
|
|
508
|
-
children:
|
|
533
|
+
children: connectionsOpen
|
|
534
|
+
? jsx(RemoteConnectionsGuide, { rows: connectionRows, scroll: Math.min(connectionScroll, connectionMaxScroll), pageSize: connectionPageSize, width: terminal.columns })
|
|
535
|
+
: showHelp
|
|
509
536
|
? jsx(Help, { width: terminal.columns })
|
|
510
537
|
: scanPrompt
|
|
511
538
|
? jsx(RepositoryScanConsent, {
|
|
@@ -548,10 +575,10 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
548
575
|
inputMode,
|
|
549
576
|
input,
|
|
550
577
|
busy,
|
|
551
|
-
notice,
|
|
578
|
+
notice: effectiveNotice,
|
|
552
579
|
confirmStop,
|
|
553
580
|
repositoryMissing: isRepositoryMissing(selectedRun),
|
|
554
|
-
modalOpen: Boolean(scanPrompt || repositoryPicker),
|
|
581
|
+
modalOpen: Boolean(scanPrompt || repositoryPicker || connectionsOpen),
|
|
555
582
|
cliUpdate,
|
|
556
583
|
width: terminal.columns,
|
|
557
584
|
}),
|
|
@@ -830,6 +857,7 @@ function Help({ width }) {
|
|
|
830
857
|
const groups = [
|
|
831
858
|
["TASK", consoleHelpLines().slice(0, 8)],
|
|
832
859
|
["RUN", consoleHelpLines().slice(8, 15)],
|
|
860
|
+
["CONNECTION", consoleHelpLines().filter(line => line.startsWith('/connections'))],
|
|
833
861
|
];
|
|
834
862
|
return jsxs(Box, {
|
|
835
863
|
width,
|
|
@@ -848,6 +876,32 @@ function Help({ width }) {
|
|
|
848
876
|
});
|
|
849
877
|
}
|
|
850
878
|
|
|
879
|
+
function remoteConnectionRows(discovery, selected, width) {
|
|
880
|
+
const host = discovery.hosts[selected];
|
|
881
|
+
const start = Math.max(0, selected - 2);
|
|
882
|
+
return [
|
|
883
|
+
'保存されているパソコン(まだ接続できるかは未確認)',
|
|
884
|
+
'SIMYがつながらないときは、別のターミナルで simy connections recover を実行してください。',
|
|
885
|
+
...discovery.hosts.slice(start, start + 5).map((item, index) =>
|
|
886
|
+
`${start + index === selected ? '›' : ' '} ${item.label} (${item.alias})${item.enabled ? '' : ' · 接続解除済み'}`),
|
|
887
|
+
...discovery.warnings,
|
|
888
|
+
'',
|
|
889
|
+
...(host ? connectionGuideLines(host.alias) : [
|
|
890
|
+
'保存されているパソコンがありません。接続設定をした人に接続先名を確認してください。',
|
|
891
|
+
'一覧: simy connections',
|
|
892
|
+
'このパソコンで simy connections を実行すると、使い方が表示されます。',
|
|
893
|
+
]),
|
|
894
|
+
].flatMap(line => wrapTerminalText(line, width - 4));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function RemoteConnectionsGuide({ rows, scroll, pageSize, width }) {
|
|
898
|
+
return jsxs(Box, { width, borderStyle: 'single', borderColor: 'cyan', paddingX: 1, flexDirection: 'column', children: [
|
|
899
|
+
jsx(Text, { bold: true, color: 'cyan', children: '別のパソコンのSIMYにつなぐ' }),
|
|
900
|
+
...rows.slice(scroll, scroll + pageSize).map((line, index) => jsx(Text, { children: line }, `guide-${index}`)),
|
|
901
|
+
jsx(Text, { dimColor: true, children: '↑/↓でパソコンを選択 · PgUp/PgDnで前後を表示 · Escで戻る' }),
|
|
902
|
+
] });
|
|
903
|
+
}
|
|
904
|
+
|
|
851
905
|
function ActionBar({
|
|
852
906
|
run,
|
|
853
907
|
detailOpen,
|
package/src/console/commands.js
CHANGED
|
@@ -16,6 +16,7 @@ const HELP_LINES = [
|
|
|
16
16
|
"/resume Resume a paused executor process",
|
|
17
17
|
"/stop Stop the selected run",
|
|
18
18
|
"/update Open the available installer download",
|
|
19
|
+
"/connections 別のパソコンへの接続方法を見る",
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
export function parseConsoleCommand(value) {
|
|
@@ -29,6 +30,8 @@ export function parseConsoleCommand(value) {
|
|
|
29
30
|
switch (command) {
|
|
30
31
|
case "/new":
|
|
31
32
|
return { type: "new" };
|
|
33
|
+
case "/connections":
|
|
34
|
+
return { type: "connections" };
|
|
32
35
|
case "/repos":
|
|
33
36
|
return { type: "repositories" };
|
|
34
37
|
case "/scan":
|
package/src/console/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { routeLegacyHooks } from './legacy-hook-routing.js';
|
|
|
20
20
|
import { activateLocalDesktop, watchLocalRuntime, installRuntimeWatchdog } from './runtime-watchdog.js';
|
|
21
21
|
import { desktopRuntimeStatus, installLocalDesktop } from './desktop-runtime.js';
|
|
22
22
|
import { consistencyMonitorStatus, consistencyFindings } from './consistency-monitor.js';
|
|
23
|
+
import { observabilityStatus } from '../cli-observability.js';
|
|
23
24
|
|
|
24
25
|
async function readCheckpointInput(input) {
|
|
25
26
|
const chunks = [];
|
|
@@ -45,6 +46,12 @@ function parseDeliveryOptions(args, allowed) {
|
|
|
45
46
|
|
|
46
47
|
export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = process.cwd(), webOrigin, input = process.stdin, output = console.log } = {}) {
|
|
47
48
|
const [action, ...args] = argv;
|
|
49
|
+
if (action === 'observability') {
|
|
50
|
+
const [operation, ...flags] = args;
|
|
51
|
+
if (operation !== 'status' || flags.length) throw new Error('Use simy delivery observability status. Change the single device-consent checkbox in SIMY settings.');
|
|
52
|
+
const sessionRoot = dirname(root);
|
|
53
|
+
output(JSON.stringify(await observabilityStatus(sessionRoot), null, 2)); return 0;
|
|
54
|
+
}
|
|
48
55
|
if (action === 'history-recovery') {
|
|
49
56
|
const [operation,...flags] = args;
|
|
50
57
|
const options = parseDeliveryOptions(flags,new Set(['audit','digest']));
|
|
@@ -211,6 +218,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
211
218
|
}
|
|
212
219
|
if (action === 'activity') return runActivityCommand(args, { root, output });
|
|
213
220
|
if (!action || action === '--help') {
|
|
221
|
+
output('simy delivery observability status — show the state-only telemetry status for this device. The single default-OFF consent checkbox is in SIMY settings; disabling it stops uploads and discards pending telemetry. It never enables Auto-continue or starts a task.');
|
|
214
222
|
output('simy delivery followup --provider codex --session ID --previous-run-id ID --objective TEXT — explicitly supervise the current goal after an exact finished run. A new goal archives the prior run; the same ordinary reported_done goal retains its identity and budgets for read-only reconciliation. Uses the existing supervisor; never reopens stopped, failed or budget-exhausted work.');
|
|
215
223
|
output('simy delivery auto on|off|status --provider codex|claude — inspect or override CLI automatic start for this project directory (on by default). The next nonempty prompt in a conversation without a loop starts checkpoint tracking. Existing loops are preserved; provider hooks must already be enabled.');
|
|
216
224
|
output('simy delivery redelegate-native --provider codex --session ID --task-id ID --consent-ref REF — explicitly delegate current native local edit/test permissions to an exact legacy task. Preserves enrollment, history, budgets and stops; does not resume or grant external actions.');
|
|
@@ -63,6 +63,29 @@ export function pinnedCliEntry(root) {
|
|
|
63
63
|
return loader + "\nif (!await selectRuntime()) { process.stderr.write('SIMY selected runtime unavailable.\\n'); process.exitCode = 1; }\n";
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Older SSH clients resolve the remote entry beside the real CLI executable.
|
|
67
|
+
// A shell launcher lives in bin rather than src, so expose a compatible entry
|
|
68
|
+
// there while retaining the same verified pin and normal remote startup.
|
|
69
|
+
export function pinnedRemoteEntry(root) {
|
|
70
|
+
const loader = pinnedCliEntry(root).replace('src/index.js', 'src/session-supervision/remote-entry.js');
|
|
71
|
+
// CommonJS-compatible .js works on Node 20 without a surrounding package.json.
|
|
72
|
+
const url = 'data:text/javascript,' + encodeURIComponent(loader);
|
|
73
|
+
return `// SIMY managed remote runtime route.\nvoid import(${JSON.stringify(url)}).catch(() => { process.stderr.write('SIMY remote runtime unavailable.\\n'); process.exitCode = 1; });\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function installPinnedRemoteEntry(root) {
|
|
77
|
+
// Like the service entry, this only writes a loader. The loader validates
|
|
78
|
+
// the pin and full source fingerprint every time it is actually executed.
|
|
79
|
+
const directory = join(root, 'bin/session-supervision');
|
|
80
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
81
|
+
const entry = join(directory, 'remote-entry.js'), temporary = `${entry}.${randomUUID()}.tmp`;
|
|
82
|
+
try {
|
|
83
|
+
await writeFile(temporary, pinnedRemoteEntry(resolve(root)), { mode: 0o600 });
|
|
84
|
+
await rename(temporary, entry);
|
|
85
|
+
} finally { await rm(temporary, { force: true }); }
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') } = {}) {
|
|
67
90
|
await pinnedWorkflowSource({ root });
|
|
68
91
|
const folder = join(codexHome, 'plugins/cache/personal/simy');
|
|
@@ -93,6 +116,7 @@ export async function routeLegacyHooks({ root, codexHome = process.env.CODEX_HOM
|
|
|
93
116
|
// Also used during service installation after a handover performed by an older
|
|
94
117
|
// watcher which did not know this launcher. Atomic replacement keeps startup safe.
|
|
95
118
|
export async function installPinnedCliEntry(root) {
|
|
119
|
+
await installPinnedRemoteEntry(root);
|
|
96
120
|
const entry = join(root, 'bin/simy-runtime.mjs');
|
|
97
121
|
await mkdir(join(root, 'bin'), { recursive: true, mode: 0o700 });
|
|
98
122
|
const temporary = `${entry}.${randomUUID()}.tmp`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { captureDispatchProvenance } from '../update-detachment/provenance.js';
|
|
2
|
-
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, confirmDispatch, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
|
|
2
|
+
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, confirmDispatch, flushDispatchIntake, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
|
|
3
3
|
import { bindCompletionArtifacts } from '../session-supervision/completion-artifacts.js';
|
|
4
4
|
import { recoveryWorkProposal, admitRecoveryWork } from '../session-supervision/recovery-frontier.js';
|
|
5
5
|
import { safeCheckpointDiagnostic } from './checkpoint-diagnostic.js';
|
|
@@ -11,9 +11,7 @@ import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervisi
|
|
|
11
11
|
import { searchEnabled, failureLimitReached } from '../codex-plugins/simy/hooks/purpose-search.mjs';
|
|
12
12
|
import { reconcilePurposeTasks } from '../session-supervision/purpose-recovery.js';
|
|
13
13
|
import { recordRecoveryIncident, recordRecoveryStage, settleRecoveryIncidents, resolveRecoveryReceipt } from '../codex-plugins/simy/hooks/recovery-incident.mjs';
|
|
14
|
-
import {
|
|
15
|
-
import { desktopRuntimeStatus } from './desktop-runtime.js';
|
|
16
|
-
import { auditConsistencyTask, recoverOutcomeAuditLease } from '../session-supervision/consistency-auditor.js';
|
|
14
|
+
import { recoverOutcomeAuditLease } from '../session-supervision/consistency-auditor.js';
|
|
17
15
|
import { minimalSufficientChangeInstruction } from '../codex-plugins/simy/hooks/minimal-sufficient.mjs';
|
|
18
16
|
import { requireProviderLink } from '../provider-links.js';
|
|
19
17
|
import { spawn } from 'node:child_process';
|
|
@@ -28,7 +26,9 @@ import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
|
|
|
28
26
|
|
|
29
27
|
const MAX_OUTPUT = 64 * 1024;
|
|
30
28
|
const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
31
|
-
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable'
|
|
29
|
+
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable',
|
|
30
|
+
'state_busy', 'dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required',
|
|
31
|
+
'task_identity_conflict', 'attempt_already_recorded', 'identity_unavailable']);
|
|
32
32
|
function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
|
|
33
33
|
function providerFailureReason(code) {
|
|
34
34
|
if (code === 'permission_restore') return 'Codex sandbox restoration could not be confirmed; inspect the original conversation before resuming.';
|
|
@@ -36,6 +36,8 @@ function providerFailureReason(code) {
|
|
|
36
36
|
if (code === 'identity') return 'Codex session identity, workspace, or model changed; inspect before resuming.';
|
|
37
37
|
if (code === 'checkpoint' || code === 'checkpoint_rejected') return 'Codex returned an invalid managed checkpoint; inspect its plan before resuming.';
|
|
38
38
|
if (code === 'turn_failed') return 'The Codex turn did not complete; inspect the original conversation.';
|
|
39
|
+
if (code === 'state_busy') return 'The local dispatch state is busy; reconcile its lock before another attempt.';
|
|
40
|
+
if (['dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required', 'task_identity_conflict', 'identity_unavailable'].includes(code)) return `Local dispatch admission requires reconciliation (${code}); preserve the existing task and provider authority.`;
|
|
39
41
|
return 'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
|
|
40
42
|
}
|
|
41
43
|
// One fresh lease read at a time; stopping the watch also fences late reads so
|
|
@@ -642,19 +644,17 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
642
644
|
}
|
|
643
645
|
|
|
644
646
|
export function createManagedSupervisor(options) {
|
|
645
|
-
let running = null,
|
|
647
|
+
let running = null, closed = false;
|
|
646
648
|
const controller = new AbortController();
|
|
647
649
|
const workers = createTaskPool({ concurrency: options.concurrency ?? 4 });
|
|
648
650
|
const readCandidates = options.readRecords || (() => listRecords(options.root));
|
|
649
651
|
return {
|
|
650
652
|
tick() {
|
|
651
653
|
if (closed) return Promise.resolve();
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
|
|
657
|
-
})().catch(() => {}).finally(() => { auditing = null; });
|
|
654
|
+
try { flushDispatchIntake(rootFromDelivery(options.root)); }
|
|
655
|
+
catch (error) { if (error.code !== 'state_busy') return Promise.reject(error); }
|
|
656
|
+
// Continue actual task work and its result checks. Starting an unrelated
|
|
657
|
+
// periodic audit here could acquire a lease that prevents that work.
|
|
658
658
|
if (!running) running = (async () => {
|
|
659
659
|
for (const record of await readCandidates()) {
|
|
660
660
|
if (closed || await options.shouldYield?.()) break;
|
|
@@ -680,8 +680,8 @@ export function createManagedSupervisor(options) {
|
|
|
680
680
|
},
|
|
681
681
|
status() { return workers.snapshot(); },
|
|
682
682
|
async idle() { await running; await workers.idle(); },
|
|
683
|
-
async drain() { closed = true; await Promise.allSettled([running,
|
|
684
|
-
async close() { closed = true; controller.abort(); await Promise.allSettled([running,
|
|
683
|
+
async drain() { closed = true; await Promise.allSettled([running, workers.stop()]); },
|
|
684
|
+
async close() { closed = true; controller.abort(); await Promise.allSettled([running, workers.stop()]); },
|
|
685
685
|
};
|
|
686
686
|
}
|
|
687
687
|
|
package/src/desktop-host.js
CHANGED
|
@@ -457,7 +457,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
457
457
|
if (!sessionRefresh && (force || Date.now() - sessionRefreshedAt >= 15_000)) {
|
|
458
458
|
const requestedScope = accessScope(currentAccess);
|
|
459
459
|
const controller = discoveryController = new AbortController();
|
|
460
|
-
|
|
460
|
+
const attempt = { id: randomUUID(), requestedAt: Date.now(), state: 'pending' };
|
|
461
|
+
sessionStatus = { ...sessionStatus, discoveryAttempt: attempt };
|
|
462
|
+
sessionRefresh = Promise.resolve().then(() => { if (controller.signal.aborted) return null; return (dependencies.discoverSessions || discoverSessions)({ root: deliveryRoot, signal: controller.signal, includeArchived: true, includeSelectionCandidates: true, connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected) }); })
|
|
461
463
|
.then(async value => {
|
|
462
464
|
if (closed || relay || controller.signal.aborted) return;
|
|
463
465
|
const latestAccess = await accessSnapshot();
|
|
@@ -474,8 +476,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
474
476
|
}
|
|
475
477
|
if (coordinator.acceptsNewWork()) await analysisManager.update(analysisSessions(sessions));
|
|
476
478
|
if (controller.signal.aborted) return;
|
|
477
|
-
sessionStatus = { ...value, sessions, initialLoading: false
|
|
478
|
-
|
|
479
|
+
sessionStatus = { ...value, sessions, initialLoading: false,
|
|
480
|
+
discoveryAttempt: { ...attempt, state: 'succeeded', completedAt: Date.now() } }; sessionRefreshedAt = Date.now(); })
|
|
481
|
+
.catch(() => { if (closed || relay || controller.signal.aborted) return; sessionStatus = { ...sessionStatus, initialLoading: false,
|
|
482
|
+
discoveryAttempt: { ...attempt, state: 'failed', completedAt: Date.now() },
|
|
483
|
+
providers: { codex: { discovery: "unavailable", reason: "Conversation discovery unavailable." }, claude: { discovery: "unavailable", reason: "Conversation discovery unavailable." } } }; sessionRefreshedAt = Date.now(); })
|
|
479
484
|
.finally(() => { if (discoveryController === controller) discoveryController = null; sessionRefresh = null; if (sessionRefreshAgain && !closed && !relay) { sessionRefreshAgain = false; refreshSessions(true); }
|
|
480
485
|
if (!closed) setImmediate(() => { void publish().catch(() => {}); }); });
|
|
481
486
|
}
|
|
@@ -758,7 +763,6 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
758
763
|
scope: requestedScope,
|
|
759
764
|
deviceSession: localCliApiSession(),
|
|
760
765
|
});
|
|
761
|
-
stopNotifier.observe(records);
|
|
762
766
|
const projection = await actionAssociations.beginProjection?.({ records });
|
|
763
767
|
status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: deliveryRoot, records: projection?.records || records }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
|
|
764
768
|
await actionAssociations.decorate(status.deliveryHooks, projection);
|
|
@@ -771,7 +775,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
771
775
|
for (const session of sessions) {
|
|
772
776
|
const key = `${session.provider}:${session.session}`;
|
|
773
777
|
session.statistics = activeStatisticsRead?.key === key && !activeStatisticsRead.controller.signal.aborted
|
|
774
|
-
?
|
|
778
|
+
? statisticsCache.has(key)
|
|
779
|
+
? { ...statisticsCache.get(key), refreshing: true }
|
|
780
|
+
: { state: 'loading', hours: [], activity: [] }
|
|
775
781
|
: statisticsCache.get(key) || unavailableStatistics();
|
|
776
782
|
session.sqmActivity = conversationSqmChecks(session, [...(status.sqmChecks?.items || []), ...(status.sqmTasks?.items || [])]);
|
|
777
783
|
session.actionDeadline = { status: 'unavailable', online: false, reminder: null };
|
|
@@ -815,7 +821,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
815
821
|
status = { ...structuredClone(latestConnectionStatus), deliveryHooks: status.deliveryHooks, sessionSupervision: status.sessionSupervision };
|
|
816
822
|
}
|
|
817
823
|
status.teamThreads = teamThreads?.snapshot();
|
|
818
|
-
if (!closed && !relay)
|
|
824
|
+
if (!closed && !relay) {
|
|
825
|
+
// Native stop admission waits for this authorized projection, so an old
|
|
826
|
+
// running frame cannot suppress a newly stopped conversation.
|
|
827
|
+
stopNotifier.observe(records);
|
|
828
|
+
emitStatus(status);
|
|
829
|
+
}
|
|
819
830
|
}).finally(() => { publishing = null; if (publishAgain && !closed) { publishAgain = false; setImmediate(() => { void publish().catch(() => {}); }); } });
|
|
820
831
|
return publishing;
|
|
821
832
|
};
|
|
@@ -1202,11 +1213,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1202
1213
|
if (!command.hostId && command.session && remoteHosts?.hasSession(command.session)) {
|
|
1203
1214
|
throw Object.assign(new Error('Select the host for this conversation before continuing.'), { code: 'remote_host_invalid' });
|
|
1204
1215
|
}
|
|
1205
|
-
if (
|
|
1216
|
+
if (['remote_host_save', 'remote_host_edit', 'remote_host_connect', 'remote_host_disconnect', 'remote_host_remove'].includes(action)) {
|
|
1206
1217
|
if (!remoteHosts) throw Object.assign(new Error('Nested remote connections are unavailable.'), { code: 'remote_host_invalid' });
|
|
1207
|
-
if (action === 'remote_host_save') await authorizeProvider('codex', commandScope);
|
|
1218
|
+
if (action === 'remote_host_save' || action === 'remote_host_connect' || action === 'remote_host_edit' && command.previousAlias !== command.alias) await authorizeProvider(PROVIDER_LINKS.find(provider => providerLinks[provider]?.connected) || 'codex', commandScope);
|
|
1208
1219
|
else await authorizeWork(commandScope);
|
|
1209
|
-
await remoteHosts.
|
|
1220
|
+
if (action === 'remote_host_edit') await remoteHosts.edit({ previousAlias: command.previousAlias, alias: command.alias, label: command.label });
|
|
1221
|
+
else if (action === 'remote_host_connect' || action === 'remote_host_disconnect') await remoteHosts.setEnabled(command.alias, action === 'remote_host_connect');
|
|
1222
|
+
else await remoteHosts.save({ alias: command.alias, label: command.label }, action === 'remote_host_remove');
|
|
1210
1223
|
emit({ type: 'result', id: command.id, ok: true }); await publish(); return;
|
|
1211
1224
|
}
|
|
1212
1225
|
if (action === 'provider_repair') {
|
|
@@ -1284,8 +1297,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1284
1297
|
await authorizeProvider(candidate.provider, commandScope, selectedLinkEpochs.get(candidate));
|
|
1285
1298
|
signal.throwIfAborted();
|
|
1286
1299
|
if (activeStatisticsRead !== request) throw Object.assign(new Error('Conversation activity request cancelled.'), { code: 'cancelled' });
|
|
1300
|
+
const previous = statisticsCache.get(request.key);
|
|
1301
|
+
if (statistics.state === 'unavailable' && ['ready', 'partial'].includes(previous?.state)) statistics = { ...previous, refreshFailed: true };
|
|
1287
1302
|
statisticsCache.clear(); // Only the selected conversation needs chart data.
|
|
1288
1303
|
statisticsCache.set(request.key, statistics);
|
|
1304
|
+
}).catch(error => {
|
|
1305
|
+
const previous = statisticsCache.get(request.key);
|
|
1306
|
+
if (activeStatisticsRead === request && !signal.aborted && ['ready', 'partial'].includes(previous?.state)) {
|
|
1307
|
+
statisticsCache.set(request.key, { ...previous, refreshFailed: true });
|
|
1308
|
+
}
|
|
1309
|
+
throw error;
|
|
1289
1310
|
}).finally(() => {
|
|
1290
1311
|
if (activeStatisticsRead === request) activeStatisticsRead = null;
|
|
1291
1312
|
if (!closed) void publish().catch(() => {});
|
|
@@ -1311,6 +1332,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1311
1332
|
}
|
|
1312
1333
|
if (action === 'desktop_access_refresh') {
|
|
1313
1334
|
await agent.refreshDesktopAccess?.();
|
|
1335
|
+
} else if (action === 'desktop_sqm_settings') {
|
|
1336
|
+
const verified = await agent.refreshDesktopAccess?.();
|
|
1337
|
+
await authorizeWork(commandScope);
|
|
1338
|
+
if (!desktopAccessReady(verified) || verified.organizationId !== access.organizationId ||
|
|
1339
|
+
!validDesktopAccessPath(verified.settingsPath, verified.organizationSlug)) throw new Error('Organization settings are unavailable; refresh access first.');
|
|
1340
|
+
const url = new URL(`/org/${encodeURIComponent(verified.organizationSlug)}/quality-monitor`, webOrigin);
|
|
1341
|
+
const result = await (dependencies.openLogin || openAuthorizationUrl)(url.toString(), { logger: silentLogger });
|
|
1342
|
+
if (!result.opened) throw new Error('Could not open SQM settings.');
|
|
1314
1343
|
} else if (action === 'desktop_access_settings' || action === 'desktop_access_members') {
|
|
1315
1344
|
const verified = await agent.refreshDesktopAccess?.();
|
|
1316
1345
|
const path = verified?.[action === 'desktop_access_settings' ? 'settingsPath' : 'membersPath'];
|
|
@@ -1340,7 +1369,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1340
1369
|
else if (action === "login" || action === "organization_select" || action === "authorization_open") {
|
|
1341
1370
|
if (action === "organization_select" && (typeof command.organizationId !== "string" || !command.organizationId)) throw new Error("Select an organization first.");
|
|
1342
1371
|
const url = action === "authorization_open" ? (agent.getPendingAuthorizationUrl?.() ?? agent.retryAuthorization?.())
|
|
1343
|
-
: await agent.requestDesktopLogin(action === "organization_select" ? command.organizationId : null
|
|
1372
|
+
: await agent.requestDesktopLogin(action === "organization_select" ? command.organizationId : null,
|
|
1373
|
+
{ reauthorize: action === 'login' && command.reauthorize === true });
|
|
1344
1374
|
if (action === 'authorization_open' && !url) throw new Error('No pending authorization.');
|
|
1345
1375
|
if (url) {
|
|
1346
1376
|
if (new URL(url).origin !== webOrigin) throw new Error("Invalid login origin.");
|
|
@@ -1593,6 +1623,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1593
1623
|
await (dependencies.trackSession || trackSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
1594
1624
|
session: command.session, tracked: action === "session_track", expectedProviderEpoch: selectedLinkEpochs.get(candidate) });
|
|
1595
1625
|
await refreshSessions(true);
|
|
1626
|
+
} else if (action === "session_add") {
|
|
1627
|
+
if (command.provider !== 'codex' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(command.session || '')) throw new Error('Choose a Codex conversation or enter its exact conversation ID.');
|
|
1628
|
+
await authorizeWork(commandScope);
|
|
1629
|
+
await (dependencies.monitorSession || monitorSession)({ root: deliveryRoot, provider: command.provider,
|
|
1630
|
+
session: command.session, monitor: true, expectedProviderEpoch: commandProviderLink?.epoch,
|
|
1631
|
+
openClient: dependencies.openCodexAppServer || openCodexAppServer });
|
|
1632
|
+
await authorizeProvider(command.provider, commandScope, commandProviderLink?.epoch);
|
|
1633
|
+
await updateRecord(deliveryRoot, command.provider, command.session, previous => ({ ...previous, listHidden: false }));
|
|
1634
|
+
await refreshSessions(true);
|
|
1596
1635
|
} else if (action === "session_monitor" || action === "session_unmonitor") {
|
|
1597
1636
|
await (dependencies.monitorSession || monitorSession)({ root: join(root, "delivery-hooks"), provider: command.provider,
|
|
1598
1637
|
session: command.session, monitor: action === "session_monitor", expectedProviderEpoch: commandProviderLink?.epoch });
|
package/src/direct-executor.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function startDirectTask(
|
|
|
108
108
|
task.error = "Worker launch outcome could not be confirmed. Reconcile saved evidence before retrying.";
|
|
109
109
|
return task;
|
|
110
110
|
}
|
|
111
|
-
if (task.stop_requested) await
|
|
111
|
+
if (task.stop_requested) await stopDirectWorker(task);
|
|
112
112
|
return await reconcileDirectWorker(task);
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -229,6 +229,12 @@ export async function reconcileDirectWorker(task, { worker = task.worker || crea
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// Launch acknowledgement and a user stop may settle together. Both paths
|
|
233
|
+
// share one owned stop RPC, including when it closes the owner's IPC socket.
|
|
234
|
+
function stopDirectWorker(task) {
|
|
235
|
+
return task.workerStopPromise ||= task.worker.stop(task.worker_handle);
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
// Only the child captured by this task may be signalled, including its owned tools.
|
|
233
239
|
export function stopDirectTask(task) {
|
|
234
240
|
if (!task) return Promise.resolve(task);
|
|
@@ -241,7 +247,7 @@ async function stopOwnedDirectTask(task) {
|
|
|
241
247
|
if (task.worker_handle) {
|
|
242
248
|
// Launch uncertainty does not prevent stopping a separately confirmed owner.
|
|
243
249
|
await task.worker_start?.catch(() => {});
|
|
244
|
-
await
|
|
250
|
+
await stopDirectWorker(task);
|
|
245
251
|
return reconcileDirectWorker(task);
|
|
246
252
|
}
|
|
247
253
|
const child = task.child;
|
package/src/doctor.js
CHANGED
|
@@ -46,6 +46,7 @@ export async function collectDiagnostics({ webOrigin, cliVersion, root = default
|
|
|
46
46
|
providers, failures, host_exit: await readHostExitDiagnostics(root),
|
|
47
47
|
purpose_verification: await purposeVerificationCapability(purposeSigningKeyPath(root)).then(value => ({ state: value ? "available" : "not_initialized", key_id: value?.key_id || null, execution: "explicit_frozen_recipes_only" })).catch(() => ({ state: "unavailable", key_id: null, execution: "explicit_frozen_recipes_only" })),
|
|
48
48
|
actions: [
|
|
49
|
+
{ id: "connection_recovery", command: "simy connections recover simy", instruction: "つながらない原因を確認し、表示される対処方法を選んで、もう一度確認します。" },
|
|
49
50
|
...Object.entries(providers).filter(([, provider]) => provider.connection === "disconnected")
|
|
50
51
|
.map(([provider]) => ({ id: `connect_${provider}`, action: "provider_connect", provider,
|
|
51
52
|
instruction: `Open SIMY settings and connect ${provider}. Provider login and SIMY connection are separate; existing tasks remain stopped.` })),
|
package/src/index.js
CHANGED
|
@@ -38,7 +38,7 @@ const commandArgv = argv.filter((value) => value !== "--no-auto-update");
|
|
|
38
38
|
// Delivery receives the resolved origin separately; global --host is not one
|
|
39
39
|
// of its command-specific options, regardless of its position on the CLI.
|
|
40
40
|
const deliveryArgv = commandArgv.filter((value, index) => value !== "--host" && commandArgv[index - 1] !== "--host");
|
|
41
|
-
const dispatchArgv = ["delivery", "sessions"].includes(deliveryArgv[0]) ? deliveryArgv : commandArgv;
|
|
41
|
+
const dispatchArgv = ["delivery", "sessions", "connections"].includes(deliveryArgv[0]) ? deliveryArgv : commandArgv;
|
|
42
42
|
const entryPath = fileURLToPath(import.meta.url);
|
|
43
43
|
const packageInfo = await readCliPackageInfo();
|
|
44
44
|
const updatesDisabled = args.has("--no-auto-update") || process.env.SIMY_AUTO_UPDATE === "0";
|
|
@@ -65,6 +65,11 @@ if (!configuredWebOrigin) {
|
|
|
65
65
|
} else if (dispatchArgv[0] === "sessions") {
|
|
66
66
|
const { runSessionListCommand } = await import("./session-supervision/session-list.js");
|
|
67
67
|
process.exitCode = await runSessionListCommand(dispatchArgv.slice(1));
|
|
68
|
+
} else if (dispatchArgv[0] === "connections") {
|
|
69
|
+
try {
|
|
70
|
+
const { runRemoteConnectionsCommand } = await import("./remote-connection-guide.js");
|
|
71
|
+
process.exitCode = await runRemoteConnectionsCommand(dispatchArgv.slice(1), { webOrigin: configuredWebOrigin });
|
|
72
|
+
} catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
68
73
|
} else if (dispatchArgv[0] === "doctor") {
|
|
69
74
|
const { runDoctor } = await import("./doctor.js");
|
|
70
75
|
process.exitCode = await runDoctor({ webOrigin: configuredWebOrigin, cliVersion: packageInfo.version });
|
|
@@ -140,6 +145,7 @@ async function runCommand(installMode) {
|
|
|
140
145
|
console.log(`Usage:
|
|
141
146
|
simy Start the local SIMY agent
|
|
142
147
|
simy --version Print the installed version without starting or updating
|
|
148
|
+
simy connections 別のパソコンへの接続や、つながらないときの手順を表示
|
|
143
149
|
simy doctor Read-only version, connection, provider and failure diagnostics
|
|
144
150
|
simy --daemon Start it in the background
|
|
145
151
|
simy --no-tui Start the foreground HTTP agent without the interactive console
|
|
@@ -177,6 +183,10 @@ async function runLocalAgent() {
|
|
|
177
183
|
const port = readPort(argv);
|
|
178
184
|
const webOrigin = configuredWebOrigin;
|
|
179
185
|
const sessionRoot = process.env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
186
|
+
const { discoverRemoteConnections, connectionStartupHint } = await import("./remote-connection-guide.js");
|
|
187
|
+
const remoteConnections = !daemon ? await discoverRemoteConnections({ root: sessionRoot }) : { hosts: [], warnings: [] };
|
|
188
|
+
const remoteHint = connectionStartupHint(remoteConnections);
|
|
189
|
+
if (remoteHint) console.log(remoteHint);
|
|
180
190
|
|
|
181
191
|
try {
|
|
182
192
|
const existing = await findReusableConnection({ webOrigin, sessionRoot, cliVersion: packageInfo.version,
|
|
@@ -277,7 +287,7 @@ async function runLocalAgent() {
|
|
|
277
287
|
if (interactive) {
|
|
278
288
|
const { runAgenticLoopConsole } = await import("./console/index.js");
|
|
279
289
|
try {
|
|
280
|
-
await runAgenticLoopConsole(agent);
|
|
290
|
+
await runAgenticLoopConsole(agent, { remoteConnections });
|
|
281
291
|
} finally {
|
|
282
292
|
await shutdownAgent();
|
|
283
293
|
}
|