@dsh-enhanced/plugin-control-plane 0.1.66 → 0.1.68

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 (97) hide show
  1. package/README.md +96 -22
  2. package/lib/cli.d.ts.map +1 -1
  3. package/lib/cli.js +66 -11
  4. package/lib/cli.js.map +1 -1
  5. package/lib/creation-capability-execution.d.ts +5 -1
  6. package/lib/creation-capability-execution.d.ts.map +1 -1
  7. package/lib/creation-capability-execution.js +42 -0
  8. package/lib/creation-capability-execution.js.map +1 -1
  9. package/lib/creation-capability-feedback.d.ts +16 -1
  10. package/lib/creation-capability-feedback.d.ts.map +1 -1
  11. package/lib/creation-capability-feedback.js +82 -5
  12. package/lib/creation-capability-feedback.js.map +1 -1
  13. package/lib/creation-capability-journal.d.ts.map +1 -1
  14. package/lib/creation-capability-journal.js +2 -0
  15. package/lib/creation-capability-journal.js.map +1 -1
  16. package/lib/creation-capability-runtime.d.ts +47 -1
  17. package/lib/creation-capability-runtime.d.ts.map +1 -1
  18. package/lib/creation-capability-runtime.js +105 -1
  19. package/lib/creation-capability-runtime.js.map +1 -1
  20. package/lib/creation-capability-types.d.ts +1 -0
  21. package/lib/creation-capability-types.d.ts.map +1 -1
  22. package/lib/index.d.ts +3 -0
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +2 -0
  25. package/lib/index.js.map +1 -1
  26. package/lib/linux-source-build.d.ts +13 -2
  27. package/lib/linux-source-build.d.ts.map +1 -1
  28. package/lib/linux-source-build.js.map +1 -1
  29. package/lib/live-qualification-authority.js +2 -2
  30. package/lib/live-qualification-authority.js.map +1 -1
  31. package/lib/owner-instruction-drain.d.ts +60 -0
  32. package/lib/owner-instruction-drain.d.ts.map +1 -0
  33. package/lib/owner-instruction-drain.js +101 -0
  34. package/lib/owner-instruction-drain.js.map +1 -0
  35. package/lib/revision-capability-feedback.d.ts +165 -0
  36. package/lib/revision-capability-feedback.d.ts.map +1 -0
  37. package/lib/revision-capability-feedback.js +316 -0
  38. package/lib/revision-capability-feedback.js.map +1 -0
  39. package/lib/revision-capability-journal.d.ts +126 -0
  40. package/lib/revision-capability-journal.d.ts.map +1 -0
  41. package/lib/revision-capability-journal.js +981 -0
  42. package/lib/revision-capability-journal.js.map +1 -0
  43. package/lib/revision-capability-runtime.d.ts +92 -0
  44. package/lib/revision-capability-runtime.d.ts.map +1 -0
  45. package/lib/revision-capability-runtime.js +635 -0
  46. package/lib/revision-capability-runtime.js.map +1 -0
  47. package/lib/revision-capability-types.d.ts +335 -0
  48. package/lib/revision-capability-types.d.ts.map +1 -0
  49. package/lib/revision-capability-types.js +2 -0
  50. package/lib/revision-capability-types.js.map +1 -0
  51. package/lib/service.d.ts +199 -4
  52. package/lib/service.d.ts.map +1 -1
  53. package/lib/service.js +925 -70
  54. package/lib/service.js.map +1 -1
  55. package/lib/source-adoption-authority.js +2 -2
  56. package/lib/source-adoption-authority.js.map +1 -1
  57. package/lib/source-approval-authority.js +3 -3
  58. package/lib/source-approval-authority.js.map +1 -1
  59. package/lib/source-build.d.ts +33 -53
  60. package/lib/source-build.d.ts.map +1 -1
  61. package/lib/source-build.js +28 -408
  62. package/lib/source-build.js.map +1 -1
  63. package/lib/source-job-types.d.ts +111 -4
  64. package/lib/source-job-types.d.ts.map +1 -1
  65. package/lib/source-jobs.d.ts +72 -3
  66. package/lib/source-jobs.d.ts.map +1 -1
  67. package/lib/source-jobs.js +314 -43
  68. package/lib/source-jobs.js.map +1 -1
  69. package/lib/source-release-authority.js +2 -2
  70. package/lib/source-release-authority.js.map +1 -1
  71. package/lib/source-revision.d.ts +29 -3
  72. package/lib/source-revision.d.ts.map +1 -1
  73. package/lib/source-revision.js +40 -9
  74. package/lib/source-revision.js.map +1 -1
  75. package/lib/source-workspace.d.ts +11 -4
  76. package/lib/source-workspace.d.ts.map +1 -1
  77. package/lib/source-workspace.js +51 -18
  78. package/lib/source-workspace.js.map +1 -1
  79. package/lib/sqlite.d.ts +6 -7
  80. package/lib/sqlite.d.ts.map +1 -1
  81. package/lib/sqlite.js +255 -17
  82. package/lib/sqlite.js.map +1 -1
  83. package/lib/store.d.ts +143 -4
  84. package/lib/store.d.ts.map +1 -1
  85. package/lib/store.js +739 -66
  86. package/lib/store.js.map +1 -1
  87. package/lib/systemd-host-authority.js +4 -4
  88. package/lib/systemd-host-authority.js.map +1 -1
  89. package/lib/task-observation-authority.js +2 -2
  90. package/lib/task-observation-authority.js.map +1 -1
  91. package/lib/version.d.ts +1 -1
  92. package/lib/version.js +1 -1
  93. package/lib/work-item-creation-contract.d.ts +3 -1
  94. package/lib/work-item-creation-contract.d.ts.map +1 -1
  95. package/lib/work-item-creation-contract.js +10 -1
  96. package/lib/work-item-creation-contract.js.map +1 -1
  97. package/package.json +11 -8
@@ -2,18 +2,36 @@ import { normalizeSourceCreationGrant } from './source-creation.js';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import { realpath } from 'node:fs/promises';
5
- import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, validateAnySourceGrowthRunBinding as validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
5
+ import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, validateAnySourceGrowthRunBinding as validateSourceGrowthRunBinding, revisionBenefitContractDigest, validateRevisionBenefitContract } from '@dsh-enhanced/assistant-growth-contract';
6
6
  import { automationDefinitionDigest } from '@dsh-enhanced/assistant-automations';
7
7
  import { controlPlaneDigest, expectedSourceRelease } from './store.js';
8
8
  import { awaitSourceSignal, inspectSourceContext } from './source-context.js';
9
9
  import { assertPluginModificationAllowed, removeSourceJobWorktree, validateScopedPluginFiles } from './source-workspace.js';
10
10
  import { assertManagedVersionPaths } from './source-versioning.js';
11
11
  import { resolveSourceBaseline, validateSourceBaselineConfig } from './source-baseline.js';
12
- import { removeSourceJobContainer, validateSourceBuildConfig } from './source-build.js';
13
- import { inspectSourceRevisionContext, validateSourceRevisionGrant } from './source-revision.js';
12
+ import { validateSourceBuildConfig } from './source-build.js';
13
+ import { inspectSourceRevisionContext, normalizeSourceRevisionGrant, validateSourceRevisionGrant } from './source-revision.js';
14
14
  import { inspectSourceCreationContext, validateSourceCreationFiles, validateSourceCreationGrant } from './source-creation.js';
15
15
  import { inheritedEnvironment } from './trust.js';
16
16
  import { SOURCE_BUILD_CHECK_FAILED } from './source-build-diagnostics.js';
17
+ const ZERO_REVIEW_WINDOW = Object.freeze({ verificationMs: 0, regressionMs: 0, adoptionMs: 0 });
18
+ const LINUX_REVIEW_WINDOW_MS = 1_800_000;
19
+ const ADOPTION_WINDOW_MS = 180_000;
20
+ /**
21
+ * The exact Host-side review/adoption windows a source job may allocate. It is
22
+ * computed from live capability presence only when an owner instruction is first
23
+ * enqueued; afterwards the frozen copy is authoritative and any difference
24
+ * fails closed instead of silently resizing the review budget.
25
+ */
26
+ export function instructionReviewWindowFor(input) {
27
+ if (!input.linuxBackend)
28
+ return ZERO_REVIEW_WINDOW;
29
+ return Object.freeze({
30
+ verificationMs: input.verification ? LINUX_REVIEW_WINDOW_MS : 0,
31
+ regressionMs: input.regression ? LINUX_REVIEW_WINDOW_MS : 0,
32
+ adoptionMs: input.adoption ? ADOPTION_WINDOW_MS : 0,
33
+ });
34
+ }
17
35
  export const SOURCE_JOB_OWNER = 'plugin-control-plane-source';
18
36
  const EXECUTOR = 'plugin-control-plane-source-check';
19
37
  const CATALOG = controlPlaneDigest({ executor: EXECUTOR, version: 1, operation: 'isolated-check-pending-plan' });
@@ -24,7 +42,7 @@ export { SourceGrowthRunUnavailableError } from '@dsh-enhanced/assistant-growth-
24
42
  class SourceGrowthNativeMismatchError extends Error {
25
43
  }
26
44
  export function validateSourceJobsConfig(value, build) {
27
- const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'allowedModes', 'repository', 'baseline', 'creation', 'revision', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
45
+ const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'allowedModes', 'repository', 'baseline', 'creation', 'revision', 'revisionBenefit', 'allowReviewOnlyFirstAdoption', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
28
46
  if (value === null || typeof value !== 'object' || Array.isArray(value) || build === undefined
29
47
  || Object.keys(value).some(key => !allowed.has(key))
30
48
  || ![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))
@@ -48,6 +66,14 @@ export function validateSourceJobsConfig(value, build) {
48
66
  validateSourceCreationGrant(value.creation);
49
67
  if (value.revision !== undefined)
50
68
  validateSourceRevisionGrant(value.revision);
69
+ if (value.revisionBenefit !== undefined)
70
+ validateRevisionBenefitContract(value.revisionBenefit);
71
+ if (value.allowReviewOnlyFirstAdoption !== undefined && typeof value.allowReviewOnlyFirstAdoption !== 'boolean') {
72
+ throw new Error('plugin-control-plane: invalid review-only adoption flag');
73
+ }
74
+ if (value.revisionBenefit !== undefined && value.revision?.benefit?.contractDigest !== revisionBenefitContractDigest(value.revisionBenefit)) {
75
+ throw new Error('plugin-control-plane: revisionBenefit contract does not match the revision grant benefit pin');
76
+ }
51
77
  if ('backend' in build && build.backend === 'macos-seatbelt'
52
78
  && (!value.allowedModes || value.allowedModes.some(mode => mode === 'modify')
53
79
  || value.creation === undefined && value.revision === undefined)) {
@@ -56,7 +82,9 @@ export function validateSourceJobsConfig(value, build) {
56
82
  }
57
83
  export function normalizeSourceJobsConfig(value, build) {
58
84
  validateSourceJobsConfig(value, build);
59
- return { ...structuredClone(value), ...(value.creation ? { creation: normalizeSourceCreationGrant(value.creation) } : {}) };
85
+ return { ...structuredClone(value),
86
+ ...(value.creation ? { creation: normalizeSourceCreationGrant(value.creation) } : {}),
87
+ ...(value.revision ? { revision: normalizeSourceRevisionGrant(value.revision) } : {}) };
60
88
  }
61
89
  function projection(job, diagnostics) {
62
90
  return { id: job.id, name: job.intent.name, gapId: job.intent.gapId, baseCommit: job.intent.baseCommit,
@@ -76,12 +104,14 @@ export class SourceJobRuntime {
76
104
  authorityDigest;
77
105
  unregister;
78
106
  unregisterContinuation;
107
+ unsubscribeWake;
79
108
  closing;
80
109
  active = false;
81
110
  generation = randomUUID();
82
111
  continuationNonce;
83
112
  continuing = new Set();
84
113
  continuationCursor = 0;
114
+ preferRequestDrain = true;
85
115
  continuationStatus;
86
116
  continuationTransition = 0;
87
117
  /** Process-local, bounded build diagnostics; the failure code itself is durable. */
@@ -121,6 +151,12 @@ export class SourceJobRuntime {
121
151
  execute: input => this.track(this.executeContinuations(input)),
122
152
  });
123
153
  this.active = true;
154
+ if (this.options.drainOwnerInstructionRequests !== undefined) {
155
+ const automations = this.options.ports.automations;
156
+ if (!automations.onSchedulerWake)
157
+ throw new Error('owner instruction drain requires native scheduler wake');
158
+ this.unsubscribeWake = automations.onSchedulerWake(() => this.reconcileContinuations());
159
+ }
124
160
  // Recovery is admitted exclusively through a production cron occurrence.
125
161
  // Reading this local durable query intentionally performs no authority call.
126
162
  this.reconcileContinuations();
@@ -130,6 +166,10 @@ export class SourceJobRuntime {
130
166
  // Registration is transactional from this runtime's point of view: an
131
167
  // unregistration fault cannot leave the other executor registered or
132
168
  // obscure the original registration failure.
169
+ try {
170
+ this.unsubscribeWake?.();
171
+ }
172
+ catch { /* original error wins */ }
133
173
  try {
134
174
  this.unregisterContinuation?.();
135
175
  }
@@ -192,8 +232,40 @@ export class SourceJobRuntime {
192
232
  }
193
233
  return Promise.allSettled(this.growthFlights).then(() => undefined);
194
234
  }
235
+ /** A revision is adoptable only when its independent review, any configured regression, and any configured benefit outcome verified. */
236
+ revisionAdoptionReady(job, planId) {
237
+ if (this.options.store.getRevisionVerificationStatus(planId) !== 'verified')
238
+ return false;
239
+ if (job.intent.revision?.growthRun?.revisionRegressionAcceptance !== undefined
240
+ && (!(this.options.verifyPreparedRevisionRegression !== undefined)
241
+ || this.options.store.getRevisionRegressionStatus(planId) !== 'verified')) {
242
+ return false;
243
+ }
244
+ const benefit = job.intent.revision?.grant.benefit;
245
+ if (benefit !== undefined) {
246
+ // Default contract semantics: a pinned benefit requires a recorded
247
+ // non-inferior outcome before adoption. Removing the benefit contract
248
+ // while the grant keeps its pin fails closed instead of silently
249
+ // degrading to review-only adoption.
250
+ if (this.options.reconcileRevisionBenefits === undefined)
251
+ return false;
252
+ const outcome = this.options.store.inspectRevisionBenefitOutcome(planId);
253
+ if (outcome === undefined) {
254
+ // Only an explicit owner opt-in may adopt once before candidate
255
+ // evidence can exist; the reconcile pass then proves non-inferior or
256
+ // rolls the revision back at its bounded observation deadline.
257
+ return this.options.allowReviewOnlyFirstAdoption === true;
258
+ }
259
+ if (outcome.contractDigest !== benefit.contractDigest || outcome.status !== 'non-inferior')
260
+ return false;
261
+ }
262
+ return true;
263
+ }
195
264
  async continuePrepared(job, signal) {
196
- if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
265
+ // Owner-instruction revisions continue on their frozen instruction; task
266
+ // revisions on their durable task-failure source. Both re-authorize below.
267
+ if (!job.planId || (job.intent.ownerInstruction === undefined
268
+ && !this.options.store.getOwnerTaskFailureReference(job.intent.gapId)))
197
269
  return;
198
270
  if (this.continuing.has(job.id))
199
271
  return;
@@ -207,7 +279,8 @@ export class SourceJobRuntime {
207
279
  }
208
280
  async continuePreparedLocked(job, signal) {
209
281
  this.assertMode(job.intent.mode);
210
- if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
282
+ if (!job.planId || (job.intent.ownerInstruction === undefined
283
+ && !this.options.store.getOwnerTaskFailureReference(job.intent.gapId)))
211
284
  return;
212
285
  const planId = job.planId;
213
286
  const current = this.options.store.getSourceJob(job.id);
@@ -230,22 +303,40 @@ export class SourceJobRuntime {
230
303
  this.assertOwner(job);
231
304
  };
232
305
  if (initial.mode === 'prepared-revise') {
233
- if (job.intent.mode !== 'revise-created' || !job.intent.revision?.growthRun.revisionAcceptance || !initial.sourceRevision?.growthRun.revisionAcceptance)
306
+ // Task-failure revisions continue only while their frozen acceptance pins
307
+ // are fresh; owner-instruction revisions carry no growth run and are
308
+ // re-authorized by assertOwner (instruction, parent and owner checks).
309
+ const revisionPins = job.intent.revision?.growthRun?.revisionAcceptance !== undefined
310
+ && initial.sourceRevision?.growthRun?.revisionAcceptance !== undefined;
311
+ if (job.intent.mode !== 'revise-created'
312
+ || (job.intent.ownerInstruction === undefined && !revisionPins))
234
313
  return;
235
314
  await assertCurrent();
236
315
  if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
237
316
  await this.options.verifyPreparedRevision(job, signal);
238
317
  if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
239
318
  && this.options.store.getRevisionRegressionStatus(planId) === undefined
240
- && job.intent.revision.growthRun.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
319
+ && job.intent.revision?.growthRun?.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
241
320
  await assertCurrent();
242
321
  await this.options.verifyPreparedRevisionRegression(job, signal);
243
322
  }
323
+ // Verified acceptance and the optional independent parent regression end the review lane.
324
+ // Replacing the stable adopted entry is a separately authorized continuation.
325
+ if (job.intent.ownerInstruction === undefined && this.revisionAdoptionReady(job, planId)
326
+ && this.options.revisionAdoptionEligible?.(planId) && this.options.adoptVerifiedRevision) {
327
+ await assertCurrent();
328
+ await this.options.adoptVerifiedRevision(job, signal);
329
+ }
244
330
  return;
245
331
  }
246
332
  if (initial.mode === 'prepared-create') {
333
+ const initialRun = initial.creation?.growthRun;
247
334
  if (job.intent.mode !== 'create'
248
- || !job.intent.creation?.growthRun?.creationAcceptance || !initial.creation?.growthRun?.creationAcceptance)
335
+ || !job.intent.creation?.growthRun
336
+ || job.intent.creation.growthRun.protocol === 'assistant-growth/source-run/v3'
337
+ || !job.intent.creation.growthRun.creationAcceptance
338
+ || !initialRun || initialRun.protocol === 'assistant-growth/source-run/v3'
339
+ || !initialRun.creationAcceptance)
249
340
  return;
250
341
  await assertCurrent();
251
342
  if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
@@ -290,11 +381,22 @@ export class SourceJobRuntime {
290
381
  const authority = { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt };
291
382
  return { authority, creation: structuredClone(config.creation), remaining: this.options.store.inspectSourceJobQuota({ authority: { ...authority, maxSubmissions: config.maxSubmissions }, creation: config.creation }) };
292
383
  }
384
+ inspectWorkItemRevisionReadiness(owner) {
385
+ if (!this.available() || !this.modeAllowed('revise-created') || ('expiresAt' in this.options.build && Date.now() >= this.options.build.expiresAt) || this.options.config.revision?.workItemFailures !== true || Date.now() >= this.options.config.revision.expiresAt)
386
+ return undefined;
387
+ const current = this.receipt();
388
+ if (!isSourceOwnerContinuation(current, owner))
389
+ return undefined;
390
+ const config = this.options.config;
391
+ const authority = { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt };
392
+ return { authority, revision: structuredClone(config.revision), remaining: this.options.store.inspectSourceJobRevisionQuota({ authority: { ...authority, maxSubmissions: config.maxSubmissions }, revision: config.revision }) };
393
+ }
293
394
  available() { return this.active && !this.abort.signal.aborted && Date.now() < this.options.config.expiresAt; }
294
395
  close() {
295
396
  return this.closing ??= (async () => {
296
397
  this.active = false;
297
398
  try {
399
+ this.unsubscribeWake?.();
298
400
  const current = this.options.ports.automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: CONTINUATION_AUTOMATION });
299
401
  if (current?.activationNonce === this.continuationNonce)
300
402
  this.options.ports.automations.reconcileSystem({ owner: SOURCE_JOB_OWNER,
@@ -342,7 +444,7 @@ export class SourceJobRuntime {
342
444
  withGapSource(gapId, owner, callback, growthRun) {
343
445
  const reference = this.options.store.getOwnerTaskFailureReference(gapId);
344
446
  if (reference?.schemaVersion === 2) {
345
- if (growthRun?.protocol !== 'assistant-growth/source-run/v2' || !this.options.withWorkItemGrowthRunFence)
447
+ if ((growthRun?.protocol !== 'assistant-growth/source-run/v2' && growthRun?.protocol !== 'assistant-growth/source-run/v3') || !this.options.withWorkItemGrowthRunFence)
346
448
  throw new SourceGrowthRunUnavailableError('work item growth source fence unavailable');
347
449
  return this.options.withWorkItemGrowthRunFence(growthRun, gapId, owner, false, callback);
348
450
  }
@@ -359,6 +461,67 @@ export class SourceJobRuntime {
359
461
  if (!this.modeAllowed(mode))
360
462
  throw new Error('source job mode is not authorized');
361
463
  }
464
+ /**
465
+ * Fail-closed re-validation of a Host-recorded owner instruction at enqueue
466
+ * and at every continuation. The instruction, not the caller, freezes the
467
+ * target, files and owner; the gap must be the synthetic instruction gap.
468
+ */
469
+ #assertOwnerInstruction(binding, input) {
470
+ if (!binding)
471
+ throw new Error('revision requires an authenticated owner task failure or owner instruction');
472
+ const stored = this.options.store.getOwnerInstruction(binding.instructionId);
473
+ if (!stored || controlPlaneDigest(stored) !== binding.digest)
474
+ throw new Error('owner instruction changed');
475
+ if (stored.expiresAt <= Date.now())
476
+ throw new Error('owner instruction expired');
477
+ if (stored.planId !== input.parentPlanId || stored.pluginName !== input.name)
478
+ throw new Error('owner instruction target changed');
479
+ if (stored.filesDigest !== controlPlaneDigest(input.files))
480
+ throw new Error('owner instruction files changed');
481
+ let gapKey;
482
+ try {
483
+ gapKey = this.options.store.getGap(input.gapId).idempotencyKey;
484
+ }
485
+ catch {
486
+ throw new Error('owner instruction gap changed');
487
+ }
488
+ if (gapKey !== `owner-instruction:${stored.instructionId}`) {
489
+ throw new Error('owner instruction gap changed');
490
+ }
491
+ const freeze = this.options.store.getOwnerInstructionReviewFreeze(stored.instructionId);
492
+ if (freeze === undefined)
493
+ throw new Error('owner instruction review freeze is unavailable');
494
+ this.#assertInstructionReviewFreeze(freeze, undefined);
495
+ return Object.freeze({ stored, freeze });
496
+ }
497
+ /** Live capability identity; only used to validate an already frozen budget. */
498
+ instructionReviewWindow() {
499
+ return instructionReviewWindowFor({
500
+ linuxBackend: 'backend' in this.options.build && this.options.build.backend === 'linux-native',
501
+ verification: this.options.verifyPreparedRevision !== undefined,
502
+ regression: this.options.verifyPreparedRevisionRegression !== undefined,
503
+ adoption: this.options.adoptVerifiedRevision !== undefined,
504
+ });
505
+ }
506
+ /**
507
+ * A frozen review budget may only be consumed while the current reviewer
508
+ * capability is exactly the one frozen at enqueue. Any absence or difference
509
+ * fails closed: this guard never substitutes live config for the freeze.
510
+ */
511
+ #assertInstructionReviewFreeze(freeze, expected) {
512
+ if (this.options.verifyPreparedRevision === undefined)
513
+ throw new Error('owner instruction revision requires an available independent review');
514
+ if (freeze.budget.authority.expiresAt <= Date.now())
515
+ throw new Error('owner instruction review authority expired');
516
+ if (controlPlaneDigest(this.instructionReviewWindow()) !== controlPlaneDigest(freeze.budget.window)) {
517
+ throw new Error('owner instruction review budget changed');
518
+ }
519
+ if (expected !== undefined && (expected.reviewFreezeDigest === undefined || expected.reviewWindow === undefined
520
+ || expected.reviewFreezeDigest !== freeze.digest
521
+ || controlPlaneDigest(expected.reviewWindow) !== controlPlaneDigest(freeze.budget.window))) {
522
+ throw new Error('owner instruction review freeze changed');
523
+ }
524
+ }
362
525
  assertOwner(job) {
363
526
  this.assertMode(job.intent.mode);
364
527
  const current = this.receipt();
@@ -371,8 +534,27 @@ export class SourceJobRuntime {
371
534
  || job.intent.authority.id !== this.options.config.authorityId
372
535
  || controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
373
536
  || (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
374
- : controlPlaneDigest(current) !== job.intent.ownerDigest))
537
+ : (job.intent.ownerInstruction !== undefined
538
+ ? !isSourceOwnerContinuation(current, job.intent.owner)
539
+ : controlPlaneDigest(current) !== job.intent.ownerDigest)))
375
540
  throw new Error('source job authority changed or expired');
541
+ // Re-validate the frozen instruction on every continuation exactly like
542
+ // enqueue; a revoked, changed or expired instruction cannot proceed.
543
+ if (job.intent.ownerInstruction !== undefined) {
544
+ const stored = this.options.store.getOwnerInstruction(job.intent.ownerInstruction.instructionId);
545
+ if (taskBacked || stored === undefined || controlPlaneDigest(stored) !== job.intent.ownerInstruction.digest
546
+ || stored.expiresAt <= Date.now() || stored.planId !== job.intent.revision?.parent.planId
547
+ || stored.pluginName !== job.intent.name || stored.filesDigest !== controlPlaneDigest(job.intent.files)
548
+ || stored.ownerDigest !== job.intent.ownerDigest
549
+ || this.options.store.getGap(job.intent.gapId).idempotencyKey !== `owner-instruction:${stored.instructionId}`) {
550
+ throw new Error('owner instruction changed or expired');
551
+ }
552
+ // Recovery consumes only the review supplier/budget frozen at enqueue.
553
+ const freeze = this.options.store.getOwnerInstructionReviewFreeze(stored.instructionId);
554
+ if (freeze === undefined)
555
+ throw new Error('owner instruction review freeze is unavailable');
556
+ this.#assertInstructionReviewFreeze(freeze, job.intent.ownerInstruction);
557
+ }
376
558
  if (job.intent.revision) {
377
559
  const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
378
560
  if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
@@ -392,6 +574,8 @@ export class SourceJobRuntime {
392
574
  validateSourceGrowthRunBinding(growthRun);
393
575
  if (growthRun.protocol === 'assistant-growth/source-run/v2' && (mode !== 'create' || this.options.config.creation?.workItemFailures !== true || this.options.store.getOwnerTaskFailureReference(gapId)?.schemaVersion !== 2))
394
576
  throw new Error('work item growth requires explicit create authority');
577
+ if (growthRun.protocol === 'assistant-growth/source-run/v3' && (mode !== 'revise-created' || this.options.config.revision?.workItemFailures !== true || this.options.store.getOwnerTaskFailureReference(gapId)?.schemaVersion !== 2 || Date.now() >= this.options.config.revision.expiresAt))
578
+ throw new Error('work item revision growth requires explicit current revision authority');
395
579
  if (!this.options.assertGrowthRun)
396
580
  throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
397
581
  this.options.assertGrowthRun(growthRun, gapId, owner, generation);
@@ -400,8 +584,14 @@ export class SourceJobRuntime {
400
584
  this.assertMode(input.mode);
401
585
  if (input.growthRun?.protocol === 'assistant-growth/source-run/v2' && input.mode !== 'create')
402
586
  throw new Error('work item growth only permits create');
587
+ if (input.growthRun?.protocol === 'assistant-growth/source-run/v3' && input.mode !== 'revise-created')
588
+ throw new Error('work item revision growth only permits revise-created');
403
589
  if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
404
590
  throw new Error('modify source job cannot carry a growth run');
591
+ if (input.ownerInstruction !== undefined && input.mode !== 'revise-created')
592
+ throw new Error('owner instructions only authorize revise-created jobs');
593
+ if (input.growthRun !== undefined && input.ownerInstruction !== undefined)
594
+ throw new Error('an owner instruction is not a growth run');
405
595
  if (input.mode === 'create' || input.mode === 'revise-created')
406
596
  validateSourceCreationFiles(input.files);
407
597
  else {
@@ -413,7 +603,8 @@ export class SourceJobRuntime {
413
603
  }
414
604
  if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
415
605
  throw new Error('source revision parent is missing or outside revision mode');
416
- return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner) }));
606
+ return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner),
607
+ ...(input.ownerInstruction === undefined ? {} : { ownerInstruction: structuredClone(input.ownerInstruction) }) }));
417
608
  }
418
609
  async enqueueOwned(input) {
419
610
  this.assertMode(input.mode);
@@ -441,11 +632,19 @@ export class SourceJobRuntime {
441
632
  throw new Error('source job repository mismatch');
442
633
  const currentOwner = this.assertCaller(input.owner);
443
634
  const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
444
- if (input.mode === 'revise-created' && !reference)
445
- throw new Error('revision requires an authenticated owner task failure');
446
- const owner = reference?.owner ?? currentOwner;
635
+ if (input.mode === 'revise-created' && reference !== undefined && input.ownerInstruction !== undefined) {
636
+ throw new Error('owner instruction cannot replace an authenticated owner task failure');
637
+ }
638
+ // A revise-created job without a task failure is legal only through a
639
+ // Host-recorded owner instruction; every frozen field must match it.
640
+ const ownerInstruction = input.mode === 'revise-created' && !reference
641
+ ? this.#assertOwnerInstruction(input.ownerInstruction, input) : undefined;
642
+ const owner = ownerInstruction?.stored.ownerReceipt ?? reference?.owner ?? currentOwner;
447
643
  if (reference && !isSourceOwnerContinuation(currentOwner, owner))
448
644
  throw new Error('source job task owner changed');
645
+ if (ownerInstruction !== undefined && !isSourceOwnerContinuation(currentOwner, ownerInstruction.stored.ownerReceipt)) {
646
+ throw new Error('owner instruction owner changed');
647
+ }
449
648
  this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true), input.growthRun);
450
649
  const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
451
650
  if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
@@ -458,13 +657,19 @@ export class SourceJobRuntime {
458
657
  const prior = this.options.store.getSourceJob(id);
459
658
  if (prior !== undefined) {
460
659
  const frozen = prior.intent;
660
+ const instructionReviewMatches = ownerInstruction === undefined || (frozen.ownerInstruction?.digest === controlPlaneDigest(ownerInstruction.stored)
661
+ && frozen.ownerInstruction.reviewFreezeDigest === ownerInstruction.freeze.digest
662
+ && frozen.ownerInstruction.reviewWindow !== undefined
663
+ && controlPlaneDigest(frozen.ownerInstruction.reviewWindow) === controlPlaneDigest(ownerInstruction.freeze.budget.window));
461
664
  if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
462
665
  || frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
463
666
  || (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
464
667
  || (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
465
668
  || (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
466
669
  || controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
467
- || controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun)))
670
+ || (frozen.revision?.growthRun === undefined) !== (input.growthRun === undefined)
671
+ || (input.growthRun !== undefined && controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun))
672
+ || !instructionReviewMatches))
468
673
  || frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
469
674
  || frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
470
675
  throw new Error('source job idempotency conflict');
@@ -495,13 +700,18 @@ export class SourceJobRuntime {
495
700
  ...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
496
701
  ...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
497
702
  ...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
498
- generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent), growthRun: structuredClone(input.growthRun) } } : {}),
703
+ generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent),
704
+ // Task-failure revisions pin a growth run; owner-instruction revisions have none.
705
+ ...(input.growthRun === undefined ? {}
706
+ : { growthRun: structuredClone(input.growthRun) }) } } : {}),
499
707
  gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
500
708
  files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
501
709
  worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`),
502
- ...('backend' in this.options.build
503
- ? { nativeResource: { backend: this.options.build.backend, key: id } }
504
- : { containerName: `dsh-source-job-${token}` }),
710
+ ...(ownerInstruction === undefined ? {} : {
711
+ ownerInstruction: { instructionId: ownerInstruction.stored.instructionId, digest: controlPlaneDigest(ownerInstruction.stored),
712
+ reviewFreezeDigest: ownerInstruction.freeze.digest, reviewWindow: structuredClone(ownerInstruction.freeze.budget.window) }
713
+ }),
714
+ nativeResource: { backend: this.options.build.backend, key: id },
505
715
  };
506
716
  await assertCurrent();
507
717
  // Synchronous acceptance and registration: no Agent signal is persisted.
@@ -573,20 +783,38 @@ export class SourceJobRuntime {
573
783
  throw error;
574
784
  }
575
785
  }
576
- linuxRevisionReviewTimeoutMs() {
786
+ /**
787
+ * Per-job review/adoption window. A supplied `job` whose revision is authorized
788
+ * by an owner instruction uses ONLY the window frozen in its intent. The
789
+ * job-less overload remains live because continuationTimeoutMs() sizes the
790
+ * shared continuation occurrence: that outer wall-clock cap may shrink with
791
+ * live config but can never enlarge the frozen inner verifier authority/budget.
792
+ * A smaller live window only truncates the continuation earlier (settled
793
+ * unknown); the per-job definition and the verifier budget stay frozen.
794
+ */
795
+ linuxRevisionReviewTimeoutMs(job) {
796
+ // An owner-instruction revision uses only the review window frozen in its
797
+ // intent: a live-config change must fail closed, never resize the budget.
798
+ if (job?.intent.ownerInstruction !== undefined) {
799
+ const window = job.intent.ownerInstruction.reviewWindow;
800
+ if (window === undefined)
801
+ throw new Error('owner instruction review window is unavailable');
802
+ return window.verificationMs + window.regressionMs + window.adoptionMs;
803
+ }
577
804
  // A Linux revision can run two independently bounded reviews after its
578
805
  // repository build. This outer window never changes their own budgets or
579
806
  // the frozen source expiry; legacy backend definitions retain their identity.
580
807
  if (!('backend' in this.options.build) || this.options.build.backend !== 'linux-native')
581
808
  return 0;
582
809
  return (this.options.verifyPreparedRevision ? 1_800_000 : 0)
583
- + (this.options.verifyPreparedRevisionRegression ? 1_800_000 : 0);
810
+ + (this.options.verifyPreparedRevisionRegression ? 1_800_000 : 0)
811
+ + (this.options.adoptVerifiedRevision ? 180_000 : 0);
584
812
  }
585
813
  definition(job) {
586
814
  const config = this.options.config;
587
815
  return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
588
816
  workspace: config.workspace, agentPreset: config.preset, timeoutMs: job.intent.build.timeoutMs + 120_000
589
- + (job.intent.mode === 'revise-created' ? this.linuxRevisionReviewTimeoutMs() : 0)
817
+ + (job.intent.mode === 'revise-created' ? this.linuxRevisionReviewTimeoutMs(job) : 0)
590
818
  + (job.intent.mode === 'create' && job.intent.creation?.growthRun?.protocol === 'assistant-growth/source-run/v2'
591
819
  && 'backend' in this.options.build
592
820
  && this.options.build.backend === 'linux-native' && this.options.verifyPreparedCreation ? 1_800_000 : 0)
@@ -674,12 +902,15 @@ export class SourceJobRuntime {
674
902
  throw new Error('source job native activation did not become active');
675
903
  }
676
904
  hasContinuations() {
677
- 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);
905
+ return !!(this.options.approvePrepared || this.options.verifyPreparedRevisionRegression || this.options.verifyPreparedRevision
906
+ || this.options.verifyPreparedCreation || this.options.adoptVerifiedCreation || this.options.adoptVerifiedRevision
907
+ || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased
908
+ || this.options.drainOwnerInstructionRequests);
678
909
  }
679
910
  continuationJobs() {
680
911
  if (!this.hasContinuations())
681
912
  return [];
682
- 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)
913
+ 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, this.options.adoptVerifiedRevision !== undefined)
683
914
  .filter(job => this.modeAllowed(job.intent.mode) && this.eligibleContinuation(job));
684
915
  }
685
916
  /** Admission reads durable state and validates the current owner; it never invokes continuation approval or release hooks. */
@@ -694,13 +925,19 @@ export class SourceJobRuntime {
694
925
  return false;
695
926
  }
696
927
  if (plan.mode === 'prepared-revise') {
697
- if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval' || !plan.sourceRevision?.growthRun.revisionAcceptance)
928
+ // Task-failure revisions require fresh acceptance pins; owner-instruction
929
+ // revisions are re-authorized through their frozen instruction row.
930
+ if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval')
931
+ return false;
932
+ if (job.intent.ownerInstruction === undefined && !plan.sourceRevision?.growthRun?.revisionAcceptance)
698
933
  return false;
699
934
  const status = this.options.store.getRevisionVerificationStatus(plan.id);
700
935
  const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
701
- const regression = status === 'verified' && plan.sourceRevision.growthRun.revisionRegressionAcceptance !== undefined
936
+ const regression = status === 'verified' && plan.sourceRevision?.growthRun?.revisionRegressionAcceptance !== undefined
702
937
  && this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
703
- if (!verify && !regression)
938
+ const adoption = this.options.revisionAdoptionEligible !== undefined && this.options.adoptVerifiedRevision !== undefined
939
+ && this.revisionAdoptionReady(job, plan.id) && this.options.revisionAdoptionEligible(plan.id) === true;
940
+ if (!verify && !regression && !adoption)
704
941
  return false;
705
942
  try {
706
943
  this.assertOwner(job);
@@ -711,8 +948,13 @@ export class SourceJobRuntime {
711
948
  return plan.expiresAt > Date.now();
712
949
  }
713
950
  if (plan.mode === 'prepared-create') {
951
+ const planRun = plan.creation?.growthRun;
714
952
  if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
715
- || !job.intent.creation?.growthRun?.creationAcceptance || !plan.creation?.growthRun?.creationAcceptance)
953
+ || !job.intent.creation?.growthRun
954
+ || job.intent.creation.growthRun.protocol === 'assistant-growth/source-run/v3'
955
+ || !job.intent.creation.growthRun.creationAcceptance
956
+ || !planRun || planRun.protocol === 'assistant-growth/source-run/v3'
957
+ || !planRun.creationAcceptance)
716
958
  return false;
717
959
  const verification = this.options.store.getCreationVerificationStatus(plan.id);
718
960
  const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
@@ -757,6 +999,16 @@ export class SourceJobRuntime {
757
999
  return this.options.advanceReleased !== undefined;
758
1000
  return plan.status === 'release-complete' && this.options.adoptReleased !== undefined;
759
1001
  }
1002
+ /**
1003
+ * Outer wall-clock cap for the shared continuation automation definition, the
1004
+ * owner-instruction request drain and the non-adoption continuation signal.
1005
+ * This is NOT the frozen review budget: an owner-instruction job's inner
1006
+ * verifier authority/budget and its own definition timeout are pinned by the
1007
+ * intent plus the write-once instruction freeze. A live config change here can
1008
+ * only truncate the continuation earlier (settled unknown); it can never
1009
+ * enlarge the frozen inner budget. Narrowing the review budget for one request
1010
+ * would require a verifier request/response protocol v2.
1011
+ */
760
1012
  continuationTimeoutMs() {
761
1013
  const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
762
1014
  const needed = (this.options.adoptReleased ? adoption : 0) + (this.options.advanceReleased ? release + 40_000 : this.options.releasePrepared ? 40_000 : 10_000);
@@ -783,7 +1035,9 @@ export class SourceJobRuntime {
783
1035
  reconcileContinuations(force = false) {
784
1036
  if (!this.active || !this.hasContinuations())
785
1037
  return;
786
- const definition = this.continuationDefinition(), desiredStatus = this.continuationJobs().length ? 'active' : 'paused';
1038
+ const definition = this.continuationDefinition();
1039
+ const requests = this.available() && this.options.hasOwnerInstructionRequests?.() === true;
1040
+ const desiredStatus = requests || this.continuationJobs().length ? 'active' : 'paused';
787
1041
  if (!force && desiredStatus === this.continuationStatus)
788
1042
  return;
789
1043
  this.options.ports.automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: CONTINUATION_AUTOMATION,
@@ -801,7 +1055,22 @@ export class SourceJobRuntime {
801
1055
  throw new Error('source continuation dispatch changed');
802
1056
  const jobs = this.continuationJobs();
803
1057
  let unsettled = false;
804
- if (jobs.length) {
1058
+ const drain = this.options.drainOwnerInstructionRequests;
1059
+ const hasRequests = drain !== undefined && this.available() && this.options.hasOwnerInstructionRequests?.() === true;
1060
+ if (hasRequests && (!jobs.length || this.preferRequestDrain)) {
1061
+ const signal = AbortSignal.any([this.abort.signal, input.signal, AbortSignal.timeout(Math.max(1, Math.min(this.continuationTimeoutMs(), this.options.config.expiresAt - Date.now())))]);
1062
+ signal.throwIfAborted();
1063
+ try {
1064
+ await drain(signal);
1065
+ signal.throwIfAborted();
1066
+ }
1067
+ catch {
1068
+ unsettled = true;
1069
+ }
1070
+ this.preferRequestDrain = false;
1071
+ }
1072
+ else if (jobs.length) {
1073
+ this.preferRequestDrain = true;
805
1074
  // One budget-admitted occurrence advances one durable row. The cursor
806
1075
  // is intentionally in-memory; a restart returning to the first row is
807
1076
  // safe because every release transition remains independently claimed.
@@ -819,6 +1088,12 @@ export class SourceJobRuntime {
819
1088
  }
820
1089
  }
821
1090
  this.reconcileContinuations();
1091
+ // P1-4 benefit evaluation rides the same cron occurrence; a failure here
1092
+ // never changes the continuation verdict.
1093
+ try {
1094
+ this.options.reconcileRevisionBenefits?.();
1095
+ }
1096
+ catch { /* owner-visible via benefit ledger absence */ }
822
1097
  if (unsettled)
823
1098
  return { outcome: 'unknown', failureClass: 'infrastructure', failurePhase: 'host-execution', failureCode: 'source-continuation-unsettled', sideEffectState: 'unknown', retryability: 'unsafe' };
824
1099
  return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
@@ -885,8 +1160,8 @@ export class SourceJobRuntime {
885
1160
  const signal = AbortSignal.any([input.signal, this.abort.signal,
886
1161
  ...(growthController ? [growthController.signal] : []),
887
1162
  AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000
888
- + (job.intent.mode === 'revise-created' && this.linuxRevisionReviewTimeoutMs()
889
- ? this.linuxRevisionReviewTimeoutMs() : this.options.verifyPreparedCreation ? 1_800_000 : 0)
1163
+ + (job.intent.mode === 'revise-created' && this.linuxRevisionReviewTimeoutMs(job)
1164
+ ? this.linuxRevisionReviewTimeoutMs(job) : this.options.verifyPreparedCreation ? 1_800_000 : 0)
890
1165
  + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
891
1166
  try {
892
1167
  this.assertOwner(job);
@@ -984,15 +1259,11 @@ export class SourceJobRuntime {
984
1259
  const trust = await this.options.trust();
985
1260
  if (controlPlaneDigest(trust) !== job.intent.trustDigest)
986
1261
  throw new Error('source job trust changed');
987
- if ('backend' in job.intent.build) {
988
- if (job.intent.nativeResource?.backend !== job.intent.build.backend || job.intent.nativeResource.key !== job.id
989
- || controlPlaneDigest(this.options.build) !== controlPlaneDigest(job.intent.build)
990
- || await this.options.inspectNativeQuiescence?.(job.id) !== 'quiescent') {
991
- throw new Error('native source job has no valid durable quiescence receipt');
992
- }
993
- }
994
- else {
995
- await removeSourceJobContainer(job.intent.build, { id: job.id, containerName: job.intent.containerName });
1262
+ if (job.intent.nativeResource === undefined || !('backend' in job.intent.build)
1263
+ || job.intent.nativeResource.backend !== job.intent.build.backend || job.intent.nativeResource.key !== job.id
1264
+ || controlPlaneDigest(this.options.build) !== controlPlaneDigest(job.intent.build)
1265
+ || await this.options.inspectNativeQuiescence?.(job.id) !== 'quiescent') {
1266
+ throw new Error('native source job has no valid durable quiescence receipt');
996
1267
  }
997
1268
  await removeSourceJobWorktree({ stateRoot: join(this.options.statePath, 'source-worktrees'), repository: job.intent.repository, worktree: job.intent.worktree, baseCommit: job.intent.baseCommit, environment: inheritedEnvironment(trust) });
998
1269
  this.inspect(input);