@awak-app/simy-cli 0.7.35 → 0.7.37
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 +50 -46
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/backend-executable.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -5
- package/src/codex-plugins/simy/hooks/README.md +3 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +2 -2
- package/src/codex-plugins/simy/skills/continue/SKILL.md +4 -4
- package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +3 -3
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +2 -2
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -3
- package/src/codex-plugins/simy/skills/sqm-loop/agents/openai.yaml +2 -2
- package/src/console/app.js +7 -7
- package/src/delivery-hooks/claude-workflow.md +1 -1
- package/src/delivery-hooks/command.js +5 -5
- package/src/desktop-host.js +58 -11
- package/src/index.js +1 -1
- package/src/local-task.js +1 -1
- package/src/orchestrator/presentation.js +1 -1
- package/src/runner.js +2 -2
- package/src/session-supervision/automatic-continuation.js +11 -11
- package/src/session-supervision/continuation-analysis.js +1 -1
- package/src/session-supervision/continuation-command.js +3 -3
- package/src/session-supervision/continuation-readiness.js +1 -1
- package/src/session-supervision/conversation-presentation.js +3 -3
- package/src/session-supervision/decision-presentation.js +8 -8
- package/src/session-supervision/index.js +13 -13
- package/src/session-supervision/owner-transport.js +53 -11
- package/src/session-supervision/remote-errors.js +2 -2
- package/src/session-supervision/remote-hosts.js +14 -3
- package/src/session-supervision/work-state.js +3 -3
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/claude-plugin.js +1 -1
|
@@ -44,7 +44,7 @@ resume or reset a task. Prompt hooks enroll eligible conversations under CLI aut
|
|
|
44
44
|
Use when the user requests this skill or SIMY Delivery Loop, or current hook context confirms automatic enrollment. Preserve that existing loop and proceed to checkpoints without starting again. Installing this plugin does not opt a task in. Do not run SQM unless separately requested.
|
|
45
45
|
|
|
46
46
|
1. Resolve the exact original provider and session ID from the SIMY hook context or verified existing session admission. If no valid connection exists, report the observed missing capability and the smallest confirmed connection step. Never invent a session ID, change hook trust, bypass permissions, or replace the conversation. Codex settings can be opened at `codex://settings`; a hooks subpage deep link is not guaranteed.
|
|
47
|
-
2. Only start or resume a Delivery Loop when the user requests it. For a new regular Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. For a Loop that must track acceptance, use the existing managed specification with `--managed-file <task.json>` as described below: freeze the original user outcome, selected delivery level when one exists, and necessary applicable acceptance criteria before work. A regular start can only end as `reported_done`; do not present it as a fully verified
|
|
47
|
+
2. Only start or resume a Delivery Loop when the user requests it. For a new regular Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. For a Loop that must track acceptance, use the existing managed specification with `--managed-file <task.json>` as described below: freeze the original user outcome, selected delivery level when one exists, and necessary applicable acceptance criteria before work. A regular start can only end as `reported_done`; do not present it as a fully verified managed Delivery Loop. A verified original `codex_app_server` admission and existing user continuation delegation can use `simy delivery continue --provider codex --session <exact-original-session-id>` without SessionStart when the user requests continuation. Preserve the original objective and acceptance. Never start another loop to reset limits or resume user-stopped work without a user request. Authorized automatic recovery from ordinary blockers is distinct from resuming user-stopped work.
|
|
48
48
|
|
|
49
49
|
3. Work in the current conversation with its normal permissions. Before ending each turn, submit a JSON checkpoint through standard input with `simy delivery checkpoint --provider <provider> --session <id> --file -` (for example, a shell heredoc). This updates SIMY state; it does not require writing a checkpoint file to the user's workspace. A file path remains supported when authorized.
|
|
50
50
|
|
|
@@ -225,18 +225,18 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
225
225
|
}
|
|
226
226
|
if (action === 'activity') return runActivityCommand(args, { root, output });
|
|
227
227
|
if (!action || action === '--help') {
|
|
228
|
-
output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID —
|
|
229
|
-
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
|
|
228
|
+
output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID — Continue this conversation automatically: turns Delivery Loop on or off for one exact conversation through the running app’s existing control, and reads its actual work state.');
|
|
229
|
+
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 turns on Delivery Loop or starts a task.');
|
|
230
230
|
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.');
|
|
231
|
-
output('simy delivery auto on|off|status --provider codex|claude — inspect or override
|
|
231
|
+
output('simy delivery auto on|off|status --provider codex|claude — Start Delivery Loop automatically in this workspace: inspect or override automatic start for future conversations in this project directory (on by default). The next nonempty prompt in a conversation without a Delivery Loop starts checkpoint tracking. Existing loops are preserved; provider hooks must already be enabled.');
|
|
232
232
|
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.');
|
|
233
233
|
output('simy delivery runtime install|status|workflows — install an immutable local candidate, inspect its version/digest, or refresh existing plugin sources/cache while preserving enabled state and hook trust. Never enables a task.');
|
|
234
234
|
output('simy delivery runtime doctor|desktop-install|desktop-activate|watch — audit live host/state/monitor alignment, or prepare a separate local desktop using the verified fixed runtime. Does not start or resume tasks.');
|
|
235
235
|
output('simy delivery runtime policy-adopt [--task-id ID [--criterion-id ID]] — explicitly delegate decisions within existing permissions. Manual acceptance stays manual unless the user names the exact task and criterion to delegate its verifier.');
|
|
236
236
|
output('simy delivery link_parent --provider codex|claude --session CHILD --parent-provider codex|claude --parent-session PARENT — record an explicit observed navigation relationship without starting work.');
|
|
237
237
|
output('simy delivery supervise [--once] — service already opted-in tasks with this runtime; refuses an active desktop host.');
|
|
238
|
-
output('simy delivery consult [--scope diagnose|repair_continue] [--request-id USER_TURN_ID] [--context-file context.json] [--provider codex|claude --session ID | --task-id ID] —
|
|
239
|
-
output('simy delivery diagnose --provider codex|claude --session ID [--task-id ID] [--mode analyze|evidence] [--issues-file issues.json] [--detail brief|full] — advisory
|
|
238
|
+
output('simy delivery consult [--scope diagnose|repair_continue] [--request-id USER_TURN_ID] [--context-file context.json] [--provider codex|claude --session ID | --task-id ID] — diagnose scope: check why it stopped, using the observed current conversation or selected original work; repair_continue scope never implies completion.');
|
|
239
|
+
output('simy delivery diagnose --provider codex|claude --session ID [--task-id ID] [--mode analyze|evidence] [--issues-file issues.json] [--detail brief|full] — advisory check of why it stopped, in a separate process; never resumes the target. Exact --task-id alone is also supported.');
|
|
240
240
|
output('simy delivery evidence --provider codex|claude --session ID — read-only execution and acceptance receipts.');
|
|
241
241
|
output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
|
|
242
242
|
output('A task stops after repeated failures, and pauses for review when no progress is confirmed. Resume starts a new progress window.');
|
package/src/desktop-host.js
CHANGED
|
@@ -35,6 +35,7 @@ import { repairProviderPlugin } from './sqm/provider-plugin-repair.js';
|
|
|
35
35
|
import { openCodexAppServer, verifyCodexArchiveState } from './session-supervision/codex-app-server.js';
|
|
36
36
|
import { conversationSqmChecks, conversationSqmResult } from './session-supervision/sqm-presentation.js';
|
|
37
37
|
import { stopOwnedDesktopHost } from "./desktop-force-quit.js";
|
|
38
|
+
import { relayToOwner } from './session-supervision/owner-transport.js';
|
|
38
39
|
import { stopDesktopWorkForUpdate } from './desktop-update-stop.js';
|
|
39
40
|
import { desktopAccessReady, desktopActionAllowed, requireDesktopAccess, validDesktopAccessPath } from './desktop-access.js';
|
|
40
41
|
import { createActionDeadlineService } from './session-supervision/action-deadline.js';
|
|
@@ -117,9 +118,9 @@ function desktopActionError(action, error) {
|
|
|
117
118
|
const messages = {
|
|
118
119
|
session_workspace_changed: "The conversation workspace changed. Refresh and select it again.",
|
|
119
120
|
session_metadata_unavailable: "Conversation details are unavailable. Refresh this provider and select the conversation again.",
|
|
120
|
-
session_model_unavailable: "This provider did not expose the conversation model;
|
|
121
|
+
session_model_unavailable: "This provider did not expose the conversation model; Delivery Loop is unavailable.",
|
|
121
122
|
existing_loop: "This conversation already has a Delivery Loop. Open its existing supervision instead.",
|
|
122
|
-
task_settled: "This task has finished. Start a new conversation before
|
|
123
|
+
task_settled: "This task has finished. Start a new conversation before turning on Delivery Loop.",
|
|
123
124
|
};
|
|
124
125
|
if (messages[error?.code]) return messages[error.code];
|
|
125
126
|
}
|
|
@@ -159,7 +160,23 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
159
160
|
const frames = createDesktopFrameWriter(output);
|
|
160
161
|
const pendingRemoteReceipts = new Map();
|
|
161
162
|
let operationJournal, ownerTransport;
|
|
162
|
-
|
|
163
|
+
// An explicitly requested window update releases only its viewer. The same
|
|
164
|
+
// authenticated owner, tasks and pending permissions remain alive on IPC.
|
|
165
|
+
let windowUpdatePrepared = false, viewerDetached = false, relayStatus = null;
|
|
166
|
+
const detachViewer = () => { viewerDetached = true; frames.close(); };
|
|
167
|
+
output.on('error', error => {
|
|
168
|
+
if (windowUpdatePrepared && ['EPIPE', 'ERR_STREAM_DESTROYED'].includes(error.code)) detachViewer();
|
|
169
|
+
else if (!closed) void close().then(() => exit(1));
|
|
170
|
+
});
|
|
171
|
+
const writeEvent = event => {
|
|
172
|
+
if (event.type === 'status' && event.status) {
|
|
173
|
+
relayStatus = { value: event.status, at: Date.now() };
|
|
174
|
+
if (ownerTransport) event = { ...event, status: { ...event.status, windowUpdate: { version: 1, preservesWork: Boolean(ownerTransport), prepared: windowUpdatePrepared } } };
|
|
175
|
+
}
|
|
176
|
+
ownerTransport?.publish(event);
|
|
177
|
+
if (event.type === 'status') ownerTransport?.publishDesktop?.(event);
|
|
178
|
+
if (!viewerDetached) frames.write(`${JSON.stringify(event)}\n`, event.type === 'status');
|
|
179
|
+
};
|
|
163
180
|
const emit = (event) => {
|
|
164
181
|
const pending = event.type === 'result' ? pendingRemoteReceipts.get(event.id) : null;
|
|
165
182
|
if (pending) {
|
|
@@ -181,6 +198,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
181
198
|
let remoteHosts;
|
|
182
199
|
const emitStatus = status => {
|
|
183
200
|
status = projectCatalogFrame(status);
|
|
201
|
+
status = { ...status, windowUpdate: { version: 1, preservesWork: Boolean(ownerTransport), prepared: windowUpdatePrepared } };
|
|
184
202
|
const updateSettings = updater?.snapshot?.() || {};
|
|
185
203
|
status = { ...status, update: { ...status.update,
|
|
186
204
|
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
@@ -205,13 +223,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
205
223
|
void controllerPreferences.change(catalogScope, state => { state.catalog = catalog; }).catch(() => {});
|
|
206
224
|
}
|
|
207
225
|
if (updateProgress) status = { ...status, update: { ...status.update, ...updateProgress } };
|
|
226
|
+
ownerTransport?.publishDesktop?.({ type: 'status', status });
|
|
208
227
|
const { sampledAt, ...content } = status;
|
|
209
228
|
if (!lastStatus || !isDeepStrictEqual(content, lastStatus)) {
|
|
210
229
|
encodedStatus = JSON.stringify(content).slice(1, -1);
|
|
211
230
|
lastStatus = structuredClone(content);
|
|
212
231
|
}
|
|
213
232
|
const observation = sampledAt === undefined ? '' : `"sampledAt":${JSON.stringify(sampledAt)}${encodedStatus ? ',' : ''}`;
|
|
214
|
-
frames.write(`{"type":"status","status":{${observation}${encodedStatus}}}\n`, true);
|
|
233
|
+
if (!viewerDetached) frames.write(`{"type":"status","status":{${observation}${encodedStatus}}}\n`, true);
|
|
215
234
|
resolveFirstStatus?.(); resolveFirstStatus = null;
|
|
216
235
|
};
|
|
217
236
|
const root = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
@@ -953,6 +972,15 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
953
972
|
scope: accessScope(status.access), controllerScope: controllerScope(status), deviceSession: reportDeviceSession(),
|
|
954
973
|
}) && nativeDevelopmentReportsScope === accessScope(currentAccess)
|
|
955
974
|
? { ...structuredClone(cached?.nativeDevelopmentReports || {}), ...structuredClone(nativeDevelopmentReports) } : { state: 'loading', enrollments: [] };
|
|
975
|
+
// Local consent is durable independently of the optional server read or
|
|
976
|
+
// conversation enrichment. Include it even when that full projection is
|
|
977
|
+
// blocked; otherwise the native confirmation timer reports a false error.
|
|
978
|
+
const preferenceState = preferenceScope && desktopAccessReady(status.access)
|
|
979
|
+
? controllerPreferences.peek(preferenceScope) : null;
|
|
980
|
+
const emailPreferences = Object.values(preferenceState?.conversations || {})
|
|
981
|
+
.flatMap(row => row.emailPreference ? [{ ...row.emailPreference.request, sync: row.emailPreference.state }] : []);
|
|
982
|
+
if (emailPreferences.length || status.nativeDevelopmentReports.preferences)
|
|
983
|
+
status.nativeDevelopmentReports.preferences = emailPreferences;
|
|
956
984
|
// Cached enrichment must follow the current connection preference even
|
|
957
985
|
// while a full projection is waiting on records or remote metadata.
|
|
958
986
|
status.deliveryHooks.runs = linkedSessions(status.deliveryHooks.runs || []);
|
|
@@ -1439,6 +1467,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1439
1467
|
let command; try { command = JSON.parse(line); } catch { return; }
|
|
1440
1468
|
if (typeof command.id !== "string" || command.id.length > 128) return;
|
|
1441
1469
|
if (relay) {
|
|
1470
|
+
if (['prepare_window_update', 'cancel_window_update'].includes(command.action)) {
|
|
1471
|
+
const authorized = Boolean(ownerTransport) && relayStatus && Date.now() - relayStatus.at < 10_000 && desktopAccessReady(relayStatus.value.access);
|
|
1472
|
+
if (authorized) windowUpdatePrepared = command.action === 'prepare_window_update';
|
|
1473
|
+
emit({ type: 'result', id: command.id, ok: Boolean(authorized) }); return;
|
|
1474
|
+
}
|
|
1442
1475
|
if (command.action === 'session_renew_claude_owner') {
|
|
1443
1476
|
emit({ type: 'result', id: command.id, ok: false, message: 'Use the verified local conversation host for connection recovery.' }); return;
|
|
1444
1477
|
}
|
|
@@ -1479,6 +1512,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1479
1512
|
emit({type:'result',id:command.id,ok:coordinator.snapshot().state==='connected' && (action!=='detach_update' || updater.snapshot().state==='updated')});
|
|
1480
1513
|
await publish();return;
|
|
1481
1514
|
}
|
|
1515
|
+
if (action === 'prepare_window_update' || action === 'cancel_window_update') {
|
|
1516
|
+
requireDesktopAccess(await accessSnapshot());
|
|
1517
|
+
if (action === 'prepare_window_update' && (!ownerTransport || closed || stoppingForUpdate))
|
|
1518
|
+
throw new Error('Keep using SIMY; the update will be available when this work finishes.');
|
|
1519
|
+
windowUpdatePrepared = action === 'prepare_window_update';
|
|
1520
|
+
emit({ type: 'result', id: command.id, ok: true });
|
|
1521
|
+
await publish(); return;
|
|
1522
|
+
}
|
|
1482
1523
|
if (action === "quit") {
|
|
1483
1524
|
if (command.forUpdate === true) {
|
|
1484
1525
|
if (command.force !== true) throw new Error('Confirm before stopping unfinished tasks.');
|
|
@@ -1970,7 +2011,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1970
2011
|
await authorizeWork(commandScope);
|
|
1971
2012
|
await actionAssociations.correct(command, analysisManager.project(candidate));
|
|
1972
2013
|
} else if (action === "session_set_auto_continue") {
|
|
1973
|
-
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether
|
|
2014
|
+
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether Delivery Loop is on.');
|
|
1974
2015
|
let admission, target, providerEpoch;
|
|
1975
2016
|
if (command.enabled) {
|
|
1976
2017
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
@@ -2163,7 +2204,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2163
2204
|
if (fresh.cwd !== candidate.cwd) throw new Error('The conversation workspace changed. Refresh and retry.');
|
|
2164
2205
|
const record = await readRecord(deliveryRoot, 'claude', command.session);
|
|
2165
2206
|
if (fresh.isRunning || fresh.autoContinue || record?.autoContinue || record?.run && !TERMINAL.has(record.run.state))
|
|
2166
|
-
throw Object.assign(new Error('Stop this conversation and turn off
|
|
2207
|
+
throw Object.assign(new Error('Stop this conversation and turn off Delivery Loop before organizing it.'), { code: 'claude_desktop_running' });
|
|
2167
2208
|
};
|
|
2168
2209
|
await verifySelection();
|
|
2169
2210
|
const operation = new AbortController();
|
|
@@ -2289,7 +2330,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2289
2330
|
}
|
|
2290
2331
|
}
|
|
2291
2332
|
};
|
|
2292
|
-
if (command.action === "quit" || command.action === "prepare_update_restart" || command.action === "update" && (command.force === true || stoppingForUpdate)) {
|
|
2333
|
+
if (["prepare_window_update", "cancel_window_update"].includes(command.action) || command.action === "quit" || command.action === "prepare_update_restart" || command.action === "update" && (command.force === true || stoppingForUpdate)) {
|
|
2293
2334
|
void execute();
|
|
2294
2335
|
} else if (["login", "organization_select", "organization_cancel", "authorization_open", "authorization_copy"].includes(command.action)) {
|
|
2295
2336
|
authorizationQueue = authorizationQueue.then(execute).catch(() => {});
|
|
@@ -2316,9 +2357,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2316
2357
|
}).catch(() => {});
|
|
2317
2358
|
}
|
|
2318
2359
|
});
|
|
2319
|
-
commands.on("close", () => { if (!closed) void close().then(() => exit(0)); });
|
|
2320
|
-
if (lock && !dependencies.startAgent) {
|
|
2321
|
-
ownerTransport = await createOwnerTransport({ root, nonce: lock.nonce, allowedActions, allowControllerArchive: env.SIMY_REMOTE_CHILD !== '1', dispatch: command => commands.emit('line', JSON.stringify(command)) });
|
|
2360
|
+
commands.on("close", () => { if (windowUpdatePrepared && ownerTransport && !closed) { detachViewer(); return; } if (!closed) void close().then(() => exit(0)); });
|
|
2361
|
+
if (lock && (!dependencies.startAgent || dependencies.createOwnerTransport)) {
|
|
2362
|
+
ownerTransport = await (dependencies.createOwnerTransport || createOwnerTransport)({ root, nonce: lock.nonce, allowedActions, allowControllerArchive: env.SIMY_REMOTE_CHILD !== '1', allowDesktopClient: env.SIMY_REMOTE_CHILD !== '1', dispatch: command => commands.emit('line', JSON.stringify(command)) });
|
|
2322
2363
|
await publish();
|
|
2323
2364
|
}
|
|
2324
2365
|
// Register controls before awaiting initial presentation. A fast, authorized
|
|
@@ -2339,7 +2380,7 @@ function sessionActionError(error, action) {
|
|
|
2339
2380
|
if (error instanceof AutoContinueUnavailableError) return error.message.replace(/[\r\n\x00-\x1f]/g, ' ').slice(0, 600);
|
|
2340
2381
|
const message = String(error?.message || "");
|
|
2341
2382
|
if (/already has a Delivery Loop|budgets cannot be reset/i.test(message)) return "This conversation already has a task. Open its existing supervision instead.";
|
|
2342
|
-
if (/model.*unavailable|did not expose.*model/i.test(message)) return "This provider did not expose the conversation model;
|
|
2383
|
+
if (/model.*unavailable|did not expose.*model/i.test(message)) return "This provider did not expose the conversation model; Delivery Loop is unavailable.";
|
|
2343
2384
|
if (/workspace changed|metadata.*unavailable|session.*changed/i.test(message)) return "The conversation identity or workspace changed. Refresh and select it again.";
|
|
2344
2385
|
if (/objective|acceptance|autonomy policy|budget/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
|
2345
2386
|
if (/question changed|answer is empty|session understanding|context/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
|
@@ -2407,6 +2448,10 @@ export function isDesktopHostEntry(entry = process.argv[1]) {
|
|
|
2407
2448
|
|
|
2408
2449
|
function desktopStartupError(error, fallbackMessage = "SIMY could not start. Check whether another SIMY window is already running, then reconnect.") {
|
|
2409
2450
|
const conflict = error?.code === PROCESS_CONFLICT_CODE;
|
|
2451
|
+
if (error?.code === 'desktop_owner_update_required') return {
|
|
2452
|
+
type: 'error', code: 'desktop_owner_update_required',
|
|
2453
|
+
message: 'The existing SIMY service cannot hand off its window yet. Keep using the original window. After its work finishes, quit SIMY there and reopen it to update; the running work was left untouched.',
|
|
2454
|
+
};
|
|
2410
2455
|
return {
|
|
2411
2456
|
type: "error",
|
|
2412
2457
|
...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
|
|
@@ -2425,6 +2470,7 @@ export function startDesktopEntry(options = {}) {
|
|
|
2425
2470
|
const diagnostics = createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" });
|
|
2426
2471
|
// stdout is the native JSONL channel; stderr is the only unstructured output
|
|
2427
2472
|
// and the native window does not display it, so keep it for support.
|
|
2473
|
+
process.stderr.on('error', error => { if (error.code !== 'EPIPE') diagnostics.write('stderr closed: ' + error.code); });
|
|
2428
2474
|
captureRuntimeOutput(diagnostics, { stderr: process.stderr });
|
|
2429
2475
|
applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost(options)).then((value) => { host = value; }).catch((error) => {
|
|
2430
2476
|
diagnostics.write(`startup failed: ${error?.stack || error}`);
|
|
@@ -2436,6 +2482,7 @@ export function startDesktopEntry(options = {}) {
|
|
|
2436
2482
|
if (isDesktopHostEntry()) {
|
|
2437
2483
|
try {
|
|
2438
2484
|
const root = process.env.SIMY_HOME?.trim() || join(homedir(), '.simy');
|
|
2485
|
+
if (process.env.SIMY_DESKTOP_ATTACH_OWNER === '1' && await relayToOwner({ root, webOrigin: resolveWebOrigin(null, process.env), desktopClient: true })) process.exit(0);
|
|
2439
2486
|
try { await adoptInstalledRuntime({ root }); }
|
|
2440
2487
|
catch (error) {
|
|
2441
2488
|
if (error.code === PROCESS_CONFLICT_CODE) throw error;
|
package/src/index.js
CHANGED
|
@@ -161,7 +161,7 @@ async function runCommand(installMode) {
|
|
|
161
161
|
Options:
|
|
162
162
|
--version, -v Print the installed CLI version and exit
|
|
163
163
|
--daemon, --deamon Run detached in the background
|
|
164
|
-
--no-tui Disable the interactive
|
|
164
|
+
--no-tui Disable the interactive Delivery Loop console
|
|
165
165
|
--no-open Do not open the browser when authorization is required
|
|
166
166
|
--port <port> Bind a specific localhost port
|
|
167
167
|
--host <url> Connect to a SIMY Web origin (default: ${DEFAULT_WEB_ORIGIN})
|
package/src/local-task.js
CHANGED
|
@@ -1257,7 +1257,7 @@ function localTaskProviderInstruction(task) {
|
|
|
1257
1257
|
? "The managed output directory is reserved for final user-requested deliverables. Keep helper scripts, temporary files, caches, and intermediate files outside it (use the task workspace or an OS temporary directory), and remove those helper files before completion."
|
|
1258
1258
|
: null;
|
|
1259
1259
|
return [
|
|
1260
|
-
"You are the local executor for a SIMY Local Task, not
|
|
1260
|
+
"You are the local executor for a SIMY Local Task, not a Delivery Loop.",
|
|
1261
1261
|
buildWorkflowReportingInstruction(),
|
|
1262
1262
|
"Complete this bounded task once. Do not create Git branches, commits, pull requests, audits, or automatic retries.",
|
|
1263
1263
|
`Permission mode: ${task.permission_mode}.`,
|
|
@@ -117,7 +117,7 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
117
117
|
};
|
|
118
118
|
case "stopped":
|
|
119
119
|
return {
|
|
120
|
-
summary: "The local human operator stopped this
|
|
120
|
+
summary: "The local human operator stopped this Delivery Loop run.",
|
|
121
121
|
result: "No further executor handoff will occur for this run.",
|
|
122
122
|
next: "Start a new run or explicitly continue this one when ready.",
|
|
123
123
|
};
|
package/src/runner.js
CHANGED
|
@@ -452,7 +452,7 @@ export async function recoverLocalCodingRun(run, dependencies = {}, now = Date.n
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
function assertRunCanContinue(run) {
|
|
455
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
455
|
+
if (run.operation || run.child) throw new Error("The selected Delivery Loop run is still active.");
|
|
456
456
|
if (!RESUMABLE_STATES.has(run.status)) {
|
|
457
457
|
throw new Error("Human guidance is available only when the selected run is waiting.");
|
|
458
458
|
}
|
|
@@ -542,7 +542,7 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
|
542
542
|
if (!approvedRepository || !approvedPath) {
|
|
543
543
|
throw new Error("An approved local repository and path are required.");
|
|
544
544
|
}
|
|
545
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
545
|
+
if (run.operation || run.child) throw new Error("The selected Delivery Loop run is still active.");
|
|
546
546
|
if (run.status !== "waiting_human") {
|
|
547
547
|
throw new Error("Repository approval is available only while the run is waiting.");
|
|
548
548
|
}
|
|
@@ -6,15 +6,15 @@ function continuationFailureReason(error) {
|
|
|
6
6
|
// Provider exceptions can contain private paths/output. Only known codes
|
|
7
7
|
// select actionable UI copy; unrecognized failures retain a safe fallback.
|
|
8
8
|
const reasons = {
|
|
9
|
-
desktop_access_required: 'Sign in to an organization with SQM and Developer access before
|
|
10
|
-
provider_sign_in_required: 'Sign in to the original AI provider, then retry
|
|
11
|
-
unavailable: 'The local provider is unavailable. Open or configure it, then retry
|
|
12
|
-
unsupported: 'Update the original AI provider to a compatible version, then retry
|
|
9
|
+
desktop_access_required: 'Sign in to an organization with SQM and Developer access before Delivery Loop can run.',
|
|
10
|
+
provider_sign_in_required: 'Sign in to the original AI provider, then retry Delivery Loop.',
|
|
11
|
+
unavailable: 'The local provider is unavailable. Open or configure it, then retry Delivery Loop.',
|
|
12
|
+
unsupported: 'Update the original AI provider to a compatible version, then retry Delivery Loop.',
|
|
13
13
|
process_exited: 'The original Claude process has exited. Return to the original conversation, then refresh.',
|
|
14
14
|
session_ended: 'This Claude session has ended. Return to the original conversation, then refresh.',
|
|
15
15
|
hook_missing: 'Waiting for the original Claude hook. SIMY recovers automatically when the next verified prompt or response-end hook arrives. If no notifications arrive, check the SIMY plugin in the original Claude window.',
|
|
16
16
|
};
|
|
17
|
-
return Object.hasOwn(reasons, error?.code) ? reasons[error.code] : 'Could not start
|
|
17
|
+
return Object.hasOwn(reasons, error?.code) ? reasons[error.code] : 'Could not start Delivery Loop. Check the original conversation and retry.';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Shared by explicit Continue and automatic enrollment. Analysis alone never
|
|
@@ -24,7 +24,7 @@ export async function continueAnalyzedSession({ candidate, manager, root, settin
|
|
|
24
24
|
const link = await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: settings.expectedProviderEpoch });
|
|
25
25
|
const recommendationOptions = { allowDiagnosis: settings.purposeDriven === true && candidate.autoContinue === true };
|
|
26
26
|
const recommendation = manager.requireRecommendation(candidate, recommendationOptions);
|
|
27
|
-
if (candidate.isRunning || candidate.capabilities?.canContinue !== true) throw new Error('
|
|
27
|
+
if (candidate.isRunning || candidate.capabilities?.canContinue !== true) throw new Error('Delivery Loop is unavailable for this conversation.');
|
|
28
28
|
const context = await readContext(candidate, { signal });
|
|
29
29
|
if (context.awaitingTurn || context.incomplete || !recommendation.contextDigest || context.contextDigest !== recommendation.contextDigest) {
|
|
30
30
|
await manager.invalidate(candidate);
|
|
@@ -34,7 +34,7 @@ export async function continueAnalyzedSession({ candidate, manager, root, settin
|
|
|
34
34
|
await beforeEnroll(candidate);
|
|
35
35
|
await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: link.epoch });
|
|
36
36
|
manager.requireRecommendation(candidate, recommendationOptions);
|
|
37
|
-
if (signal?.aborted) throw new Error('
|
|
37
|
+
if (signal?.aborted) throw new Error('Delivery Loop was cancelled before enrollment.');
|
|
38
38
|
return enroll({ root, provider: candidate.provider, session: candidate.session, ...settings, expectedProviderEpoch: link.epoch });
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -46,7 +46,7 @@ function startingPresentation(candidate, status, reason) {
|
|
|
46
46
|
const canPresent = work && !work.needsAttention && !['running', 'stopped', 'archived', 'approval', 'question'].includes(work.state);
|
|
47
47
|
return { ...candidate, autoContinueStatus: status, autoContinueReason: reason,
|
|
48
48
|
...(canPresent ? { workState: { ...work, state: status,
|
|
49
|
-
label: status === 'starting' ? '
|
|
49
|
+
label: status === 'starting' ? 'Delivery Loop を準備中' : '自動復旧中', reason,
|
|
50
50
|
nextAction: null, needsAttention: false } } : {}) };
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -133,7 +133,7 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
|
|
|
133
133
|
retries.set(key, { attempt, count, at: now() + Math.min(60_000, 1000 * 2 ** Math.min(count - 1, 6)) });
|
|
134
134
|
} else retries.delete(key);
|
|
135
135
|
errors.set(key, { epoch: candidate.autoContinueEpoch, recoverable,
|
|
136
|
-
reason: recoverable ? 'The AI connection is temporarily unavailable.
|
|
136
|
+
reason: recoverable ? 'The AI connection is temporarily unavailable. Delivery Loop will retry when the connection is available.' : continuationFailureReason(error) });
|
|
137
137
|
}
|
|
138
138
|
}).finally(() => {
|
|
139
139
|
if (controller.signal.aborted) { attempted.delete(key); retries.delete(key); }
|
|
@@ -152,14 +152,14 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
|
|
|
152
152
|
if (candidate.autoContinue === true && candidate.capabilities?.canAutoContinue === true &&
|
|
153
153
|
!['managed', 'pending_takeover'].includes(candidate.binding?.mode)) {
|
|
154
154
|
if (pending.has(key)) return startingPresentation(candidate, 'starting',
|
|
155
|
-
'
|
|
155
|
+
'Delivery Loop is on. Preparing Delivery Loop for this conversation in the background.');
|
|
156
156
|
const failure = errors.get(key);
|
|
157
157
|
if (failure?.recoverable && failure.epoch === candidate.autoContinueEpoch)
|
|
158
158
|
return startingPresentation(candidate, 'recovering', failure.reason);
|
|
159
159
|
}
|
|
160
160
|
if (candidate.autoContinue === true && candidate.isRunning !== true &&
|
|
161
161
|
['unmanaged', 'monitor'].includes(candidate.binding?.mode || 'unmanaged') && candidate.analysis?.state === 'needs_user') return {
|
|
162
|
-
...candidate, autoContinueStatus: 'needs_user', autoContinueReason: candidate.analysis.reason || 'Your decision is needed before
|
|
162
|
+
...candidate, autoContinueStatus: 'needs_user', autoContinueReason: candidate.analysis.reason || 'Your decision is needed before Delivery Loop can proceed.',
|
|
163
163
|
};
|
|
164
164
|
const error = errors.get(`${candidate.provider}:${candidate.session}`);
|
|
165
165
|
if (error && candidate.autoContinue === true && candidate.autoContinueEpoch === error.epoch &&
|
|
@@ -83,7 +83,7 @@ async function unavailableAnalysis(candidate, error, inspectProvider, signal) {
|
|
|
83
83
|
if (signal?.aborted) return uncertain('Conversation analysis was cancelled.', 'cancelled');
|
|
84
84
|
if (error?.code === 'provider_sign_in_required' || status?.state === 'signed_out') {
|
|
85
85
|
const label = candidate.provider === 'claude' ? 'Claude Code' : 'Codex';
|
|
86
|
-
return uncertain(`Sign in to ${label}. SIMY rechecks this conversation automatically when sign-in is detected. You can also retry manually. Unfinished work
|
|
86
|
+
return uncertain(`Sign in to ${label}. SIMY rechecks this conversation automatically when sign-in is detected. You can also retry manually. Unfinished work continues only when "Continue automatically" is on.`, 'provider_sign_in_required');
|
|
87
87
|
}
|
|
88
88
|
return uncertain('The local AI app could not check this turn. Check its connection, then retry.', 'analysis_unavailable');
|
|
89
89
|
}
|
|
@@ -8,7 +8,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
8
8
|
export async function runContinuationCommand(args, { root, output, signal,
|
|
9
9
|
request = requestOwnerContinuation } = {}) {
|
|
10
10
|
if (args.includes('--help') || args.includes('-h')) {
|
|
11
|
-
output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID\
|
|
11
|
+
output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID\nContinue this conversation automatically: turns Delivery Loop on or off for one exact conversation. Uses the running local SIMY app and its existing controls. ON saves delegation; only workState describes execution. Explicit stops, approvals, original goals and history remain in force.');
|
|
12
12
|
return 0;
|
|
13
13
|
}
|
|
14
14
|
const [operation, ...flags] = args;
|
|
@@ -34,12 +34,12 @@ export async function runContinuationCommand(args, { root, output, signal,
|
|
|
34
34
|
try {
|
|
35
35
|
const receipt = await request({ root, signal, command: command('session_get_status') });
|
|
36
36
|
output(JSON.stringify({ ...receipt, provider, session, accepted,
|
|
37
|
-
message: receipt?.message || (operation === 'on' && accepted ? '
|
|
37
|
+
message: receipt?.message || (operation === 'on' && accepted ? 'Delivery Loop delegation saved. Read workState and its nextAction for actual progress; this acknowledgement does not prove a provider turn executed.' : undefined) }));
|
|
38
38
|
return receipt?.ok ? 0 : 2;
|
|
39
39
|
} catch (error) {
|
|
40
40
|
if (!accepted) throw error;
|
|
41
41
|
output(JSON.stringify({ ok: false, provider, session, accepted, code: error.code || 'status_unavailable',
|
|
42
|
-
message: 'The setting was saved, but execution status could not be read. Check auto-continue status
|
|
42
|
+
message: 'The setting was saved, but execution status could not be read. Check `simy delivery auto-continue status`; do not repeat ON to infer execution.' }));
|
|
43
43
|
return 2;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -54,5 +54,5 @@ export function projectContinuationWait(candidate, wait, now, noticeAfterMs = CO
|
|
|
54
54
|
const needsAttention = wait.reasonCode === 'analysis_retry_exhausted' || now - wait.since >= noticeAfterMs;
|
|
55
55
|
return { ...candidate, continuationWait: { id: wait.id, since: wait.since, reasonCode: wait.reasonCode, needsAttention },
|
|
56
56
|
...(needsAttention ? { autoContinueStatus: 'needs_user',
|
|
57
|
-
autoContinueReason: '
|
|
57
|
+
autoContinueReason: 'Delivery Loop is on, but work has been waiting for execution conditions. Check the original conversation or refresh its connection.' } : {}) };
|
|
58
58
|
}
|
|
@@ -82,7 +82,7 @@ export function conversationPresentation(candidate = {}, projectedRun, { rawReco
|
|
|
82
82
|
const pendingDecision = ['approval', 'question'].includes(work.state) || field('pendingApproval') && !field('pendingApproval').denied ||
|
|
83
83
|
field('understandingQuestion') || managed.userQuestion;
|
|
84
84
|
if (candidate.autoContinueStatus === 'save_failed') {
|
|
85
|
-
ready = readiness('save_failed', 'danger', reason?.text || 'Could not save
|
|
85
|
+
ready = readiness('save_failed', 'danger', reason?.text || 'Could not save Delivery Loop. Retry.');
|
|
86
86
|
} else if (candidate.autoContinueStatus === 'saving') {
|
|
87
87
|
ready = readiness('saving', 'accent', 'Saving On…');
|
|
88
88
|
} else if (candidate.autoContinue !== true) {
|
|
@@ -116,13 +116,13 @@ export function conversationPresentation(candidate = {}, projectedRun, { rawReco
|
|
|
116
116
|
} else if (terminal) {
|
|
117
117
|
ready = readiness('finished', 'secondary', state === 'reported_done' ? 'The AI reported completion; verification is pending.' : 'No current work to continue.');
|
|
118
118
|
} else if (candidate.provider === 'claude' && candidate.isRunning === false && !['starting', 'recovering'].includes(candidate.autoContinueStatus)) {
|
|
119
|
-
ready = readiness('original_turn_required', 'attention', 'Send one message in the original conversation.
|
|
119
|
+
ready = readiness('original_turn_required', 'attention', 'Send one message in the original conversation. Delivery Loop waits for its next verified hook.', action('open_original', 'Open original conversation'));
|
|
120
120
|
} else if (control && candidate.autoContinueStatus === 'supervising' && ['active', 'verification_pending'].includes(state) &&
|
|
121
121
|
ownership === 'simy_continues' && blocker.state === 'ready' &&
|
|
122
122
|
!field('executionUncertain') && !field('providerWarning') && !managed.nativeWait && !managed.purpose?.nativeBoundary) {
|
|
123
123
|
ready = readiness('ready', 'accent', 'Will continue after this turn');
|
|
124
124
|
} else if (['starting', 'recovering'].includes(candidate.autoContinueStatus)) {
|
|
125
|
-
ready = readiness(candidate.autoContinueStatus, 'accent', reason?.text || 'Checking
|
|
125
|
+
ready = readiness(candidate.autoContinueStatus, 'accent', reason?.text || 'Checking Delivery Loop readiness.');
|
|
126
126
|
} else {
|
|
127
127
|
ready = readiness('waiting', 'secondary', reason?.text || 'Checking whether the next step can be sent.', action('refresh', 'Refresh status'));
|
|
128
128
|
}
|
|
@@ -78,8 +78,8 @@ export function sessionDecisionPresentation(candidate, run) {
|
|
|
78
78
|
const acceptancePending = run.state === 'verification_pending';
|
|
79
79
|
const providerName = candidate.provider === 'claude' ? 'Claude' : candidate.provider === 'codex' ? 'Codex' : 'AI';
|
|
80
80
|
const reasons = {
|
|
81
|
-
session_end: pair(`元の${providerName}
|
|
82
|
-
process_exit: pair('元のAI
|
|
81
|
+
session_end: pair(`元の${providerName}会話が終了し、Delivery Loop が止まっています。`, `The original ${providerName} session ended, stopping Delivery Loop.`),
|
|
82
|
+
process_exit: pair('元のAIが終了したため、Delivery Loop を続けられません。', 'The original AI process exited. Delivery Loop cannot proceed.'),
|
|
83
83
|
provider_error: pair('AIの実行でエラーが発生し、作業が止まっています。', 'An AI execution error stopped the work.'),
|
|
84
84
|
shutdown_cancelled: pair('SIMYの終了時に作業が中断されました。', 'Work was interrupted when SIMY exited.'),
|
|
85
85
|
hook_owner_mismatch: pair('元のAI会話の接続が変わりました。', 'The connection to the original AI session changed.'),
|
|
@@ -91,9 +91,9 @@ export function sessionDecisionPresentation(candidate, run) {
|
|
|
91
91
|
const choices = [];
|
|
92
92
|
if (run.canReplan && frozen) choices.push(choice(pair('AIに状況確認を任せる', 'Ask AI to investigate'), pair('目的・履歴・制限を維持して原因と安全な次の手順を調べます。操作の許可を追加することはありません。', 'Investigate the cause and next steps while keeping the goal, history and limits. No new permission is granted.'), 'delivery_replan', {}, 'primary'));
|
|
93
93
|
const enablesAutoContinue = candidate.provider === 'codex' && run.managed !== true && run.state === 'stopped' && candidate.autoContinue !== true;
|
|
94
|
-
if (run.canResume && frozen) choices.push(choice(pair(enablesAutoContinue ? '
|
|
94
|
+
if (run.canResume && frozen) choices.push(choice(pair(enablesAutoContinue ? 'Delivery Loop をオンにして再開' : acceptancePending ? '未完了の作業を続ける' : '再開を依頼する', enablesAutoContinue ? 'Resume with Delivery Loop' : acceptancePending ? 'Continue remaining work' : 'Request resume'), pair(enablesAutoContinue ? 'Delivery Loop をオンにして元の会話の読み取り専用の状況確認から再開します。元の目的・権限・停止履歴・予算を維持します。' : acceptancePending ? '元の目的・履歴・制限を維持して残作業を確認します。最終確認を済ませたことにはしません。' : '保存した目的と履歴を引き継ぎます。接続や権限が不足する場合は、その内容を案内します。', enablesAutoContinue ? 'Turn on Delivery Loop and resume with a read-only status check in the original conversation. Preserve its goal, permissions, stop history and budgets.' : acceptancePending ? 'Keep the goal, history and limits while checking what remains. This does not accept the result.' : 'Keep the saved goal and history. Any missing connection or permission will be shown.'), 'delivery_resume', {}, choices.length ? 'secondary' : 'primary'));
|
|
95
95
|
choices.push(choice(pair(`元の${providerName}会話を開く`, 'Open original conversation ↗'), pair('接続や権限の確認が必要な場合、元のAI会話で確認できます。会話を開くだけでは作業を再開しません。', 'Check the original conversation. Opening it does not resume work.'), 'open_original', {}, 'link'));
|
|
96
|
-
if (candidate.autoContinue === true) choices.push(choice(pair('
|
|
96
|
+
if (candidate.autoContinue === true) choices.push(choice(pair('Delivery Loop をオフにする', 'Turn off Delivery Loop'), pair('今後の Delivery Loop をオフにします。作業の履歴は残ります。', 'Turn off Delivery Loop for future work and retain the history.'), 'session_set_auto_continue', { ...identity, enabled: false }));
|
|
97
97
|
const actionable = frozen && (run.canReplan || run.canResume);
|
|
98
98
|
return model('recovery', {
|
|
99
99
|
title: pair(acceptancePending ? '最終確認を待ちながら、残作業を続けられます' : '作業が止まっています。対応を選んでください', acceptancePending ? 'Continue work while acceptance is pending' : 'Work stopped. Choose how to proceed'),
|
|
@@ -105,12 +105,12 @@ export function sessionDecisionPresentation(candidate, run) {
|
|
|
105
105
|
candidate.capabilities?.canAutoContinue === true && candidate.autoContinueAvailability?.canEnable !== false) return model('continue', {
|
|
106
106
|
title: pair('続けますか?', 'Continue?'),
|
|
107
107
|
question: pair(clean(candidate.analysis.reason) || '許可済みの残作業があります。', clean(candidate.analysis.reason) || 'Authorized work remains.'),
|
|
108
|
-
context: pair('
|
|
109
|
-
}, { choices: [choice(pair('
|
|
108
|
+
context: pair('Delivery Loop はオフです。オンにすると、元の目的と権限の範囲で残作業を確認します。結果を受け入れる操作ではありません。', 'Delivery Loop is off. Turning it on checks remaining work within the original goal and permissions. It does not accept the result.'),
|
|
109
|
+
}, { choices: [choice(pair('Delivery Loop をオンにする', 'Turn on Delivery Loop'), pair('Delivery Loop の設定を保存します。作業の開始や完了を証明するものではありません。', 'Save consent for Delivery Loop. This does not confirm that work started or finished.'), 'session_set_auto_continue', { ...identity, enabled: true }, 'primary'),
|
|
110
110
|
choice(pair('元の会話を開く', 'Open original conversation ↗'), pair('会話を開くだけでは作業を再開しません。', 'Opening the conversation does not resume work.'), 'open_original', {}, 'link')] });
|
|
111
111
|
if (candidate.continuationWait?.needsAttention === true && candidate.autoContinue === true) return model('continuation_wait', {
|
|
112
|
-
title: pair('
|
|
113
|
-
question: pair('
|
|
112
|
+
title: pair('Delivery Loop の条件を確認してください', 'Check Delivery Loop conditions'),
|
|
113
|
+
question: pair('Delivery Loop はオンのまま、実行条件の確認を待っています。元の会話や接続状態を確認してください。', 'Delivery Loop is still on, waiting for execution conditions. Check the original conversation or its connection.'),
|
|
114
114
|
context: pair('設定・目的・停止指示・承認待ちは維持しています。この通知で作業を再開したり、権限を追加したりしません。', 'The setting, goal, stops and pending approvals are preserved. This notice does not resume work or grant permission.'),
|
|
115
115
|
}, { choices: [choice(pair('状況を再確認する', 'Refresh status'), pair('条件を再確認します。', 'Recheck conditions.'), 'refresh', identity),
|
|
116
116
|
choice(pair('元の会話を開く', 'Open original conversation ↗'), pair('元の会話を確認します。', 'Check the original conversation.'), 'open_original', {}, 'link')] }, 'helper');
|
|
@@ -24,7 +24,7 @@ import { readDispatchGate, rootFromDelivery, sessionDispatchReadiness } from '..
|
|
|
24
24
|
// error sanitizer; native provider errors never acquire this type.
|
|
25
25
|
export class AutoContinueUnavailableError extends Error {
|
|
26
26
|
constructor(availability) {
|
|
27
|
-
super(availability.reason || '
|
|
27
|
+
super(availability.reason || 'Delivery Loop is unavailable for this conversation.');
|
|
28
28
|
this.code = availability.reasonCode;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -54,8 +54,8 @@ function codexContinuationChannel(source, model, issue = null) {
|
|
|
54
54
|
// The saved ON preference is consent, not execution readiness. Each blocker
|
|
55
55
|
// names its own recovery route instead of reading as provider activity.
|
|
56
56
|
const DISPATCH_BLOCKERS = {
|
|
57
|
-
update_recovery_required: '
|
|
58
|
-
writer_reconnect_required: '
|
|
57
|
+
update_recovery_required: 'Delivery Loop is on, but SIMY is recovering from an interrupted update and cannot send work. Open Settings > App & updates and choose Try again.',
|
|
58
|
+
writer_reconnect_required: 'Delivery Loop is on, but this conversation was opened before SIMY recovered from an update. Send one message in the original conversation so SIMY can verify the same process and reconnect it.',
|
|
59
59
|
dispatch_history_quarantined: 'Earlier unconfirmed work in this conversation was quarantined during update recovery and will not be resent. Review the original conversation and start the next request yourself.',
|
|
60
60
|
task_admission_blocked: 'This task needs fresh verified provider permissions before SIMY can send work. Continue it once in the original conversation.',
|
|
61
61
|
};
|
|
@@ -63,7 +63,7 @@ function autoContinueState(record, isRunning, availability, readiness = null) {
|
|
|
63
63
|
if (record?.autoContinue !== true) return { autoContinueStatus: 'off', autoContinueReason: null };
|
|
64
64
|
const state = record.run?.state;
|
|
65
65
|
if (readiness?.state === 'blocked' && !TERMINAL_RUNS.has(state)) return { autoContinueStatus: 'needs_user',
|
|
66
|
-
autoContinueReason: DISPATCH_BLOCKERS[readiness.code] || '
|
|
66
|
+
autoContinueReason: DISPATCH_BLOCKERS[readiness.code] || 'Delivery Loop is on, but SIMY cannot send work for this conversation yet.' };
|
|
67
67
|
if (state === 'budget_exhausted') return { autoContinueStatus: 'needs_user', autoContinueReason: 'Local execution limits have been removed. Resume this same conversation to continue.' };
|
|
68
68
|
if (completionClaimUnbound(record)) return { autoContinueStatus: 'watching', autoContinueReason: 'Verification of the recorded completion remains pending.' };
|
|
69
69
|
if (state === 'reported_done') return { autoContinueStatus: 'watching', autoContinueReason: 'The provider reported completion; the actual result has not yet been confirmed.' };
|
|
@@ -383,7 +383,7 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
383
383
|
} catch { /* Missing workspaces cannot grant an admission. */ }
|
|
384
384
|
}
|
|
385
385
|
const availability = archived ? { canEnable: false, reasonCode: 'session_archived',
|
|
386
|
-
reason: 'Restore the original provider conversation before
|
|
386
|
+
reason: 'Restore the original provider conversation before turning on Delivery Loop.' }
|
|
387
387
|
: autoContinueAvailability(run, claudeChannel || codexChannel);
|
|
388
388
|
const upgradeable = canUpgradeDelivery(run);
|
|
389
389
|
const continuationIssue = liveHook && run ? !run.managed && !upgradeable
|
|
@@ -469,7 +469,7 @@ function hasAutoContinueAdmission(record, provider, session) {
|
|
|
469
469
|
(provider !== 'codex' || typeof admission.model === 'string' && admission.model.trim());
|
|
470
470
|
}
|
|
471
471
|
function requireAutoContinueTask(record) {
|
|
472
|
-
if (ownerRenewalPending(record)) throw new AutoContinueUnavailableError({ reasonCode: 'owner_renewal_pending', reason: 'Finish the original conversation connection recovery before
|
|
472
|
+
if (ownerRenewalPending(record)) throw new AutoContinueUnavailableError({ reasonCode: 'owner_renewal_pending', reason: 'Finish the original conversation connection recovery before turning on Delivery Loop.' });
|
|
473
473
|
if (record?.run?.state === 'stopped') throw new AutoContinueUnavailableError({ reasonCode: 'task_stopped', reason: 'This task was stopped. Review the original conversation and explicitly request resumption before continuing.' });
|
|
474
474
|
if (record?.run && !record.run.managed && !canUpgradeDelivery(record.run)) throw Object.assign(new Error('This conversation already has a Delivery Loop. Use its existing controls.'), { code: 'existing_loop' });
|
|
475
475
|
if (TERMINAL_RUNS.has(record?.run?.state)) throw new AutoContinueUnavailableError({ reason: 'This supervised task has settled. Review its existing status before continuing.', reasonCode: 'task_settled' });
|
|
@@ -487,7 +487,7 @@ export async function saveSessionAutoContinuePreference(options) {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
export async function setSessionAutoContinue({ root, provider, session, enabled, continuationPolicy, initialContinuationPolicy, ...options }) {
|
|
490
|
-
if (!['codex', 'claude'].includes(provider) || typeof enabled !== 'boolean') throw new Error('Unsupported
|
|
490
|
+
if (!['codex', 'claude'].includes(provider) || typeof enabled !== 'boolean') throw new Error('Unsupported Delivery Loop request.');
|
|
491
491
|
const link = enabled ? await requireProviderLink({ root, provider, expectedEpoch: options.expectedProviderEpoch }) : null;
|
|
492
492
|
let discoveredAdmission;
|
|
493
493
|
const preferenceOnly = options.preferenceOnly === true;
|
|
@@ -496,7 +496,7 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
496
496
|
const purposeRules = enabled && provider === 'codex' && !preferenceOnly ? await loadPurposeRuleSet() : null;
|
|
497
497
|
if (enabled && !preferenceOnly) {
|
|
498
498
|
const record = await readRecord(root, provider, session);
|
|
499
|
-
if (record?.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before
|
|
499
|
+
if (record?.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before turning on Delivery Loop.'), { code: 'session_archived' });
|
|
500
500
|
requireAutoContinueTask(record);
|
|
501
501
|
if (!hasAutoContinueAdmission(record, provider, session)) {
|
|
502
502
|
// Only the host's verified discovery catalog supplies this option; command
|
|
@@ -533,8 +533,8 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
533
533
|
// identity with the earlier catalog snapshot, including a mismatch.
|
|
534
534
|
previous = previous?.admission ? previous : { ...(previous || { schemaVersion: 1, provider, session, cwd: discoveredAdmission.cwd, run: null }), admission: discoveredAdmission };
|
|
535
535
|
}
|
|
536
|
-
if (!previous) throw new Error('Refresh this conversation before changing
|
|
537
|
-
if (enabled && previous.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before
|
|
536
|
+
if (!previous) throw new Error('Refresh this conversation before changing Delivery Loop.');
|
|
537
|
+
if (enabled && previous.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before turning on Delivery Loop.'), { code: 'session_archived' });
|
|
538
538
|
if (enabled && !preferenceOnly) {
|
|
539
539
|
const channel = provider === 'claude' ? await liveClaudeChannel(previous,
|
|
540
540
|
{ session, cwd: previous.cwd, observation: { source: previous.admission?.source } }, options.inspectProcess || processIdentity)
|
|
@@ -609,7 +609,7 @@ export async function trackSession({ root, provider, session, tracked = true, op
|
|
|
609
609
|
const cwd = await realpath(metadata.cwd);
|
|
610
610
|
await updateRecord(root, provider, session, async previous => {
|
|
611
611
|
await requireProviderLink({ root, provider, expectedEpoch: link.epoch });
|
|
612
|
-
if (previous?.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before
|
|
612
|
+
if (previous?.providerArchived === true) throw Object.assign(new Error('Restore the original provider conversation before turning on Delivery Loop or tracking.'), { code: 'session_archived' });
|
|
613
613
|
if (previous && previous.cwd !== cwd) throw new Error('Session workspace changed; refresh its metadata.');
|
|
614
614
|
return { ...(previous || { schemaVersion: 1, provider, session, cwd, run: null }),
|
|
615
615
|
...(link.consentEpoch ? { providerConsentEpoch: link.consentEpoch } : {}), tracked: true, monitor: true,
|
|
@@ -663,7 +663,7 @@ export async function enrollSession(options) {
|
|
|
663
663
|
await requireClaudeChannel(await readRecord(root, provider, session), metadata, inspectProcess);
|
|
664
664
|
} else {
|
|
665
665
|
metadata = await monitorSession({ root, provider, session, monitor: true, openClient, expectedProviderEpoch: link.epoch, now });
|
|
666
|
-
if (!metadata.model) throw new Error('Codex did not expose this conversation’s model;
|
|
666
|
+
if (!metadata.model) throw new Error('Codex did not expose this conversation’s model; Delivery Loop is unavailable.');
|
|
667
667
|
}
|
|
668
668
|
const cwd = await realpath(metadata.cwd);
|
|
669
669
|
const initialPlan = initialDiagnosisPlan(objective.trim(), contextBootstrap);
|
|
@@ -700,7 +700,7 @@ export async function continueSession(options) {
|
|
|
700
700
|
options = sessionLoopOptions(options);
|
|
701
701
|
const record = await readRecord(options.root, options.provider, options.session);
|
|
702
702
|
if (options.upgradeRunId && (record?.run?.id !== options.upgradeRunId || record.run.managed))
|
|
703
|
-
throw Object.assign(new Error('The original task changed before
|
|
703
|
+
throw Object.assign(new Error('The original task changed before Delivery Loop started.'), { code: 'session_task_changed' });
|
|
704
704
|
if (record?.run && !record.run.managed) {
|
|
705
705
|
// The user asked to continue this task. Adopt its goal and failure budget,
|
|
706
706
|
// rather than enrolling the generic context-bootstrap goal as a new run.
|