@awak-app/simy-cli 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +225 -98
  2. package/package.json +5 -3
  3. package/src/agent.js +129 -7
  4. package/src/backend-executable.js +7 -6
  5. package/src/bounded-local-task-subtask-pool.js +32 -6
  6. package/src/bounded-local-task-subtasks-contract.js +1 -1
  7. package/src/cli-contract.js +1 -1
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  9. package/src/codex-plugins/simy/hooks/README.md +8 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
  11. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  12. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  19. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  20. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
  21. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  22. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
  23. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
  24. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
  25. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
  26. package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
  27. package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
  28. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
  29. package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
  30. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  31. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  32. package/src/coding-run-recovery.js +4 -2
  33. package/src/delivery-hooks/claude-workflow.md +10 -7
  34. package/src/delivery-hooks/command.js +76 -10
  35. package/src/delivery-hooks/consistency-monitor.js +20 -4
  36. package/src/delivery-hooks/consult.js +60 -0
  37. package/src/delivery-hooks/diagnosis.js +2 -2
  38. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  39. package/src/delivery-hooks/local-runtime.js +81 -1
  40. package/src/delivery-hooks/local-supervisor.js +13 -4
  41. package/src/delivery-hooks/repair-continuation.js +80 -0
  42. package/src/delivery-hooks/repair.js +164 -0
  43. package/src/delivery-hooks/supervisor-handover.js +84 -0
  44. package/src/delivery-hooks/supervisor-owner.js +3 -2
  45. package/src/delivery-hooks/supervisor-service.js +38 -6
  46. package/src/delivery-hooks/supervisor-watchdog.js +15 -4
  47. package/src/delivery-hooks/supervisor.js +151 -18
  48. package/src/desktop-access.js +2 -1
  49. package/src/desktop-host.js +35 -2
  50. package/src/durable-local-task-worker.js +372 -58
  51. package/src/execution-capability-contract.js +15 -1
  52. package/src/index.js +3 -3
  53. package/src/local-photos-read.swift +33 -0
  54. package/src/local-photos.js +47 -0
  55. package/src/local-task-attachment-store.js +31 -0
  56. package/src/local-task-destination.js +174 -0
  57. package/src/local-task.js +119 -7
  58. package/src/native-delegation-contract.js +68 -0
  59. package/src/native-provider-identity.js +60 -0
  60. package/src/orchestrator/audit.js +24 -323
  61. package/src/orchestrator/budget.js +22 -11
  62. package/src/orchestrator/charter-validation.js +3 -41
  63. package/src/orchestrator/checklist-continuation.js +2 -14
  64. package/src/orchestrator/completion-contract.js +4 -49
  65. package/src/orchestrator/contract.js +5 -9
  66. package/src/orchestrator/delivery/WORKFLOW.md +30 -710
  67. package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
  68. package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
  69. package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
  70. package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
  71. package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
  72. package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
  73. package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
  74. package/src/orchestrator/delivery/references/pr-closure.md +4 -200
  75. package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
  76. package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
  77. package/src/orchestrator/delivery/references/state-model.md +4 -185
  78. package/src/orchestrator/delivery/references/ux-specification.md +4 -95
  79. package/src/orchestrator/delivery-level.js +2 -10
  80. package/src/orchestrator/evidence.js +2 -3
  81. package/src/orchestrator/independent-audit.js +15 -164
  82. package/src/orchestrator/instruction.js +19 -254
  83. package/src/orchestrator/loop.js +35 -95
  84. package/src/orchestrator/outcome-monitor.js +21 -0
  85. package/src/orchestrator/pr-closure.js +2 -16
  86. package/src/orchestrator/recovery.js +32 -2
  87. package/src/orchestrator/ux-specification.js +3 -12
  88. package/src/provider-delegation.js +336 -0
  89. package/src/runner.js +5 -2
  90. package/src/session-supervision/acceptance-reads.js +34 -0
  91. package/src/session-supervision/artifact-observer.js +138 -0
  92. package/src/session-supervision/browser-cdp.js +59 -0
  93. package/src/session-supervision/browser-recovery.js +97 -0
  94. package/src/session-supervision/codex-app-server.js +61 -22
  95. package/src/session-supervision/completion-artifacts.js +44 -0
  96. package/src/session-supervision/consistency-auditor.js +8 -11
  97. package/src/session-supervision/continuation-analysis.js +47 -28
  98. package/src/session-supervision/final-response-recovery.js +5 -0
  99. package/src/session-supervision/index.js +22 -5
  100. package/src/session-supervision/interaction-proxy.js +250 -0
  101. package/src/session-supervision/interaction-task.js +189 -0
  102. package/src/session-supervision/native-interaction.js +102 -0
  103. package/src/session-supervision/provider-failure.js +40 -0
  104. package/src/session-supervision/purpose-instruction.js +14 -14
  105. package/src/session-supervision/purpose-recovery.js +32 -12
  106. package/src/session-supervision/purpose-verification.js +50 -216
  107. package/src/session-supervision/recovery-frontier.js +55 -0
  108. package/src/session-supervision/stop-notifications.js +8 -8
  109. package/src/sqm/agent-skill.js +1 -1
  110. package/src/sqm/checkers.js +12 -6
  111. package/src/sqm/claude-plugin-update.js +22 -8
  112. package/src/sqm/claude-plugin.js +6 -0
  113. package/src/sqm/local-tests.js +2 -1
  114. package/src/sqm/plugin-cache.js +67 -0
  115. package/src/sqm/proof.js +25 -0
  116. package/src/sqm/report.js +1 -0
  117. package/src/sqm/workflow-onboarding.js +3 -39
  118. package/src/sqm/workflow-update.js +17 -6
  119. package/src/startup-preflight.js +2 -20
  120. package/src/team-status-fence-store.js +32 -0
  121. package/src/team-threads.js +457 -0
@@ -1,3 +1,6 @@
1
+ import { readLocalPhotoSource } from "./local-photos.js";
2
+ import { normalizeLocalTaskDestination, resolveLocalTaskDestination, saveLocalTaskDestination } from "./local-task-destination.js";
3
+ import { nativeDelegationFromClaim, nativeDelegationResult, nativeContinuationInstruction, nativeDelegationFailureResult } from './native-delegation-contract.js';
1
4
  import { createHash, randomUUID } from "node:crypto";
2
5
  import { execFile } from "node:child_process";
3
6
  import { constants as fsConstants } from "node:fs";
@@ -33,6 +36,7 @@ import {
33
36
  import {
34
37
  LOCAL_TASK_FILE_CAPABILITY_VERSION,
35
38
  validateLocalTaskFile,
39
+ validateLocalTaskFileCollection,
36
40
  } from "./local-task-file-capabilities.js";
37
41
  import {
38
42
  normalizeLocalTaskScenarioPack,
@@ -51,6 +55,7 @@ import {
51
55
  } from "./durable-local-task-steps-contract.js";
52
56
  import {
53
57
  normalizeDurableLocalTaskExecutionClaimV2,
58
+ normalizeParentUploadedRef,
54
59
  } from "./bounded-local-task-subtasks-contract.js";
55
60
  import {
56
61
  createBoundedLocalTaskSubtaskPool,
@@ -98,6 +103,7 @@ export function createDurableLocalTaskApi({
98
103
  requestTimeoutMs = REQUEST_TIMEOUT_MS,
99
104
  artifactUploadTimeoutMs = ARTIFACT_UPLOAD_TIMEOUT_MS,
100
105
  } = {}) {
106
+ let narrativeImageSupported = true;
101
107
  if (typeof getConnection !== "function") {
102
108
  throw new TypeError("getConnection is required");
103
109
  }
@@ -127,7 +133,7 @@ export function createDurableLocalTaskApi({
127
133
  method: "POST",
128
134
  headers: webApiHeaders(
129
135
  { token: connection.token },
130
- { "Content-Type": "application/json" },
136
+ { "Content-Type": "application/json", "X-SIMY-Native-Delegation": "provider_delegation_result.v1", "X-SIMY-Device-IO": "local_device_io.v1" },
131
137
  ),
132
138
  body: JSON.stringify(body),
133
139
  signal: AbortSignal.timeout(requestTimeoutMs),
@@ -193,14 +199,22 @@ export function createDurableLocalTaskApi({
193
199
  worker_id: workerId,
194
200
  lease_seconds: leaseSeconds,
195
201
  turn_id: turnId,
196
- fp_narrative_image: narrativeImageCapabilityMetadata(),
202
+ ...(narrativeImageSupported ? { fp_narrative_image: narrativeImageCapabilityMetadata() } : {}),
197
203
  };
198
204
  if (controlledPlan !== null) body.controlled_plan = controlledPlan;
199
- const payload = await request(
200
- "local-tasks/claim",
201
- body,
202
- { ambiguousOnTransport: controlledPlan !== null },
203
- );
205
+ let payload;
206
+ try {
207
+ payload = await request("local-tasks/claim", body, { ambiguousOnTransport: controlledPlan !== null });
208
+ } catch (error) {
209
+ // Older gateways reject unknown fields before admission. Only that exact
210
+ // validation rejection permits a retry; uncertain claims never replay.
211
+ if (!narrativeImageSupported || error.status !== 400 ||
212
+ error.code !== "invalid_local_task_worker_request" ||
213
+ error.message !== "unsupported request field: fp_narrative_image") throw error;
214
+ narrativeImageSupported = false;
215
+ delete body.fp_narrative_image;
216
+ payload = await request("local-tasks/claim", body, { ambiguousOnTransport: controlledPlan !== null });
217
+ }
204
218
  if (isLocalTaskPlanOrchestration(payload)) return payload;
205
219
  return payload?.claim ?? null;
206
220
  },
@@ -242,6 +256,7 @@ export function createDurableLocalTaskApi({
242
256
  resultSummary = null,
243
257
  artifactRefs = [],
244
258
  failureCode = null,
259
+ delegationResult = null,
245
260
  }) {
246
261
  const payload = await request(
247
262
  `local-tasks/${encodeURIComponent(turnId)}/status`,
@@ -252,10 +267,11 @@ export function createDurableLocalTaskApi({
252
267
  result_summary: resultSummary,
253
268
  artifact_refs: artifactRefs,
254
269
  failure_code: failureCode,
270
+ ...(delegationResult ? { delegation_result: delegationResult } : {}),
255
271
  },
256
272
  { ambiguousOnTransport: true },
257
273
  );
258
- return payload?.ok === true;
274
+ return payload?.ok === true && (!delegationResult || payload?.delegation_result_accepted === true);
259
275
  },
260
276
  async prepareArtifact({
261
277
  turnId,
@@ -277,7 +293,8 @@ export function createDurableLocalTaskApi({
277
293
  mime_type: mimeType,
278
294
  size_bytes: sizeBytes,
279
295
  sha256,
280
- ...(nativeImageReceipt ? { native_image_receipt: nativeImageReceipt, worker_id: workerId, lease_epoch: leaseEpoch } : {}),
296
+ ...(workerId !== null ? { worker_id: workerId, lease_epoch: leaseEpoch } : {}),
297
+ ...(nativeImageReceipt ? { native_image_receipt: nativeImageReceipt } : {}),
281
298
  },
282
299
  { ambiguousOnTransport: true },
283
300
  );
@@ -338,7 +355,8 @@ export function createDurableLocalTaskApi({
338
355
  local_artifact_id: localArtifactId,
339
356
  sha256,
340
357
  size_bytes: sizeBytes,
341
- ...(nativeImageReceipt ? { native_image_receipt: nativeImageReceipt, worker_id: workerId, lease_epoch: leaseEpoch } : {}),
358
+ ...(workerId !== null ? { worker_id: workerId, lease_epoch: leaseEpoch } : {}),
359
+ ...(nativeImageReceipt ? { native_image_receipt: nativeImageReceipt } : {}),
342
360
  },
343
361
  { ambiguousOnTransport: true },
344
362
  );
@@ -476,6 +494,28 @@ export function createDurableLocalTaskApi({
476
494
  );
477
495
  return payload;
478
496
  },
497
+ async download_local_task_source_v2({ turnId, workerId, leaseEpoch, source }) {
498
+ const payload = await request(`local-tasks/${encodeURIComponent(turnId)}/sources/download`, {
499
+ worker_id: workerId, lease_epoch: leaseEpoch,
500
+ source_id: source.source_id, upload_id: source.upload_id, sha256: source.sha256,
501
+ });
502
+ const prepared = validateSubtaskSourceDownload(payload, source, 'local_task_source_download.v2');
503
+ const response = await fetchImpl(prepared.url, {
504
+ method: 'GET', headers: prepared.headers, redirect: 'error',
505
+ signal: AbortSignal.timeout(artifactUploadTimeoutMs),
506
+ });
507
+ if (!response.ok) throw new DurableLocalTaskApiError('SIMY could not download the authorized source.',
508
+ { status: response.status, code: 'local_task_source_download_failed' });
509
+ const maxBytes = Number(source.size_bytes);
510
+ // Bound streamed bytes even if storage misreports Content-Length.
511
+ const chunks = []; let length = 0;
512
+ for await (const chunk of response.body ?? []) {
513
+ length += chunk.length;
514
+ if (length > maxBytes) throw new DurableLocalTaskApiError('Source size changed.', { code: 'local_task_source_integrity_failed' });
515
+ chunks.push(Buffer.from(chunk));
516
+ }
517
+ return Buffer.concat(chunks);
518
+ },
479
519
  async download_local_task_subtask_source_v2({
480
520
  turnId,
481
521
  batchId,
@@ -573,7 +613,7 @@ function boundedSubtaskFenceBody(input) {
573
613
  };
574
614
  }
575
615
 
576
- function validateSubtaskSourceDownload(value, expected) {
616
+ function validateSubtaskSourceDownload(value, expected, schema = "local_task_subtask_source_download.v2") {
577
617
  const topKeys = new Set(["schema_version", "source", "download"]);
578
618
  const sourceKeys = new Set([
579
619
  "source_id",
@@ -594,7 +634,7 @@ function validateSubtaskSourceDownload(value, expected) {
594
634
  Array.isArray(value) ||
595
635
  Object.keys(value).some((key) => !topKeys.has(key)) ||
596
636
  Object.keys(value).length !== topKeys.size ||
597
- value.schema_version !== "local_task_subtask_source_download.v2" ||
637
+ value.schema_version !== schema ||
598
638
  !value.source ||
599
639
  typeof value.source !== "object" ||
600
640
  Array.isArray(value.source) ||
@@ -639,10 +679,13 @@ export function createDurableLocalTaskWorker({
639
679
  materializeArtifacts = materializePipelineArtifacts,
640
680
  attachmentStore = null,
641
681
  getDeviceId = () => null,
682
+ readPhotoSource = readLocalPhotoSource,
642
683
  beginWork = () => () => {},
643
684
  workerId = `simy-cli-${randomUUID()}`,
644
685
  leaseSeconds = DEFAULT_LEASE_SECONDS,
686
+ enableBoundedSubtasks = true,
645
687
  heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
688
+ now = Date.now,
646
689
  setIntervalImpl = setInterval,
647
690
  clearIntervalImpl = clearInterval,
648
691
  onError = () => {},
@@ -689,6 +732,7 @@ export function createDurableLocalTaskWorker({
689
732
  });
690
733
  task.durable_turn_id = runtime.parent_turn_id;
691
734
  task.durable_step_id = runtime.step_id;
735
+ task.durable_subtask_attempt_id = runtime.attempt_id;
692
736
  await persist(task);
693
737
  registry.create(task);
694
738
  boundedTasks.set(runtime.attempt_id, task);
@@ -734,6 +778,8 @@ export function createDurableLocalTaskWorker({
734
778
  let knownLeaseExpiry = Number.NaN;
735
779
  let boundary = "not_started";
736
780
  let leaseLost = false;
781
+ let resolveLeaseLoss;
782
+ const leaseLoss = new Promise(resolve => { resolveLeaseLoss = resolve; });
737
783
  let cancelRequested = false;
738
784
  let normalized = null;
739
785
  const executionStartedAt = Date.now();
@@ -748,13 +794,21 @@ export function createDurableLocalTaskWorker({
748
794
  "local_task_step_lease_lost",
749
795
  ),
750
796
  );
751
- if (task) await stopTask(task).catch(() => undefined);
797
+ if (task) {
798
+ task.reason_code ||= "local_task_lease_lost";
799
+ await stopTask(task).catch(() => undefined);
800
+ }
801
+ resolveLeaseLoss();
752
802
  };
753
803
 
754
804
  try {
755
805
  normalized = normalizeDurableLocalTaskStepClaim(claim, {
806
+ nowMs: now(),
756
807
  leaseSeconds,
757
808
  });
809
+ if (claim.execution_spec?.local_destination !== undefined || claim.execution_spec?.local_photo_source !== undefined) {
810
+ throw workerError("Local destination delivery is not supported for stepped execution.", "local_task_destination_unconfirmed");
811
+ }
758
812
  if (normalized.disposition !== "execute") {
759
813
  throw workerError(
760
814
  normalized.disposition === "approval_required"
@@ -813,6 +867,16 @@ export function createDurableLocalTaskWorker({
813
867
  registry.create(task);
814
868
  if (active) active.task = task;
815
869
 
870
+ const checkLease = async () => {
871
+ if (!leaseLost && now() >= knownLeaseExpiry) {
872
+ await stopForLeaseLoss(workerError(
873
+ "The local execution lease expired while waiting for SIMY Web.",
874
+ "local_task_lease_expired",
875
+ ));
876
+ }
877
+ if (leaseLost) await leaseLoss;
878
+ return !leaseLost;
879
+ };
816
880
  const heartbeat = async () => {
817
881
  if (
818
882
  !active ||
@@ -821,6 +885,8 @@ export function createDurableLocalTaskWorker({
821
885
  ) {
822
886
  return;
823
887
  }
888
+ const renewalStartedAt = now();
889
+ if (!await checkLease()) return;
824
890
  try {
825
891
  const response = await api.heartbeatStep({
826
892
  turnId: normalized.turnId,
@@ -829,6 +895,7 @@ export function createDurableLocalTaskWorker({
829
895
  leaseEpoch: normalized.leaseEpoch,
830
896
  leaseSeconds,
831
897
  });
898
+ if (!await checkLease()) return;
832
899
  if (response.ok !== true) {
833
900
  await stopForLeaseLoss(
834
901
  workerError(
@@ -838,7 +905,7 @@ export function createDurableLocalTaskWorker({
838
905
  );
839
906
  return;
840
907
  }
841
- knownLeaseExpiry = Date.now() + leaseSeconds * 1_000;
908
+ knownLeaseExpiry = renewalStartedAt + leaseSeconds * 1_000;
842
909
  if (response.cancelRequested) {
843
910
  cancelRequested = true;
844
911
  await stopTask(task);
@@ -860,7 +927,7 @@ export function createDurableLocalTaskWorker({
860
927
  );
861
928
  if (
862
929
  Number.isFinite(knownLeaseExpiry) &&
863
- Date.now() + heartbeatIntervalMs >= knownLeaseExpiry
930
+ now() + heartbeatIntervalMs >= knownLeaseExpiry
864
931
  ) {
865
932
  await stopForLeaseLoss(error);
866
933
  }
@@ -872,16 +939,18 @@ export function createDurableLocalTaskWorker({
872
939
  heartbeatInFlight = null;
873
940
  });
874
941
  }
875
- return heartbeatInFlight;
942
+ return Promise.race([heartbeatInFlight, leaseLoss]);
876
943
  };
877
944
 
878
945
  heartbeatTimer = setIntervalImpl(() => {
879
- void runHeartbeat();
946
+ // Expiry must be checked even while a heartbeat request is still pending.
947
+ void checkLease().then((valid) => { if (valid) void runHeartbeat(); });
880
948
  }, heartbeatIntervalMs);
881
949
  heartbeatTimer?.unref?.();
882
950
 
883
951
  const completed = await runLocalTask(task, {
884
952
  beforeProviderInvocation: async () => {
953
+ await checkLease();
885
954
  if (leaseLost || cancelRequested || closed) {
886
955
  throw workerError(
887
956
  "The durable step was stopped before the Provider started.",
@@ -904,8 +973,13 @@ export function createDurableLocalTaskWorker({
904
973
  },
905
974
  );
906
975
  boundary = "started";
976
+ // The server ACK can arrive after cancellation or lease expiry.
977
+ if (!await checkLease() || cancelRequested || closed) {
978
+ throw workerError("The execution authority expired before Provider dispatch.",
979
+ "local_task_cancelled_before_invocation");
980
+ }
907
981
  } catch (error) {
908
- boundary = error?.ambiguous ? "unknown" : "not_started";
982
+ boundary = boundary === "started" || error?.ambiguous ? "unknown" : "not_started";
909
983
  throw workerError(
910
984
  error?.ambiguous
911
985
  ? "SIMY could not confirm the durable step Provider boundary. This step will not be started again."
@@ -919,12 +993,13 @@ export function createDurableLocalTaskWorker({
919
993
  },
920
994
  });
921
995
 
996
+ if (!await checkLease()) return;
997
+ await runHeartbeat();
998
+ if (leaseLost) return;
922
999
  if (heartbeatTimer) {
923
1000
  clearIntervalImpl(heartbeatTimer);
924
1001
  heartbeatTimer = null;
925
1002
  }
926
- await runHeartbeat();
927
- if (leaseLost) return;
928
1003
  const status = completed.status === "stopped"
929
1004
  ? "cancelled"
930
1005
  : boundary === "unknown"
@@ -1090,6 +1165,8 @@ export function createDurableLocalTaskWorker({
1090
1165
  let knownLeaseExpiry = Number.NaN;
1091
1166
  let boundary = "not_started";
1092
1167
  let leaseLost = false;
1168
+ let resolveLeaseLoss;
1169
+ const leaseLoss = new Promise(resolve => { resolveLeaseLoss = resolve; });
1093
1170
  let cancelRequested = false;
1094
1171
  let materializedAttachmentRefs = null;
1095
1172
 
@@ -1103,12 +1180,22 @@ export function createDurableLocalTaskWorker({
1103
1180
  "local_task_lease_lost",
1104
1181
  ),
1105
1182
  );
1106
- if (task) await stopTask(task).catch(() => undefined);
1183
+ if (task) {
1184
+ task.reason_code ||= "local_task_lease_lost";
1185
+ await stopTask(task).catch(() => undefined);
1186
+ }
1187
+ resolveLeaseLoss();
1107
1188
  };
1108
1189
 
1109
1190
  try {
1110
- const normalized = normalizeClaim(claim, { leaseSeconds });
1191
+ const normalized = normalizeClaim(claim, { leaseSeconds, nowMs: now() });
1192
+ const native = nativeDelegationFromClaim(claim, deviceId);
1111
1193
  knownLeaseExpiry = normalized.leaseExpiresAtMs;
1194
+ const requestedDestination = normalizeLocalTaskDestination(normalized.spec.local_destination);
1195
+ if (requestedDestination && requestedDestination.device_id !== deviceId) throw workerError("The destination belongs to another device.", "local_task_destination_unconfirmed");
1196
+ if (requestedDestination?.authorized_root && normalized.spec.delegation_target?.provider !== "codex") throw workerError("This folder requires an existing native workspace authorization.", "local_task_destination_unconfirmed");
1197
+ let destination = requestedDestination && !requestedDestination.authorized_root ? await resolveLocalTaskDestination(normalized.spec, { deviceId, resolveRepository }) : null;
1198
+ const photoContext = normalized.spec.local_photo_source ? await readPhotoSource(normalized.spec.local_photo_source, { deviceId }) : null;
1112
1199
  const workspace = await resolveWorkspace(normalized, {
1113
1200
  storageRoot,
1114
1201
  workspaceRoot,
@@ -1116,6 +1203,12 @@ export function createDurableLocalTaskWorker({
1116
1203
  resolveRepository,
1117
1204
  });
1118
1205
  let attachments = [];
1206
+ if (normalized.uploadedRefs.length) {
1207
+ if (!native || typeof api.download_local_task_source_v2 !== 'function')
1208
+ throw workerError('Native uploaded source download is unavailable.', 'local_task_source_download_unavailable');
1209
+ attachments = await materializeNativeUploadedInputs({ refs: normalized.uploadedRefs, inputsRoot: workspace.inputs_root,
1210
+ download: source => api.download_local_task_source_v2({ turnId: normalized.turnId, workerId, leaseEpoch: normalized.leaseEpoch, source }) });
1211
+ }
1119
1212
  if (normalized.attachmentRefs.length > 0) {
1120
1213
  if (!attachmentStore || typeof attachmentStore.materialize !== "function") {
1121
1214
  throw workerError(
@@ -1123,11 +1216,18 @@ export function createDurableLocalTaskWorker({
1123
1216
  "durable_local_task_attachments_unavailable",
1124
1217
  );
1125
1218
  }
1126
- attachments = await attachmentStore.materialize({
1127
- refs: normalized.attachmentRefs,
1128
- deviceId,
1129
- inputsRoot: workspace.inputs_root,
1130
- });
1219
+ try {
1220
+ attachments.push(...await attachmentStore.materialize({ refs: normalized.attachmentRefs, deviceId, inputsRoot: workspace.inputs_root }));
1221
+ } catch (error) {
1222
+ const parent = native && claim.execution_spec.continuation?.parent_turn_id;
1223
+ const retained = parent && registry.list().find(prior => prior.durable_turn_id === parent &&
1224
+ prior.native_identity?.device_id === deviceId && prior.native_identity?.account_id === native.nativeIdentity.account_id &&
1225
+ prior.delegation?.origin_conversation_id === claim.thread_id);
1226
+ if (!retained || !['ENOENT', 'local_task_attachment_ref_not_found'].includes(error.code) ||
1227
+ typeof attachmentStore.materializeRetained !== 'function') throw error;
1228
+ attachments.push(...await attachmentStore.materializeRetained({ refs: normalized.attachmentRefs, deviceId,
1229
+ inputsRoot: workspace.inputs_root, retainedTask: retained }));
1230
+ }
1131
1231
  materializedAttachmentRefs = normalized.attachmentRefs;
1132
1232
  }
1133
1233
  validateLocalTaskScenarioInputs(normalized.scenarioPack, {
@@ -1136,9 +1236,10 @@ export function createDurableLocalTaskWorker({
1136
1236
  });
1137
1237
  task = createLocalTask({
1138
1238
  id: workspace.localTaskId,
1139
- instruction: normalized.instruction,
1239
+ instruction: native ? nativeContinuationInstruction(claim) : normalized.instruction,
1140
1240
  backend: normalized.spec.backend,
1141
1241
  model: normalized.spec.model ?? null,
1242
+ ...(native ? { delegation: native.delegation, nativeIdentity: native.nativeIdentity } : {}),
1142
1243
  operation: normalized.spec.operation,
1143
1244
  permissionMode: normalized.spec.permission_mode,
1144
1245
  workspaceMode: normalized.spec.workspace_mode,
@@ -1149,6 +1250,9 @@ export function createDurableLocalTaskWorker({
1149
1250
  outputsRoot: workspace.outputs_root,
1150
1251
  outputsRootExpected: workspace.outputs_root_expected,
1151
1252
  attachments,
1253
+ localDestination: requestedDestination,
1254
+ localPhotoSource: normalized.spec.local_photo_source,
1255
+ localPhotoContext: photoContext,
1152
1256
  expectedOutputs: normalized.spec.expected_outputs,
1153
1257
  scenarioPack: normalized.scenarioPack,
1154
1258
  providerTokenBudget: normalized.spec.provider_token_budget,
@@ -1161,8 +1265,20 @@ export function createDurableLocalTaskWorker({
1161
1265
  registry.create(task);
1162
1266
  if (active) active.task = task;
1163
1267
 
1268
+ const checkLease = async () => {
1269
+ if (!leaseLost && now() >= knownLeaseExpiry) {
1270
+ await stopForLeaseLoss(workerError(
1271
+ "The local execution lease expired while waiting for SIMY Web.",
1272
+ "local_task_lease_expired",
1273
+ ));
1274
+ }
1275
+ if (leaseLost) await leaseLoss;
1276
+ return !leaseLost;
1277
+ };
1164
1278
  const heartbeat = async () => {
1165
- if (!active || active.claim.turn_id !== normalized.turnId) return;
1279
+ if (!active || active.claim.turn_id !== normalized.turnId || leaseLost) return;
1280
+ const renewalStartedAt = now();
1281
+ if (!await checkLease()) return;
1166
1282
  try {
1167
1283
  const response = await api.heartbeat({
1168
1284
  turnId: normalized.turnId,
@@ -1170,6 +1286,7 @@ export function createDurableLocalTaskWorker({
1170
1286
  leaseEpoch: normalized.leaseEpoch,
1171
1287
  leaseSeconds,
1172
1288
  });
1289
+ if (!await checkLease()) return;
1173
1290
  if (response.ok !== true) {
1174
1291
  await stopForLeaseLoss(
1175
1292
  workerError(
@@ -1179,7 +1296,7 @@ export function createDurableLocalTaskWorker({
1179
1296
  );
1180
1297
  return;
1181
1298
  }
1182
- knownLeaseExpiry = Date.now() + leaseSeconds * 1_000;
1299
+ knownLeaseExpiry = renewalStartedAt + leaseSeconds * 1_000;
1183
1300
  if (response.cancelRequested) {
1184
1301
  cancelRequested = true;
1185
1302
  await stopTask(task);
@@ -1201,7 +1318,7 @@ export function createDurableLocalTaskWorker({
1201
1318
  );
1202
1319
  if (
1203
1320
  Number.isFinite(knownLeaseExpiry) &&
1204
- Date.now() + heartbeatIntervalMs >= knownLeaseExpiry
1321
+ now() + heartbeatIntervalMs >= knownLeaseExpiry
1205
1322
  ) {
1206
1323
  await stopForLeaseLoss(error);
1207
1324
  }
@@ -1213,17 +1330,32 @@ export function createDurableLocalTaskWorker({
1213
1330
  heartbeatInFlight = null;
1214
1331
  });
1215
1332
  }
1216
- return heartbeatInFlight;
1333
+ return Promise.race([heartbeatInFlight, leaseLoss]);
1217
1334
  };
1218
1335
 
1219
1336
  heartbeatTimer = setIntervalImpl(() => {
1220
- void runHeartbeat();
1337
+ // Expiry must be checked even while a heartbeat request is still pending.
1338
+ void checkLease().then((valid) => { if (valid) void runHeartbeat(); });
1221
1339
  }, heartbeatIntervalMs);
1222
1340
  heartbeatTimer?.unref?.();
1223
1341
 
1224
1342
  const runner = normalized.narrativeImageRequest ? runNarrativeImageTask : runLocalTask;
1225
1343
  const completed = await runner(task, {
1344
+ ...(native ? { delegationOptions: { onWorkspaceAuthorized: async roots => {
1345
+ await runHeartbeat();
1346
+ if (leaseLost || cancelRequested || closed || Date.now() >= knownLeaseExpiry)
1347
+ throw workerError('The Local Task lease no longer permits destination access.', 'local_task_cancelled_before_invocation');
1348
+ if (requestedDestination) destination = await resolveLocalTaskDestination(normalized.spec, {
1349
+ deviceId, resolveRepository, nativeWritableRoots: roots,
1350
+ });
1351
+ task.native_writable_roots = [...roots];
1352
+ } }, beforeSpawn: async () => {
1353
+ await runHeartbeat();
1354
+ if (leaseLost || cancelRequested || closed || Date.now() >= knownLeaseExpiry)
1355
+ throw workerError('The Local Task lease no longer permits dispatch.', 'local_task_cancelled_before_invocation');
1356
+ } } : {}),
1226
1357
  beforeProviderInvocation: async () => {
1358
+ await checkLease();
1227
1359
  if (leaseLost || cancelRequested || closed) {
1228
1360
  throw workerError(
1229
1361
  "The Local Task was stopped before the Provider started.",
@@ -1237,8 +1369,13 @@ export function createDurableLocalTaskWorker({
1237
1369
  leaseEpoch: normalized.leaseEpoch,
1238
1370
  });
1239
1371
  boundary = "started";
1372
+ // The server ACK can arrive after cancellation or lease expiry.
1373
+ if (!await checkLease() || cancelRequested || closed) {
1374
+ throw workerError("The execution authority expired before Provider dispatch.",
1375
+ "local_task_cancelled_before_invocation");
1376
+ }
1240
1377
  } catch (error) {
1241
- boundary = error?.ambiguous ? "unknown" : "not_started";
1378
+ boundary = boundary === "started" || error?.ambiguous ? "unknown" : "not_started";
1242
1379
  throw workerError(
1243
1380
  error?.ambiguous
1244
1381
  ? "SIMY could not confirm whether the Provider boundary was recorded. The task was not started again."
@@ -1252,16 +1389,17 @@ export function createDurableLocalTaskWorker({
1252
1389
  },
1253
1390
  });
1254
1391
 
1392
+ if (!await checkLease()) return;
1393
+ await runHeartbeat();
1394
+ if (leaseLost) return;
1255
1395
  if (heartbeatTimer) {
1256
1396
  clearIntervalImpl(heartbeatTimer);
1257
1397
  heartbeatTimer = null;
1258
1398
  }
1259
- await runHeartbeat();
1260
- if (leaseLost) return;
1261
1399
  // Report the Provider operation's observed outcome. A cancellation that
1262
1400
  // arrives after Provider completion still cancels the parent Turn on the
1263
1401
  // server, but must not rewrite a known successful operation as cancelled.
1264
- const status = completed.native_image_unknown === true
1402
+ const status = completed.native_image_unknown === true || completed.delegation_state === "result_unknown"
1265
1403
  ? "unknown"
1266
1404
  : completed.status === "stopped"
1267
1405
  ? "cancelled"
@@ -1278,15 +1416,41 @@ export function createDurableLocalTaskWorker({
1278
1416
  let reportFailureCode = failureCode;
1279
1417
  let reportSummary = durableResultSummary(completed, status, failureCode);
1280
1418
  let artifactRefs = [];
1419
+ let deliveryStage = "artifacts";
1281
1420
  try {
1282
1421
  if (reportStatus === "succeeded" && normalized.narrativeImageRequest &&
1283
1422
  !completed.native_image_receipt) {
1284
1423
  throw workerError("Native image receipt is required before upload.", "narrative_image_receipt_missing");
1285
1424
  }
1286
1425
  artifactRefs = buildLocalTaskArtifactRefs(completed.artifacts, completed.id);
1426
+ if (reportStatus === "succeeded" && requestedDestination) {
1427
+ deliveryStage = "destination";
1428
+ const currentDestination = await resolveLocalTaskDestination(normalized.spec, { deviceId, resolveRepository, nativeWritableRoots: completed.native_writable_roots });
1429
+ if (destination && destination.root !== currentDestination.root) {
1430
+ throw workerError("The save destination does not match this device and its authorized workspace.", "local_task_destination_unconfirmed");
1431
+ }
1432
+ destination = currentDestination;
1433
+ completed.local_destination_receipts = [];
1434
+ await saveLocalTaskDestination({
1435
+ task: completed,
1436
+ destination,
1437
+ onReceipt: async (receipt) => {
1438
+ completed.local_destination_receipts.push(receipt);
1439
+ await persist(completed);
1440
+ },
1441
+ assertActive: () => {
1442
+ if (leaseLost || cancelRequested || closed || Date.now() >= knownLeaseExpiry) {
1443
+ throw workerError("The Local Task stopped before local file delivery.", "local_task_destination_unconfirmed");
1444
+ }
1445
+ },
1446
+ });
1447
+ await persist(completed);
1448
+ reportSummary += `\nSaved and verified on the selected device: ${completed.local_destination_receipts.map((receipt) => receipt.local_path).join(", ")}`;
1449
+ }
1450
+ deliveryStage = "artifacts";
1287
1451
  if (
1288
- reportStatus === "succeeded" &&
1289
- (normalized.pipelineContext || normalized.narrativeImageRequest) &&
1452
+ (reportStatus === "succeeded" || native) &&
1453
+ (normalized.pipelineContext || normalized.narrativeImageRequest || native) &&
1290
1454
  artifactRefs.length > 0
1291
1455
  ) {
1292
1456
  artifactRefs = await materializeArtifacts({
@@ -1294,6 +1458,7 @@ export function createDurableLocalTaskWorker({
1294
1458
  turnId: normalized.turnId,
1295
1459
  task: completed,
1296
1460
  artifactRefs,
1461
+ ...(native ? { preserveAmbiguity: true, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
1297
1462
  ...(normalized.narrativeImageRequest ? { nativeImageReceipt: completed.native_image_receipt, workerId, leaseEpoch: normalized.leaseEpoch } : {}),
1298
1463
  });
1299
1464
  }
@@ -1306,19 +1471,26 @@ export function createDurableLocalTaskWorker({
1306
1471
  if (status !== "cancelled") {
1307
1472
  // A gateway 5xx can hide a committed image. Never present that outcome
1308
1473
  // as a clean failure that invites a second generation.
1309
- reportStatus = status === "unknown" || (normalized.narrativeImageRequest &&
1474
+ reportStatus = status === "unknown" || ((normalized.narrativeImageRequest || native) &&
1310
1475
  (error?.ambiguous === true || error?.status >= 500)) ? "unknown" : "failed";
1311
1476
  reportFailureCode = normalized.narrativeImageRequest && error?.code === "narrative_image_receipt_missing"
1312
1477
  ? "narrative_image_receipt_missing"
1478
+ : deliveryStage === "destination" || error?.code === "local_task_destination_unconfirmed"
1479
+ ? "local_task_destination_unconfirmed"
1313
1480
  : contractFailure
1314
1481
  ? "artifact_verification_failed"
1315
1482
  : "artifact_materialization_failed";
1316
1483
  reportSummary =
1317
- normalized.narrativeImageRequest
1484
+ native ? recoverySummary(reportStatus, reportFailureCode) : normalized.narrativeImageRequest
1318
1485
  ? "SIMY could not confirm the generated image delivery. Keep the local output for verification; do not regenerate it."
1486
+ : deliveryStage === "destination"
1487
+ ? "SIMY could not confirm delivery to the selected local folder. Existing files and local outputs were preserved."
1319
1488
  : contractFailure
1320
1489
  ? "SIMY could not safely verify the Local Task output files."
1321
1490
  : "SIMY created the output files locally but could not safely attach them to this Pipeline run. Keep SIMY CLI running, check the connection, and start a new Pipeline run.";
1491
+ if (completed.local_destination_receipts?.length) {
1492
+ reportSummary += `\nSaved and verified on the selected device: ${completed.local_destination_receipts.map((receipt) => receipt.local_path).join(", ")}`;
1493
+ }
1322
1494
  completed.status = reportStatus === "failed" ? "failed" : completed.status;
1323
1495
  completed.reason_code = reportFailureCode;
1324
1496
  completed.error = error.message;
@@ -1342,18 +1514,40 @@ export function createDurableLocalTaskWorker({
1342
1514
  error,
1343
1515
  ));
1344
1516
  }
1517
+ const terminalPayload = {
1518
+ turnId: normalized.turnId,
1519
+ workerId,
1520
+ leaseEpoch: normalized.leaseEpoch,
1521
+ status: reportStatus,
1522
+ resultSummary: reportSummary,
1523
+ artifactRefs,
1524
+ failureCode: reportFailureCode,
1525
+ ...(native ? { delegationResult: nativeDelegationResult(completed) } : {}),
1526
+ };
1527
+ completed.durable_pending_status = { device_id: deviceId, payload: terminalPayload };
1528
+ await persist(completed);
1345
1529
  let statusConfirmed;
1346
1530
  try {
1347
- statusConfirmed = await updateStatusWithResolution(api, {
1348
- turnId: normalized.turnId,
1349
- workerId,
1350
- leaseEpoch: normalized.leaseEpoch,
1351
- status: reportStatus,
1352
- resultSummary: reportSummary,
1353
- artifactRefs,
1354
- failureCode: reportFailureCode,
1355
- });
1531
+ statusConfirmed = await updateStatusWithResolution(api, terminalPayload);
1532
+ completed.durable_pending_status = null;
1533
+ await persist(completed);
1356
1534
  } catch (error) {
1535
+ if (isProvenFinalStatusRejection(error)) {
1536
+ // The final receipt was definitively rejected. Retain the completed
1537
+ // task as unconfirmed evidence, but do not let an unresendable
1538
+ // receipt block every later claim or invite a Provider replay.
1539
+ completed.durable_pending_status = null;
1540
+ completed.durable_status_unconfirmed = true;
1541
+ await persist(completed);
1542
+ onError(
1543
+ recoveryError(
1544
+ "SIMY completed the local Provider call but the final server state was rejected. It will not rerun the Provider.",
1545
+ error,
1546
+ "local_task_status_unconfirmed",
1547
+ ),
1548
+ );
1549
+ return;
1550
+ }
1357
1551
  onError(
1358
1552
  recoveryError(
1359
1553
  "SIMY completed the local Provider call but could not save its final server state. It will not rerun the Provider.",
@@ -1364,6 +1558,8 @@ export function createDurableLocalTaskWorker({
1364
1558
  return;
1365
1559
  }
1366
1560
  if (!statusConfirmed) {
1561
+ completed.durable_status_unconfirmed = true;
1562
+ await persist(completed);
1367
1563
  onError(
1368
1564
  recoveryError(
1369
1565
  "SIMY stopped this Local Task but could not confirm its final server state. It will not rerun the Provider.",
@@ -1383,6 +1579,10 @@ export function createDurableLocalTaskWorker({
1383
1579
  );
1384
1580
  const claimStatus =
1385
1581
  boundary === "not_started" ? "failed" : "unknown";
1582
+ let rejectedNative = null;
1583
+ if (boundary === 'not_started' && claim.execution_spec?.delegation_target) {
1584
+ try { rejectedNative = nativeDelegationFailureResult(claim, deviceId); } catch { /* Unbound claims cannot produce an owner receipt. */ }
1585
+ }
1386
1586
  const statusConfirmed = await updateStatusWithResolution(api, {
1387
1587
  turnId: claim.turn_id,
1388
1588
  workerId,
@@ -1396,6 +1596,7 @@ export function createDurableLocalTaskWorker({
1396
1596
  ),
1397
1597
  artifactRefs: [],
1398
1598
  failureCode: safeMachineCode(error?.code, "durable_local_task_failed"),
1599
+ ...(rejectedNative ? { delegationResult: rejectedNative } : {}),
1399
1600
  }).catch((statusError) => {
1400
1601
  onError(statusError);
1401
1602
  return false;
@@ -1459,9 +1660,59 @@ export function createDurableLocalTaskWorker({
1459
1660
  pollPromise = (async () => {
1460
1661
  const { api, attachmentStore, deviceId } = context;
1461
1662
  try {
1663
+ // The provider operation and saved files are already known. After an
1664
+ // offline result write, retry only that exact lease-bound receipt on
1665
+ // the same paired device; never claim or rerun the provider here.
1666
+ const pendingTask = registry.list?.().find(task => task.durable_pending_status);
1667
+ if (pendingTask) {
1668
+ const pending = pendingTask.durable_pending_status;
1669
+ if (!deviceId || !pending.device_id) {
1670
+ // Pairing state can be temporarily unavailable during a refresh.
1671
+ // Keep the exact receipt pending until the original device can
1672
+ // be identified; never reinterpret a missing device ID as a
1673
+ // device change.
1674
+ onError(workerError("The saved result is waiting for its original paired device.", "local_task_result_device_mismatch"));
1675
+ releaseWork();
1676
+ return false;
1677
+ }
1678
+ if (pending.device_id !== deviceId) {
1679
+ // A receipt belongs to its original pairing. Keeping it pending
1680
+ // on another device would permanently prevent that device from
1681
+ // claiming unrelated work, while resending would violate the
1682
+ // device-bound receipt contract.
1683
+ pendingTask.durable_pending_status = null;
1684
+ pendingTask.durable_status_unconfirmed = true;
1685
+ await persist(pendingTask);
1686
+ onError(workerError("The saved result belongs to a different paired device and will not be resent or replayed.", "local_task_result_device_mismatch"));
1687
+ } else {
1688
+ try {
1689
+ const confirmed = await updateStatusWithResolution(api, pending.payload);
1690
+ pendingTask.durable_pending_status = null;
1691
+ if (!confirmed) pendingTask.durable_status_unconfirmed = true;
1692
+ await persist(pendingTask);
1693
+ if (!confirmed) onError(workerError("The original result lease was fenced; provider replay remains disabled.", "local_task_status_unconfirmed"));
1694
+ releaseWork();
1695
+ return confirmed;
1696
+ } catch (error) {
1697
+ if (!isProvenFinalStatusRejection(error)) throw error;
1698
+ // A proven request/authorization/not-found/validation
1699
+ // rejection cannot become valid by retrying this receipt.
1700
+ // Preserve local evidence and continue to an unrelated claim.
1701
+ pendingTask.durable_pending_status = null;
1702
+ pendingTask.durable_status_unconfirmed = true;
1703
+ await persist(pendingTask);
1704
+ onError(recoveryError(
1705
+ "SIMY rejected the saved final result. It will not rerun the Provider.",
1706
+ error,
1707
+ "local_task_status_unconfirmed",
1708
+ ));
1709
+ }
1710
+ }
1711
+ }
1462
1712
  const requestedTurnId = pendingStepTurnId;
1463
1713
  if (
1464
1714
  !requestedTurnId &&
1715
+ enableBoundedSubtasks &&
1465
1716
  typeof api.claimSubtaskParent === "function"
1466
1717
  ) {
1467
1718
  const parentEnvelope = await api.claimSubtaskParent({
@@ -1488,6 +1739,7 @@ export function createDurableLocalTaskWorker({
1488
1739
  prepareWorkspace,
1489
1740
  leaseSeconds,
1490
1741
  heartbeatIntervalMs,
1742
+ now,
1491
1743
  setIntervalImpl,
1492
1744
  clearIntervalImpl,
1493
1745
  onError,
@@ -1535,6 +1787,9 @@ export function createDurableLocalTaskWorker({
1535
1787
  releaseWork();
1536
1788
  return false;
1537
1789
  }
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');
1538
1793
  if (isLocalTaskPlanOrchestration(claim)) {
1539
1794
  const orchestration = claim;
1540
1795
  const controlledPlan = buildControlledLocalTaskPlan(orchestration);
@@ -1572,6 +1827,20 @@ export function createDurableLocalTaskWorker({
1572
1827
  "local_task_plan_claim_mismatch",
1573
1828
  );
1574
1829
  }
1830
+ if (registry.list?.().some(task =>
1831
+ task.durable_status_unconfirmed === true && task.durable_turn_id === claim.turn_id
1832
+ )) {
1833
+ // This device has already completed this exact Provider turn but
1834
+ // cannot prove the server accepted its terminal receipt. A lease
1835
+ // replay must never cause a second Provider invocation; unrelated
1836
+ // claims remain eligible on the following poll.
1837
+ releaseWork();
1838
+ onError(workerError(
1839
+ "SIMY will not rerun a Local Task whose prior Provider result is unconfirmed.",
1840
+ "local_task_status_unconfirmed",
1841
+ ));
1842
+ return false;
1843
+ }
1575
1844
  if (isDurableLocalTaskStepClaim(claim)) {
1576
1845
  const normalized = normalizeDurableLocalTaskStepClaim(claim, {
1577
1846
  leaseSeconds,
@@ -1694,15 +1963,23 @@ async function updateStatusWithResolution(api, input) {
1694
1963
  if (error instanceof DurableLocalTaskApiError && error.status === 409) {
1695
1964
  return false;
1696
1965
  }
1697
- const retryable =
1698
- error?.ambiguous === true ||
1699
- (Number.isInteger(error?.status) && error.status >= 500);
1966
+ const retryable = isRetryableStatusResolutionError(error);
1700
1967
  if (!retryable || attempt > 0) throw error;
1701
1968
  }
1702
1969
  }
1703
1970
  throw lastError;
1704
1971
  }
1705
1972
 
1973
+ function isRetryableStatusResolutionError(error) {
1974
+ return error?.ambiguous === true ||
1975
+ (Number.isInteger(error?.status) && error.status >= 500);
1976
+ }
1977
+
1978
+ function isProvenFinalStatusRejection(error) {
1979
+ return error instanceof DurableLocalTaskApiError &&
1980
+ [400, 403, 404, 422].includes(error.status);
1981
+ }
1982
+
1706
1983
  function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
1707
1984
  const turnId = String(claim?.turn_id || "");
1708
1985
  const leaseEpoch = Number(claim?.lease_epoch);
@@ -1733,7 +2010,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
1733
2010
  if (!instruction) {
1734
2011
  throw workerError("The Local Task has no instruction.", "local_task_instruction_required");
1735
2012
  }
1736
- const attachmentRefs = payload.attachment_refs ?? [];
2013
+ let attachmentRefs = payload.attachment_refs ?? [];
1737
2014
  if (!Array.isArray(attachmentRefs)) {
1738
2015
  throw workerError(
1739
2016
  "SIMY Web returned invalid attached-file references.",
@@ -1741,6 +2018,16 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
1741
2018
  );
1742
2019
  }
1743
2020
  validateExecutionSpec(spec);
2021
+ const uploadedRefs = attachmentRefs.filter(ref => ref?.schema_version === 'simy_uploaded_attachment_ref.v1');
2022
+ if (uploadedRefs.length) {
2023
+ if (!spec.delegation_target || spec.uploaded_attachment_capability_version !== 'local_task_uploaded_sources.v2')
2024
+ throw workerError('Unsupported uploaded source capability.', 'unsupported_local_task_attachment_capability');
2025
+ uploadedRefs.forEach(ref => normalizeParentUploadedRef(ref));
2026
+ if (new Set(uploadedRefs.map(ref => ref.source_id)).size !== uploadedRefs.length)
2027
+ throw workerError('Duplicate uploaded source.', 'invalid_local_task_claim');
2028
+ validateLocalTaskFileCollection(attachmentRefs, { direction: 'input' });
2029
+ attachmentRefs = attachmentRefs.filter(ref => ref?.schema_version !== 'simy_uploaded_attachment_ref.v1');
2030
+ }
1744
2031
  if (
1745
2032
  attachmentRefs.length > 0 &&
1746
2033
  spec.attachment_capability_version !== LOCAL_TASK_FILE_CAPABILITY_VERSION
@@ -1762,6 +2049,7 @@ function normalizeClaim(claim, { leaseSeconds, nowMs = Date.now() } = {}) {
1762
2049
  leaseExpiresAtMs,
1763
2050
  instruction,
1764
2051
  attachmentRefs,
2052
+ uploadedRefs,
1765
2053
  spec,
1766
2054
  pipelineContext,
1767
2055
  scenarioPack,
@@ -1879,6 +2167,7 @@ async function resolveWorkspace(
1879
2167
  repositoryPath: repository.local_path,
1880
2168
  expectedRepository: normalized.spec.repository,
1881
2169
  localTaskId,
2170
+ prepareOutputs: normalized.spec.permission_mode === "workspace_write",
1882
2171
  });
1883
2172
  return {
1884
2173
  ...prepared,
@@ -2174,10 +2463,11 @@ async function persistDurableStepAttachmentManifest({
2174
2463
  }
2175
2464
  }
2176
2465
 
2177
- async function prepareRepositoryWorkspace({
2466
+ export async function prepareRepositoryWorkspace({
2178
2467
  repositoryPath,
2179
2468
  expectedRepository,
2180
2469
  localTaskId,
2470
+ prepareOutputs = true,
2181
2471
  }) {
2182
2472
  const candidate = path.resolve(String(repositoryPath || ""));
2183
2473
  const candidateDetails = await lstat(candidate).catch(() => null);
@@ -2216,6 +2506,8 @@ async function prepareRepositoryWorkspace({
2216
2506
  );
2217
2507
  }
2218
2508
 
2509
+ if (!prepareOutputs) return { workspace_path: repositoryRoot };
2510
+
2219
2511
  const trackedOutputs = await gitOutput(repositoryRoot, [
2220
2512
  "ls-files",
2221
2513
  "--",
@@ -2364,6 +2656,7 @@ export async function materializePipelineArtifacts({
2364
2656
  task,
2365
2657
  artifactRefs,
2366
2658
  nativeImageReceipt = null,
2659
+ preserveAmbiguity = false,
2367
2660
  workerId = null,
2368
2661
  leaseEpoch = null,
2369
2662
  }) {
@@ -2405,7 +2698,8 @@ export async function materializePipelineArtifacts({
2405
2698
  mimeType: ref.mime_type,
2406
2699
  sizeBytes: ref.size_bytes,
2407
2700
  sha256: ref.sha256.toLowerCase(),
2408
- ...(nativeImageReceipt ? { nativeImageReceipt, workerId, leaseEpoch } : {}),
2701
+ ...(workerId !== null ? { workerId, leaseEpoch } : {}),
2702
+ ...(nativeImageReceipt ? { nativeImageReceipt } : {}),
2409
2703
  }));
2410
2704
  validatePreparedArtifact(prepared);
2411
2705
  await retryAmbiguousArtifactStep(() =>
@@ -2422,8 +2716,9 @@ export async function materializePipelineArtifacts({
2422
2716
  localArtifactId: ref.id,
2423
2717
  sha256: ref.sha256.toLowerCase(),
2424
2718
  sizeBytes: ref.size_bytes,
2425
- ...(nativeImageReceipt ? { nativeImageReceipt, workerId, leaseEpoch } : {}),
2426
- }), { preserveAmbiguity: Boolean(nativeImageReceipt) });
2719
+ ...(workerId !== null ? { workerId, leaseEpoch } : {}),
2720
+ ...(nativeImageReceipt ? { nativeImageReceipt } : {}),
2721
+ }), { preserveAmbiguity: Boolean(nativeImageReceipt) || preserveAmbiguity });
2427
2722
  materialized.push(validateCommittedArtifact(committed, ref));
2428
2723
  }
2429
2724
  return materialized;
@@ -2684,3 +2979,22 @@ function plainRecovery(error) {
2684
2979
  function errorMessage(error) {
2685
2980
  return error instanceof Error ? error.message : String(error || "");
2686
2981
  }
2982
+
2983
+ async function materializeNativeUploadedInputs({ refs, inputsRoot, download }) {
2984
+ const root = await realpath(inputsRoot), inputs = [];
2985
+ for (const ref of refs) {
2986
+ const bytes = await download(ref);
2987
+ validateLocalTaskFile({ ...ref, size_bytes: Number(ref.size_bytes), bytes }, { direction: 'input', requireBytes: true });
2988
+ if (!Buffer.isBuffer(bytes) || bytes.length !== Number(ref.size_bytes) || createHash('sha256').update(bytes).digest('hex') !== ref.sha256)
2989
+ throw workerError('Uploaded source integrity failed.', 'local_task_source_integrity_failed');
2990
+ const target = path.join(root, `${createHash('sha256').update(ref.source_id).digest('hex').slice(0, 16)}-${ref.name}`);
2991
+ if (path.dirname(target) !== root) throw workerError('Unsafe uploaded source path.', 'local_task_source_path_unsafe');
2992
+ await writeFile(target, bytes, { flag: 'wx', mode: 0o400 });
2993
+ const details = await lstat(target), canonical = await realpath(target);
2994
+ if (!details.isFile() || details.isSymbolicLink() || path.dirname(canonical) !== root ||
2995
+ createHash('sha256').update(await readFile(canonical)).digest('hex') !== ref.sha256)
2996
+ throw workerError('Uploaded source changed.', 'local_task_source_integrity_failed');
2997
+ inputs.push({ ...ref, id: ref.source_id, local_path: canonical, integrity_status: 'verified' });
2998
+ }
2999
+ return inputs;
3000
+ }