@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.
- package/README.md +44 -8
- package/lib/config.d.ts +5 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +19 -2
- package/lib/config.js.map +1 -1
- package/lib/growth-agent.d.ts +34 -9
- package/lib/growth-agent.d.ts.map +1 -1
- package/lib/growth-agent.js +275 -34
- package/lib/growth-agent.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +65 -11
- package/lib/index.js.map +1 -1
- package/lib/scanner-owner-runtime.d.ts +34 -0
- package/lib/scanner-owner-runtime.d.ts.map +1 -0
- package/lib/scanner-owner-runtime.js +99 -0
- package/lib/scanner-owner-runtime.js.map +1 -0
- package/lib/source-port.d.ts +24 -2
- package/lib/source-port.d.ts.map +1 -1
- package/lib/source-port.js.map +1 -1
- package/lib/usage-runtime.d.ts +35 -4
- package/lib/usage-runtime.d.ts.map +1 -1
- package/lib/usage-runtime.js +391 -15
- package/lib/usage-runtime.js.map +1 -1
- package/lib/usage-store.d.ts +51 -4
- package/lib/usage-store.d.ts.map +1 -1
- package/lib/usage-store.js +172 -6
- package/lib/usage-store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-source.d.ts +51 -0
- package/lib/work-item-source.d.ts.map +1 -0
- package/lib/work-item-source.js +105 -0
- package/lib/work-item-source.js.map +1 -0
- package/package.json +23 -16
package/lib/usage-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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.
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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 = () => {
|