@dsh-enhanced/plugin-control-plane 0.1.67 → 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.
- package/README.md +54 -20
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +66 -11
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +16 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +82 -5
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +2 -0
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +2 -0
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-types.d.ts +1 -0
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/linux-source-build.d.ts +13 -2
- package/lib/linux-source-build.d.ts.map +1 -1
- package/lib/linux-source-build.js.map +1 -1
- package/lib/live-qualification-authority.js +2 -2
- package/lib/live-qualification-authority.js.map +1 -1
- package/lib/owner-instruction-drain.d.ts +60 -0
- package/lib/owner-instruction-drain.d.ts.map +1 -0
- package/lib/owner-instruction-drain.js +101 -0
- package/lib/owner-instruction-drain.js.map +1 -0
- package/lib/revision-capability-feedback.d.ts +165 -0
- package/lib/revision-capability-feedback.d.ts.map +1 -0
- package/lib/revision-capability-feedback.js +316 -0
- package/lib/revision-capability-feedback.js.map +1 -0
- package/lib/revision-capability-journal.d.ts +23 -0
- package/lib/revision-capability-journal.d.ts.map +1 -1
- package/lib/revision-capability-journal.js +124 -15
- package/lib/revision-capability-journal.js.map +1 -1
- package/lib/revision-capability-runtime.d.ts +19 -1
- package/lib/revision-capability-runtime.d.ts.map +1 -1
- package/lib/revision-capability-runtime.js +80 -7
- package/lib/revision-capability-runtime.js.map +1 -1
- package/lib/revision-capability-types.d.ts +50 -4
- package/lib/revision-capability-types.d.ts.map +1 -1
- package/lib/service.d.ts +174 -3
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +732 -68
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.js +2 -2
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-approval-authority.js +3 -3
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build.d.ts +33 -53
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +28 -408
- package/lib/source-build.js.map +1 -1
- package/lib/source-job-types.d.ts +111 -4
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +68 -4
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +296 -46
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-release-authority.js +2 -2
- package/lib/source-release-authority.js.map +1 -1
- package/lib/source-revision.d.ts +29 -3
- package/lib/source-revision.d.ts.map +1 -1
- package/lib/source-revision.js +40 -9
- package/lib/source-revision.js.map +1 -1
- package/lib/source-workspace.d.ts +11 -4
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +51 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +6 -7
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +255 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +142 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +732 -63
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.js +4 -4
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-authority.js +2 -2
- package/lib/task-observation-authority.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +3 -1
- package/lib/work-item-creation-contract.d.ts.map +1 -1
- package/lib/work-item-creation-contract.js +10 -1
- package/lib/work-item-creation-contract.js.map +1 -1
- package/package.json +11 -8
package/lib/source-jobs.js
CHANGED
|
@@ -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 {
|
|
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),
|
|
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,17 +232,40 @@ export class SourceJobRuntime {
|
|
|
192
232
|
}
|
|
193
233
|
return Promise.allSettled(this.growthFlights).then(() => undefined);
|
|
194
234
|
}
|
|
195
|
-
/** A revision is adoptable only when its independent review,
|
|
235
|
+
/** A revision is adoptable only when its independent review, any configured regression, and any configured benefit outcome verified. */
|
|
196
236
|
revisionAdoptionReady(job, planId) {
|
|
197
237
|
if (this.options.store.getRevisionVerificationStatus(planId) !== 'verified')
|
|
198
238
|
return false;
|
|
199
|
-
if (job.intent.revision?.growthRun
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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;
|
|
203
263
|
}
|
|
204
264
|
async continuePrepared(job, signal) {
|
|
205
|
-
|
|
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)))
|
|
206
269
|
return;
|
|
207
270
|
if (this.continuing.has(job.id))
|
|
208
271
|
return;
|
|
@@ -216,7 +279,8 @@ export class SourceJobRuntime {
|
|
|
216
279
|
}
|
|
217
280
|
async continuePreparedLocked(job, signal) {
|
|
218
281
|
this.assertMode(job.intent.mode);
|
|
219
|
-
if (!job.planId ||
|
|
282
|
+
if (!job.planId || (job.intent.ownerInstruction === undefined
|
|
283
|
+
&& !this.options.store.getOwnerTaskFailureReference(job.intent.gapId)))
|
|
220
284
|
return;
|
|
221
285
|
const planId = job.planId;
|
|
222
286
|
const current = this.options.store.getSourceJob(job.id);
|
|
@@ -239,20 +303,26 @@ export class SourceJobRuntime {
|
|
|
239
303
|
this.assertOwner(job);
|
|
240
304
|
};
|
|
241
305
|
if (initial.mode === 'prepared-revise') {
|
|
242
|
-
|
|
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))
|
|
243
313
|
return;
|
|
244
314
|
await assertCurrent();
|
|
245
315
|
if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
|
|
246
316
|
await this.options.verifyPreparedRevision(job, signal);
|
|
247
317
|
if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
|
|
248
318
|
&& this.options.store.getRevisionRegressionStatus(planId) === undefined
|
|
249
|
-
&& job.intent.revision
|
|
319
|
+
&& job.intent.revision?.growthRun?.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
|
|
250
320
|
await assertCurrent();
|
|
251
321
|
await this.options.verifyPreparedRevisionRegression(job, signal);
|
|
252
322
|
}
|
|
253
323
|
// Verified acceptance and the optional independent parent regression end the review lane.
|
|
254
324
|
// Replacing the stable adopted entry is a separately authorized continuation.
|
|
255
|
-
if (this.revisionAdoptionReady(job, planId)
|
|
325
|
+
if (job.intent.ownerInstruction === undefined && this.revisionAdoptionReady(job, planId)
|
|
256
326
|
&& this.options.revisionAdoptionEligible?.(planId) && this.options.adoptVerifiedRevision) {
|
|
257
327
|
await assertCurrent();
|
|
258
328
|
await this.options.adoptVerifiedRevision(job, signal);
|
|
@@ -260,8 +330,13 @@ export class SourceJobRuntime {
|
|
|
260
330
|
return;
|
|
261
331
|
}
|
|
262
332
|
if (initial.mode === 'prepared-create') {
|
|
333
|
+
const initialRun = initial.creation?.growthRun;
|
|
263
334
|
if (job.intent.mode !== 'create'
|
|
264
|
-
|| !job.intent.creation?.growthRun
|
|
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)
|
|
265
340
|
return;
|
|
266
341
|
await assertCurrent();
|
|
267
342
|
if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
|
|
@@ -306,11 +381,22 @@ export class SourceJobRuntime {
|
|
|
306
381
|
const authority = { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt };
|
|
307
382
|
return { authority, creation: structuredClone(config.creation), remaining: this.options.store.inspectSourceJobQuota({ authority: { ...authority, maxSubmissions: config.maxSubmissions }, creation: config.creation }) };
|
|
308
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
|
+
}
|
|
309
394
|
available() { return this.active && !this.abort.signal.aborted && Date.now() < this.options.config.expiresAt; }
|
|
310
395
|
close() {
|
|
311
396
|
return this.closing ??= (async () => {
|
|
312
397
|
this.active = false;
|
|
313
398
|
try {
|
|
399
|
+
this.unsubscribeWake?.();
|
|
314
400
|
const current = this.options.ports.automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: CONTINUATION_AUTOMATION });
|
|
315
401
|
if (current?.activationNonce === this.continuationNonce)
|
|
316
402
|
this.options.ports.automations.reconcileSystem({ owner: SOURCE_JOB_OWNER,
|
|
@@ -358,7 +444,7 @@ export class SourceJobRuntime {
|
|
|
358
444
|
withGapSource(gapId, owner, callback, growthRun) {
|
|
359
445
|
const reference = this.options.store.getOwnerTaskFailureReference(gapId);
|
|
360
446
|
if (reference?.schemaVersion === 2) {
|
|
361
|
-
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)
|
|
362
448
|
throw new SourceGrowthRunUnavailableError('work item growth source fence unavailable');
|
|
363
449
|
return this.options.withWorkItemGrowthRunFence(growthRun, gapId, owner, false, callback);
|
|
364
450
|
}
|
|
@@ -375,6 +461,67 @@ export class SourceJobRuntime {
|
|
|
375
461
|
if (!this.modeAllowed(mode))
|
|
376
462
|
throw new Error('source job mode is not authorized');
|
|
377
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
|
+
}
|
|
378
525
|
assertOwner(job) {
|
|
379
526
|
this.assertMode(job.intent.mode);
|
|
380
527
|
const current = this.receipt();
|
|
@@ -387,8 +534,27 @@ export class SourceJobRuntime {
|
|
|
387
534
|
|| job.intent.authority.id !== this.options.config.authorityId
|
|
388
535
|
|| controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
|
|
389
536
|
|| (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
|
|
390
|
-
:
|
|
537
|
+
: (job.intent.ownerInstruction !== undefined
|
|
538
|
+
? !isSourceOwnerContinuation(current, job.intent.owner)
|
|
539
|
+
: controlPlaneDigest(current) !== job.intent.ownerDigest)))
|
|
391
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
|
+
}
|
|
392
558
|
if (job.intent.revision) {
|
|
393
559
|
const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
|
|
394
560
|
if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
|
|
@@ -408,6 +574,8 @@ export class SourceJobRuntime {
|
|
|
408
574
|
validateSourceGrowthRunBinding(growthRun);
|
|
409
575
|
if (growthRun.protocol === 'assistant-growth/source-run/v2' && (mode !== 'create' || this.options.config.creation?.workItemFailures !== true || this.options.store.getOwnerTaskFailureReference(gapId)?.schemaVersion !== 2))
|
|
410
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');
|
|
411
579
|
if (!this.options.assertGrowthRun)
|
|
412
580
|
throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
|
|
413
581
|
this.options.assertGrowthRun(growthRun, gapId, owner, generation);
|
|
@@ -416,8 +584,14 @@ export class SourceJobRuntime {
|
|
|
416
584
|
this.assertMode(input.mode);
|
|
417
585
|
if (input.growthRun?.protocol === 'assistant-growth/source-run/v2' && input.mode !== 'create')
|
|
418
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');
|
|
419
589
|
if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
|
|
420
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');
|
|
421
595
|
if (input.mode === 'create' || input.mode === 'revise-created')
|
|
422
596
|
validateSourceCreationFiles(input.files);
|
|
423
597
|
else {
|
|
@@ -429,7 +603,8 @@ export class SourceJobRuntime {
|
|
|
429
603
|
}
|
|
430
604
|
if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
|
|
431
605
|
throw new Error('source revision parent is missing or outside revision mode');
|
|
432
|
-
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) }) }));
|
|
433
608
|
}
|
|
434
609
|
async enqueueOwned(input) {
|
|
435
610
|
this.assertMode(input.mode);
|
|
@@ -457,11 +632,19 @@ export class SourceJobRuntime {
|
|
|
457
632
|
throw new Error('source job repository mismatch');
|
|
458
633
|
const currentOwner = this.assertCaller(input.owner);
|
|
459
634
|
const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
|
|
460
|
-
if (input.mode === 'revise-created' &&
|
|
461
|
-
throw new Error('
|
|
462
|
-
|
|
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;
|
|
463
643
|
if (reference && !isSourceOwnerContinuation(currentOwner, owner))
|
|
464
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
|
+
}
|
|
465
648
|
this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true), input.growthRun);
|
|
466
649
|
const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
|
|
467
650
|
if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
|
|
@@ -474,13 +657,19 @@ export class SourceJobRuntime {
|
|
|
474
657
|
const prior = this.options.store.getSourceJob(id);
|
|
475
658
|
if (prior !== undefined) {
|
|
476
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));
|
|
477
664
|
if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
|
|
478
665
|
|| frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
|
|
479
666
|
|| (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
|
|
480
667
|
|| (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
|
|
481
668
|
|| (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
|
|
482
669
|
|| controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
|
|
483
|
-
||
|
|
670
|
+
|| (frozen.revision?.growthRun === undefined) !== (input.growthRun === undefined)
|
|
671
|
+
|| (input.growthRun !== undefined && controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun))
|
|
672
|
+
|| !instructionReviewMatches))
|
|
484
673
|
|| frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
|
|
485
674
|
|| frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
|
|
486
675
|
throw new Error('source job idempotency conflict');
|
|
@@ -511,13 +700,18 @@ export class SourceJobRuntime {
|
|
|
511
700
|
...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
|
|
512
701
|
...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
|
|
513
702
|
...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
|
|
514
|
-
generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent),
|
|
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) }) } } : {}),
|
|
515
707
|
gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
|
|
516
708
|
files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
|
|
517
709
|
worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`),
|
|
518
|
-
...(
|
|
519
|
-
|
|
520
|
-
|
|
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 },
|
|
521
715
|
};
|
|
522
716
|
await assertCurrent();
|
|
523
717
|
// Synchronous acceptance and registration: no Agent signal is persisted.
|
|
@@ -589,7 +783,24 @@ export class SourceJobRuntime {
|
|
|
589
783
|
throw error;
|
|
590
784
|
}
|
|
591
785
|
}
|
|
592
|
-
|
|
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
|
+
}
|
|
593
804
|
// A Linux revision can run two independently bounded reviews after its
|
|
594
805
|
// repository build. This outer window never changes their own budgets or
|
|
595
806
|
// the frozen source expiry; legacy backend definitions retain their identity.
|
|
@@ -603,7 +814,7 @@ export class SourceJobRuntime {
|
|
|
603
814
|
const config = this.options.config;
|
|
604
815
|
return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
|
|
605
816
|
workspace: config.workspace, agentPreset: config.preset, timeoutMs: job.intent.build.timeoutMs + 120_000
|
|
606
|
-
+ (job.intent.mode === 'revise-created' ? this.linuxRevisionReviewTimeoutMs() : 0)
|
|
817
|
+
+ (job.intent.mode === 'revise-created' ? this.linuxRevisionReviewTimeoutMs(job) : 0)
|
|
607
818
|
+ (job.intent.mode === 'create' && job.intent.creation?.growthRun?.protocol === 'assistant-growth/source-run/v2'
|
|
608
819
|
&& 'backend' in this.options.build
|
|
609
820
|
&& this.options.build.backend === 'linux-native' && this.options.verifyPreparedCreation ? 1_800_000 : 0)
|
|
@@ -693,7 +904,8 @@ export class SourceJobRuntime {
|
|
|
693
904
|
hasContinuations() {
|
|
694
905
|
return !!(this.options.approvePrepared || this.options.verifyPreparedRevisionRegression || this.options.verifyPreparedRevision
|
|
695
906
|
|| this.options.verifyPreparedCreation || this.options.adoptVerifiedCreation || this.options.adoptVerifiedRevision
|
|
696
|
-
|| this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased
|
|
907
|
+
|| this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased
|
|
908
|
+
|| this.options.drainOwnerInstructionRequests);
|
|
697
909
|
}
|
|
698
910
|
continuationJobs() {
|
|
699
911
|
if (!this.hasContinuations())
|
|
@@ -713,11 +925,15 @@ export class SourceJobRuntime {
|
|
|
713
925
|
return false;
|
|
714
926
|
}
|
|
715
927
|
if (plan.mode === 'prepared-revise') {
|
|
716
|
-
|
|
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)
|
|
717
933
|
return false;
|
|
718
934
|
const status = this.options.store.getRevisionVerificationStatus(plan.id);
|
|
719
935
|
const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
|
|
720
|
-
const regression = status === 'verified' && plan.sourceRevision
|
|
936
|
+
const regression = status === 'verified' && plan.sourceRevision?.growthRun?.revisionRegressionAcceptance !== undefined
|
|
721
937
|
&& this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
|
|
722
938
|
const adoption = this.options.revisionAdoptionEligible !== undefined && this.options.adoptVerifiedRevision !== undefined
|
|
723
939
|
&& this.revisionAdoptionReady(job, plan.id) && this.options.revisionAdoptionEligible(plan.id) === true;
|
|
@@ -732,8 +948,13 @@ export class SourceJobRuntime {
|
|
|
732
948
|
return plan.expiresAt > Date.now();
|
|
733
949
|
}
|
|
734
950
|
if (plan.mode === 'prepared-create') {
|
|
951
|
+
const planRun = plan.creation?.growthRun;
|
|
735
952
|
if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
|
|
736
|
-
|| !job.intent.creation?.growthRun
|
|
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)
|
|
737
958
|
return false;
|
|
738
959
|
const verification = this.options.store.getCreationVerificationStatus(plan.id);
|
|
739
960
|
const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
|
|
@@ -778,6 +999,16 @@ export class SourceJobRuntime {
|
|
|
778
999
|
return this.options.advanceReleased !== undefined;
|
|
779
1000
|
return plan.status === 'release-complete' && this.options.adoptReleased !== undefined;
|
|
780
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
|
+
*/
|
|
781
1012
|
continuationTimeoutMs() {
|
|
782
1013
|
const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
|
|
783
1014
|
const needed = (this.options.adoptReleased ? adoption : 0) + (this.options.advanceReleased ? release + 40_000 : this.options.releasePrepared ? 40_000 : 10_000);
|
|
@@ -804,7 +1035,9 @@ export class SourceJobRuntime {
|
|
|
804
1035
|
reconcileContinuations(force = false) {
|
|
805
1036
|
if (!this.active || !this.hasContinuations())
|
|
806
1037
|
return;
|
|
807
|
-
const definition = this.continuationDefinition()
|
|
1038
|
+
const definition = this.continuationDefinition();
|
|
1039
|
+
const requests = this.available() && this.options.hasOwnerInstructionRequests?.() === true;
|
|
1040
|
+
const desiredStatus = requests || this.continuationJobs().length ? 'active' : 'paused';
|
|
808
1041
|
if (!force && desiredStatus === this.continuationStatus)
|
|
809
1042
|
return;
|
|
810
1043
|
this.options.ports.automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: CONTINUATION_AUTOMATION,
|
|
@@ -822,7 +1055,22 @@ export class SourceJobRuntime {
|
|
|
822
1055
|
throw new Error('source continuation dispatch changed');
|
|
823
1056
|
const jobs = this.continuationJobs();
|
|
824
1057
|
let unsettled = false;
|
|
825
|
-
|
|
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;
|
|
826
1074
|
// One budget-admitted occurrence advances one durable row. The cursor
|
|
827
1075
|
// is intentionally in-memory; a restart returning to the first row is
|
|
828
1076
|
// safe because every release transition remains independently claimed.
|
|
@@ -840,6 +1088,12 @@ export class SourceJobRuntime {
|
|
|
840
1088
|
}
|
|
841
1089
|
}
|
|
842
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 */ }
|
|
843
1097
|
if (unsettled)
|
|
844
1098
|
return { outcome: 'unknown', failureClass: 'infrastructure', failurePhase: 'host-execution', failureCode: 'source-continuation-unsettled', sideEffectState: 'unknown', retryability: 'unsafe' };
|
|
845
1099
|
return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
|
|
@@ -906,8 +1160,8 @@ export class SourceJobRuntime {
|
|
|
906
1160
|
const signal = AbortSignal.any([input.signal, this.abort.signal,
|
|
907
1161
|
...(growthController ? [growthController.signal] : []),
|
|
908
1162
|
AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000
|
|
909
|
-
+ (job.intent.mode === 'revise-created' && this.linuxRevisionReviewTimeoutMs()
|
|
910
|
-
? 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)
|
|
911
1165
|
+ (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
|
|
912
1166
|
try {
|
|
913
1167
|
this.assertOwner(job);
|
|
@@ -1005,15 +1259,11 @@ export class SourceJobRuntime {
|
|
|
1005
1259
|
const trust = await this.options.trust();
|
|
1006
1260
|
if (controlPlaneDigest(trust) !== job.intent.trustDigest)
|
|
1007
1261
|
throw new Error('source job trust changed');
|
|
1008
|
-
if ('backend' in job.intent.build)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
else {
|
|
1016
|
-
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');
|
|
1017
1267
|
}
|
|
1018
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) });
|
|
1019
1269
|
this.inspect(input);
|