@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,449 @@
1
+ import { createHash } from "node:crypto";
2
+ import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
3
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
4
+ export const CURRENT_DURABLE_JOB_SCHEMA_VERSION = 1;
5
+ export const DURABLE_JOB_TERMINAL_STATUSES = [
6
+ "succeeded",
7
+ "failed",
8
+ "timed-out",
9
+ "cancelled",
10
+ "unknown-needs-attention"
11
+ ];
12
+ export function isDurableJobTerminal(status) {
13
+ return DURABLE_JOB_TERMINAL_STATUSES.includes(status);
14
+ }
15
+ export function createDurableJob(input, now) {
16
+ const timestamp = now.toISOString();
17
+ const idempotencyKey = input.retryOf === undefined
18
+ ? durableJobIdempotencyKey({
19
+ owner: input.owner,
20
+ projectId: input.projectId,
21
+ head: input.head,
22
+ steps: input.steps,
23
+ workspace: input.workspace,
24
+ env: input.env
25
+ })
26
+ : retryDurableJobIdempotencyKey(durableJobIdempotencyKey({
27
+ owner: input.owner,
28
+ projectId: input.projectId,
29
+ head: input.head,
30
+ steps: input.steps,
31
+ workspace: input.workspace,
32
+ env: input.env
33
+ }), input.retryOf);
34
+ return validateDurableJob({
35
+ schemaVersion: CURRENT_DURABLE_JOB_SCHEMA_VERSION,
36
+ id: input.id,
37
+ taskId: input.taskId,
38
+ owner: input.owner,
39
+ projectId: input.projectId,
40
+ head: input.head,
41
+ workspace: input.workspace,
42
+ env: { ...input.env },
43
+ steps: input.steps.map((step) => ({ ...step })),
44
+ idempotencyKey,
45
+ status: "queued",
46
+ artifactsLocator: input.artifactsLocator,
47
+ createdAt: timestamp,
48
+ ...(input.retryOf === undefined ? {} : { retryOf: input.retryOf }),
49
+ updatedAt: timestamp
50
+ });
51
+ }
52
+ export function startDurableJob(job, process, now) {
53
+ validateDurableJob(job);
54
+ if (job.status !== "queued") {
55
+ throw new Error(`DurableJob can only start from queued: ${job.status}.`);
56
+ }
57
+ const timestamp = now.toISOString();
58
+ return validateDurableJob({
59
+ ...job,
60
+ status: "running",
61
+ process: { ...process },
62
+ startedAt: timestamp,
63
+ heartbeatAt: timestamp,
64
+ updatedAt: timestamp
65
+ });
66
+ }
67
+ export function touchDurableJobHeartbeat(job, now) {
68
+ validateDurableJob(job);
69
+ if (job.status !== "running") {
70
+ throw new Error(`DurableJob heartbeat only applies to running jobs: ${job.status}.`);
71
+ }
72
+ return validateDurableJob({
73
+ ...job,
74
+ heartbeatAt: now.toISOString(),
75
+ updatedAt: now.toISOString()
76
+ });
77
+ }
78
+ export function completeDurableJob(job, result, now) {
79
+ validateDurableJob(job);
80
+ if (job.status !== "running") {
81
+ throw new Error(`DurableJob can only complete from running: ${job.status}.`);
82
+ }
83
+ if (!isDurableJobTerminal(result.outcome)) {
84
+ throw new Error(`DurableJob result outcome must be terminal: ${result.outcome}.`);
85
+ }
86
+ const timestamp = now.toISOString();
87
+ return validateDurableJob({
88
+ ...job,
89
+ status: result.outcome,
90
+ result: normalizeDurableJobResult(result),
91
+ terminalAt: timestamp,
92
+ updatedAt: timestamp
93
+ });
94
+ }
95
+ export function markDurableJobUnknown(job, unknownReason, completedSteps, now) {
96
+ validateDurableJob(job);
97
+ if (job.status !== "running") {
98
+ throw new Error(`DurableJob can only be marked unknown from running: ${job.status}.`);
99
+ }
100
+ const timestamp = now.toISOString();
101
+ return validateDurableJob({
102
+ ...job,
103
+ status: "unknown-needs-attention",
104
+ result: {
105
+ outcome: "unknown-needs-attention",
106
+ exitCode: null,
107
+ signal: null,
108
+ unknownReason: requireText(unknownReason, "DurableJob unknown reason"),
109
+ steps: completedSteps.map((step) => ({ ...step }))
110
+ },
111
+ terminalAt: timestamp,
112
+ updatedAt: timestamp
113
+ });
114
+ }
115
+ export function requestDurableJobCancel(job, now) {
116
+ validateDurableJob(job);
117
+ // A terminal job is already past cancellation: return it unchanged so the
118
+ // caller (e.g. `task integration abort` after the job finished but before
119
+ // `integration continue`) gets an idempotent no-op instead of an error.
120
+ if (isDurableJobTerminal(job.status))
121
+ return job;
122
+ if (job.cancelRequestedAt !== undefined)
123
+ return job;
124
+ const timestamp = now.toISOString();
125
+ return validateDurableJob({
126
+ ...job,
127
+ cancelRequestedAt: timestamp,
128
+ updatedAt: timestamp
129
+ });
130
+ }
131
+ /**
132
+ * Cancel a queued job that never started. The supervisor calls this when a
133
+ * queued job has `cancelRequestedAt` so it converges to `cancelled` without
134
+ * spawning a runner. A queued job has no steps to prove, so the result is
135
+ * an empty cancelled outcome.
136
+ */
137
+ export function cancelQueuedDurableJob(job, now) {
138
+ validateDurableJob(job);
139
+ if (job.status !== "queued") {
140
+ throw new Error(`DurableJob can only cancel-queued from queued: ${job.status}.`);
141
+ }
142
+ const timestamp = now.toISOString();
143
+ return validateDurableJob({
144
+ ...job,
145
+ status: "cancelled",
146
+ result: {
147
+ outcome: "cancelled",
148
+ exitCode: null,
149
+ signal: null,
150
+ steps: []
151
+ },
152
+ terminalAt: timestamp,
153
+ updatedAt: timestamp
154
+ });
155
+ }
156
+ export function markDurableJobWakeupNotified(job, now) {
157
+ validateDurableJob(job);
158
+ if (!isDurableJobTerminal(job.status)) {
159
+ throw new Error("DurableJob wakeup notification requires a terminal job.");
160
+ }
161
+ if (job.wakeupNotified === true)
162
+ return job;
163
+ return validateDurableJob({
164
+ ...job,
165
+ wakeupNotified: true,
166
+ updatedAt: now.toISOString()
167
+ });
168
+ }
169
+ /**
170
+ * Acknowledge an unknown-needs-attention job. The Leader has inspected the
171
+ * runner's evidence and accepts responsibility for follow-up; the job no
172
+ * longer blocks Task lifecycle gates. Only unknown jobs can be acknowledged,
173
+ * and the flag is one-way (a terminal job stays terminal).
174
+ */
175
+ export function acknowledgeUnknownDurableJob(job, now) {
176
+ validateDurableJob(job);
177
+ if (job.status !== "unknown-needs-attention") {
178
+ throw new Error(`DurableJob can only acknowledge from unknown-needs-attention: ${job.status}.`);
179
+ }
180
+ if (job.acknowledgedAt !== undefined)
181
+ return job;
182
+ return validateDurableJob({
183
+ ...job,
184
+ acknowledgedAt: now.toISOString(),
185
+ updatedAt: now.toISOString()
186
+ });
187
+ }
188
+ export function durableJobIdempotencyKey(input) {
189
+ const canonical = canonicalJson({
190
+ owner: input.owner,
191
+ projectId: input.projectId,
192
+ head: input.head,
193
+ steps: input.steps,
194
+ workspace: input.workspace,
195
+ env: input.env
196
+ });
197
+ return createHash("sha256").update(canonical).digest("hex");
198
+ }
199
+ export function retryDurableJobIdempotencyKey(previousKey, previousJobId) {
200
+ return createHash("sha256")
201
+ .update(`${previousKey}|retry-of|${previousJobId}`)
202
+ .digest("hex");
203
+ }
204
+ export function validateDurableJob(job) {
205
+ if (job.schemaVersion !== CURRENT_DURABLE_JOB_SCHEMA_VERSION) {
206
+ throw new Error(`DurableJob must use schemaVersion ${CURRENT_DURABLE_JOB_SCHEMA_VERSION}.`);
207
+ }
208
+ validateTaskRecordReference({
209
+ taskId: job.taskId,
210
+ localId: job.id
211
+ }, "durableJob");
212
+ validateDurableJobOwner(job.owner, job.taskId);
213
+ requireIdentity(job.projectId, "DurableJob project id");
214
+ requireCommit(job.head, "DurableJob head");
215
+ requireText(job.workspace, "DurableJob workspace");
216
+ if (!job.workspace.startsWith("/")) {
217
+ throw new Error("DurableJob workspace must be an absolute path.");
218
+ }
219
+ validateEnvMap(job.env);
220
+ if (!Array.isArray(job.steps) || job.steps.length === 0) {
221
+ throw new Error("DurableJob requires at least one step.");
222
+ }
223
+ const stepNames = new Set();
224
+ for (const step of job.steps) {
225
+ const name = requireText(step.name, "DurableJob step name");
226
+ if (stepNames.has(name)) {
227
+ throw new Error(`DurableJob step names must be unique: ${name}.`);
228
+ }
229
+ stepNames.add(name);
230
+ requireText(step.command, `DurableJob step command (${name})`);
231
+ if (step.argv !== undefined) {
232
+ if (!Array.isArray(step.argv) || step.argv.length === 0) {
233
+ throw new Error(`DurableJob step argv must be a non-empty array (${name}).`);
234
+ }
235
+ for (const value of step.argv) {
236
+ requireText(value, `DurableJob step argv (${name})`);
237
+ }
238
+ }
239
+ if (step.cwd !== undefined) {
240
+ requireText(step.cwd, `DurableJob step cwd (${name})`);
241
+ if (!step.cwd.startsWith("/")) {
242
+ throw new Error(`DurableJob step cwd must be absolute (${name}).`);
243
+ }
244
+ }
245
+ if (step.env !== undefined) {
246
+ validateEnvMap(step.env);
247
+ }
248
+ if (step.timeoutMs !== undefined) {
249
+ requirePositiveInteger(step.timeoutMs, `DurableJob step timeout (${name})`);
250
+ }
251
+ }
252
+ requireText(job.idempotencyKey, "DurableJob idempotency key");
253
+ if (!/^[a-f0-9]{64}$/u.test(job.idempotencyKey)) {
254
+ throw new Error("DurableJob idempotency key is invalid.");
255
+ }
256
+ if (![
257
+ "queued",
258
+ "running",
259
+ "succeeded",
260
+ "failed",
261
+ "timed-out",
262
+ "cancelled",
263
+ "unknown-needs-attention"
264
+ ].includes(job.status)) {
265
+ throw new Error(`DurableJob status is invalid: ${String(job.status)}.`);
266
+ }
267
+ requireText(job.artifactsLocator, "DurableJob artifacts locator");
268
+ if (job.artifactsLocator.startsWith("/") || job.artifactsLocator.includes("..")) {
269
+ throw new Error("DurableJob artifacts locator must be a home-relative path.");
270
+ }
271
+ requireTimestamp(job.createdAt, "DurableJob createdAt");
272
+ requireTimestamp(job.updatedAt, "DurableJob updatedAt");
273
+ if (job.startedAt !== undefined) {
274
+ requireTimestamp(job.startedAt, "DurableJob startedAt");
275
+ }
276
+ if (job.heartbeatAt !== undefined) {
277
+ requireTimestamp(job.heartbeatAt, "DurableJob heartbeatAt");
278
+ }
279
+ if (job.terminalAt !== undefined) {
280
+ requireTimestamp(job.terminalAt, "DurableJob terminalAt");
281
+ }
282
+ if (job.process !== undefined) {
283
+ requirePositiveInteger(job.process.pid, "DurableJob process pid");
284
+ requireText(job.process.startIdentity, "DurableJob process startIdentity");
285
+ }
286
+ if (job.checkpoint !== undefined) {
287
+ requireTimestamp(job.checkpoint.updatedAt, "DurableJob checkpoint updatedAt");
288
+ for (const step of job.checkpoint.completedSteps) {
289
+ validateStepResult(step);
290
+ }
291
+ }
292
+ if (job.result !== undefined) {
293
+ normalizeDurableJobResult(job.result);
294
+ }
295
+ if (job.retryOf !== undefined) {
296
+ validateTaskRecordReference({ taskId: job.taskId, localId: job.retryOf }, "durableJob");
297
+ }
298
+ if (job.cancelRequestedAt !== undefined) {
299
+ requireTimestamp(job.cancelRequestedAt, "DurableJob cancelRequestedAt");
300
+ }
301
+ if (job.wakeupNotified !== undefined && typeof job.wakeupNotified !== "boolean") {
302
+ throw new Error("DurableJob wakeupNotified must be a boolean.");
303
+ }
304
+ if (job.acknowledgedAt !== undefined) {
305
+ requireTimestamp(job.acknowledgedAt, "DurableJob acknowledgedAt");
306
+ }
307
+ if (isDurableJobTerminal(job.status)) {
308
+ requireTimestamp(job.terminalAt ?? "", "DurableJob terminalAt");
309
+ }
310
+ if (job.status === "queued" && job.startedAt !== undefined) {
311
+ throw new Error("A queued DurableJob must not have startedAt.");
312
+ }
313
+ if (job.status === "running" && job.startedAt === undefined) {
314
+ throw new Error("A running DurableJob must have startedAt.");
315
+ }
316
+ return job;
317
+ }
318
+ export function validDurableJobTransition(before, after) {
319
+ if (before.id !== after.id
320
+ || before.taskId !== after.taskId
321
+ || before.projectId !== after.projectId
322
+ || before.head !== after.head
323
+ || before.workspace !== after.workspace
324
+ || before.idempotencyKey !== after.idempotencyKey
325
+ || before.artifactsLocator !== after.artifactsLocator
326
+ || before.createdAt !== after.createdAt
327
+ || !isDeepStrictEqual(before.owner, after.owner)
328
+ || !isDeepStrictEqual(before.env, after.env)
329
+ || !isDeepStrictEqual(before.steps, after.steps))
330
+ return false;
331
+ if (isDurableJobTerminal(before.status)) {
332
+ // A terminal job is immutable except for two one-way flags:
333
+ // wakeupNotified (set by the supervisor after enqueuing the Leader
334
+ // wakeup) and acknowledgedAt (set by the Leader on an unknown job so
335
+ // Task lifecycle gates can proceed). Without these exceptions the
336
+ // flags could never be recorded and every terminal/unknown job would
337
+ // block forever.
338
+ const wakeupFlip = before.wakeupNotified !== true && after.wakeupNotified === true;
339
+ const acknowledgeFlip = before.acknowledgedAt === undefined
340
+ && after.acknowledgedAt !== undefined;
341
+ return before.status === after.status
342
+ && isDeepStrictEqual(before.result, after.result)
343
+ && before.terminalAt === after.terminalAt
344
+ && (before.wakeupNotified === after.wakeupNotified || wakeupFlip)
345
+ && (before.acknowledgedAt === after.acknowledgedAt || acknowledgeFlip);
346
+ }
347
+ const allowed = {
348
+ queued: ["queued", "running", "cancelled"],
349
+ running: [
350
+ "running",
351
+ "succeeded",
352
+ "failed",
353
+ "timed-out",
354
+ "cancelled",
355
+ "unknown-needs-attention"
356
+ ]
357
+ };
358
+ return (allowed[before.status] ?? []).includes(after.status);
359
+ }
360
+ function validateDurableJobOwner(owner, taskId) {
361
+ if (owner.kind === "integration-attempt") {
362
+ validateTaskRecordReference({ taskId, localId: owner.integrationAttemptId }, "integrationAttempt");
363
+ }
364
+ else if (owner.kind === "work-item") {
365
+ validateTaskRecordReference({ taskId, localId: owner.workItemId }, "workItem");
366
+ }
367
+ else if (owner.kind !== "task") {
368
+ throw new Error(`DurableJob owner kind is invalid: ${String(owner.kind)}.`);
369
+ }
370
+ }
371
+ function validateEnvMap(env) {
372
+ if (typeof env !== "object" || env === null || Array.isArray(env)) {
373
+ throw new Error("DurableJob env must be a map.");
374
+ }
375
+ for (const [key, value] of Object.entries(env)) {
376
+ if (typeof key !== "string" || key.length === 0 || key.includes("=") || key.includes("\0")) {
377
+ throw new Error(`DurableJob env key is invalid: ${key}.`);
378
+ }
379
+ if (typeof value !== "string" || value.includes("\0")) {
380
+ throw new Error(`DurableJob env value is invalid: ${key}.`);
381
+ }
382
+ }
383
+ }
384
+ function validateStepResult(step) {
385
+ requireText(step.name, "DurableJob step result name");
386
+ if (step.exitCode !== null && (!Number.isSafeInteger(step.exitCode) || step.exitCode < 0)) {
387
+ throw new Error(`DurableJob step exitCode is invalid: ${step.name}.`);
388
+ }
389
+ if (step.signal !== null && typeof step.signal !== "string") {
390
+ throw new Error(`DurableJob step signal is invalid: ${step.name}.`);
391
+ }
392
+ if (typeof step.timedOut !== "boolean") {
393
+ throw new Error(`DurableJob step timedOut must be boolean: ${step.name}.`);
394
+ }
395
+ requirePositiveInteger(step.durationMs, `DurableJob step durationMs (${step.name})`);
396
+ requireText(step.logPath, `DurableJob step logPath (${step.name})`);
397
+ requireCommit(step.head, `DurableJob step head (${step.name})`);
398
+ }
399
+ function normalizeDurableJobResult(result) {
400
+ if (!isDurableJobTerminal(result.outcome)) {
401
+ throw new Error(`DurableJob result outcome must be terminal: ${result.outcome}.`);
402
+ }
403
+ if (result.exitCode !== null && (!Number.isSafeInteger(result.exitCode) || result.exitCode < 0)) {
404
+ throw new Error("DurableJob result exitCode is invalid.");
405
+ }
406
+ if (result.signal !== null && typeof result.signal !== "string") {
407
+ throw new Error("DurableJob result signal is invalid.");
408
+ }
409
+ if (result.failedStep !== undefined) {
410
+ requireText(result.failedStep, "DurableJob result failedStep");
411
+ }
412
+ if (result.unknownReason !== undefined) {
413
+ requireText(result.unknownReason, "DurableJob result unknownReason");
414
+ }
415
+ if (result.evidenceSource !== undefined
416
+ && result.evidenceSource !== "exit-artifact"
417
+ && result.evidenceSource !== "checkpoint") {
418
+ throw new Error(`DurableJob result evidenceSource is invalid: ${String(result.evidenceSource)}.`);
419
+ }
420
+ if (!Array.isArray(result.steps)) {
421
+ throw new Error("DurableJob result steps must be an array.");
422
+ }
423
+ for (const step of result.steps)
424
+ validateStepResult(step);
425
+ return result;
426
+ }
427
+ function requireCommit(value, label) {
428
+ const normalized = requireText(value, label).toLowerCase();
429
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
430
+ throw new Error(`${label} is invalid.`);
431
+ }
432
+ return normalized;
433
+ }
434
+ function canonicalJson(value) {
435
+ return JSON.stringify(canonicalize(value));
436
+ }
437
+ function canonicalize(value) {
438
+ if (Array.isArray(value))
439
+ return value.map(canonicalize);
440
+ if (typeof value === "object" && value !== null) {
441
+ const entries = Object.entries(value)
442
+ .sort(([left], [right]) => left.localeCompare(right));
443
+ return Object.fromEntries(entries.map(([key, entry]) => [key, canonicalize(entry)]));
444
+ }
445
+ return value;
446
+ }
447
+ function isDeepStrictEqual(left, right) {
448
+ return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
449
+ }