@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.
- package/README.md +96 -22
- 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-execution.d.ts +5 -1
- package/lib/creation-capability-execution.d.ts.map +1 -1
- package/lib/creation-capability-execution.js +42 -0
- package/lib/creation-capability-execution.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 +47 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +105 -1
- 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/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.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 +126 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +981 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +92 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +635 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +335 -0
- package/lib/revision-capability-types.d.ts.map +1 -0
- package/lib/revision-capability-types.js +2 -0
- package/lib/revision-capability-types.js.map +1 -0
- package/lib/service.d.ts +199 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +925 -70
- 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 +72 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +314 -43
- 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 -18
- 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 +143 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +739 -66
- 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,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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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' &&
|
|
445
|
-
throw new Error('
|
|
446
|
-
|
|
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
|
-
||
|
|
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),
|
|
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
|
-
...(
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
936
|
+
const regression = status === 'verified' && plan.sourceRevision?.growthRun?.revisionRegressionAcceptance !== undefined
|
|
702
937
|
&& this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
|
|
703
|
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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);
|