@awak-app/simy-cli 0.7.18 → 0.7.20
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 +10 -6
- package/package.json +1 -1
- package/src/auto-update.js +17 -1
- package/src/cli-contract.js +3 -1
- package/src/desktop-access.js +1 -1
- package/src/desktop-host.js +21 -6
- package/src/desktop-update-settings.js +15 -0
- package/src/durable-local-task-steps-contract.js +19 -0
- package/src/durable-local-task-worker.js +160 -24
- package/src/provider-delegation.js +11 -1
- package/src/update-detachment/runtime.js +8 -4
package/README.md
CHANGED
|
@@ -248,15 +248,19 @@ Closing the window keeps the agent running. Open the app again or choose
|
|
|
248
248
|
**Show SIMY** from its menu bar icon to return. **Quit SIMY** asks before
|
|
249
249
|
interrupting active tasks. The normal `simy` command remains available.
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
The macOS **Automatic updates** switch saves this Mac's choice across restarts.
|
|
252
|
+
Compatible signed installations enable it by default. Patch/minor updates wait
|
|
253
|
+
for idle and verify the new runtime before handoff; failed startup restores the
|
|
254
|
+
previous installed version. The app then switches to the verified new interface
|
|
255
|
+
when no unfinished work or confirmation is pending. Turning the switch off keeps
|
|
256
|
+
manual checks and **Download DMG** available. An explicit `SIMY_AUTO_UPDATE=0`
|
|
257
|
+
remains authoritative, and major updates continue to use the DMG.
|
|
258
|
+
**Stop task…** in a task
|
|
255
259
|
detail asks for confirmation and terminates its local Provider process, keeping
|
|
256
260
|
existing output and history. An explicit update can likewise stop active work
|
|
257
261
|
after confirmation; automatic updates still wait for idle. **⌘Q** exits SIMY and
|
|
258
|
-
its background services directly, including when work is running. The window
|
|
259
|
-
|
|
262
|
+
its background services directly, including when work is running. The window shows
|
|
263
|
+
the app version, and normal app launches select the newer installed interface.
|
|
260
264
|
|
|
261
265
|
Older installers that predate native app relaunch support need a one-time
|
|
262
266
|
reinstallation from the official download page to update their Applications
|
package/package.json
CHANGED
package/src/auto-update.js
CHANGED
|
@@ -86,6 +86,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
86
86
|
install_mode: options.installMode,
|
|
87
87
|
automatic: Boolean(
|
|
88
88
|
!options.disabled &&
|
|
89
|
+
options.autoUpdatesEnabled !== false &&
|
|
89
90
|
options.daemon &&
|
|
90
91
|
(options.installMode === "global_npm" ||
|
|
91
92
|
(options.installMode === MACOS_INSTALLER_MODE && options.installerMarker?.teamId)) &&
|
|
@@ -101,7 +102,20 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
snapshot() {
|
|
104
|
-
|
|
105
|
+
const supported = !this.#options.disabled && this.#options.daemon && this.#options.requestedPort === 0 &&
|
|
106
|
+
(this.#options.installMode === 'global_npm' || this.#options.installMode === MACOS_INSTALLER_MODE && Boolean(this.#options.installerMarker?.teamId));
|
|
107
|
+
return structuredClone({ ...this.#snapshot, auto_update_supported: Boolean(supported),
|
|
108
|
+
auto_update_enabled: this.#snapshot.automatic, auto_update_busy: Boolean(this.#applying || this.#confirmedUpdate), requires_task_stop: !this.#isIdle() });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setAutomaticUpdates(enabled) {
|
|
112
|
+
if (typeof enabled !== 'boolean') throw new Error('Choose whether automatic updates are enabled.');
|
|
113
|
+
if (enabled && !this.snapshot().auto_update_supported) throw new Error('Automatic updates are unavailable for this installation.');
|
|
114
|
+
if (this.#applying || this.#confirmedUpdate) throw new Error('Wait for the current update to finish.');
|
|
115
|
+
this.#options.autoUpdatesEnabled = enabled;
|
|
116
|
+
if (!enabled) this.#clearIdleTimer();
|
|
117
|
+
this.#setState(!enabled && this.#snapshot.state === 'waiting_for_idle' ? 'update_available' : this.#snapshot.state,
|
|
118
|
+
{ automatic: enabled, error: null });
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
// Close admission before cancellation begins. The same transaction owns the
|
|
@@ -435,6 +449,7 @@ export async function createCliAutoUpdater({
|
|
|
435
449
|
interactive,
|
|
436
450
|
requestedPort = 0,
|
|
437
451
|
disabled = false,
|
|
452
|
+
autoUpdatesEnabled = true,
|
|
438
453
|
packageRoot = PACKAGE_ROOT,
|
|
439
454
|
webOrigin,
|
|
440
455
|
dependencies = {},
|
|
@@ -458,6 +473,7 @@ export async function createCliAutoUpdater({
|
|
|
458
473
|
interactive: Boolean(interactive),
|
|
459
474
|
requestedPort,
|
|
460
475
|
disabled,
|
|
476
|
+
autoUpdatesEnabled,
|
|
461
477
|
currentVersion: packageInfo.version,
|
|
462
478
|
installMode,
|
|
463
479
|
installerMarker,
|
package/src/cli-contract.js
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
LOCAL_TASK_STEP_CONTEXT_SCHEMA,
|
|
11
11
|
LOCAL_TASK_STEP_SCHEMA,
|
|
12
12
|
MAX_LOCAL_TASK_PLAN_STEPS,
|
|
13
|
+
WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY,
|
|
13
14
|
} from "./durable-local-task-steps-contract.js";
|
|
14
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
16
|
|
|
16
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.20";
|
|
17
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
19
|
|
|
19
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -43,6 +44,7 @@ export function withCliContract(
|
|
|
43
44
|
plan_schema_version: LOCAL_TASK_PLAN_SCHEMA,
|
|
44
45
|
step_schema_version: LOCAL_TASK_STEP_SCHEMA,
|
|
45
46
|
context_schema_version: LOCAL_TASK_STEP_CONTEXT_SCHEMA,
|
|
47
|
+
workflow_artifact_delivery: WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY,
|
|
46
48
|
max_steps: MAX_LOCAL_TASK_PLAN_STEPS,
|
|
47
49
|
},
|
|
48
50
|
execution_capabilities: buildExecutionCapabilityContract({
|
package/src/desktop-access.js
CHANGED
|
@@ -8,7 +8,7 @@ export function validDesktopAccessPath(path, slug) {
|
|
|
8
8
|
}
|
|
9
9
|
export const desktopAccessReady = value => value?.state === 'ready';
|
|
10
10
|
const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization_open', 'organization_select', 'organization_cancel', 'organizations_refresh',
|
|
11
|
-
'check_update', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
|
|
11
|
+
'check_update', 'auto_update_set_enabled', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
|
|
12
12
|
'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
|
|
13
13
|
export function desktopActionAllowed(access, command) {
|
|
14
14
|
return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
|
package/src/desktop-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRemoteHostManager } from './session-supervision/remote-hosts.js';
|
|
2
|
+
import { readDesktopAutoUpdate, writeDesktopAutoUpdate } from './desktop-update-settings.js';
|
|
2
3
|
import { createUpdateRuntime } from './update-detachment/runtime.js';
|
|
3
4
|
import { createCoordinatorReconciler } from "./update-detachment/reconciliation-adapter.js";
|
|
4
5
|
import { createLedgerCoordinatorAdapters } from "./update-detachment/ledger-adapter.js";
|
|
@@ -169,6 +170,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
169
170
|
let encodedStatus = null, lastStatus = null, updateProgress = null;
|
|
170
171
|
let remoteHosts;
|
|
171
172
|
const emitStatus = status => {
|
|
173
|
+
const updateSettings = updater?.snapshot?.() || {};
|
|
174
|
+
status = { ...status, update: { ...status.update,
|
|
175
|
+
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
176
|
+
auto_update_enabled: updateSettings.auto_update_enabled === true,
|
|
177
|
+
auto_update_busy: updateSettings.auto_update_busy === true } };
|
|
172
178
|
if (env.SIMY_REMOTE_CHILD === "1") status = { ...status, remoteSessionProtocol: 1 };
|
|
173
179
|
if (remoteHosts) {
|
|
174
180
|
void remoteHosts.update(providerLinks.codex.connected ? status : { ...status, access: { state: "unavailable" } }).catch(() => {});
|
|
@@ -644,8 +650,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
644
650
|
const sampledAccess = structuredClone(status.access || { state: 'unavailable', organizationId: null, organizationName: null });
|
|
645
651
|
if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
|
|
646
652
|
status.access = sampledAccess;
|
|
647
|
-
status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.());
|
|
648
|
-
if(!desktopAccessReady(sampledAccess))status.updateDetachment={...status.updateDetachment,tasks:[],actions:status.updateDetachment.actions.filter(action=>action==='cancel_detach_update')};
|
|
653
|
+
status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(sampledAccess)});
|
|
649
654
|
const requestedLinks = await decorateProviderSettings(status);
|
|
650
655
|
const sameScope = desktopAccessReady(currentAccess) &&
|
|
651
656
|
accessScope(lastStatus?.access) === accessScope(currentAccess);
|
|
@@ -694,8 +699,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
694
699
|
const sampledAccess = structuredClone(status.access || { state: 'unavailable', organizationId: null, organizationName: null });
|
|
695
700
|
if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
|
|
696
701
|
status.access = sampledAccess;
|
|
697
|
-
status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.());
|
|
698
|
-
if(!desktopAccessReady(sampledAccess))status.updateDetachment={...status.updateDetachment,tasks:[],actions:status.updateDetachment.actions.filter(action=>action==='cancel_detach_update')};
|
|
702
|
+
status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(sampledAccess)});
|
|
699
703
|
const requestedScope = accessScope(currentAccess);
|
|
700
704
|
const requestedLinks = await decorateProviderSettings(status);
|
|
701
705
|
const allRecords = await recordReader.list();
|
|
@@ -841,6 +845,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
841
845
|
updater = await (dependencies.createUpdater || (process.platform === "win32" ? (() => createWindowsDesktopUpdater({ version: packageInfo.version, disabled: env.SIMY_AUTO_UPDATE === "0" })) : createCliAutoUpdater))({
|
|
842
846
|
daemon: true, interactive: true, webOrigin,
|
|
843
847
|
disabled: env.SIMY_AUTO_UPDATE === "0",
|
|
848
|
+
autoUpdatesEnabled: readDesktopAutoUpdate(root),
|
|
844
849
|
...(updatePackageRoot ? { packageRoot: updatePackageRoot } : {}),
|
|
845
850
|
dependencies: { packageInfo: updatePackageRoot ? await readCliPackageInfo(updatePackageRoot) : packageInfo,
|
|
846
851
|
logger: message => diagnostics.write(`update: ${message}`) },
|
|
@@ -1055,7 +1060,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1055
1060
|
return;
|
|
1056
1061
|
}
|
|
1057
1062
|
if(command.action==='cancel_detach_update') {
|
|
1058
|
-
try {updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.());coordinator.cancel();emit({type:'result',id:command.id,ok:true});}
|
|
1063
|
+
try {updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(currentAccess)});coordinator.cancel();emit({type:'result',id:command.id,ok:true});}
|
|
1059
1064
|
catch {emit({type:'result',id:command.id,ok:false,message:'This update operation changed. Refresh its status.'});}
|
|
1060
1065
|
return;
|
|
1061
1066
|
}
|
|
@@ -1124,7 +1129,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1124
1129
|
const access = await accessSnapshot();
|
|
1125
1130
|
const commandScope = accessScope(access);
|
|
1126
1131
|
if (!desktopActionAllowed(access, command)) throw Object.assign(new Error('Desktop access is required.'), { code: 'desktop_access_required' });
|
|
1127
|
-
const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) ||
|
|
1132
|
+
const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) || action === 'auto_update_set_enabled' && command.enabled === false ||
|
|
1128
1133
|
['session_set_auto_continue', 'session_set_email_reports', 'team_thread_share'].includes(action) && command.enabled === false;
|
|
1129
1134
|
if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
|
|
1130
1135
|
else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
|
|
@@ -1278,6 +1283,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1278
1283
|
if (!result.opened) throw new Error('Browser unavailable.');
|
|
1279
1284
|
} catch { throw Object.assign(new Error('Browser unavailable.'), { code: 'desktop_browser_unavailable' }); }
|
|
1280
1285
|
}
|
|
1286
|
+
} else if (action === 'auto_update_set_enabled') {
|
|
1287
|
+
if (typeof command.enabled !== 'boolean' || typeof updater.setAutomaticUpdates !== 'function') throw new Error('Automatic updates are unavailable.');
|
|
1288
|
+
const previous = updater.snapshot().auto_update_enabled === true;
|
|
1289
|
+
updater.setAutomaticUpdates(command.enabled);
|
|
1290
|
+
try { writeDesktopAutoUpdate(root, command.enabled); }
|
|
1291
|
+
catch (error) { updater.setAutomaticUpdates(previous); throw error; }
|
|
1292
|
+
emit({ type: 'result', id: command.id, ok: true });
|
|
1293
|
+
await publish();
|
|
1294
|
+
if (command.enabled) setImmediate(() => { if (!closed && !relay) void updater.checkNow().catch(() => {}); });
|
|
1295
|
+
return;
|
|
1281
1296
|
} else if (action === "check_update") await updater.checkNow();
|
|
1282
1297
|
else if (action === "update") {
|
|
1283
1298
|
const status = await agent.getDesktopStatus();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readState, atomicSave } from './update-detachment/state-store.js';
|
|
3
|
+
|
|
4
|
+
export function readDesktopAutoUpdate(root) {
|
|
5
|
+
try {
|
|
6
|
+
const settings = readState(join(root, 'desktop-update-settings.json'));
|
|
7
|
+
return settings === null ? true : settings.enabled === true;
|
|
8
|
+
}
|
|
9
|
+
catch { return false; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function writeDesktopAutoUpdate(root, enabled) {
|
|
13
|
+
if (typeof enabled !== 'boolean') throw new Error('Choose whether automatic updates are enabled.');
|
|
14
|
+
atomicSave(join(root, 'desktop-update-settings.json'), { schemaVersion: 1, enabled });
|
|
15
|
+
}
|
|
@@ -20,6 +20,11 @@ export const LOCAL_TASK_PLAN_ORCHESTRATION_SCHEMA =
|
|
|
20
20
|
export const LOCAL_TASK_PLAN_SCHEMA = "local_task_plan.v1";
|
|
21
21
|
export const LOCAL_TASK_STEP_SCHEMA = "local_task_step.v1";
|
|
22
22
|
export const LOCAL_TASK_STEP_CONTEXT_SCHEMA = "local_task_step_context.v1";
|
|
23
|
+
// This optional field stays on the existing closed claim schema. Backend only
|
|
24
|
+
// emits it to devices that advertise the matching capability, so older strict
|
|
25
|
+
// clients never receive a field they cannot parse.
|
|
26
|
+
export const WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY =
|
|
27
|
+
"workflow_local_task_artifact.v1";
|
|
23
28
|
export const MAX_LOCAL_TASK_PLAN_STEPS = 8;
|
|
24
29
|
|
|
25
30
|
const UUID_PATTERN =
|
|
@@ -254,6 +259,9 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
254
259
|
"lease_expires_at",
|
|
255
260
|
"claim_replayed",
|
|
256
261
|
...(Object.hasOwn(claim, "model") ? ["model"] : []),
|
|
262
|
+
...(Object.hasOwn(claim, "workflow_artifact_delivery")
|
|
263
|
+
? ["workflow_artifact_delivery"]
|
|
264
|
+
: []),
|
|
257
265
|
]),
|
|
258
266
|
"claim",
|
|
259
267
|
);
|
|
@@ -264,6 +272,15 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
264
272
|
fail("Unsupported durable Local Task step disposition.");
|
|
265
273
|
}
|
|
266
274
|
const model = claim.model == null ? null : requiredString(claim.model, "model", 160);
|
|
275
|
+
const workflowArtifactDelivery = claim.workflow_artifact_delivery === undefined
|
|
276
|
+
? false
|
|
277
|
+
: claim.workflow_artifact_delivery === WORKFLOW_LOCAL_TASK_ARTIFACT_DELIVERY;
|
|
278
|
+
if (
|
|
279
|
+
claim.workflow_artifact_delivery !== undefined &&
|
|
280
|
+
!workflowArtifactDelivery
|
|
281
|
+
) {
|
|
282
|
+
fail("Unsupported durable Local Task WorkFlow artifact delivery contract.");
|
|
283
|
+
}
|
|
267
284
|
const turnId = requiredUuid(claim.turn_id, "turn_id");
|
|
268
285
|
const plan = normalizePlan(claim.plan);
|
|
269
286
|
const currentStepId = requiredStepId(claim.current_step_id, "current_step_id");
|
|
@@ -309,6 +326,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
309
326
|
leaseEpoch: null,
|
|
310
327
|
leaseExpiresAtMs: null,
|
|
311
328
|
claimReplayed: false,
|
|
329
|
+
workflowArtifactDelivery,
|
|
312
330
|
};
|
|
313
331
|
}
|
|
314
332
|
|
|
@@ -366,6 +384,7 @@ export function normalizeDurableLocalTaskStepClaim(
|
|
|
366
384
|
leaseEpoch,
|
|
367
385
|
leaseExpiresAtMs,
|
|
368
386
|
claimReplayed: claim.claim_replayed,
|
|
387
|
+
workflowArtifactDelivery,
|
|
369
388
|
remainingProviderTokens: remainingTokens,
|
|
370
389
|
remainingTimeoutMs: remainingMs,
|
|
371
390
|
};
|
|
@@ -361,6 +361,19 @@ export function createDurableLocalTaskApi({
|
|
|
361
361
|
{ ambiguousOnTransport: true },
|
|
362
362
|
);
|
|
363
363
|
},
|
|
364
|
+
async prepareWorkflowArtifact({ turnId, workerId, leaseEpoch, localArtifactId, name, mimeType, sizeBytes, sha256 }) {
|
|
365
|
+
return request(`local-tasks/${encodeURIComponent(turnId)}/workflow-artifacts/prepare`, {
|
|
366
|
+
turn_id: turnId, worker_id: workerId, lease_epoch: leaseEpoch,
|
|
367
|
+
local_artifact_id: localArtifactId, name, mime_type: mimeType,
|
|
368
|
+
size_bytes: sizeBytes, sha256,
|
|
369
|
+
}, { ambiguousOnTransport: true });
|
|
370
|
+
},
|
|
371
|
+
async commitWorkflowArtifact({ uploadId, turnId, workerId, leaseEpoch, localArtifactId, sha256, sizeBytes }) {
|
|
372
|
+
return request(`local-tasks/${encodeURIComponent(turnId)}/workflow-artifacts/commit`, {
|
|
373
|
+
upload_id: uploadId, turn_id: turnId, worker_id: workerId, lease_epoch: leaseEpoch,
|
|
374
|
+
local_artifact_id: localArtifactId, sha256, size_bytes: sizeBytes,
|
|
375
|
+
}, { ambiguousOnTransport: true });
|
|
376
|
+
},
|
|
364
377
|
async heartbeatStep(input) {
|
|
365
378
|
const contract = stepContract.heartbeat(input);
|
|
366
379
|
const payload = await request(
|
|
@@ -677,6 +690,7 @@ export function createDurableLocalTaskWorker({
|
|
|
677
690
|
persist = persistLocalTask,
|
|
678
691
|
stopTask = stopLocalTask,
|
|
679
692
|
materializeArtifacts = materializePipelineArtifacts,
|
|
693
|
+
materializeWorkflowArtifacts = materializeWorkflowLocalTaskArtifacts,
|
|
680
694
|
attachmentStore = null,
|
|
681
695
|
getDeviceId = () => null,
|
|
682
696
|
readPhotoSource = readLocalPhotoSource,
|
|
@@ -996,10 +1010,6 @@ export function createDurableLocalTaskWorker({
|
|
|
996
1010
|
if (!await checkLease()) return;
|
|
997
1011
|
await runHeartbeat();
|
|
998
1012
|
if (leaseLost) return;
|
|
999
|
-
if (heartbeatTimer) {
|
|
1000
|
-
clearIntervalImpl(heartbeatTimer);
|
|
1001
|
-
heartbeatTimer = null;
|
|
1002
|
-
}
|
|
1003
1013
|
const status = completed.status === "stopped"
|
|
1004
1014
|
? "cancelled"
|
|
1005
1015
|
: boundary === "unknown"
|
|
@@ -1017,25 +1027,59 @@ export function createDurableLocalTaskWorker({
|
|
|
1017
1027
|
status,
|
|
1018
1028
|
reportFailureCode,
|
|
1019
1029
|
);
|
|
1030
|
+
const workflowArtifactDelivery =
|
|
1031
|
+
normalized.workflowArtifactDelivery ||
|
|
1032
|
+
context.workflowArtifactDelivery === true;
|
|
1020
1033
|
let artifactRefs = [];
|
|
1021
1034
|
try {
|
|
1022
1035
|
artifactRefs = buildLocalTaskArtifactRefs(
|
|
1023
1036
|
completed.artifacts,
|
|
1024
1037
|
completed.id,
|
|
1025
1038
|
);
|
|
1039
|
+
// Durable step claims intentionally have a closed contract and do not
|
|
1040
|
+
// repeat the parent execution spec. Preserve the parent-scoped
|
|
1041
|
+
// WorkFlow delivery contract in the same worker context so each
|
|
1042
|
+
// step's verified bytes are materialized before its terminal receipt.
|
|
1043
|
+
if (
|
|
1044
|
+
reportStatus === "succeeded" &&
|
|
1045
|
+
workflowArtifactDelivery &&
|
|
1046
|
+
artifactRefs.length > 0
|
|
1047
|
+
) {
|
|
1048
|
+
artifactRefs = await materializeWorkflowArtifacts({
|
|
1049
|
+
api,
|
|
1050
|
+
turnId: normalized.turnId,
|
|
1051
|
+
task: completed,
|
|
1052
|
+
artifactRefs,
|
|
1053
|
+
workerId,
|
|
1054
|
+
leaseEpoch: normalized.leaseEpoch,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1026
1057
|
} catch (error) {
|
|
1027
1058
|
artifactRefs = [];
|
|
1028
1059
|
if (status !== "cancelled") {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1060
|
+
const contractFailure = error instanceof LocalTaskArtifactContractError;
|
|
1061
|
+
const ambiguousDelivery =
|
|
1062
|
+
workflowArtifactDelivery &&
|
|
1063
|
+
(error?.ambiguous === true || error?.status >= 500);
|
|
1064
|
+
reportStatus = status === "unknown" || ambiguousDelivery
|
|
1065
|
+
? "unknown"
|
|
1066
|
+
: "failed";
|
|
1067
|
+
reportFailureCode = contractFailure
|
|
1068
|
+
? "artifact_verification_failed"
|
|
1069
|
+
: "artifact_materialization_failed";
|
|
1070
|
+
reportSummary = workflowArtifactDelivery && ambiguousDelivery
|
|
1071
|
+
? workflowArtifactRecoverySummary()
|
|
1072
|
+
: reportFailureCode === "artifact_verification_failed"
|
|
1073
|
+
? "SIMY could not safely verify this durable step's output files."
|
|
1074
|
+
: "SIMY created the durable step output files locally but could not safely attach them to this WorkFlow run.";
|
|
1033
1075
|
completed.status =
|
|
1034
1076
|
reportStatus === "failed" ? "failed" : completed.status;
|
|
1035
1077
|
completed.reason_code = reportFailureCode;
|
|
1036
1078
|
completed.error = error.message;
|
|
1037
|
-
|
|
1038
|
-
|
|
1079
|
+
if (contractFailure) {
|
|
1080
|
+
completed.artifacts = [];
|
|
1081
|
+
if (completed.result) completed.result.artifacts = [];
|
|
1082
|
+
}
|
|
1039
1083
|
await persist(completed);
|
|
1040
1084
|
}
|
|
1041
1085
|
onError(
|
|
@@ -1450,17 +1494,16 @@ export function createDurableLocalTaskWorker({
|
|
|
1450
1494
|
deliveryStage = "artifacts";
|
|
1451
1495
|
if (
|
|
1452
1496
|
(reportStatus === "succeeded" || native) &&
|
|
1453
|
-
(normalized.pipelineContext || normalized.narrativeImageRequest || native) &&
|
|
1497
|
+
(normalized.pipelineContext || normalized.workflowArtifactDelivery || normalized.narrativeImageRequest || native) &&
|
|
1454
1498
|
artifactRefs.length > 0
|
|
1455
1499
|
) {
|
|
1456
|
-
artifactRefs =
|
|
1457
|
-
api,
|
|
1458
|
-
|
|
1459
|
-
task: completed,
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
});
|
|
1500
|
+
artifactRefs = normalized.workflowArtifactDelivery
|
|
1501
|
+
? await materializeWorkflowArtifacts({ api, turnId: normalized.turnId, task: completed,
|
|
1502
|
+
artifactRefs, workerId, leaseEpoch: normalized.leaseEpoch })
|
|
1503
|
+
: await materializeArtifacts({ api, turnId: normalized.turnId, task: completed, artifactRefs,
|
|
1504
|
+
...(native ? { preserveAmbiguity: true, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
|
|
1505
|
+
...(normalized.narrativeImageRequest ? { nativeImageReceipt: completed.native_image_receipt, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
|
|
1506
|
+
});
|
|
1464
1507
|
}
|
|
1465
1508
|
} catch (error) {
|
|
1466
1509
|
const contractFailure = error instanceof LocalTaskArtifactContractError;
|
|
@@ -1471,7 +1514,7 @@ export function createDurableLocalTaskWorker({
|
|
|
1471
1514
|
if (status !== "cancelled") {
|
|
1472
1515
|
// A gateway 5xx can hide a committed image. Never present that outcome
|
|
1473
1516
|
// as a clean failure that invites a second generation.
|
|
1474
|
-
reportStatus = status === "unknown" || ((normalized.narrativeImageRequest || native) &&
|
|
1517
|
+
reportStatus = status === "unknown" || ((normalized.narrativeImageRequest || native || normalized.workflowArtifactDelivery) &&
|
|
1475
1518
|
(error?.ambiguous === true || error?.status >= 500)) ? "unknown" : "failed";
|
|
1476
1519
|
reportFailureCode = normalized.narrativeImageRequest && error?.code === "narrative_image_receipt_missing"
|
|
1477
1520
|
? "narrative_image_receipt_missing"
|
|
@@ -1483,6 +1526,8 @@ export function createDurableLocalTaskWorker({
|
|
|
1483
1526
|
reportSummary =
|
|
1484
1527
|
native ? recoverySummary(reportStatus, reportFailureCode) : normalized.narrativeImageRequest
|
|
1485
1528
|
? "SIMY could not confirm the generated image delivery. Keep the local output for verification; do not regenerate it."
|
|
1529
|
+
: normalized.workflowArtifactDelivery && reportStatus === "unknown"
|
|
1530
|
+
? workflowArtifactRecoverySummary()
|
|
1486
1531
|
: deliveryStage === "destination"
|
|
1487
1532
|
? "SIMY could not confirm delivery to the selected local folder. Existing files and local outputs were preserved."
|
|
1488
1533
|
: contractFailure
|
|
@@ -1507,7 +1552,7 @@ export function createDurableLocalTaskWorker({
|
|
|
1507
1552
|
reportFailureCode,
|
|
1508
1553
|
)
|
|
1509
1554
|
: workerError(
|
|
1510
|
-
normalized.narrativeImageRequest
|
|
1555
|
+
normalized.narrativeImageRequest || (normalized.workflowArtifactDelivery && reportStatus === "unknown")
|
|
1511
1556
|
? reportSummary
|
|
1512
1557
|
: `SIMY could not persist the Local Task output files for this Pipeline run. ${errorMessage(error)} Keep SIMY CLI running, check the connection, and start a new Pipeline run.`,
|
|
1513
1558
|
reportFailureCode,
|
|
@@ -1787,11 +1832,18 @@ export function createDurableLocalTaskWorker({
|
|
|
1787
1832
|
releaseWork();
|
|
1788
1833
|
return false;
|
|
1789
1834
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1835
|
+
// An orchestration envelope only authorizes the CLI to submit a
|
|
1836
|
+
// controlled plan. It does not dispatch a Provider. Its follow-up
|
|
1837
|
+
// may be the existing single-provider native claim, but must never
|
|
1838
|
+
// become a durable step: executeStepClaim has no native delegation
|
|
1839
|
+
// receipt contract.
|
|
1840
|
+
const orchestrationCarriesNativeDelegation = isLocalTaskPlanOrchestration(claim) &&
|
|
1841
|
+
Boolean(claim.execution_spec?.delegation || claim.execution_spec?.delegation_target || claim.execution_spec?.native_identity);
|
|
1793
1842
|
if (isLocalTaskPlanOrchestration(claim)) {
|
|
1794
1843
|
const orchestration = claim;
|
|
1844
|
+
context.workflowArtifactDelivery =
|
|
1845
|
+
orchestration.execution_spec?.workflow_artifact_delivery ===
|
|
1846
|
+
"workflow_local_task_artifact.v1";
|
|
1795
1847
|
const controlledPlan = buildControlledLocalTaskPlan(orchestration);
|
|
1796
1848
|
await prepareDurableStepAttachments({
|
|
1797
1849
|
orchestration,
|
|
@@ -1820,6 +1872,9 @@ export function createDurableLocalTaskWorker({
|
|
|
1820
1872
|
"local_task_plan_claim_mismatch",
|
|
1821
1873
|
);
|
|
1822
1874
|
}
|
|
1875
|
+
if (orchestrationCarriesNativeDelegation && isDurableLocalTaskStepClaim(claim)) {
|
|
1876
|
+
throw workerError('Native delegation requires the admitted single provider turn.', 'native_delegation_execution_mode_unsupported');
|
|
1877
|
+
}
|
|
1823
1878
|
}
|
|
1824
1879
|
if (requestedTurnId && claim.turn_id !== requestedTurnId) {
|
|
1825
1880
|
throw workerError(
|
|
@@ -2042,6 +2097,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
|
|
|
2042
2097
|
throw workerError("Unsupported narrative image execution scope.", "invalid_narrative_image_execution_scope");
|
|
2043
2098
|
}
|
|
2044
2099
|
const pipelineContext = normalizePipelineContext(spec);
|
|
2100
|
+
const workflowArtifactDelivery = spec.workflow_artifact_delivery === "workflow_local_task_artifact.v1";
|
|
2045
2101
|
const scenarioPack = normalizeLocalTaskScenarioPack(spec.scenario_pack);
|
|
2046
2102
|
return {
|
|
2047
2103
|
turnId,
|
|
@@ -2052,6 +2108,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
|
|
|
2052
2108
|
uploadedRefs,
|
|
2053
2109
|
spec,
|
|
2054
2110
|
pipelineContext,
|
|
2111
|
+
workflowArtifactDelivery,
|
|
2055
2112
|
scenarioPack,
|
|
2056
2113
|
narrativeImageRequest,
|
|
2057
2114
|
};
|
|
@@ -2724,6 +2781,61 @@ export async function materializePipelineArtifacts({
|
|
|
2724
2781
|
return materialized;
|
|
2725
2782
|
}
|
|
2726
2783
|
|
|
2784
|
+
export async function materializeWorkflowLocalTaskArtifacts({
|
|
2785
|
+
api, turnId, task, artifactRefs, workerId, leaseEpoch,
|
|
2786
|
+
}) {
|
|
2787
|
+
if (
|
|
2788
|
+
!api || typeof api.prepareWorkflowArtifact !== "function" ||
|
|
2789
|
+
typeof api.uploadArtifact !== "function" || typeof api.commitWorkflowArtifact !== "function"
|
|
2790
|
+
) {
|
|
2791
|
+
throw workerError(
|
|
2792
|
+
"This SIMY CLI cannot persist WorkFlow output files. Update SIMY CLI and retry the WorkFlow run.",
|
|
2793
|
+
"artifact_materialization_unsupported",
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
const materialized = [];
|
|
2797
|
+
for (const ref of artifactRefs) {
|
|
2798
|
+
const artifact = await localTaskArtifact(task, ref.id);
|
|
2799
|
+
if (!artifact) throw workerError("The WorkFlow output changed before it could be attached.", "artifact_materialization_source_changed");
|
|
2800
|
+
const bytes = await readFile(artifact.local_path);
|
|
2801
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
2802
|
+
if (bytes.byteLength !== ref.size_bytes || digest !== ref.sha256.toLowerCase()) {
|
|
2803
|
+
throw workerError("The WorkFlow output changed before it could be attached.", "artifact_materialization_source_changed");
|
|
2804
|
+
}
|
|
2805
|
+
const prepared = await retryAmbiguousArtifactStep(() => api.prepareWorkflowArtifact({
|
|
2806
|
+
turnId, workerId, leaseEpoch, localArtifactId: ref.id, name: ref.name,
|
|
2807
|
+
mimeType: ref.mime_type, sizeBytes: ref.size_bytes, sha256: ref.sha256.toLowerCase(),
|
|
2808
|
+
}), { preserveAmbiguity: true });
|
|
2809
|
+
validatePreparedArtifact(prepared);
|
|
2810
|
+
await uploadWorkflowArtifactForCommitVerification(api, {
|
|
2811
|
+
uploadUrl: prepared.upload_url, method: prepared.method, headers: prepared.headers, bytes,
|
|
2812
|
+
});
|
|
2813
|
+
const committed = await retryAmbiguousArtifactStep(() => api.commitWorkflowArtifact({
|
|
2814
|
+
uploadId: prepared.upload_id, turnId, workerId, leaseEpoch, localArtifactId: ref.id,
|
|
2815
|
+
sha256: ref.sha256.toLowerCase(), sizeBytes: ref.size_bytes,
|
|
2816
|
+
}), { preserveAmbiguity: true });
|
|
2817
|
+
materialized.push(validateWorkflowCommittedArtifact(committed, ref));
|
|
2818
|
+
}
|
|
2819
|
+
return materialized;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
async function uploadWorkflowArtifactForCommitVerification(api, input) {
|
|
2823
|
+
try {
|
|
2824
|
+
await retryAmbiguousArtifactStep(
|
|
2825
|
+
() => api.uploadArtifact(input),
|
|
2826
|
+
{ preserveAmbiguity: true },
|
|
2827
|
+
);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
// An immutable signed S3 PUT may have stored the exact bytes before its
|
|
2830
|
+
// response was lost. A later precondition failure only permits the
|
|
2831
|
+
// lease-fenced commit to verify that possibility; it is never publish
|
|
2832
|
+
// confirmation by itself.
|
|
2833
|
+
if (!(error instanceof DurableLocalTaskApiError) || error.status !== 412) {
|
|
2834
|
+
throw error;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2727
2839
|
async function retryAmbiguousArtifactStep(operation, { preserveAmbiguity = false } = {}) {
|
|
2728
2840
|
let lastError = null;
|
|
2729
2841
|
let ambiguous = false;
|
|
@@ -2791,6 +2903,26 @@ function validateCommittedArtifact(value, expected) {
|
|
|
2791
2903
|
};
|
|
2792
2904
|
}
|
|
2793
2905
|
|
|
2906
|
+
function validateWorkflowCommittedArtifact(value, expected) {
|
|
2907
|
+
const artifact = value?.workflow_artifact;
|
|
2908
|
+
if (
|
|
2909
|
+
!artifact || typeof artifact !== "object" || !isUuid(String(artifact.artifact_id || "")) ||
|
|
2910
|
+
artifact.name !== expected.name || artifact.mime_type !== expected.mime_type ||
|
|
2911
|
+
artifact.size_bytes !== expected.size_bytes ||
|
|
2912
|
+
String(artifact.sha256 || "").toLowerCase() !== expected.sha256.toLowerCase()
|
|
2913
|
+
) {
|
|
2914
|
+
throw workerError(
|
|
2915
|
+
"SIMY could not confirm the persisted WorkFlow output file.",
|
|
2916
|
+
"artifact_materialization_invalid_response",
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
id: expected.id, local_task_id: expected.local_task_id, name: expected.name,
|
|
2921
|
+
relative_path: expected.relative_path, mime_type: expected.mime_type,
|
|
2922
|
+
size_bytes: expected.size_bytes, sha256: expected.sha256.toLowerCase(), verified: true,
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2794
2926
|
function trustedArtifactUploadUrl(value) {
|
|
2795
2927
|
let target;
|
|
2796
2928
|
try {
|
|
@@ -2938,6 +3070,10 @@ function recoverySummary(status, failureCode) {
|
|
|
2938
3070
|
return `The Local Task failed (${failureCode || "unknown error"}).`;
|
|
2939
3071
|
}
|
|
2940
3072
|
|
|
3073
|
+
function workflowArtifactRecoverySummary() {
|
|
3074
|
+
return "SIMY could not confirm WorkFlow output delivery. Existing local output files were preserved. Keep SIMY CLI running while delivery is reconciled; do not rerun this WorkFlow.";
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2941
3077
|
function durableResultSummary(completed, status, failureCode) {
|
|
2942
3078
|
// Process exit details such as signal-derived code 130 are useful only in
|
|
2943
3079
|
// the local technical record. A user-requested stop has one stable public
|
|
@@ -156,7 +156,17 @@ async function runCodexDelegation({ selection, instruction, getInstruction, repo
|
|
|
156
156
|
if (!accountMatches(account.account?.email)) throw fail('provider_account_mismatch');
|
|
157
157
|
const metadata = await readCodexSession(client, selection.session_id, { signal });
|
|
158
158
|
if (metadata.isRunning === true) throw fail('provider_session_busy');
|
|
159
|
-
|
|
159
|
+
if (metadata.model !== selection.model) throw fail('provider_session_mismatch');
|
|
160
|
+
// A fresh app-server process otherwise resolves a resumed read-only
|
|
161
|
+
// session against the local default sandbox. Reapply only the already
|
|
162
|
+
// bound, narrower read contract before inspecting it; this never grants
|
|
163
|
+
// workspace-write authority or changes the selected session identity.
|
|
164
|
+
const resumeParams = { threadId: selection.session_id, excludeTurns: true };
|
|
165
|
+
if (permissionMode === 'read_only') {
|
|
166
|
+
resumeParams.model = selection.model;
|
|
167
|
+
resumeParams.sandbox = 'read-only';
|
|
168
|
+
}
|
|
169
|
+
const configuration = await client.request('thread/resume', resumeParams);
|
|
160
170
|
if (configuration.thread?.id !== selection.session_id || configuration.model !== selection.model ||
|
|
161
171
|
!configuration.cwd || await realpath(configuration.cwd) !== await realpath(nativeIdentity ? metadata.cwd : repositoryPath) ||
|
|
162
172
|
workspaceMode === 'repository' && await realpath(configuration.cwd) !== await realpath(repositoryPath)) throw fail('provider_session_mismatch');
|
|
@@ -259,18 +259,22 @@ export function createUpdateRuntime({ root, origin, readContext, listRecords, ob
|
|
|
259
259
|
};
|
|
260
260
|
return {bind,boundary,reconcile,store,gate:adapters.gate,
|
|
261
261
|
async initialize() {if(!transaction)write({connectionState:readDispatchGate(root).connection,phase:'startup',updateState:'pending'});},
|
|
262
|
-
snapshot(base,update) {
|
|
262
|
+
snapshot(base,update,{accessReady=true}={}) {
|
|
263
263
|
if(ownerTransferred)transaction=readState(file);
|
|
264
264
|
const action=base.state==='connected' && update?.state==='waiting_for_idle'?'detach_update':base.state==='detaching'?'cancel_detach_update':!base.busy && ['detached','recovery_required'].includes(base.state)?'update_reconnect':null;
|
|
265
265
|
const frame={...base,schemaVersion:1,connectionState:base.state,
|
|
266
|
-
updateState:transaction?.updateState || 'pending',tasks:rows(),actions:action?[action]:[]};
|
|
266
|
+
updateState:transaction?.updateState || 'pending',updaterState:update?.state || 'unknown',tasks:rows(),actions:action?[action]:[]};
|
|
267
|
+
// Revision the exact frame the native client sees. Filtering after the
|
|
268
|
+
// fingerprint makes an access change look like conflicting data at the
|
|
269
|
+
// same revision, which correctly fences all native update actions.
|
|
270
|
+
if(!accessReady) {frame.tasks=[];frame.actions=frame.actions.filter(action=>action==='cancel_detach_update');}
|
|
267
271
|
const fingerprint=digest(frame);
|
|
268
272
|
if(!ownerTransferred && transaction && transaction.presentation!==fingerprint)write({presentation:fingerprint});
|
|
269
273
|
presented={...frame,operationId:transaction?.operationId,revision:transaction?.revision || 0};
|
|
270
274
|
return presented;
|
|
271
275
|
},
|
|
272
|
-
validate(command,base,update) {
|
|
273
|
-
const frame=this.snapshot(base,update);
|
|
276
|
+
validate(command,base,update,options) {
|
|
277
|
+
const frame=this.snapshot(base,update,options);
|
|
274
278
|
if(command.operationId!==frame.operationId || command.revision!==frame.revision || !frame.actions.includes(command.action) || transaction?.claimedCommands?.includes(command.id))throw failure('stale_operation');
|
|
275
279
|
write({claimedCommands:[...(transaction?.claimedCommands || []).slice(-255),command.id]});
|
|
276
280
|
},
|