@awak-app/simy-cli 0.7.18 → 0.7.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.18",
3
+ "version": "0.7.19",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.18";
17
+ export const CLI_VERSION = "0.7.19";
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({
@@ -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
- reportStatus = status === "unknown" ? "unknown" : "failed";
1030
- reportFailureCode = "artifact_verification_failed";
1031
- reportSummary =
1032
- "SIMY could not safely verify this durable step's output files.";
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
- completed.artifacts = [];
1038
- if (completed.result) completed.result.artifacts = [];
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 = await materializeArtifacts({
1457
- api,
1458
- turnId: normalized.turnId,
1459
- task: completed,
1460
- artifactRefs,
1461
- ...(native ? { preserveAmbiguity: true, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
1462
- ...(normalized.narrativeImageRequest ? { nativeImageReceipt: completed.native_image_receipt, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
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
- if ((isLocalTaskPlanOrchestration(claim) || isDurableLocalTaskStepClaim(claim)) &&
1791
- (claim.execution_spec?.delegation || claim.execution_spec?.delegation_target || claim.execution_spec?.native_identity))
1792
- throw workerError('Native delegation requires the admitted single provider turn.', 'native_delegation_execution_mode_unsupported');
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
- const configuration = await client.request('thread/resume', { threadId: selection.session_id, excludeTurns: true });
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');