@dsh-enhanced/assistant-growth-driver 0.1.64 → 0.1.66

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.
@@ -1,6 +1,12 @@
1
+ import { ScannerOwnerGuard, ScannerOwnerUnavailable, drainScannerFlights } from './scanner-owner-runtime.js';
1
2
  import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
2
- import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, sourceGrowthEvidenceDigest, sourceGrowthRunDigest, validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
3
- import { UsageStore } from './usage-store.js';
3
+ import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, sourceGrowthEvidenceDigest, sourceGrowthRunDigest, validateSourceGrowthRunBinding, validateAnySourceGrowthRunBinding, anySourceGrowthRunDigest } from '@dsh-enhanced/assistant-growth-contract';
4
+ import { UsageStore, isWorkItemUsageIntent } from './usage-store.js';
5
+ import { freezeWorkItemData, ownData, isWorkItemCreationReady, workItemAuthorityDeadline, workItemTextDigest, workItemRunSource, workItemStableOwner } from './work-item-source.js';
6
+ class WorkItemSourcePending extends Error {
7
+ }
8
+ class WorkItemSourceObsolete extends Error {
9
+ }
4
10
  const OWNER = 'assistant-growth-usage';
5
11
  const EXECUTOR = 'assistant-growth-usage-v1';
6
12
  const CATALOG = acceptanceDigest({ executor: EXECUTOR, version: 1 });
@@ -17,6 +23,9 @@ export class UsageLearningRuntime {
17
23
  flights = new Set();
18
24
  unregister;
19
25
  unsubscribe;
26
+ ownerGuard;
27
+ unsubscribeWake;
28
+ initialized = false;
20
29
  active = false;
21
30
  scanning = false;
22
31
  lastError = null;
@@ -30,6 +39,10 @@ export class UsageLearningRuntime {
30
39
  if (typeof ports.evaluation.listTrustedTaskLearningProjections !== 'function'
31
40
  || typeof ports.evaluation.withTrustedCanonicalTaskWriterFence !== 'function'
32
41
  || typeof ports.evaluation.onTrustedTaskChange !== 'function'
42
+ || typeof ports.delivery.inspectOwnerRouteState !== 'function'
43
+ || typeof ports.automations.inspectSystemOwnedRetirement !== 'function'
44
+ || typeof ports.automations.retireSystemOwned !== 'function'
45
+ || typeof ports.automations.onSchedulerWake !== 'function'
33
46
  || typeof ports.delivery.inspectOwnerForegroundLearningTask !== 'function'
34
47
  || typeof ports.automations.registerHostExecutor !== 'function'
35
48
  || typeof ports.automations.inspectSystemOwnedActivation !== 'function') {
@@ -44,11 +57,22 @@ export class UsageLearningRuntime {
44
57
  ...(allowRevision ? { allowRevision: true } : {}),
45
58
  ...(allowModification === false ? { allowModification: false } : {}) } });
46
59
  this.scanId = `usage-scan-${this.lane}`;
60
+ this.ownerGuard = new ScannerOwnerGuard(OWNER, this.scanId, { authorityId: config.scope.ownerRouteId,
61
+ principalId: config.scope.principalId, workspace: config.scope.workspace, agentPreset: config.scope.preset }, ports.delivery, ports.automations);
47
62
  }
48
- health = () => ({ enabled: true, connected: this.active, lastScanAt: this.lastScanAt,
63
+ health = () => ({ enabled: true, connected: this.active && !this.ownerGuard.retired, lastScanAt: this.lastScanAt,
49
64
  lastError: this.lastError, counts: this.store.counts(this.lane) });
50
65
  start() {
51
- this.store.interrupt(this.lane);
66
+ let recoverNativeScanner = false;
67
+ try {
68
+ this.ownerGuard.check();
69
+ }
70
+ catch (error) {
71
+ this.lastError = String(error).slice(0, 200);
72
+ if (this.ownerGuard.retired)
73
+ return;
74
+ recoverNativeScanner = this.ownerGuard.canReconcileScanner;
75
+ }
52
76
  this.unregister = this.ports.automations.registerHostExecutor({
53
77
  descriptor: { executorId: EXECUTOR, contractVersion: 1, catalogDigest: CATALOG },
54
78
  accepts: spec => spec.executorId === EXECUTOR && spec.executorContractVersion === 1
@@ -61,31 +85,66 @@ export class UsageLearningRuntime {
61
85
  },
62
86
  });
63
87
  this.active = true;
88
+ this.unsubscribeWake = this.ports.automations.onSchedulerWake(() => {
89
+ if (!this.active || this.initialized || this.ownerGuard.retired)
90
+ return;
91
+ try {
92
+ this.initializeScanner();
93
+ }
94
+ catch (error) {
95
+ this.lastError = String(error).slice(0, 200);
96
+ }
97
+ });
64
98
  this.unsubscribe = this.ports.evaluation.onTrustedTaskChange(() => this.scan());
65
99
  // This persisted scan also discovers writes made by another Host process.
66
- this.ports.automations.reconcileSystem({ owner: OWNER, automationId: this.scanId,
67
- idempotencyKey: `${this.scanId}:${this.configDigest}`, desiredStatus: 'active', definition: this.definition() });
100
+ if (recoverNativeScanner) {
101
+ try {
102
+ this.ports.automations.reconcileSystem({ owner: OWNER, automationId: this.scanId,
103
+ idempotencyKey: `${this.scanId}:${this.configDigest}`, desiredStatus: 'active', definition: this.definition() });
104
+ }
105
+ catch (error) {
106
+ this.lastError = String(error).slice(0, 200);
107
+ }
108
+ }
68
109
  this.scan();
69
110
  }
111
+ initializeScanner() {
112
+ const owner = this.owner();
113
+ if (!this.initialized) {
114
+ this.store.interrupt(this.lane);
115
+ this.ports.automations.reconcileSystem({ owner: OWNER, automationId: this.scanId,
116
+ idempotencyKey: `${this.scanId}:${this.configDigest}`, desiredStatus: 'active', definition: this.definition() });
117
+ this.initialized = true;
118
+ }
119
+ return owner;
120
+ }
70
121
  close() {
71
122
  return this.closing ??= (async () => {
72
123
  this.active = false;
124
+ this.unsubscribeWake?.();
73
125
  this.unsubscribe?.();
74
126
  this.unregister?.();
75
127
  this.abort.abort(new Error('usage learning provider disposed'));
76
- await Promise.allSettled(this.flights);
77
- this.store.close();
128
+ try {
129
+ await drainScannerFlights(this.flights);
130
+ }
131
+ finally {
132
+ this.store.close();
133
+ }
78
134
  })();
79
135
  }
80
136
  owner() {
81
137
  if (!this.active)
82
138
  throw new Error('usage learning unavailable');
139
+ this.ownerGuard.check();
83
140
  const scope = this.config.scope;
84
141
  return this.ports.delivery.validateOwnerRoute({ authorityId: scope.ownerRouteId, principalId: scope.principalId,
85
142
  workspace: scope.workspace, agentPreset: scope.preset });
86
143
  }
87
144
  source(job) {
88
145
  this.abort.signal.throwIfAborted();
146
+ if (isWorkItemUsageIntent(job.intent))
147
+ throw new Error('foreground usage source required');
89
148
  if (job.intent.configDigest !== this.configDigest || job.intent.expiresAt <= Date.now()
90
149
  || !isSourceOwnerContinuation(this.owner(), job.intent.source.owner))
91
150
  throw new Error('usage authority changed or expired');
@@ -107,12 +166,14 @@ export class UsageLearningRuntime {
107
166
  throw new SourceGrowthRunUnavailableError('usage runtime is temporarily unavailable');
108
167
  try {
109
168
  const job = this.store.get(request.runId);
110
- if (!job || job.digest !== request.intentDigest || !['running', 'reviewed'].includes(job.state)
169
+ if (!job || isWorkItemUsageIntent(job.intent) || job.digest !== request.intentDigest || !['running', 'reviewed'].includes(job.state)
111
170
  || !job.intent.modelOrigin || !job.intent.budget || !job.sourceRun
112
171
  || job.definitionHash === null || job.occurrenceId === null)
113
172
  return undefined;
114
173
  const source = this.source(job);
115
174
  const binding = job.sourceRun;
175
+ if (binding.protocol !== 'assistant-growth/source-run/v1')
176
+ return undefined;
116
177
  validateSourceGrowthRunBinding(binding);
117
178
  if (binding.creationAcceptance !== undefined) {
118
179
  const current = this.ports.inspectCreationAcceptanceAuthority?.();
@@ -142,6 +203,8 @@ export class UsageLearningRuntime {
142
203
  return structuredClone(binding);
143
204
  }
144
205
  catch (error) {
206
+ if (error instanceof ScannerOwnerUnavailable && !this.ownerGuard.retired)
207
+ throw new SourceGrowthRunUnavailableError(error.message);
145
208
  const code = error instanceof Error ? error.code : undefined;
146
209
  if (code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED'
147
210
  || code === 'ERR_SQLITE_ERROR' && error instanceof Error && /database (?:is )?(?:busy|locked)/iu.test(error.message)) {
@@ -150,20 +213,305 @@ export class UsageLearningRuntime {
150
213
  return undefined;
151
214
  }
152
215
  };
216
+ workItemScope(owner) {
217
+ return { authorityId: owner.authorityId, principalId: owner.principalId, workspace: owner.workspace, agentPreset: owner.agentPreset,
218
+ expectedOwner: { authorityHash: owner.authorityHash, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion } };
219
+ }
220
+ assertWorkItemReadiness(owner, admission = true) {
221
+ const pins = this.config.usageLearning.workItemAuthorities;
222
+ const currentOwner = this.owner();
223
+ if (!isSourceOwnerContinuation(currentOwner, owner))
224
+ throw new WorkItemSourceObsolete('work item source owner changed');
225
+ if (!this.config.usageLearning.workItemFailures || !pins || !this.ports.reviewWorkItem
226
+ || this.ports.inspectWorkItemToolPreauthorization?.() !== true
227
+ || !isWorkItemCreationReady(this.ports.inspectWorkItemCreationReadiness?.({ owner: currentOwner }), pins, Date.now(), admission)) {
228
+ throw new WorkItemSourcePending('work item engineering peers or authorizations unavailable');
229
+ }
230
+ }
231
+ currentWorkItem(locator) {
232
+ if (locator.configDigest !== this.configDigest || locator.expiresAt <= Date.now()
233
+ || !same(workItemStableOwner(this.owner()), locator.owner))
234
+ throw new WorkItemSourceObsolete('work item usage authority changed or expired');
235
+ const get = this.ports.evaluation.getTrustedWorkItemLearningProjection;
236
+ if (typeof get !== 'function')
237
+ throw new WorkItemSourcePending('work item canonical reader unavailable');
238
+ const scope = this.ports.evaluation.canonicalHostScope({ workspace: locator.owner.workspace, preset: locator.owner.agentPreset });
239
+ const current = get.call(this.ports.evaluation, { scope, itemId: locator.itemId });
240
+ if (!current || current.projection.disposition !== 'upsert' || current.objective?.status !== 'not-achieved'
241
+ || current.objective.outcomeId !== locator.canonical.outcomeId || current.projection.version !== locator.canonical.version
242
+ || current.projection.digest !== locator.canonical.digest)
243
+ throw new WorkItemSourceObsolete('work item canonical source changed');
244
+ }
245
+ expectation(source) {
246
+ const task = source.task, outcome = task.canonical.objective;
247
+ return { ...this.workItemScope(task.owner), itemId: task.proofIdentity.itemId, outcomeId: outcome.outcomeId,
248
+ expectedSourceDigest: task.sourceDigest, expectedContentDigest: task.presentationDigest,
249
+ canonical: { version: task.canonical.projection.version, digest: task.canonical.projection.digest, objectiveStatus: 'not-achieved' },
250
+ expectedManifestDigest: source.manifest.manifestDigest, expectedObjectiveDigest: source.manifest.objective.fullTextDigest };
251
+ }
252
+ locatorFor(intent) {
253
+ const source = workItemRunSource(intent.source), owner = intent.source.task.owner;
254
+ return { protocol: 'assistant-growth/work-item-usage-locator/v1', configDigest: intent.configDigest,
255
+ owner: { authorityId: owner.authorityId, authorityHash: owner.authorityHash, principalId: owner.principalId,
256
+ principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion, workspace: owner.workspace, agentPreset: owner.agentPreset },
257
+ itemId: source.itemId, canonical: { outcomeId: source.outcomeId, version: source.projection.version,
258
+ digest: source.projection.digest, objectiveStatus: 'not-achieved' }, createdAt: intent.createdAt, expiresAt: intent.expiresAt };
259
+ }
260
+ withWorkItemSource(intent, callback) {
261
+ this.abort.signal.throwIfAborted();
262
+ this.currentWorkItem(this.locatorFor(intent));
263
+ if (!same(intent.authorities, this.config.usageLearning.workItemAuthorities))
264
+ throw new WorkItemSourceObsolete('work item authority pins changed');
265
+ const fence = this.ports.delivery.withOwnerWorkItemEngineeringContextFence;
266
+ if (!fence)
267
+ throw new WorkItemSourcePending('work item engineering fence unavailable');
268
+ return fence.call(this.ports.delivery, this.expectation(intent.source), source => {
269
+ this.abort.signal.throwIfAborted();
270
+ if (!this.active || intent.configDigest !== this.configDigest || Date.now() >= Math.min(intent.expiresAt, workItemAuthorityDeadline(intent.authorities)))
271
+ throw new WorkItemSourceObsolete('work item usage lifecycle or authority expired');
272
+ if (!same(workItemRunSource({ ...intent.source, ...source }), workItemRunSource(intent.source))
273
+ || !isSourceOwnerContinuation(source.task.owner, intent.source.task.owner)
274
+ || !same({ ...source.task, owner: intent.source.task.owner, canonical: intent.source.task.canonical }, intent.source.task)
275
+ || !same(source.manifest, intent.source.manifest) || source.objective !== intent.source.objective)
276
+ throw new WorkItemSourceObsolete('frozen work item engineering source changed');
277
+ return callback(source);
278
+ });
279
+ }
280
+ readWorkItem(locator) {
281
+ this.currentWorkItem(locator);
282
+ const inspect = this.ports.delivery.inspectOwnerWorkItemEngineeringContext, read = this.ports.delivery.readOwnerWorkItemEngineeringContext;
283
+ if (!inspect || !read)
284
+ throw new WorkItemSourcePending('work item engineering context reader unavailable');
285
+ const context = inspect.call(this.ports.delivery, { ...this.workItemScope(locator.owner), itemId: locator.itemId, outcomeId: locator.canonical.outcomeId });
286
+ if (!context)
287
+ throw new WorkItemSourcePending('complete work item context temporarily unreadable');
288
+ let shell = { protocol: 'assistant-delivery/work-item-engineering-context/v1', ...context, objective: '', parentReports: [] };
289
+ const expectation = this.expectation(shell);
290
+ const readAll = (selector, fullTextDigest, fullBytes) => {
291
+ if (!Number.isSafeInteger(fullBytes) || fullBytes < 1 || fullBytes > (selector.kind === 'objective' ? 16_384 : 65_536))
292
+ throw new WorkItemSourceObsolete('work item context exceeds complete input bound');
293
+ let offset = 0, text = '';
294
+ for (let pageNumber = 0; pageNumber < 32; pageNumber++) {
295
+ const page = read.call(this.ports.delivery, { ...expectation, selector, offset, maxBytes: 8192 });
296
+ if (!page || page.manifestDigest !== context.manifest.manifestDigest || page.fullTextDigest !== fullTextDigest
297
+ || page.fullBytes !== fullBytes || page.offset !== offset || !page.text.isWellFormed()
298
+ || page.returnedBytes !== Buffer.byteLength(page.text) || page.returnedBytes < 1)
299
+ throw new WorkItemSourcePending('work item context pagination changed');
300
+ text += page.text;
301
+ offset += page.returnedBytes;
302
+ if (offset > fullBytes || page.nextOffset !== undefined && page.nextOffset !== offset)
303
+ throw new WorkItemSourceObsolete('invalid work item context pagination');
304
+ if (page.nextOffset === undefined) {
305
+ if (offset !== fullBytes || workItemTextDigest(text) !== fullTextDigest)
306
+ throw new WorkItemSourceObsolete('work item context was not fully read');
307
+ return text;
308
+ }
309
+ }
310
+ throw new WorkItemSourceObsolete('work item context pagination exceeds bound');
311
+ };
312
+ shell = { ...shell, objective: readAll({ kind: 'objective' }, context.manifest.objective.fullTextDigest, context.manifest.objective.fullBytes) };
313
+ let reportBytes = 0;
314
+ shell.parentReports = context.manifest.reports.map(report => {
315
+ reportBytes += report.fullBytes;
316
+ if (report.truncated || reportBytes > 262_144)
317
+ throw new WorkItemSourceObsolete('sealed background reports exceed bound');
318
+ return { itemId: report.itemId, revision: report.revision, fullTextDigest: report.fullTextDigest, fullBytes: report.fullBytes,
319
+ text: readAll({ kind: 'parent-report', itemId: report.itemId, revision: report.revision }, report.fullTextDigest, report.fullBytes) };
320
+ });
321
+ workItemRunSource(shell);
322
+ return freezeWorkItemData(shell);
323
+ }
324
+ scanWorkItems(owner, scope) {
325
+ for (let pageNumber = 0, stopped = false; pageNumber < 10 && !stopped; pageNumber++) {
326
+ const after = this.store.workItemCursor(this.lane);
327
+ const page = this.ports.evaluation.listTrustedTaskLearningProjections({ scope, ...(after ? { after } : {}), limit: 100 });
328
+ for (const item of page.items) {
329
+ const canonical = item.receipt, now = Date.now(), at = canonical.objective?.occurredAt ?? 0;
330
+ const workItem = canonical.projection.subjectKind === 'work-item';
331
+ const eligible = workItem && canonical.projection.disposition === 'upsert' && canonical.objective?.status === 'not-achieved'
332
+ && at <= now && now - at < this.config.usageLearning.lookbackMs;
333
+ const locator = eligible ? { protocol: 'assistant-growth/work-item-usage-locator/v1',
334
+ configDigest: this.configDigest, owner: { authorityId: owner.authorityId, authorityHash: owner.authorityHash,
335
+ principalId: owner.principalId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion,
336
+ workspace: owner.workspace, agentPreset: owner.agentPreset }, itemId: canonical.projection.subjectRef,
337
+ canonical: { outcomeId: canonical.objective.outcomeId, version: canonical.projection.version,
338
+ digest: canonical.projection.digest, objectiveStatus: 'not-achieved' }, createdAt: now, expiresAt: at + this.config.usageLearning.lookbackMs } : undefined;
339
+ const staged = this.ports.evaluation.withTrustedCanonicalTaskWriterFence({ scope, scopeWatermark: page.scopeWatermark,
340
+ evidence: [canonical.projection] }, () => this.store.consumeWorkItem({ lane: this.lane, scopeKey: page.nextCursor.scopeKey,
341
+ watermark: item.watermark, ...(workItem ? { itemId: canonical.projection.subjectRef } : {}),
342
+ ...(locator ? { locator } : {}), maxPending: this.config.usageLearning.maxPending }));
343
+ if (!staged.matched || !staged.value) {
344
+ stopped = true;
345
+ break;
346
+ }
347
+ }
348
+ if (!page.hasMore)
349
+ break;
350
+ }
351
+ for (const pending of this.store.pendingWorkItems(this.lane)) {
352
+ try {
353
+ this.currentWorkItem(pending.locator);
354
+ const source = this.readWorkItem(pending.locator);
355
+ this.assertWorkItemReadiness(source.task.owner);
356
+ const fixed = this.config.provider !== null && this.config.model !== null;
357
+ const intent = { sourceKind: 'work-item', configDigest: this.configDigest, source,
358
+ model: fixed ? { provider: this.config.provider, model: this.config.model, ...(this.config.reasoningEffort === null ? {} : { reasoningEffort: this.config.reasoningEffort }) }
359
+ : structuredClone(source.task.proofIdentity.supplier), modelOrigin: fixed ? 'explicit-growth-override' : 'inherited-owner-task',
360
+ budget: { budgetId: this.config.budgetId, amount: this.config.budgetAmount, maxModelCalls: this.config.maxModelCalls,
361
+ maxToolCalls: this.config.maxToolCalls, maxOutputTokens: this.config.maxOutputTokens, maxDurationMs: this.config.maxDurationMs,
362
+ maxPlansPerWake: this.config.pluginSourceProposals.maxPlansPerWake }, authorities: this.config.usageLearning.workItemAuthorities,
363
+ createdAt: pending.locator.createdAt, expiresAt: pending.locator.expiresAt };
364
+ this.withWorkItemSource(intent, () => this.store.freezeWorkItem(pending.id, intent));
365
+ }
366
+ catch (error) {
367
+ if (error instanceof ScannerOwnerUnavailable)
368
+ throw error;
369
+ if (error instanceof WorkItemSourceObsolete)
370
+ this.store.abandonWorkItem(pending.id);
371
+ else if (!(error instanceof WorkItemSourcePending))
372
+ this.lastError = String(error).slice(0, 200);
373
+ }
374
+ }
375
+ }
376
+ inspectAnySourceGrowthRun = (request) => {
377
+ if (!this.active)
378
+ throw new SourceGrowthRunUnavailableError('usage runtime temporarily unavailable');
379
+ const job = this.store.get(request.runId);
380
+ if (!job || !isWorkItemUsageIntent(job.intent))
381
+ return this.inspectSourceGrowthRun(request);
382
+ try {
383
+ if (job.digest !== request.intentDigest || !['running', 'reviewed'].includes(job.state) || !job.sourceRun
384
+ || job.sourceRun.protocol !== 'assistant-growth/source-run/v2')
385
+ return undefined;
386
+ this.assertWorkItemReadiness(job.intent.source.task.owner, false);
387
+ return this.withWorkItemSource(job.intent, () => this.store.withWorkItemRunWriter(request, current => {
388
+ if (!current.sourceRun || current.sourceRun.protocol !== 'assistant-growth/source-run/v2')
389
+ throw new Error('work item run absent');
390
+ this.assertWorkItemBinding(current, current.sourceRun);
391
+ return freezeWorkItemData(current.sourceRun);
392
+ }));
393
+ }
394
+ catch (error) {
395
+ if (error instanceof WorkItemSourcePending || error instanceof ScannerOwnerUnavailable)
396
+ throw new SourceGrowthRunUnavailableError(error.message);
397
+ return undefined;
398
+ }
399
+ };
400
+ withWorkItemRunFence = (input, callback) => {
401
+ if (!this.active)
402
+ throw new SourceGrowthRunUnavailableError('usage runtime temporarily unavailable');
403
+ input = ownData(input);
404
+ if (Object.keys(input).sort().join(',') !== 'expectedGrowthRunDigest,intentDigest,runId' || typeof callback !== 'function'
405
+ || typeof input.runId !== 'string' || !input.runId || input.runId.length > 500
406
+ || typeof input.intentDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(input.intentDigest)
407
+ || typeof input.expectedGrowthRunDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(input.expectedGrowthRunDigest))
408
+ throw new Error('invalid work item run fence input');
409
+ const job = this.store.get(input.runId);
410
+ if (!job || job.digest !== input.intentDigest || !isWorkItemUsageIntent(job.intent))
411
+ throw new Error('work item run not found');
412
+ const intent = job.intent;
413
+ this.assertWorkItemReadiness(intent.source.task.owner, false);
414
+ return this.withWorkItemSource(intent, () => this.store.withWorkItemRunWriter(input, current => {
415
+ const binding = current.sourceRun;
416
+ if (!binding || binding.protocol !== 'assistant-growth/source-run/v2' || anySourceGrowthRunDigest(binding) !== input.expectedGrowthRunDigest
417
+ || binding.configDigest !== this.configDigest || !same(binding.authorities, this.config.usageLearning.workItemAuthorities)
418
+ || !same(binding.source, workItemRunSource(intent.source)))
419
+ throw new Error('work item run binding changed');
420
+ this.assertWorkItemBinding(current, binding);
421
+ return callback(freezeWorkItemData(binding));
422
+ }));
423
+ };
424
+ assertWorkItemBinding(job, binding) {
425
+ validateAnySourceGrowthRunBinding(binding);
426
+ if (!isWorkItemUsageIntent(job.intent) || binding.runId !== job.id || binding.intentDigest !== job.digest
427
+ || binding.configDigest !== this.configDigest || binding.ownerDigest !== sourceGrowthEvidenceDigest(job.intent.source.task.owner)
428
+ || binding.native.automationId !== job.id || binding.native.definitionHash !== job.definitionHash || binding.native.occurrenceId !== job.occurrenceId
429
+ || !same(binding.source, workItemRunSource(job.intent.source)) || !same(binding.model, job.intent.model)
430
+ || binding.modelOrigin !== job.intent.modelOrigin || !same(binding.budget, job.intent.budget)
431
+ || !same(binding.authorities, job.intent.authorities) || binding.expiresAt !== job.intent.expiresAt)
432
+ throw new Error('work item persisted run differs from frozen job');
433
+ }
434
+ async executeWorkItem(input, queued, result) {
435
+ if (!isWorkItemUsageIntent(queued.intent))
436
+ throw new Error('work item usage expected');
437
+ const intent = queued.intent;
438
+ let claimed = false;
439
+ try {
440
+ this.assertWorkItemReadiness(intent.source.task.owner);
441
+ this.withWorkItemSource(intent, () => { this.store.claim(queued.id, input.definitionHash, input.occurrenceId); claimed = true; });
442
+ const signal = AbortSignal.any([input.signal, this.abort.signal, AbortSignal.timeout(Math.max(1, Math.min(this.config.maxDurationMs, intent.expiresAt - Date.now(), workItemAuthorityDeadline(intent.authorities) - Date.now())))]);
443
+ const assertCurrent = () => {
444
+ signal.throwIfAborted();
445
+ // Peer/owner probes can synchronously change source state. Run them
446
+ // before the final complete source frame; its callback is local only.
447
+ this.assertWorkItemReadiness(intent.source.task.owner, false);
448
+ this.withWorkItemSource(intent, () => {
449
+ signal.throwIfAborted();
450
+ const current = this.store.get(queued.id);
451
+ if (current?.state !== 'running' || current.occurrenceId !== input.occurrenceId
452
+ || current.digest !== queued.digest)
453
+ throw new Error('work item usage no longer owns dispatch');
454
+ });
455
+ };
456
+ const bindSourceRun = actual => {
457
+ assertCurrent();
458
+ this.assertWorkItemReadiness(intent.source.task.owner);
459
+ if (!same(actual.model, intent.model) || !same(actual.creationAcceptance, intent.authorities.creationAcceptance)
460
+ || actual.revisionAcceptance !== undefined || actual.revisionRegressionAcceptance !== undefined
461
+ || actual.generationDeadlineAt > workItemAuthorityDeadline(intent.authorities))
462
+ throw new Error('work item run setup authority changed');
463
+ const binding = { protocol: 'assistant-growth/source-run/v2', mode: 'create',
464
+ runId: queued.id, intentDigest: queued.digest, configDigest: intent.configDigest, ownerDigest: sourceGrowthEvidenceDigest(intent.source.task.owner),
465
+ source: workItemRunSource(intent.source), model: actual.model, modelOrigin: intent.modelOrigin, budget: intent.budget,
466
+ creationAcceptance: structuredClone(intent.authorities.creationAcceptance), authorities: structuredClone(intent.authorities),
467
+ native: { owner: OWNER, automationId: queued.id, definitionHash: input.definitionHash, occurrenceId: input.occurrenceId },
468
+ sessionId: actual.sessionId, toolContractDigest: actual.toolContractDigest, executionContractDigest: actual.executionContractDigest,
469
+ createdAt: actual.createdAt, generationDeadlineAt: actual.generationDeadlineAt, expiresAt: intent.expiresAt };
470
+ return this.withWorkItemSource(intent, () => this.store.bindWorkItemSourceRun(queued.id, queued.digest, input.definitionHash, input.occurrenceId, binding));
471
+ };
472
+ this.assertWorkItemReadiness(intent.source.task.owner);
473
+ assertCurrent();
474
+ const outcome = await this.ports.reviewWorkItem({ sourceKind: 'work-item', id: queued.id, intentDigest: queued.digest,
475
+ definitionHash: input.definitionHash, occurrenceId: input.occurrenceId, expiresAt: Math.min(intent.expiresAt, workItemAuthorityDeadline(intent.authorities)),
476
+ model: intent.model, source: intent.source, signal, assertCurrent, assertFirstModelAdmission: () => { this.assertWorkItemReadiness(intent.source.task.owner); assertCurrent(); }, bindSourceRun });
477
+ assertCurrent();
478
+ this.store.settle(queued.id, outcome, outcome === 'reviewed' ? 'review-complete-not-adopted' : 'review-incomplete', input.occurrenceId);
479
+ return result(outcome === 'reviewed' ? 'succeeded' : outcome);
480
+ }
481
+ catch {
482
+ if (claimed)
483
+ this.store.settle(queued.id, 'unknown', 'interrupted-after-dispatch', input.occurrenceId);
484
+ return result(claimed ? 'unknown' : 'failed');
485
+ }
486
+ }
153
487
  /** Synchronous nudge, bounded to 1000 heads. Native scan resumes the cursor. */
154
488
  scan = () => {
155
489
  if (!this.active || this.scanning)
156
490
  return;
157
491
  this.scanning = true;
158
492
  try {
159
- const owner = this.owner();
493
+ const owner = this.initializeScanner();
160
494
  for (const job of this.store.pending(this.lane))
161
495
  if (job.state === 'queued') {
162
496
  try {
163
- this.source(job);
164
- this.schedule(job);
497
+ if (isWorkItemUsageIntent(job.intent)) {
498
+ this.withWorkItemSource(job.intent, () => undefined);
499
+ this.schedule(job);
500
+ }
501
+ else {
502
+ this.source(job);
503
+ this.schedule(job);
504
+ }
165
505
  }
166
- catch {
506
+ catch (error) {
507
+ if (isWorkItemUsageIntent(job.intent)) {
508
+ if (error instanceof WorkItemSourceObsolete)
509
+ this.store.settle(job.id, 'superseded', error.message);
510
+ continue;
511
+ }
512
+ ;
513
+ if (error instanceof ScannerOwnerUnavailable)
514
+ throw error;
167
515
  this.store.settle(job.id, 'failed', 'queued-source-or-schedule-unavailable');
168
516
  }
169
517
  }
@@ -211,12 +559,27 @@ export class UsageLearningRuntime {
211
559
  if (!page.hasMore)
212
560
  break;
213
561
  }
562
+ if (this.config.usageLearning.workItemFailures)
563
+ this.scanWorkItems(owner, scope);
214
564
  for (const job of this.store.pending(this.lane))
215
565
  if (job.state === 'queued') {
566
+ if (isWorkItemUsageIntent(job.intent)) {
567
+ try {
568
+ this.schedule(job);
569
+ }
570
+ catch (error) {
571
+ if (error instanceof WorkItemSourceObsolete)
572
+ this.store.settle(job.id, 'superseded', error.message);
573
+ }
574
+ ;
575
+ continue;
576
+ }
216
577
  try {
217
578
  this.schedule(job);
218
579
  }
219
- catch {
580
+ catch (error) {
581
+ if (error instanceof ScannerOwnerUnavailable)
582
+ throw error;
220
583
  this.store.settle(job.id, 'failed', 'schedule-rejected');
221
584
  }
222
585
  }
@@ -246,7 +609,12 @@ export class UsageLearningRuntime {
246
609
  }
247
610
  schedule(input) {
248
611
  let job = input;
249
- this.source(job);
612
+ if (isWorkItemUsageIntent(job.intent)) {
613
+ this.assertWorkItemReadiness(job.intent.source.task.owner);
614
+ this.withWorkItemSource(job.intent, () => undefined);
615
+ }
616
+ else
617
+ this.source(job);
250
618
  const definition = this.definition(job);
251
619
  if (job.definitionHash === null) {
252
620
  this.ports.automations.reconcileSystem({ owner: OWNER, automationId: job.id,
@@ -264,6 +632,10 @@ export class UsageLearningRuntime {
264
632
  this.store.settle(job.id, 'failed', 'native-run-ended-before-claim');
265
633
  return;
266
634
  }
635
+ if (isWorkItemUsageIntent(job.intent)) {
636
+ this.assertWorkItemReadiness(job.intent.source.task.owner);
637
+ this.withWorkItemSource(job.intent, () => undefined);
638
+ }
267
639
  this.ports.automations.reconcileSystem({ owner: OWNER, automationId: job.id,
268
640
  idempotencyKey: `${job.id}:activate`, desiredStatus: 'active', definition });
269
641
  }
@@ -290,9 +662,13 @@ export class UsageLearningRuntime {
290
662
  const queued = this.store.get(input.automationId);
291
663
  if (!queued || queued.state !== 'queued' || queued.digest !== input.activationNonce || queued.definitionHash !== input.definitionHash)
292
664
  throw new Error('usage review is not queued');
665
+ if (isWorkItemUsageIntent(queued.intent))
666
+ return await this.executeWorkItem(input, queued, result);
293
667
  this.source(queued);
294
668
  claimed = this.store.claim(queued.id, input.definitionHash, input.occurrenceId);
295
669
  const job = claimed;
670
+ if (isWorkItemUsageIntent(job.intent))
671
+ throw new Error('foreground usage claim required');
296
672
  const signal = AbortSignal.any([input.signal, this.abort.signal,
297
673
  AbortSignal.timeout(Math.max(1, Math.min(this.config.maxDurationMs, job.intent.expiresAt - Date.now())))]);
298
674
  const assertCurrent = () => {