@dsh-enhanced/plugin-control-plane 0.1.47 → 0.1.49

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 (167) hide show
  1. package/README.md +183 -15
  2. package/bin/dsh-live-qualification-authority.js +15 -0
  3. package/bin/dsh-local-release-adapter.js +328 -8
  4. package/bin/dsh-systemd-host-attestor.js +359 -9
  5. package/bin/dsh-systemd-host-authority.js +12 -0
  6. package/lib/adoption-coordinator.d.ts +3 -1
  7. package/lib/adoption-coordinator.d.ts.map +1 -1
  8. package/lib/adoption-coordinator.js +24 -9
  9. package/lib/adoption-coordinator.js.map +1 -1
  10. package/lib/cli.d.ts.map +1 -1
  11. package/lib/cli.js +33 -14
  12. package/lib/cli.js.map +1 -1
  13. package/lib/creation-capability-feedback.d.ts +23 -0
  14. package/lib/creation-capability-feedback.d.ts.map +1 -0
  15. package/lib/creation-capability-feedback.js +132 -0
  16. package/lib/creation-capability-feedback.js.map +1 -0
  17. package/lib/creation-capability-journal.d.ts +66 -0
  18. package/lib/creation-capability-journal.d.ts.map +1 -0
  19. package/lib/creation-capability-journal.js +751 -0
  20. package/lib/creation-capability-journal.js.map +1 -0
  21. package/lib/creation-capability-runtime.d.ts +63 -0
  22. package/lib/creation-capability-runtime.d.ts.map +1 -0
  23. package/lib/creation-capability-runtime.js +564 -0
  24. package/lib/creation-capability-runtime.js.map +1 -0
  25. package/lib/creation-capability-source.d.ts +37 -0
  26. package/lib/creation-capability-source.d.ts.map +1 -0
  27. package/lib/creation-capability-source.js +328 -0
  28. package/lib/creation-capability-source.js.map +1 -0
  29. package/lib/creation-capability-types.d.ts +252 -0
  30. package/lib/creation-capability-types.d.ts.map +1 -0
  31. package/lib/creation-capability-types.js +2 -0
  32. package/lib/creation-capability-types.js.map +1 -0
  33. package/lib/deployment-readiness.d.ts +16 -0
  34. package/lib/deployment-readiness.d.ts.map +1 -1
  35. package/lib/deployment-readiness.js +48 -1
  36. package/lib/deployment-readiness.js.map +1 -1
  37. package/lib/effect-blocked-replay.js +2 -2
  38. package/lib/effect-blocked-replay.js.map +1 -1
  39. package/lib/foreground-deployment-runtime.d.ts +3 -0
  40. package/lib/foreground-deployment-runtime.d.ts.map +1 -1
  41. package/lib/foreground-deployment-runtime.js +55 -13
  42. package/lib/foreground-deployment-runtime.js.map +1 -1
  43. package/lib/foreground-deployment.d.ts.map +1 -1
  44. package/lib/foreground-deployment.js +5 -0
  45. package/lib/foreground-deployment.js.map +1 -1
  46. package/lib/host-attestor.d.ts +2 -0
  47. package/lib/host-attestor.d.ts.map +1 -1
  48. package/lib/host-attestor.js +8 -1
  49. package/lib/host-attestor.js.map +1 -1
  50. package/lib/host-inputs.d.ts +13 -0
  51. package/lib/host-inputs.d.ts.map +1 -0
  52. package/lib/host-inputs.js +162 -0
  53. package/lib/host-inputs.js.map +1 -0
  54. package/lib/host-maintenance.d.ts +61 -0
  55. package/lib/host-maintenance.d.ts.map +1 -0
  56. package/lib/host-maintenance.js +158 -0
  57. package/lib/host-maintenance.js.map +1 -0
  58. package/lib/host-readiness.d.ts +5 -0
  59. package/lib/host-readiness.d.ts.map +1 -0
  60. package/lib/host-readiness.js +22 -0
  61. package/lib/host-readiness.js.map +1 -0
  62. package/lib/index.d.ts +16 -0
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +7 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/live-qualification-authority.d.ts +32 -0
  67. package/lib/live-qualification-authority.d.ts.map +1 -0
  68. package/lib/live-qualification-authority.js +280 -0
  69. package/lib/live-qualification-authority.js.map +1 -0
  70. package/lib/live-qualification-client.d.ts +4 -0
  71. package/lib/live-qualification-client.d.ts.map +1 -0
  72. package/lib/live-qualification-client.js +22 -0
  73. package/lib/live-qualification-client.js.map +1 -0
  74. package/lib/live-qualification-runtime.d.ts +78 -0
  75. package/lib/live-qualification-runtime.d.ts.map +1 -0
  76. package/lib/live-qualification-runtime.js +367 -0
  77. package/lib/live-qualification-runtime.js.map +1 -0
  78. package/lib/live-qualification.d.ts +73 -0
  79. package/lib/live-qualification.d.ts.map +1 -0
  80. package/lib/live-qualification.js +164 -0
  81. package/lib/live-qualification.js.map +1 -0
  82. package/lib/owner-task-gaps.d.ts +2 -0
  83. package/lib/owner-task-gaps.d.ts.map +1 -1
  84. package/lib/owner-task-gaps.js +9 -1
  85. package/lib/owner-task-gaps.js.map +1 -1
  86. package/lib/runtime-epoch.d.ts +69 -0
  87. package/lib/runtime-epoch.d.ts.map +1 -0
  88. package/lib/runtime-epoch.js +139 -0
  89. package/lib/runtime-epoch.js.map +1 -0
  90. package/lib/runtime-observer.d.ts.map +1 -1
  91. package/lib/runtime-observer.js +42 -6
  92. package/lib/runtime-observer.js.map +1 -1
  93. package/lib/service.d.ts +247 -4
  94. package/lib/service.d.ts.map +1 -1
  95. package/lib/service.js +1332 -27
  96. package/lib/service.js.map +1 -1
  97. package/lib/source-adoption-authority.d.ts +3 -0
  98. package/lib/source-adoption-authority.d.ts.map +1 -1
  99. package/lib/source-adoption-authority.js +13 -3
  100. package/lib/source-adoption-authority.js.map +1 -1
  101. package/lib/source-adoption-runner.d.ts +3 -0
  102. package/lib/source-adoption-runner.d.ts.map +1 -1
  103. package/lib/source-adoption-runner.js +16 -4
  104. package/lib/source-adoption-runner.js.map +1 -1
  105. package/lib/source-baseline.d.ts +37 -0
  106. package/lib/source-baseline.d.ts.map +1 -0
  107. package/lib/source-baseline.js +250 -0
  108. package/lib/source-baseline.js.map +1 -0
  109. package/lib/source-build.d.ts +17 -1
  110. package/lib/source-build.d.ts.map +1 -1
  111. package/lib/source-build.js +501 -7
  112. package/lib/source-build.js.map +1 -1
  113. package/lib/source-context.d.ts +18 -0
  114. package/lib/source-context.d.ts.map +1 -1
  115. package/lib/source-context.js +85 -11
  116. package/lib/source-context.js.map +1 -1
  117. package/lib/source-creation.d.ts +106 -0
  118. package/lib/source-creation.d.ts.map +1 -0
  119. package/lib/source-creation.js +664 -0
  120. package/lib/source-creation.js.map +1 -0
  121. package/lib/source-job-types.d.ts +19 -0
  122. package/lib/source-job-types.d.ts.map +1 -1
  123. package/lib/source-jobs.d.ts +30 -3
  124. package/lib/source-jobs.d.ts.map +1 -1
  125. package/lib/source-jobs.js +382 -40
  126. package/lib/source-jobs.js.map +1 -1
  127. package/lib/source-maintenance.d.ts +66 -0
  128. package/lib/source-maintenance.d.ts.map +1 -0
  129. package/lib/source-maintenance.js +177 -0
  130. package/lib/source-maintenance.js.map +1 -0
  131. package/lib/source-revision.d.ts +57 -0
  132. package/lib/source-revision.d.ts.map +1 -0
  133. package/lib/source-revision.js +175 -0
  134. package/lib/source-revision.js.map +1 -0
  135. package/lib/source-workspace.d.ts +19 -14
  136. package/lib/source-workspace.d.ts.map +1 -1
  137. package/lib/source-workspace.js +90 -17
  138. package/lib/source-workspace.js.map +1 -1
  139. package/lib/sqlite.d.ts +13 -3
  140. package/lib/sqlite.d.ts.map +1 -1
  141. package/lib/sqlite.js +362 -1
  142. package/lib/sqlite.js.map +1 -1
  143. package/lib/store.d.ts +258 -3
  144. package/lib/store.d.ts.map +1 -1
  145. package/lib/store.js +2211 -95
  146. package/lib/store.js.map +1 -1
  147. package/lib/systemd-host-authority.d.ts +68 -0
  148. package/lib/systemd-host-authority.d.ts.map +1 -0
  149. package/lib/systemd-host-authority.js +424 -0
  150. package/lib/systemd-host-authority.js.map +1 -0
  151. package/lib/task-observation-evidence.d.ts +32 -0
  152. package/lib/task-observation-evidence.d.ts.map +1 -0
  153. package/lib/task-observation-evidence.js +62 -0
  154. package/lib/task-observation-evidence.js.map +1 -0
  155. package/lib/task-observation-runtime.d.ts +2 -3
  156. package/lib/task-observation-runtime.d.ts.map +1 -1
  157. package/lib/task-observation-runtime.js +7 -39
  158. package/lib/task-observation-runtime.js.map +1 -1
  159. package/lib/task-observation-store.js +1 -1
  160. package/lib/trust.d.ts.map +1 -1
  161. package/lib/trust.js +0 -0
  162. package/lib/trust.js.map +1 -1
  163. package/lib/types.d.ts +43 -6
  164. package/lib/types.d.ts.map +1 -1
  165. package/lib/version.d.ts +1 -1
  166. package/lib/version.js +1 -1
  167. package/package.json +21 -14
@@ -1,11 +1,16 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { isAbsolute, join, resolve } from 'node:path';
3
3
  import { realpath } from 'node:fs/promises';
4
+ import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
5
+ import { automationDefinitionDigest } from '@dsh-enhanced/assistant-automations';
4
6
  import { controlPlaneDigest, expectedSourceRelease } from './store.js';
5
7
  import { awaitSourceSignal, inspectSourceContext } from './source-context.js';
6
8
  import { assertPluginModificationAllowed, removeSourceJobWorktree, validateScopedPluginFiles } from './source-workspace.js';
7
9
  import { assertManagedVersionPaths } from './source-versioning.js';
10
+ import { resolveSourceBaseline, validateSourceBaselineConfig } from './source-baseline.js';
8
11
  import { removeSourceJobContainer } from './source-build.js';
12
+ import { inspectSourceRevisionContext, validateSourceRevisionGrant } from './source-revision.js';
13
+ import { inspectSourceCreationContext, validateSourceCreationFiles, validateSourceCreationGrant } from './source-creation.js';
9
14
  import { inheritedEnvironment } from './trust.js';
10
15
  export const SOURCE_JOB_OWNER = 'plugin-control-plane-source';
11
16
  const EXECUTOR = 'plugin-control-plane-source-check';
@@ -13,8 +18,11 @@ const CATALOG = controlPlaneDigest({ executor: EXECUTOR, version: 1, operation:
13
18
  const CONTINUATION_EXECUTOR = 'plugin-control-plane-source-continuations-v1';
14
19
  const CONTINUATION_CATALOG = controlPlaneDigest({ executor: CONTINUATION_EXECUTOR, version: 1, operation: 'continue-prepared-source-plan' });
15
20
  const CONTINUATION_AUTOMATION = 'source-job-prepared-continuations';
21
+ export { SourceGrowthRunUnavailableError } from '@dsh-enhanced/assistant-growth-contract';
22
+ class SourceGrowthNativeMismatchError extends Error {
23
+ }
16
24
  export function validateSourceJobsConfig(value, build) {
17
- const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'repository', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
25
+ const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'repository', 'baseline', 'creation', 'revision', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
18
26
  if (value === null || typeof value !== 'object' || Array.isArray(value) || build === undefined
19
27
  || Object.keys(value).some(key => !allowed.has(key))
20
28
  || ![value.authorityId, value.ownerRouteId, value.principalId, value.repository, value.workspace, value.preset].every(text => typeof text === 'string' && text !== '' && text.normalize('NFC').trim() === text && text.length <= 4096 && !/[\p{Cc}]/u.test(text))
@@ -26,9 +34,16 @@ export function validateSourceJobsConfig(value, build) {
26
34
  || !Number.isSafeInteger(value.budgetAmount) || value.budgetAmount < 1) {
27
35
  throw new Error('plugin-control-plane: invalid sourceJobs authority configuration');
28
36
  }
37
+ if (value.baseline !== undefined)
38
+ validateSourceBaselineConfig(value.baseline);
39
+ if (value.creation !== undefined)
40
+ validateSourceCreationGrant(value.creation);
41
+ if (value.revision !== undefined)
42
+ validateSourceRevisionGrant(value.revision);
29
43
  }
30
44
  function projection(job) {
31
45
  return { id: job.id, name: job.intent.name, gapId: job.intent.gapId, baseCommit: job.intent.baseCommit,
46
+ ...(job.intent.mode === undefined ? {} : { mode: job.intent.mode }),
32
47
  status: job.status, createdAt: job.createdAt, expiresAt: job.expiresAt,
33
48
  ...(job.planId === undefined ? {} : { planId: job.planId }), ...(job.failureCode === undefined ? {} : { failureCode: job.failureCode }) };
34
49
  }
@@ -37,6 +52,8 @@ export class SourceJobRuntime {
37
52
  options;
38
53
  abort = new AbortController();
39
54
  flights = new Set();
55
+ growthFlights = new Set();
56
+ growthControllers = new Set();
40
57
  authorityDigest;
41
58
  unregister;
42
59
  unregisterContinuation;
@@ -59,7 +76,15 @@ export class SourceJobRuntime {
59
76
  const executor = {
60
77
  descriptor: { executorId: EXECUTOR, contractVersion: 1, catalogDigest: CATALOG },
61
78
  accepts: spec => spec.executorId === EXECUTOR && spec.executorContractVersion === 1 && spec.catalogDigest === CATALOG && spec.runbookId === EXECUTOR && spec.runbookVersion === 1,
62
- execute: input => this.track(this.execute(input)),
79
+ execute: input => {
80
+ const job = this.options.store.getSourceJobByAutomation(input.automationId);
81
+ const flight = this.track(this.execute(input));
82
+ if ((job?.intent.creation?.growthRun ?? job?.intent.revision?.growthRun) !== undefined) {
83
+ this.growthFlights.add(flight);
84
+ void flight.then(() => this.growthFlights.delete(flight), () => this.growthFlights.delete(flight));
85
+ }
86
+ return flight;
87
+ },
63
88
  };
64
89
  try {
65
90
  this.unregister = this.options.ports.automations.registerHostExecutor(executor);
@@ -90,23 +115,57 @@ export class SourceJobRuntime {
90
115
  catch { /* original error wins */ }
91
116
  throw error;
92
117
  }
118
+ this.reconcileQueued();
119
+ }
120
+ /** Nudge the existing native one-shot registrations after a Host provider appears. */
121
+ reconcileQueued() {
122
+ if (!this.active)
123
+ return;
93
124
  // Reconcile only queued intents. A previous claim is unknown and never replayed.
94
125
  for (let job of this.options.store.listSourceJobs(100)) {
95
126
  if (job.status !== 'queued')
96
127
  continue;
97
128
  try {
98
129
  job = this.refreshQueued(job);
99
- if (job.status === 'queued') {
100
- this.assertOwner(job);
130
+ if (job.status === 'queued')
101
131
  this.schedule(job);
102
- }
103
132
  }
104
- catch {
133
+ catch (error) {
134
+ if (error instanceof SourceGrowthRunUnavailableError)
135
+ continue;
105
136
  const current = this.options.store.getSourceJob(job.id);
137
+ // A persisted rearm is an in-progress cross-ledger handoff. Retain it
138
+ // for exact native receipt replay after a crash or transient fault.
139
+ if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
140
+ continue;
106
141
  if (current?.status === 'queued')
107
142
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-reconcile-rejected' });
108
143
  }
109
144
  }
145
+ this.reconcileContinuations();
146
+ }
147
+ /** Provider removal revokes in-flight authority without replaying a claimed job. */
148
+ growthProviderDisposed() {
149
+ for (const controller of this.growthControllers)
150
+ controller.abort(new Error('source growth run producer disposed'));
151
+ this.options.store.interruptGrowthSourceJobs();
152
+ if (!this.active)
153
+ return Promise.allSettled(this.growthFlights).then(() => undefined);
154
+ for (const job of this.options.store.listSourceJobs(100)) {
155
+ if (job.status !== 'queued' || (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined || job.definitionHash === undefined)
156
+ continue;
157
+ try {
158
+ const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
159
+ if (health.definitionHash !== job.definitionHash)
160
+ continue;
161
+ if (health.automationStatus === 'active')
162
+ this.options.ports.automations.pauseSystemOwned({ owner: SOURCE_JOB_OWNER,
163
+ operationId: `${job.id}:growth-provider-pause:v${health.definitionVersion}`, automationId: job.automationId,
164
+ definitionHash: job.definitionHash, expectedVersion: health.definitionVersion });
165
+ }
166
+ catch { /* claim guard remains authoritative */ }
167
+ }
168
+ return Promise.allSettled(this.growthFlights).then(() => undefined);
110
169
  }
111
170
  async continuePrepared(job, signal) {
112
171
  if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
@@ -130,6 +189,8 @@ export class SourceJobRuntime {
130
189
  throw new Error('source job continuation changed');
131
190
  job = current;
132
191
  const initial = this.options.store.getSourcePlan(planId);
192
+ if (initial.mode !== 'modify' && initial.mode !== 'prepared-create' && initial.mode !== 'prepared-revise')
193
+ return;
133
194
  // An ordinary grant or plan must not be advanced after expiry. Adoption has
134
195
  // its own durable post-release path and is deliberately retained below.
135
196
  if (initial.expiresAt <= Date.now() && initial.status !== 'release-complete')
@@ -142,6 +203,35 @@ export class SourceJobRuntime {
142
203
  signal.throwIfAborted();
143
204
  this.assertOwner(job);
144
205
  };
206
+ if (initial.mode === 'prepared-revise') {
207
+ if (job.intent.mode !== 'revise-created' || !job.intent.revision?.growthRun.revisionAcceptance || !initial.sourceRevision?.growthRun.revisionAcceptance)
208
+ return;
209
+ await assertCurrent();
210
+ if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
211
+ await this.options.verifyPreparedRevision(job, signal);
212
+ if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
213
+ && this.options.store.getRevisionRegressionStatus(planId) === undefined
214
+ && job.intent.revision.growthRun.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
215
+ await assertCurrent();
216
+ await this.options.verifyPreparedRevisionRegression(job, signal);
217
+ }
218
+ return;
219
+ }
220
+ if (initial.mode === 'prepared-create') {
221
+ if (job.intent.mode !== 'create'
222
+ || !job.intent.creation?.growthRun?.creationAcceptance || !initial.creation?.growthRun?.creationAcceptance)
223
+ return;
224
+ await assertCurrent();
225
+ if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
226
+ await this.options.verifyPreparedCreation(job, signal);
227
+ }
228
+ if (this.options.store.getCreationVerificationStatus(planId) === 'verified'
229
+ && this.options.creationAdoptionEligible?.(planId) && this.options.adoptVerifiedCreation) {
230
+ await assertCurrent();
231
+ await this.options.adoptVerifiedCreation(job, signal);
232
+ }
233
+ return;
234
+ }
145
235
  if (this.options.adoptReleased && this.options.store.getSourcePlan(planId).status === 'release-complete') {
146
236
  await this.options.adoptReleased(job, signal, assertCurrent);
147
237
  return;
@@ -220,15 +310,53 @@ export class SourceJobRuntime {
220
310
  return this.options.withGapSourceFence(gapId, owner, callback);
221
311
  }
222
312
  assertOwner(job) {
223
- if (!this.available() || job.expiresAt <= Date.now() || job.intent.authority.digest !== this.authorityDigest
224
- || job.intent.authority.id !== this.options.config.authorityId || controlPlaneDigest(this.receipt()) !== job.intent.ownerDigest)
313
+ const current = this.receipt();
314
+ const taskBacked = this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined;
315
+ if (!this.available() || job.expiresAt <= Date.now() || (job.intent.mode === 'create' && (!this.options.config.creation || !job.intent.creation
316
+ || job.intent.creation.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.creation.grant) !== controlPlaneDigest(this.options.config.creation)))
317
+ || (job.intent.mode === 'revise-created' && (!this.options.config.revision || !job.intent.revision
318
+ || job.intent.revision.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.revision.grant) !== controlPlaneDigest(this.options.config.revision)))
319
+ || job.intent.authority.digest !== this.authorityDigest
320
+ || job.intent.authority.id !== this.options.config.authorityId
321
+ || controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
322
+ || (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
323
+ : controlPlaneDigest(current) !== job.intent.ownerDigest))
225
324
  throw new Error('source job authority changed or expired');
226
- this.withGapSource(job.intent.gapId, job.intent.owner, () => { });
325
+ if (job.intent.revision) {
326
+ const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
327
+ if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
328
+ throw new Error('source revision parent changed');
329
+ }
330
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false));
331
+ }
332
+ assertGrowthRun(growthRun, mode, gapId, owner, generation) {
333
+ const taskBacked = this.options.store.getOwnerTaskFailureReference(gapId) !== undefined;
334
+ if ((mode !== 'create' && mode !== 'revise-created') || !taskBacked) {
335
+ if (growthRun !== undefined)
336
+ throw new Error('source growth run is outside a task-backed creation');
337
+ return;
338
+ }
339
+ if (growthRun === undefined)
340
+ throw new Error('task-backed creation requires a frozen source growth run');
341
+ validateSourceGrowthRunBinding(growthRun);
342
+ if (!this.options.assertGrowthRun)
343
+ throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
344
+ this.options.assertGrowthRun(growthRun, gapId, owner, generation);
227
345
  }
228
346
  enqueue(input) {
229
- validateScopedPluginFiles(input.files);
230
- if (this.options.build.versioning === 'patch')
231
- assertManagedVersionPaths(input.files);
347
+ if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
348
+ throw new Error('modify source job cannot carry a growth run');
349
+ if (input.mode === 'create' || input.mode === 'revise-created')
350
+ validateSourceCreationFiles(input.files);
351
+ else {
352
+ if (input.mode !== undefined)
353
+ throw new Error('invalid source job mode');
354
+ validateScopedPluginFiles(input.files);
355
+ if (this.options.build.versioning === 'patch')
356
+ assertManagedVersionPaths(input.files);
357
+ }
358
+ if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
359
+ throw new Error('source revision parent is missing or outside revision mode');
232
360
  return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner) }));
233
361
  }
234
362
  async enqueueOwned(input) {
@@ -236,17 +364,35 @@ export class SourceJobRuntime {
236
364
  const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, input.assertCurrent); this.withGapSource(input.gapId, this.assertCaller(input.owner), () => { }); if (!this.available())
237
365
  throw new Error('source job authority unavailable'); signal.throwIfAborted(); };
238
366
  await assertCurrent();
239
- validateScopedPluginFiles(input.files);
240
- if (this.options.build.versioning === 'patch')
241
- assertManagedVersionPaths(input.files);
242
- assertPluginModificationAllowed(input.name);
367
+ if (input.mode === 'create' || input.mode === 'revise-created')
368
+ validateSourceCreationFiles(input.files);
369
+ else {
370
+ validateScopedPluginFiles(input.files);
371
+ if (this.options.build.versioning === 'patch')
372
+ assertManagedVersionPaths(input.files);
373
+ assertPluginModificationAllowed(input.name);
374
+ }
243
375
  if (typeof input.idempotencyKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(input.idempotencyKey)
244
376
  || !Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000)
245
377
  throw new Error('invalid source job request');
246
378
  const config = this.options.config;
379
+ if (input.mode === 'create' && (config.creation === undefined || config.creation.expiresAt <= Date.now()))
380
+ throw new Error('source creation grant unavailable or expired');
381
+ if (input.mode === 'revise-created' && (config.revision === undefined || config.revision.expiresAt <= Date.now()))
382
+ throw new Error('source revision grant unavailable or expired');
247
383
  if (input.repository !== config.repository || await realpath(input.repository) !== config.repository)
248
384
  throw new Error('source job repository mismatch');
249
- const owner = this.assertCaller(input.owner);
385
+ const currentOwner = this.assertCaller(input.owner);
386
+ const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
387
+ if (input.mode === 'revise-created' && !reference)
388
+ throw new Error('revision requires an authenticated owner task failure');
389
+ const owner = reference?.owner ?? currentOwner;
390
+ if (reference && !isSourceOwnerContinuation(currentOwner, owner))
391
+ throw new Error('source job task owner changed');
392
+ this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true));
393
+ const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
394
+ if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
395
+ throw new Error('adopted revision parent unavailable or mismatched');
250
396
  const trust = await awaitSourceSignal(signal, this.options.trust);
251
397
  for (const outstanding of this.options.store.listSourceJobs(1))
252
398
  this.refreshQueued(outstanding);
@@ -256,6 +402,12 @@ export class SourceJobRuntime {
256
402
  if (prior !== undefined) {
257
403
  const frozen = prior.intent;
258
404
  if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
405
+ || frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
406
+ || (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
407
+ || (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
408
+ || (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
409
+ || controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
410
+ || controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun)))
259
411
  || frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
260
412
  || frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
261
413
  throw new Error('source job idempotency conflict');
@@ -267,17 +419,36 @@ export class SourceJobRuntime {
267
419
  const gap = this.options.store.getGap(input.gapId);
268
420
  if (gap.status !== 'open' || gap.candidateId !== undefined)
269
421
  throw new Error('source job gap is not open');
270
- const source = await inspectSourceContext({ repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit, environment: inheritedEnvironment(trust), signal, assertCurrent });
422
+ const environment = inheritedEnvironment(trust);
423
+ const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: config.repository,
424
+ config: config.baseline, environment, signal, assertCurrent, trust,
425
+ readHistory: () => this.options.store.getSourceBaselineHistory(config.repository),
426
+ readMaintenance: () => this.options.store.getSourceMaintenanceRecords(config.repository) });
427
+ const context = { repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit,
428
+ ...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent };
429
+ const source = input.mode === 'create'
430
+ ? await inspectSourceCreationContext({ ...context, grant: config.creation })
431
+ : input.mode === 'revise-created'
432
+ ? await inspectSourceRevisionContext({ ...context, grant: config.revision, parent: parent.parent, parentSource: parent.source, parentCertificate: parent.certificate })
433
+ : await inspectSourceContext(context);
271
434
  const intent = {
272
435
  authority: { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt, maxSubmissions: config.maxSubmissions },
273
436
  owner, ownerDigest: controlPlaneDigest(owner), trustDigest: controlPlaneDigest(trust), repository: config.repository, name: input.name,
437
+ ...(config.baseline === undefined ? {} : { baseline: structuredClone(config.baseline) }),
438
+ ...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
439
+ ...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
440
+ ...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
441
+ generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent), growthRun: structuredClone(input.growthRun) } } : {}),
274
442
  gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
275
443
  files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
276
444
  worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`), containerName: `dsh-source-job-${token}`,
277
445
  };
278
446
  await assertCurrent();
279
447
  // Synchronous acceptance and registration: no Agent signal is persisted.
280
- const job = this.withGapSource(gap.id, owner, () => this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent }));
448
+ const job = this.withGapSource(gap.id, owner, () => {
449
+ this.assertGrowthRun(input.growthRun, input.mode, gap.id, owner, true);
450
+ return this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent });
451
+ });
281
452
  if (job.status === 'queued')
282
453
  this.scheduleOrFail(job);
283
454
  return projection(this.options.store.getSourceJob(id));
@@ -285,8 +456,8 @@ export class SourceJobRuntime {
285
456
  inspect(input) {
286
457
  const owner = this.assertCaller(input.owner);
287
458
  const job = this.options.store.getSourceJob(input.id);
288
- // A new session binding may inspect/clean this owner's old job. Execution
289
- // still requires the complete frozen receipt in assertOwner; never replay.
459
+ // Inspection and cleanup may cross a session binding. A task-backed job
460
+ // retains its frozen owner digest and may continue under the same owner.
290
461
  if (job === undefined || job.intent.owner.authorityId !== owner.authorityId
291
462
  || job.intent.owner.principalId !== owner.principalId || job.intent.owner.principalRecordId !== owner.principalRecordId
292
463
  || job.intent.owner.principalVersion !== owner.principalVersion || job.intent.owner.workspace !== owner.workspace
@@ -299,15 +470,21 @@ export class SourceJobRuntime {
299
470
  if (job.status !== 'queued')
300
471
  return job;
301
472
  let code;
302
- try {
303
- this.withGapSource(job.intent.gapId, job.intent.owner, () => { });
304
- }
305
- catch {
306
- code = 'source-job-task-source-changed';
473
+ let providerUnavailable = false;
474
+ if (job.intent.mode === 'create' && this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined
475
+ && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined)
476
+ code = 'source-job-growth-run-missing';
477
+ else {
478
+ try {
479
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => { providerUnavailable = this.growthProducerUnavailable(job); });
480
+ }
481
+ catch {
482
+ code = 'source-job-task-source-changed';
483
+ }
307
484
  }
308
485
  if (job.expiresAt <= Date.now())
309
486
  code = 'source-job-expired';
310
- else if (job.definitionHash !== undefined) {
487
+ else if (code === undefined && job.definitionHash !== undefined && !providerUnavailable && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined) {
311
488
  const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
312
489
  const run = health.latestTerminalRuns.production;
313
490
  if (run !== undefined && run.createdAt >= job.createdAt && run.immutableContext.state === 'verified'
@@ -319,9 +496,20 @@ export class SourceJobRuntime {
319
496
  // loses its claim. No resubmission or second native occurrence is created.
320
497
  return code === undefined ? job : this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: code });
321
498
  }
499
+ growthProducerUnavailable(job) {
500
+ try {
501
+ this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false);
502
+ return false;
503
+ }
504
+ catch (error) {
505
+ if (error instanceof SourceGrowthRunUnavailableError)
506
+ return true;
507
+ throw error;
508
+ }
509
+ }
322
510
  definition(job) {
323
511
  const config = this.options.config;
324
- return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.createdAt + 1000).toISOString() },
512
+ return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
325
513
  workspace: config.workspace, agentPreset: config.preset, timeoutMs: job.intent.build.timeoutMs + 120_000 + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0),
326
514
  misfire: { kind: 'latest' }, overlap: 'skip', retrySafety: 'never', maxRetries: 0, principal: config.principalId,
327
515
  ...(config.budgetId === undefined ? {} : { budgetId: config.budgetId, budgetAmount: config.budgetAmount }),
@@ -332,26 +520,86 @@ export class SourceJobRuntime {
332
520
  schedule(job) {
333
521
  this.assertOwner(job);
334
522
  const automations = this.options.ports.automations;
523
+ if (job.definitionHash !== undefined && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && job.occurrenceId === undefined) {
524
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
525
+ if (health.definitionHash !== job.definitionHash)
526
+ throw new Error('source job native definition changed before rearm');
527
+ const dueAt = job.dispatchAt ?? job.createdAt + 1000;
528
+ const terminal = health.latestTerminalRuns.production;
529
+ if ((health.automationStatus === 'paused' && dueAt <= Date.now())
530
+ || (terminal !== undefined && terminal.immutableContext.state === 'verified'
531
+ && terminal.immutableContext.definitionHash === job.definitionHash && terminal.createdAt >= job.createdAt)) {
532
+ job = this.withGapSource(job.intent.gapId, job.intent.owner, () => {
533
+ this.assertGrowthRun(job.intent.creation?.growthRun ?? job.intent.revision?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
534
+ return this.options.store.rearmQueuedGrowthSourceJob({ id: job.id, revision: job.revision,
535
+ priorDefinitionHash: job.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
536
+ });
537
+ }
538
+ }
335
539
  const definition = this.definition(job);
540
+ const expectedHash = automationDefinitionDigest(definition);
541
+ if (job.previousDefinitionHash !== undefined) {
542
+ if (typeof automations.reconcileSystemExact !== 'function')
543
+ throw new SourceGrowthRunUnavailableError('exact native source rearm unavailable');
544
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
545
+ const activation = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
546
+ if (activation?.activationNonce !== job.intentDigest || activation.ownerRouteId !== this.options.config.ownerRouteId
547
+ || !((health.definitionHash === job.previousDefinitionHash && health.definitionVersion === job.previousDefinitionVersion)
548
+ || (health.definitionHash === expectedHash && health.definitionVersion === job.previousDefinitionVersion + 1))) {
549
+ throw new SourceGrowthNativeMismatchError('source job native rearm lineage changed');
550
+ }
551
+ automations.reconcileSystemExact({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
552
+ idempotencyKey: `${job.id}:rearm:${job.dispatchAt}`, desiredStatus: 'active', definition,
553
+ expectedDefinitionHash: job.previousDefinitionHash, expectedVersion: job.previousDefinitionVersion });
554
+ const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
555
+ if (activated.definitionHash !== expectedHash || activated.definitionVersion !== job.previousDefinitionVersion + 1
556
+ || activated.automationStatus !== 'active')
557
+ throw new SourceGrowthNativeMismatchError('source job exact native rearm was not active');
558
+ this.assertOwner(job);
559
+ this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: expectedHash });
560
+ return;
561
+ }
336
562
  if (job.definitionHash === undefined) {
337
- automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId, idempotencyKey: `${job.id}:prepare`, desiredStatus: 'paused', definition });
563
+ const prior = job.previousDefinitionHash;
564
+ const existing = (() => {
565
+ try {
566
+ return automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
567
+ }
568
+ catch {
569
+ return undefined;
570
+ }
571
+ })();
572
+ if (existing !== undefined && (existing.activationNonce !== job.intentDigest || existing.ownerRouteId !== this.options.config.ownerRouteId
573
+ || (existing.definitionHash !== expectedHash && existing.definitionHash !== prior))) {
574
+ throw new Error('source job native definition differs from persisted rearm');
575
+ }
576
+ automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
577
+ idempotencyKey: `${job.id}:prepare:${job.dispatchAt ?? job.createdAt + 1000}`, desiredStatus: 'paused', definition });
338
578
  const registered = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
339
- if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId)
579
+ if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId
580
+ || registered.definitionHash !== expectedHash)
340
581
  throw new Error('source job registration binding mismatch');
341
582
  job = this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: registered.definitionHash });
342
583
  }
343
584
  const current = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
344
585
  if (current === undefined || current.definitionHash !== job.definitionHash || current.activationNonce !== job.intentDigest || current.ownerRouteId !== this.options.config.ownerRouteId)
345
586
  throw new Error('source job automation changed');
346
- automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId, idempotencyKey: `${job.id}:activate`, desiredStatus: 'active', definition });
587
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
588
+ if (health.definitionHash !== job.definitionHash)
589
+ throw new Error('source job native definition changed');
590
+ automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
591
+ idempotencyKey: `${job.id}:activate:v${health.definitionVersion}`, desiredStatus: 'active', definition });
592
+ const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
593
+ if (activated.definitionHash !== job.definitionHash || activated.automationStatus !== 'active')
594
+ throw new Error('source job native activation did not become active');
347
595
  }
348
596
  hasContinuations() {
349
- return !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
597
+ return !!(this.options.approvePrepared || this.options.verifyPreparedRevisionRegression || this.options.verifyPreparedRevision || this.options.verifyPreparedCreation || this.options.adoptVerifiedCreation || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
350
598
  }
351
599
  continuationJobs() {
352
600
  if (!this.hasContinuations())
353
601
  return [];
354
- return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined)
602
+ return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined, this.options.verifyPreparedCreation !== undefined, this.options.adoptVerifiedCreation !== undefined, this.options.verifyPreparedRevision !== undefined, this.options.verifyPreparedRevisionRegression !== undefined)
355
603
  .filter(job => this.eligibleContinuation(job));
356
604
  }
357
605
  /** Admission reads durable state and validates the current owner; it never invokes continuation approval or release hooks. */
@@ -365,8 +613,50 @@ export class SourceJobRuntime {
365
613
  catch {
366
614
  return false;
367
615
  }
616
+ if (plan.mode === 'prepared-revise') {
617
+ if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval' || !plan.sourceRevision?.growthRun.revisionAcceptance)
618
+ return false;
619
+ const status = this.options.store.getRevisionVerificationStatus(plan.id);
620
+ const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
621
+ const regression = status === 'verified' && plan.sourceRevision.growthRun.revisionRegressionAcceptance !== undefined
622
+ && this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
623
+ if (!verify && !regression)
624
+ return false;
625
+ try {
626
+ this.assertOwner(job);
627
+ }
628
+ catch {
629
+ return false;
630
+ }
631
+ return plan.expiresAt > Date.now();
632
+ }
633
+ if (plan.mode === 'prepared-create') {
634
+ if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
635
+ || !job.intent.creation?.growthRun?.creationAcceptance || !plan.creation?.growthRun?.creationAcceptance)
636
+ return false;
637
+ const verification = this.options.store.getCreationVerificationStatus(plan.id);
638
+ const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
639
+ const adopt = verification === 'verified' && this.options.adoptVerifiedCreation !== undefined
640
+ && this.options.creationAdoptionEligible?.(plan.id) === true;
641
+ if (!verify && !adopt)
642
+ return false;
643
+ if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
644
+ return false;
645
+ try {
646
+ this.assertOwner(job);
647
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => {
648
+ this.assertGrowthRun(job.intent.creation?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
649
+ });
650
+ }
651
+ catch {
652
+ return false;
653
+ }
654
+ return plan.expiresAt > Date.now();
655
+ }
656
+ if (plan.mode !== 'modify' || job.intent.mode === 'create')
657
+ return false;
368
658
  const adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
369
- const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
659
+ const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
370
660
  if (recovery)
371
661
  return this.options.adoptReleased !== undefined;
372
662
  if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
@@ -390,12 +680,12 @@ export class SourceJobRuntime {
390
680
  continuationTimeoutMs() {
391
681
  const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
392
682
  const needed = (this.options.adoptReleased ? adoption : 0) + (this.options.advanceReleased ? release + 40_000 : this.options.releasePrepared ? 40_000 : 10_000);
393
- return Math.max(10_000, needed);
683
+ return Math.max(this.options.verifyPreparedCreation ? 1_800_000 : 10_000, needed);
394
684
  }
395
685
  continuationSignalTimeoutMs(job) {
396
686
  if (job.planId) {
397
687
  const plan = this.options.store.getSourcePlan(job.planId), adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
398
- if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
688
+ if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
399
689
  return this.continuationTimeoutMs();
400
690
  }
401
691
  return Math.max(1, Math.min(this.continuationTimeoutMs(), this.options.config.expiresAt - Date.now()));
@@ -461,8 +751,12 @@ export class SourceJobRuntime {
461
751
  try {
462
752
  this.schedule(job);
463
753
  }
464
- catch {
754
+ catch (error) {
755
+ if (error instanceof SourceGrowthRunUnavailableError)
756
+ return;
465
757
  const current = this.options.store.getSourceJob(job.id);
758
+ if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
759
+ return;
466
760
  if (current?.status === 'queued')
467
761
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-registration-rejected' });
468
762
  }
@@ -472,8 +766,45 @@ export class SourceJobRuntime {
472
766
  const failure = (unknown) => ({ outcome: unknown ? 'unknown' : 'failed', failureClass: unknown ? 'unknown' : 'configuration', failurePhase: 'host-execution', failureCode: 'source-job-rejected', sideEffectState: unknown ? 'unknown' : 'none', retryability: unknown ? 'unsafe' : 'after-intervention' });
473
767
  if (job === undefined || job.status !== 'queued')
474
768
  return failure(job?.status === 'unknown' || job?.status === 'running');
769
+ // A stale occurrence from before a persisted rearm can never claim this
770
+ // job. It has acquired no source resource, so leave the queued CAS intact.
771
+ if ((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && input.definitionHash !== job.definitionHash) {
772
+ const expectedPending = job.previousDefinitionHash === undefined ? undefined : automationDefinitionDigest(this.definition(job));
773
+ if (expectedPending === input.definitionHash && input.executionMode === 'production'
774
+ && input.activationNonce === job.intentDigest && input.catalogDigest === CATALOG
775
+ && input.ownerRouteId === job.intent.owner.authorityId && input.principal === job.intent.owner.principalId
776
+ && input.targetScope.workspace === job.intent.owner.workspace && input.targetScope.preset === job.intent.owner.agentPreset) {
777
+ try {
778
+ // The new native occurrence reached us before its cross-ledger DB
779
+ // acknowledgement. It has not claimed source resources. Bind the
780
+ // exact native receipt, then persist another one-shot before this
781
+ // occurrence can be considered consumed.
782
+ this.schedule(job);
783
+ const rebound = this.options.store.getSourceJob(job.id);
784
+ if (rebound?.status === 'queued' && rebound.definitionHash === input.definitionHash && rebound.occurrenceId === undefined) {
785
+ const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
786
+ if (health.definitionHash !== rebound.definitionHash)
787
+ throw new Error('source job premature occurrence native binding changed');
788
+ const pending = this.withGapSource(rebound.intent.gapId, rebound.intent.owner, () => {
789
+ this.assertGrowthRun(rebound.intent.creation?.growthRun ?? rebound.intent.revision?.growthRun, rebound.intent.mode, rebound.intent.gapId, rebound.intent.owner, false);
790
+ return this.options.store.rearmQueuedGrowthSourceJob({ id: rebound.id, revision: rebound.revision,
791
+ priorDefinitionHash: rebound.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
792
+ });
793
+ this.schedule(pending);
794
+ }
795
+ }
796
+ catch { /* persisted queued state remains fail-closed for exact recovery */ }
797
+ }
798
+ return failure(false);
799
+ }
475
800
  let claimed = false;
476
- const signal = AbortSignal.any([input.signal, this.abort.signal, AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
801
+ const growthController = (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined ? undefined : new AbortController();
802
+ if (growthController)
803
+ this.growthControllers.add(growthController);
804
+ const signal = AbortSignal.any([input.signal, this.abort.signal,
805
+ ...(growthController ? [growthController.signal] : []),
806
+ AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.verifyPreparedCreation ? 1_800_000 : 0)
807
+ + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
477
808
  try {
478
809
  this.assertOwner(job);
479
810
  signal.throwIfAborted();
@@ -499,7 +830,9 @@ export class SourceJobRuntime {
499
830
  await this.options.prepare(owned, signal, assertCurrent);
500
831
  if (this.options.store.getSourceJob(owned.id)?.status !== 'prepared')
501
832
  throw new Error('source job completion was not committed');
502
- if ((this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased) && this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
833
+ if ((owned.intent.mode === 'create' ? this.options.verifyPreparedCreation !== undefined
834
+ : owned.intent.mode === 'revise-created' ? this.options.verifyPreparedRevision !== undefined : !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased))
835
+ && this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
503
836
  this.reconcileContinuations();
504
837
  this.assertOwner(owned);
505
838
  await this.continuePrepared(this.options.store.getSourceJob(owned.id), signal);
@@ -507,13 +840,22 @@ export class SourceJobRuntime {
507
840
  }
508
841
  return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
509
842
  }
510
- catch {
843
+ catch (error) {
511
844
  const current = this.options.store.getSourceJob(job.id);
845
+ if (!claimed && error instanceof SourceGrowthRunUnavailableError)
846
+ return failure(false);
847
+ if (!claimed && current?.status === 'queued'
848
+ && (current.revision !== job.revision || current.definitionHash !== input.definitionHash))
849
+ return failure(false);
512
850
  if (current?.status === 'queued' || (current?.status === 'running' && current.occurrenceId === input.occurrenceId)) {
513
851
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: claimed ? 'unknown' : 'failed', failureCode: claimed ? 'source-job-interrupted' : 'source-job-preflight-rejected' });
514
852
  }
515
853
  return failure(claimed);
516
854
  }
855
+ finally {
856
+ if (growthController)
857
+ this.growthControllers.delete(growthController);
858
+ }
517
859
  }
518
860
  /** Explicit Host action. Unknown keeps its capacity until exact cleanup succeeds. */
519
861
  reconcileUnknown(input) {