@dsh-enhanced/assistant-work-items 0.0.0-stage → 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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +685 -2
  3. package/cordis.patch.yml +11 -0
  4. package/lib/acceptance-binding.d.ts +86 -0
  5. package/lib/acceptance-binding.d.ts.map +1 -0
  6. package/lib/acceptance-binding.js +250 -0
  7. package/lib/acceptance-binding.js.map +1 -0
  8. package/lib/admission.d.ts +95 -0
  9. package/lib/admission.d.ts.map +1 -0
  10. package/lib/admission.js +642 -0
  11. package/lib/admission.js.map +1 -0
  12. package/lib/authority.d.ts +92 -0
  13. package/lib/authority.d.ts.map +1 -0
  14. package/lib/authority.js +205 -0
  15. package/lib/authority.js.map +1 -0
  16. package/lib/config.d.ts +365 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +672 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/continuation-contract.d.ts +126 -0
  21. package/lib/continuation-contract.d.ts.map +1 -0
  22. package/lib/continuation-contract.js +109 -0
  23. package/lib/continuation-contract.js.map +1 -0
  24. package/lib/continuation-record.d.ts +32 -0
  25. package/lib/continuation-record.d.ts.map +1 -0
  26. package/lib/continuation-record.js +57 -0
  27. package/lib/continuation-record.js.map +1 -0
  28. package/lib/continuation.d.ts +89 -0
  29. package/lib/continuation.d.ts.map +1 -0
  30. package/lib/continuation.js +607 -0
  31. package/lib/continuation.js.map +1 -0
  32. package/lib/diagnosis-contract.d.ts +95 -0
  33. package/lib/diagnosis-contract.d.ts.map +1 -0
  34. package/lib/diagnosis-contract.js +167 -0
  35. package/lib/diagnosis-contract.js.map +1 -0
  36. package/lib/diagnosis.d.ts +70 -0
  37. package/lib/diagnosis.d.ts.map +1 -0
  38. package/lib/diagnosis.js +466 -0
  39. package/lib/diagnosis.js.map +1 -0
  40. package/lib/dispatch.d.ts +100 -0
  41. package/lib/dispatch.d.ts.map +1 -0
  42. package/lib/dispatch.js +317 -0
  43. package/lib/dispatch.js.map +1 -0
  44. package/lib/engineering-context.d.ts +86 -0
  45. package/lib/engineering-context.d.ts.map +1 -0
  46. package/lib/engineering-context.js +263 -0
  47. package/lib/engineering-context.js.map +1 -0
  48. package/lib/evaluation.d.ts +125 -0
  49. package/lib/evaluation.d.ts.map +1 -0
  50. package/lib/evaluation.js +295 -0
  51. package/lib/evaluation.js.map +1 -0
  52. package/lib/event-follow.d.ts +93 -0
  53. package/lib/event-follow.d.ts.map +1 -0
  54. package/lib/event-follow.js +150 -0
  55. package/lib/event-follow.js.map +1 -0
  56. package/lib/feedback-targets.d.ts +91 -0
  57. package/lib/feedback-targets.d.ts.map +1 -0
  58. package/lib/feedback-targets.js +362 -0
  59. package/lib/feedback-targets.js.map +1 -0
  60. package/lib/index.d.ts +27 -0
  61. package/lib/index.d.ts.map +1 -0
  62. package/lib/index.js +25 -0
  63. package/lib/index.js.map +1 -0
  64. package/lib/intake-admission.d.ts +49 -0
  65. package/lib/intake-admission.d.ts.map +1 -0
  66. package/lib/intake-admission.js +100 -0
  67. package/lib/intake-admission.js.map +1 -0
  68. package/lib/intake-evaluation-runtime.d.ts +74 -0
  69. package/lib/intake-evaluation-runtime.d.ts.map +1 -0
  70. package/lib/intake-evaluation-runtime.js +343 -0
  71. package/lib/intake-evaluation-runtime.js.map +1 -0
  72. package/lib/intake-evaluation.d.ts +180 -0
  73. package/lib/intake-evaluation.d.ts.map +1 -0
  74. package/lib/intake-evaluation.js +440 -0
  75. package/lib/intake-evaluation.js.map +1 -0
  76. package/lib/intake.d.ts +118 -0
  77. package/lib/intake.d.ts.map +1 -0
  78. package/lib/intake.js +137 -0
  79. package/lib/intake.js.map +1 -0
  80. package/lib/ledger.d.ts +86 -0
  81. package/lib/ledger.d.ts.map +1 -0
  82. package/lib/ledger.js +145 -0
  83. package/lib/ledger.js.map +1 -0
  84. package/lib/mandate.d.ts +45 -0
  85. package/lib/mandate.d.ts.map +1 -0
  86. package/lib/mandate.js +168 -0
  87. package/lib/mandate.js.map +1 -0
  88. package/lib/outcome.d.ts +137 -0
  89. package/lib/outcome.d.ts.map +1 -0
  90. package/lib/outcome.js +303 -0
  91. package/lib/outcome.js.map +1 -0
  92. package/lib/owner-access.d.ts +35 -0
  93. package/lib/owner-access.d.ts.map +1 -0
  94. package/lib/owner-access.js +152 -0
  95. package/lib/owner-access.js.map +1 -0
  96. package/lib/owner-continuation.d.ts +27 -0
  97. package/lib/owner-continuation.d.ts.map +1 -0
  98. package/lib/owner-continuation.js +131 -0
  99. package/lib/owner-continuation.js.map +1 -0
  100. package/lib/owner-recall.d.ts +85 -0
  101. package/lib/owner-recall.d.ts.map +1 -0
  102. package/lib/owner-recall.js +134 -0
  103. package/lib/owner-recall.js.map +1 -0
  104. package/lib/owner-replies.d.ts +44 -0
  105. package/lib/owner-replies.d.ts.map +1 -0
  106. package/lib/owner-replies.js +232 -0
  107. package/lib/owner-replies.js.map +1 -0
  108. package/lib/peers.d.ts +622 -0
  109. package/lib/peers.d.ts.map +1 -0
  110. package/lib/peers.js +10 -0
  111. package/lib/peers.js.map +1 -0
  112. package/lib/policy-notice.d.ts +31 -0
  113. package/lib/policy-notice.d.ts.map +1 -0
  114. package/lib/policy-notice.js +137 -0
  115. package/lib/policy-notice.js.map +1 -0
  116. package/lib/reply-contract.d.ts +67 -0
  117. package/lib/reply-contract.d.ts.map +1 -0
  118. package/lib/reply-contract.js +58 -0
  119. package/lib/reply-contract.js.map +1 -0
  120. package/lib/service.d.ts +625 -0
  121. package/lib/service.d.ts.map +1 -0
  122. package/lib/service.js +2388 -0
  123. package/lib/service.js.map +1 -0
  124. package/lib/settlement.d.ts +74 -0
  125. package/lib/settlement.d.ts.map +1 -0
  126. package/lib/settlement.js +233 -0
  127. package/lib/settlement.js.map +1 -0
  128. package/lib/signals.d.ts +176 -0
  129. package/lib/signals.d.ts.map +1 -0
  130. package/lib/signals.js +497 -0
  131. package/lib/signals.js.map +1 -0
  132. package/lib/store.d.ts +789 -0
  133. package/lib/store.d.ts.map +1 -0
  134. package/lib/store.js +3266 -0
  135. package/lib/store.js.map +1 -0
  136. package/lib/tools.d.ts +7 -0
  137. package/lib/tools.d.ts.map +1 -0
  138. package/lib/tools.js +443 -0
  139. package/lib/tools.js.map +1 -0
  140. package/lib/types.d.ts +831 -0
  141. package/lib/types.d.ts.map +1 -0
  142. package/lib/types.js +29 -0
  143. package/lib/types.js.map +1 -0
  144. package/lib/version.d.ts +2 -0
  145. package/lib/version.d.ts.map +1 -0
  146. package/lib/version.js +2 -0
  147. package/lib/version.js.map +1 -0
  148. package/lib/workspace.d.ts +43 -0
  149. package/lib/workspace.d.ts.map +1 -0
  150. package/lib/workspace.js +137 -0
  151. package/lib/workspace.js.map +1 -0
  152. package/package.json +101 -3
package/lib/service.js ADDED
@@ -0,0 +1,2388 @@
1
+ import { WorkItemEngineeringTargets } from './engineering-context.js';
2
+ import { Service } from '@deepseek-ai/cordis';
3
+ import { classifyIntake, intakeSourcePolicy } from './intake.js';
4
+ import { authenticatesIntakeText, WorkItemIntakeEvaluationRuntime } from './intake-evaluation-runtime.js';
5
+ import { WorkItemIntakeAdmissionBridge } from './intake-admission.js';
6
+ import { WorkItemAdmission } from './admission.js';
7
+ import { storedBasisHolds, WorkItemAuthority } from './authority.js';
8
+ import { ConfigSchema, isActive, normalizeConfig, text } from './config.js';
9
+ import { WorkItemDispatch } from './dispatch.js';
10
+ import { eventFollowReader, eventFollowRegistration } from './event-follow.js';
11
+ import { WorkItemEvaluationExporter } from './evaluation.js';
12
+ import { normalizeWorkItemReportSummary, WorkItemLedger } from './ledger.js';
13
+ import { settleOutcomes as runSettlement } from './settlement.js';
14
+ import { WorkItemOwnerRecall } from './owner-recall.js';
15
+ import { MALFORMED_ACCEPTANCE_BINDING, WorkItemStore } from './store.js';
16
+ import { normalizeFollowUp, normalizePolicyChange, noticePolicyChange } from './policy-notice.js';
17
+ import { resolveAcceptanceBinding, settleAcceptance, workerRunTaskRef } from './acceptance-binding.js';
18
+ import { WorkItemSignalIntake, } from './signals.js';
19
+ import { registerWorkItemTools, workItemReportKey } from './tools.js';
20
+ import { TERMINAL_STATES, WorkItemError } from './types.js';
21
+ import { WorkItemWorkspace } from './workspace.js';
22
+ import { WorkItemDiagnosis, persistedDiagnosisIntervention } from './diagnosis.js';
23
+ import { WorkItemContinuation } from './continuation.js';
24
+ import { WorkItemOwnerAccess } from './owner-access.js';
25
+ import { WorkItemOwnerContinuation } from './owner-continuation.js';
26
+ import { WorkItemOwnerReplies } from './owner-replies.js';
27
+ import { WorkItemOwnerFeedbackTargets } from './feedback-targets.js';
28
+ function fail(code, message, detail) {
29
+ throw new WorkItemError(code, message, detail);
30
+ }
31
+ /** Snapshot this shallow tool grammar without evaluating untrusted accessors. */
32
+ function workItemToolArguments(candidate, allowed) {
33
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)
34
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(candidate)))
35
+ fail('invalid-input', 'work-item tool arguments must be a plain object');
36
+ const names = Reflect.ownKeys(candidate);
37
+ if (names.length > allowed.length || names.some(key => typeof key !== 'string' || !allowed.includes(key)))
38
+ fail('invalid-input', 'work-item tool arguments contain an unknown field');
39
+ const args = Object.create(null);
40
+ for (const key of names) {
41
+ const field = Object.getOwnPropertyDescriptor(candidate, key);
42
+ if (field === undefined || !field.enumerable || !('value' in field) || field.value === undefined)
43
+ fail('invalid-input', 'work-item tool arguments must contain only enumerable data fields');
44
+ if (key !== 'artifacts') {
45
+ args[key] = field.value;
46
+ continue;
47
+ }
48
+ const paths = field.value;
49
+ if (!Array.isArray(paths) || Object.getPrototypeOf(paths) !== Array.prototype)
50
+ fail('invalid-input', 'artifacts must be a plain array');
51
+ const length = Object.getOwnPropertyDescriptor(paths, 'length')?.value;
52
+ if (!Number.isSafeInteger(length) || length < 0 || length > 20)
53
+ fail('invalid-input', 'at most 20 artifact paths are accepted');
54
+ const indices = Reflect.ownKeys(paths);
55
+ if (indices.length !== length + 1 || indices.some(index => typeof index !== 'string'
56
+ || (index !== 'length' && !/^(?:0|[1-9][0-9]*)$/u.test(index))))
57
+ fail('invalid-input', 'artifacts must not contain symbols, holes or extra fields');
58
+ const copied = [];
59
+ for (let index = 0; index < length; index += 1) {
60
+ const entry = Object.getOwnPropertyDescriptor(paths, String(index));
61
+ if (entry === undefined || !entry.enumerable || !('value' in entry) || typeof entry.value !== 'string')
62
+ fail('invalid-input', 'artifacts must contain only enumerable string data entries');
63
+ copied.push(entry.value);
64
+ }
65
+ args[key] = copied;
66
+ }
67
+ return args;
68
+ }
69
+ // Neither the runtime nor the plan-mint closure is published on a Cordis service receiver.
70
+ /** Rows `bindWorkerRun` may scan per wake, parked ones included. */
71
+ const WORKER_RUN_BIND_SCAN_LIMIT = 100;
72
+ /** Upper bound of in-memory parked (item, occurrence) pairs and warned malformed rows. */
73
+ const WORKER_RUN_BIND_SKIP_LIMIT = 1_000;
74
+ /** Consecutive `owner-route-drift` reads of one (item, occurrence) before a single warn. */
75
+ const WORKER_RUN_DRIFT_WARN_AFTER = 5;
76
+ /** Held reports released per scheduler wake; a full page advances the in-memory round-robin cursor. */
77
+ const HELD_REPORT_RELEASE_LIMIT = 10;
78
+ /**
79
+ * `bindWorkerRun` refusals that cannot change while the item keeps its
80
+ * occurrence: an expired, mismatched (ref, owner scope, digest), settled or
81
+ * non-independent contract, a changed or revoked owner route, a different
82
+ * frozen binding, a contract already bound elsewhere, or a malformed row.
83
+ * Everything else (no contract yet, missing or throwing peers) is transient.
84
+ */
85
+ const WORKER_RUN_BIND_TERMINAL = new Set([
86
+ 'mandate-bound', 'owner-changed', 'binding-mismatch', 'owner-route-binding-unknown',
87
+ 'acceptance-mismatch', 'acceptance-expired', 'acceptance-already-settled', 'acceptance-not-awaiting', 'acceptance-not-independent',
88
+ 'owner-route-invalid', 'owner-route-expired', 'identity-mismatch', 'malformed-binding',
89
+ ]);
90
+ const continuationRuntimes = new WeakMap();
91
+ const continuationAdmissions = new WeakMap();
92
+ const continuationDispatches = new WeakMap();
93
+ const replyConsumers = new WeakMap();
94
+ const engineeringReaders = new WeakMap();
95
+ const feedbackReaders = new WeakMap();
96
+ const diagnosisRuntimes = new WeakMap();
97
+ const diagnosisAdmissions = new WeakMap();
98
+ /**
99
+ * Work-item intake kernel.
100
+ *
101
+ * This service owns configuration, the private ledger and identity resolution;
102
+ * it deliberately owns no agent loop, timer, outbox, budget or session. Every
103
+ * side effect is a call into an existing peer plugin (`assistant-automations`
104
+ * for the worker run, `assistant-delivery` for the owner notification), and the
105
+ * disabled default opens no file at all.
106
+ */
107
+ export class AssistantWorkItemsService extends Service {
108
+ static Config = ConfigSchema;
109
+ // Cordis traces this Service through caller-context proxies. JavaScript #
110
+ // private brands reject those receivers; TS-private members preserve the
111
+ // proxy's ctx without binding methods back to the provider instance.
112
+ workItemsConfig;
113
+ workItemsEvaluation;
114
+ /** Host-registered followed-thread sources; empty means no follow can close. */
115
+ workItemsThreadSources = new Map();
116
+ workItemsFollowDegraded = null;
117
+ workItemsComponents;
118
+ workItemsClosed = false;
119
+ workItemsIntakeObserver;
120
+ /** Present only when `intakeLedger.evaluation` is enabled. */
121
+ workItemsIntakeEvaluation;
122
+ /** Present only when `intakeLedger.evaluation.admit` is enabled. */
123
+ workItemsIntakeAdmission;
124
+ workItemsDiagnosisKey = Object.freeze({});
125
+ workItemsContinuationKey = Object.freeze({});
126
+ workItemsReplyKey = Object.freeze({});
127
+ workItemsFeedbackKey = Object.freeze({});
128
+ workItemsDiagnosisError = null;
129
+ workItemsHasDiagnosisHistory = false;
130
+ workItemsSchedulerUnsubscribers = new Set();
131
+ workItemsSchedulerWaking = false;
132
+ workItemsSchedulerLastWakeAt = null;
133
+ workItemsSchedulerLastError = null;
134
+ /** In-memory round-robin position of `acceptance.autoSettle`; a restart rescans from the start. */
135
+ workItemsAutoSettleCursor = null;
136
+ /** In-memory round-robin position of `acceptance.bindWorkerRun`; a restart rescans from the start. */
137
+ workItemsBindWorkerRunCursor = null;
138
+ /** In-memory round-robin position of `acceptance.holdReport`; a restart rescans from the start. */
139
+ workItemsHeldReportCursor = null;
140
+ /** itemId → occurrenceId refused deterministically by `bindWorkerRun`; in memory, bounded, dropped on occurrence change. */
141
+ workItemsBindWorkerRunSkips = new Map();
142
+ /**
143
+ * itemId → consecutive `owner-route-drift` streak of `bindWorkerRun` for one
144
+ * occurrence; in memory, bounded. Any other outcome or a new occurrence
145
+ * drops it. Only for visibility: drift is re-read either way.
146
+ */
147
+ workItemsBindWorkerRunDrifts = new Map();
148
+ /** Whether the missing Delivery `inspectOwnerRouteBinding` was already warned about. */
149
+ workItemsOwnerRouteBindingWarned = false;
150
+ /** `itemId:row` malformed `acceptance-bound` rows already warned about. */
151
+ workItemsMalformedBindingWarned = new Set();
152
+ workItemsOwnerReads;
153
+ workItemsOwnerContinuations;
154
+ constructor(ctx, input) {
155
+ super(ctx, 'assistantWorkItems');
156
+ this.workItemsConfig = Object.freeze(normalizeConfig(input, Date.now()));
157
+ const ownerDependencies = { config: this.workItemsConfig,
158
+ store: () => this.workItemsClosed ? undefined : this.workItemsComponents?.store,
159
+ delivery: () => this.workItemsDelivery(), policy: () => this.workItemsPolicy() };
160
+ const access = new WorkItemOwnerAccess(ownerDependencies);
161
+ const intakeEvaluation = this.workItemsConfig.enabled ? this.workItemsConfig.intakeLedger?.evaluation : undefined;
162
+ this.workItemsIntakeEvaluation = intakeEvaluation === undefined ? undefined : new WorkItemIntakeEvaluationRuntime({
163
+ config: intakeEvaluation,
164
+ store: () => this.workItemsClosed ? undefined : this.workItemsComponents?.store,
165
+ log: message => ctx.logger.debug(message),
166
+ });
167
+ this.workItemsIntakeAdmission = intakeEvaluation?.admit === undefined ? undefined : new WorkItemIntakeAdmissionBridge({
168
+ config: intakeEvaluation.admit,
169
+ maxPerRun: intakeEvaluation.maxPerRun,
170
+ store: () => this.workItemsClosed ? undefined : this.workItemsComponents?.store,
171
+ // The public Host path, so an admitted item wakes its worker like any decision.
172
+ admit: decision => this.admitDecision(decision),
173
+ log: message => ctx.logger.debug(message),
174
+ });
175
+ this.workItemsOwnerReads = new WorkItemOwnerRecall({ ...ownerDependencies, access });
176
+ this.workItemsOwnerContinuations = new WorkItemOwnerContinuation({ ...ownerDependencies, access,
177
+ runtime: () => continuationRuntimes.get(this.workItemsContinuationKey),
178
+ admit: (input, continuation) => {
179
+ const components = this.workItemsRequired(), mint = continuationAdmissions.get(components);
180
+ if (!mint)
181
+ fail('disabled', 'owner continuation admission is unavailable');
182
+ return mint(input, continuation);
183
+ } });
184
+ // The Evaluation outbox is service state, not ledger state: it exists even
185
+ // for a disabled intake so `assistant-evaluation` can bind a generation,
186
+ // and it never opens SQLite until an export pass actually runs.
187
+ this.workItemsEvaluation = new WorkItemEvaluationExporter({
188
+ store: () => this.workItemsComponents?.store,
189
+ scope: () => {
190
+ const scope = this.workItemsConfig.scope;
191
+ return scope === null ? null : Object.freeze({ workspace: scope.workspace, preset: scope.preset });
192
+ },
193
+ enabled: () => !this.workItemsClosed && isActive(this.workItemsConfig) && this.workItemsConfig.settlement.enabled,
194
+ now: () => Date.now(),
195
+ warn: message => this.ctx.logger.warn(message),
196
+ });
197
+ ctx.effect(() => () => this.close(), 'assistant-work-items.store');
198
+ ctx.inject(['tools'], toolsCtx => {
199
+ registerWorkItemTools(toolsCtx, this);
200
+ if (this.workItemsConfig.ownerContinuation) {
201
+ // The pinned native guard runs after extensible approval listeners and
202
+ // cannot be overridden by a later allow. Cover every tool at native
203
+ // dispatch preparation; the exclusive report tool is a scheduling barrier.
204
+ toolsCtx.tools.guard(execution => {
205
+ const context = execution.agent?.ctx.get('assistantAutomationExecution');
206
+ if (context?.automationId !== this.workItemsConfig.ownerContinuation.automationId)
207
+ return undefined;
208
+ try {
209
+ const occurrenceId = this.workItemsOccurrenceId(execution.agent);
210
+ const runtime = continuationRuntimes.get(this.workItemsContinuationKey);
211
+ if (!runtime)
212
+ return 'continuation invocation is unavailable';
213
+ runtime.assertTool(occurrenceId, execution.name);
214
+ return undefined;
215
+ }
216
+ catch {
217
+ return 'continuation tool authority or execution phase changed';
218
+ }
219
+ });
220
+ }
221
+ });
222
+ // The disabled default must not create a directory, lock a file or open a
223
+ // database, so persistence starts only for an explicitly enabled intake.
224
+ if (this.workItemsConfig.enabled)
225
+ this.workItemsOpen();
226
+ if (this.workItemsConfig.enabled && this.workItemsConfig.intakeLedger) {
227
+ // WP20 slice 2: inbound adapters reach the ledger only through Delivery's
228
+ // metadata-only observation. Observe and active modes behave the same;
229
+ // the observer writes the intake ledger and nothing else.
230
+ ctx.inject(['assistantDelivery'], intakeCtx => {
231
+ if (this.workItemsClosed)
232
+ return;
233
+ const peer = intakeCtx.get('assistantDelivery');
234
+ if (typeof peer?.registerInboundIntakeObserver !== 'function') {
235
+ intakeCtx.logger.warn('assistant-work-items: Delivery inbound intake observation is unavailable; the intake ledger stays Host-only');
236
+ return;
237
+ }
238
+ const evaluation = this.workItemsIntakeEvaluation;
239
+ const observer = Object.freeze({ protocol: 1, observe: (signal) => {
240
+ if (this.workItemsClosed || this.workItemsIntakeObserver !== observer)
241
+ return;
242
+ try {
243
+ if (evaluation === undefined) {
244
+ this.recordIntake(signal);
245
+ return;
246
+ }
247
+ // The transient text never reaches the ledger: it is split off here and only hashed or held in memory.
248
+ const { sourceText, ...metadata } = signal;
249
+ this.recordIntake(metadata, typeof sourceText === 'string' ? sourceText : undefined);
250
+ }
251
+ catch (error) {
252
+ intakeCtx.logger.debug(`assistant-work-items: intake observation not recorded: ${describe(error)}`);
253
+ }
254
+ }, ...(evaluation === undefined ? {} : { acceptsSourceText: true }) });
255
+ this.workItemsIntakeObserver = observer;
256
+ let unregister;
257
+ try {
258
+ unregister = peer.registerInboundIntakeObserver(observer);
259
+ }
260
+ catch (error) {
261
+ this.workItemsIntakeObserver = undefined;
262
+ intakeCtx.logger.warn(`assistant-work-items: inbound intake observer unavailable: ${describe(error)}`);
263
+ return;
264
+ }
265
+ intakeCtx.effect(() => () => {
266
+ unregister();
267
+ if (this.workItemsIntakeObserver === observer)
268
+ this.workItemsIntakeObserver = undefined;
269
+ }, 'assistant-work-items.intake-observer');
270
+ });
271
+ }
272
+ if (this.workItemsConfig.enabled && this.workItemsConfig.follows?.eventTriggers === true) {
273
+ // Subscription pass-through: Event Triggers becomes a followed-thread
274
+ // source. Resolution is passive (settlement pass only); a re-provided
275
+ // Event Triggers service re-runs this callback with a fresh generation.
276
+ ctx.inject(['eventTriggers', 'assistantDelivery'], followCtx => {
277
+ if (this.workItemsClosed)
278
+ return;
279
+ const reader = eventFollowReader(followCtx.get('eventTriggers'));
280
+ const scope = this.workItemsConfig.scope;
281
+ if (reader === undefined || scope === null) {
282
+ followCtx.logger.warn('assistant-work-items: Event Triggers source reads are unavailable; event follows stay unregistered');
283
+ return;
284
+ }
285
+ let unregister;
286
+ try {
287
+ unregister = this.registerThreadSource(eventFollowRegistration({
288
+ reader, owner: () => this.workItemsEventFollowOwner(scope),
289
+ acceptHeadRef: this.workItemsConfig.follows?.eventTriggersAcceptHeadRef === true,
290
+ }));
291
+ }
292
+ catch (error) {
293
+ followCtx.logger.warn(`assistant-work-items: event follow source unavailable: ${describe(error)}`);
294
+ return;
295
+ }
296
+ followCtx.effect(() => unregister, 'assistant-work-items.event-follows');
297
+ });
298
+ }
299
+ // Evaluation reuses the native scheduler wake; without it nothing new subscribes.
300
+ if (isActive(this.workItemsConfig) || this.workItemsIntakeEvaluation !== undefined) {
301
+ ctx.inject(['assistantAutomations'], wakeCtx => {
302
+ if (this.workItemsClosed)
303
+ return;
304
+ const peer = wakeCtx.get('assistantAutomations');
305
+ if (typeof peer?.onSchedulerWake !== 'function') {
306
+ wakeCtx.logger.warn('assistant-work-items: native scheduler wake is unavailable; waiting resumes retain startup/claim recovery');
307
+ return;
308
+ }
309
+ // Resolve on the consumer Context so Automations owns this effect on
310
+ // the injecting Fiber, not on its own provider or a cached root proxy.
311
+ let active = true;
312
+ const unregister = peer.onSchedulerWake(() => {
313
+ if (!active)
314
+ return;
315
+ this.workItemsOnSchedulerWake();
316
+ if (!this.workItemsClosed)
317
+ void this.workItemsIntakeEvaluation?.run().then(() => this.runIntakeAdmission());
318
+ });
319
+ const remove = () => {
320
+ if (!active)
321
+ return;
322
+ active = false;
323
+ this.workItemsSchedulerUnsubscribers.delete(remove);
324
+ unregister();
325
+ };
326
+ if (this.workItemsClosed) {
327
+ remove();
328
+ return;
329
+ }
330
+ this.workItemsSchedulerUnsubscribers.add(remove);
331
+ wakeCtx.effect(() => remove, 'assistant-work-items.scheduler-wake');
332
+ });
333
+ }
334
+ if (isActive(this.workItemsConfig) && this.workItemsConfig.diagnosis) {
335
+ ctx.inject(['assistantAutomations', 'assistantDelivery', 'tools'], diagnosisCtx => {
336
+ if (this.workItemsClosed)
337
+ return;
338
+ const components = this.workItemsOpen();
339
+ const mint = diagnosisAdmissions.get(components);
340
+ if (!mint)
341
+ fail('storage', 'private diagnosis admission is unavailable');
342
+ const runtime = new WorkItemDiagnosis(diagnosisCtx, this.workItemsConfig, components.store, components.admission, components.dispatch, {
343
+ admit: mint,
344
+ assertOwner: item => {
345
+ const receipt = this.workItemsAssertOwnerRoute(item);
346
+ if (receipt.principalVersion !== item.owner.principalVersion || receipt.bindingVersion !== item.owner.bindingVersion
347
+ || receipt.generation !== item.owner.generation)
348
+ fail('owner-route-invalid', 'diagnosis owner generation changed');
349
+ return receipt;
350
+ },
351
+ assertMandate: item => {
352
+ const decision = components.authority.check(item);
353
+ if (decision.kind !== 'allow')
354
+ fail('authority-revoked', 'diagnosis admission mandate is unavailable');
355
+ },
356
+ });
357
+ diagnosisRuntimes.set(this.workItemsDiagnosisKey, runtime);
358
+ try {
359
+ runtime.start();
360
+ this.workItemsDiagnosisError = null;
361
+ }
362
+ catch (error) {
363
+ runtime.close();
364
+ if (diagnosisRuntimes.get(this.workItemsDiagnosisKey) === runtime)
365
+ diagnosisRuntimes.delete(this.workItemsDiagnosisKey);
366
+ this.workItemsDiagnosisError = 'diagnosis-activation-unavailable';
367
+ diagnosisCtx.logger.warn(`assistant-work-items: diagnosis activation unavailable: ${describe(error)}`);
368
+ }
369
+ diagnosisCtx.effect(() => () => {
370
+ runtime.close();
371
+ if (diagnosisRuntimes.get(this.workItemsDiagnosisKey) === runtime)
372
+ diagnosisRuntimes.delete(this.workItemsDiagnosisKey);
373
+ }, 'assistant-work-items.diagnosis');
374
+ });
375
+ }
376
+ if (isActive(this.workItemsConfig) && this.workItemsConfig.ownerContinuation) {
377
+ ctx.inject(['assistantAutomations', 'assistantDelivery', 'tools'], continuationCtx => {
378
+ if (this.workItemsClosed)
379
+ return;
380
+ const components = this.workItemsOpen(), dispatch = continuationDispatches.get(components);
381
+ if (!dispatch)
382
+ fail('storage', 'continuation dispatch is unavailable');
383
+ const runtime = new WorkItemContinuation(continuationCtx, this.workItemsConfig, components.store, {
384
+ assertOwner: item => {
385
+ const receipt = this.workItemsAssertOwnerRoute(item);
386
+ if (receipt.principalVersion !== item.owner.principalVersion || receipt.bindingVersion !== item.owner.bindingVersion
387
+ || receipt.generation !== item.owner.generation)
388
+ fail('owner-route-invalid', 'continuation owner generation changed');
389
+ return receipt;
390
+ },
391
+ assertMandate: item => {
392
+ if (components.authority.check(item).kind !== 'allow')
393
+ fail('authority-revoked', 'continuation admission mandate is unavailable');
394
+ },
395
+ wake: item => dispatch.wake(item),
396
+ isReplyConsumerCurrent: () => !this.workItemsClosed && replyConsumers.get(this.workItemsReplyKey)?.isCurrent() === true,
397
+ });
398
+ continuationRuntimes.set(this.workItemsContinuationKey, runtime);
399
+ try {
400
+ runtime.start();
401
+ }
402
+ catch (error) {
403
+ runtime.close();
404
+ if (continuationRuntimes.get(this.workItemsContinuationKey) === runtime)
405
+ continuationRuntimes.delete(this.workItemsContinuationKey);
406
+ continuationCtx.logger.warn(`assistant-work-items: continuation activation unavailable: ${describe(error)}`);
407
+ }
408
+ continuationCtx.effect(() => () => {
409
+ runtime.close();
410
+ if (continuationRuntimes.get(this.workItemsContinuationKey) === runtime)
411
+ continuationRuntimes.delete(this.workItemsContinuationKey);
412
+ }, 'assistant-work-items.continuation');
413
+ });
414
+ }
415
+ if (isActive(this.workItemsConfig) && (this.workItemsConfig.ownerReplyContinuation || this.workItemsConfig.ownerReportFeedback)) {
416
+ // Historical proof/report metadata survive an expired execution grant,
417
+ // a paused worker or an absent tools catalog. This owned read fiber is
418
+ // deliberately separate from continuation runtime.start().
419
+ ctx.inject(['assistantDelivery', 'assistantAutomations'], reportCtx => {
420
+ if (this.workItemsClosed)
421
+ return;
422
+ const components = this.workItemsOpen();
423
+ let reader;
424
+ let engineering;
425
+ if (this.workItemsConfig.ownerReportFeedback) {
426
+ const current = new WorkItemOwnerFeedbackTargets({ config: this.workItemsConfig,
427
+ store: () => this.workItemsClosed ? undefined : components.store,
428
+ delivery: () => reportCtx.get('assistantDelivery'),
429
+ automations: () => reportCtx.get('assistantAutomations'),
430
+ current: () => !this.workItemsClosed && feedbackReaders.get(this.workItemsFeedbackKey) === current });
431
+ reader = current;
432
+ feedbackReaders.set(this.workItemsFeedbackKey, current);
433
+ const source = new WorkItemEngineeringTargets({ config: this.workItemsConfig,
434
+ store: () => this.workItemsClosed ? undefined : components.store,
435
+ feedback: () => feedbackReaders.get(this.workItemsFeedbackKey),
436
+ current: () => !this.workItemsClosed && engineeringReaders.get(this.workItemsFeedbackKey) === source });
437
+ engineering = source;
438
+ engineeringReaders.set(this.workItemsFeedbackKey, source);
439
+ }
440
+ const replies = new WorkItemOwnerReplies({ config: this.workItemsConfig, store: components.store,
441
+ delivery: () => reportCtx.get('assistantDelivery'),
442
+ runtime: () => continuationRuntimes.get(this.workItemsContinuationKey),
443
+ admit: (input, continuation) => {
444
+ const mint = continuationAdmissions.get(components);
445
+ if (!mint || this.workItemsClosed)
446
+ fail('disabled', 'reply continuation admission unavailable');
447
+ return mint(input, continuation);
448
+ }, notification: (item, report) => components.dispatch.notification(item, report),
449
+ ...(reader ? { inspectFeedbackReport: input => reader?.inspectReport(input, (item, report) => components.dispatch.notification(item, report)) } : {}),
450
+ retire: ids => {
451
+ for (const id of ids) {
452
+ const item = components.store.findItemById(id);
453
+ if (item && components.store.getContinuation(id))
454
+ components.store.withdrawByDedupKey(item.dedupKey);
455
+ }
456
+ continuationRuntimes.get(this.workItemsContinuationKey)?.retire(ids);
457
+ } });
458
+ replyConsumers.set(this.workItemsReplyKey, replies);
459
+ try {
460
+ replies.start();
461
+ }
462
+ catch (error) {
463
+ replies.close();
464
+ reportCtx.logger.warn(`assistant-work-items: report consumer unavailable: ${describe(error)}`);
465
+ }
466
+ reportCtx.effect(() => () => {
467
+ engineering?.close();
468
+ if (engineeringReaders.get(this.workItemsFeedbackKey) === engineering)
469
+ engineeringReaders.delete(this.workItemsFeedbackKey);
470
+ reader?.close();
471
+ if (feedbackReaders.get(this.workItemsFeedbackKey) === reader)
472
+ feedbackReaders.delete(this.workItemsFeedbackKey);
473
+ replies.close();
474
+ if (replyConsumers.get(this.workItemsReplyKey) === replies)
475
+ replyConsumers.delete(this.workItemsReplyKey);
476
+ }, 'assistant-work-items.owner-report-targets');
477
+ });
478
+ }
479
+ }
480
+ /** One native wake may admit at most ten already-due waiting occurrences. */
481
+ workItemsOnSchedulerWake() {
482
+ if (this.workItemsClosed || !isActive(this.workItemsConfig) || this.workItemsSchedulerWaking)
483
+ return;
484
+ const components = this.workItemsComponents;
485
+ if (components === undefined)
486
+ return;
487
+ this.workItemsSchedulerWaking = true;
488
+ this.workItemsSchedulerLastWakeAt = Date.now();
489
+ this.workItemsSchedulerLastError = null;
490
+ try {
491
+ for (const selected of components.store.listDueWaitingResumes(10)) {
492
+ if (this.workItemsClosed)
493
+ break;
494
+ // Advance before examining authority, including on denied items. The
495
+ // next tick can reach later rows; pending rows remain available on wrap.
496
+ components.store.advanceWaitingResumeCursor(selected);
497
+ try {
498
+ const dispatch = components.store.getDispatch(selected.itemId);
499
+ const item = components.store.findItemById(selected.itemId);
500
+ const scope = this.workItemsConfig.scope;
501
+ if (dispatch === undefined || dispatch.state !== 'pending' || dispatch.reason !== 'waiting-resume'
502
+ || dispatch.eventId !== selected.eventId || dispatch.generation !== selected.generation
503
+ || dispatch.occurredAt > Date.now() || item === undefined || item.state !== 'waiting'
504
+ || item.lastReportStatus !== 'waiting' || item.observed || item.leaseUntil !== null
505
+ || components.store.activeClaim(item.id) !== undefined || item.attempt >= item.maxAttempts
506
+ || item.authority === null || scope === null || scope.expiresAt <= Date.now()
507
+ || item.scope.workspace !== scope.workspace || item.scope.preset !== scope.preset
508
+ || item.scope.principalId !== scope.principalId || item.scope.ownerRouteId !== scope.ownerRouteId)
509
+ continue;
510
+ const owner = this.workItemsAssertOwnerRoute(item);
511
+ if (owner.principalVersion !== item.owner.principalVersion
512
+ || owner.bindingVersion !== item.owner.bindingVersion || owner.generation !== item.owner.generation)
513
+ continue;
514
+ // Reuse the existing mandate check, stable event identity, ingestion
515
+ // Policy and unknown-outcome handling. This does not run an Agent.
516
+ components.dispatch.wake(item);
517
+ }
518
+ catch (error) {
519
+ this.workItemsSchedulerLastError = error instanceof WorkItemError ? error.code : 'scheduler-wake-failed';
520
+ this.ctx.logger.warn(`assistant-work-items: waiting resume wake failed: ${describe(error)}`);
521
+ }
522
+ }
523
+ if (this.workItemsConfig.diagnosis)
524
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.sweep(false);
525
+ replyConsumers.get(this.workItemsReplyKey)?.drain();
526
+ continuationRuntimes.get(this.workItemsContinuationKey)?.wakePending();
527
+ }
528
+ catch (error) {
529
+ this.workItemsSchedulerLastError = error instanceof WorkItemError ? error.code : 'scheduler-wake-failed';
530
+ this.ctx.logger.warn(`assistant-work-items: waiting resume scan failed: ${describe(error)}`);
531
+ }
532
+ finally {
533
+ // Bind first so a run that already finished can settle on this same wake.
534
+ try {
535
+ this.workItemsBindWorkerRuns(components);
536
+ }
537
+ catch (error) {
538
+ this.ctx.logger.warn(`assistant-work-items: acceptance worker-run bind scan failed: ${describe(error)}`);
539
+ }
540
+ try {
541
+ this.workItemsAutoSettle(components);
542
+ }
543
+ catch (error) {
544
+ this.ctx.logger.warn(`assistant-work-items: acceptance auto-settle scan failed: ${describe(error)}`);
545
+ }
546
+ // Release only after auto-settle, so a verdict that arrived on this wake
547
+ // decides the same pass whether the held report is sent or blocked.
548
+ try {
549
+ this.workItemsReleaseHeldReports(components);
550
+ }
551
+ catch (error) {
552
+ this.ctx.logger.warn(`assistant-work-items: acceptance held-report scan failed: ${describe(error)}`);
553
+ }
554
+ this.workItemsSchedulerWaking = false;
555
+ }
556
+ }
557
+ /**
558
+ * WP21 slice 6: on the same native wake, bind at most `maxPerTick`
559
+ * dispatched items to the acceptance contract their own worker run froze.
560
+ * Automations only names the handle for the exact occurrence; the contract
561
+ * itself is re-read from the verifier and checked against the live owner
562
+ * route. Items whose mandate already binds a contract are never touched.
563
+ * Round-robin like auto-settle. A deterministic refusal parks the
564
+ * (item, occurrence) pair in memory so it is not re-read until the item's
565
+ * occurrence changes; transient ones (no contract yet, peer missing or
566
+ * throwing, Delivery failing other than `missing-binding`) are simply
567
+ * tried again on a later wake. Parked pairs do not consume the per-wake budget.
568
+ */
569
+ workItemsBindWorkerRuns(components) {
570
+ const bind = this.workItemsConfig.acceptanceBindWorkerRun;
571
+ const worker = this.workItemsConfig.workerAutomationId;
572
+ const automations = this.workItemsAutomations();
573
+ if (bind === undefined || worker === null || this.workItemsClosed || this.workItemsVerifier() === undefined
574
+ || typeof automations?.inspectOccurrenceAcceptance !== 'function')
575
+ return;
576
+ const limit = bind.maxPerTick;
577
+ const skips = this.workItemsBindWorkerRunSkips;
578
+ const seen = new Set();
579
+ let processed = 0, scanned = 0, wrapped = this.workItemsBindWorkerRunCursor === null, done = false;
580
+ while (!done && processed < limit && scanned < WORKER_RUN_BIND_SCAN_LIMIT && !this.workItemsClosed) {
581
+ const page = components.store.listLateAcceptanceCandidates(worker, this.workItemsBindWorkerRunCursor, 10);
582
+ if (page.length === 0) {
583
+ if (wrapped)
584
+ break;
585
+ wrapped = true;
586
+ this.workItemsBindWorkerRunCursor = null;
587
+ continue;
588
+ }
589
+ for (const candidate of page) {
590
+ if (processed >= limit || scanned >= WORKER_RUN_BIND_SCAN_LIMIT || this.workItemsClosed) {
591
+ done = true;
592
+ break;
593
+ }
594
+ const { item, occurrenceId } = candidate;
595
+ if (seen.has(item.id)) {
596
+ done = true;
597
+ break;
598
+ }
599
+ seen.add(item.id);
600
+ scanned += 1;
601
+ this.workItemsBindWorkerRunCursor = Object.freeze({ createdAt: item.createdAt, id: item.id });
602
+ const parked = skips.get(item.id);
603
+ if (parked === occurrenceId)
604
+ continue;
605
+ if (parked !== undefined)
606
+ skips.delete(item.id);
607
+ processed += 1;
608
+ let outcome;
609
+ try {
610
+ outcome = this.workItemsBindWorkerRun(components, automations, candidate);
611
+ }
612
+ catch (error) {
613
+ if (this.workItemsWarnMalformedLateBinding(item.id, error))
614
+ outcome = 'malformed-binding';
615
+ else {
616
+ outcome = error instanceof WorkItemError ? error.code : 'error';
617
+ this.ctx.logger.debug(`assistant-work-items: acceptance worker-run bind for ${item.id} not recorded: ${describe(error)}`);
618
+ }
619
+ }
620
+ this.workItemsTrackBindWorkerRunDrift(item.id, occurrenceId, outcome);
621
+ if (WORKER_RUN_BIND_TERMINAL.has(outcome)) {
622
+ // Bounded: the oldest parked pair is forgotten first and simply re-read once.
623
+ if (skips.size >= WORKER_RUN_BIND_SKIP_LIMIT)
624
+ skips.delete(skips.keys().next().value);
625
+ skips.set(item.id, occurrenceId);
626
+ }
627
+ if (outcome !== 'bound' && outcome !== 'no-contract') {
628
+ this.ctx.logger.debug(`assistant-work-items: acceptance worker-run bind for ${item.id}: ${outcome}`);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ /**
634
+ * Count consecutive `owner-route-drift` outcomes of one (item, occurrence)
635
+ * and warn once when the streak reaches `WORKER_RUN_DRIFT_WARN_AFTER`: a
636
+ * route that keeps moving between the two Delivery reads would otherwise be
637
+ * re-read silently forever. Any other outcome, including a successful bind,
638
+ * or a new occurrence restarts the streak. Never changes the re-read.
639
+ */
640
+ workItemsTrackBindWorkerRunDrift(itemId, occurrenceId, outcome) {
641
+ const drifts = this.workItemsBindWorkerRunDrifts;
642
+ if (outcome !== 'owner-route-drift') {
643
+ drifts.delete(itemId);
644
+ return;
645
+ }
646
+ const previous = drifts.get(itemId);
647
+ const count = previous?.occurrenceId === occurrenceId ? previous.count + 1 : 1;
648
+ if (previous === undefined && drifts.size >= WORKER_RUN_BIND_SKIP_LIMIT)
649
+ drifts.delete(drifts.keys().next().value);
650
+ drifts.set(itemId, { occurrenceId, count });
651
+ if (count === WORKER_RUN_DRIFT_WARN_AFTER) {
652
+ this.ctx.logger.warn(`assistant-work-items: work item ${itemId} occurrence ${occurrenceId} saw owner-route-drift on ${String(count)} consecutive bindWorkerRun reads; still re-reading, the owner route keeps moving between validateOwnerRoute and inspectOwnerRouteBinding`);
653
+ }
654
+ }
655
+ /**
656
+ * Warn once per (item, row) about a malformed latest `acceptance-bound`
657
+ * row. The item stays fail-closed (never bound or settled); later reads of
658
+ * the same row stay silent. Returns whether `error` was that condition.
659
+ */
660
+ workItemsWarnMalformedLateBinding(itemId, error) {
661
+ if (!(error instanceof WorkItemError) || error.code !== 'storage' || error.message !== MALFORMED_ACCEPTANCE_BINDING)
662
+ return false;
663
+ const key = `${itemId}:${error.detail ?? ''}`;
664
+ if (this.workItemsMalformedBindingWarned.has(key))
665
+ return true;
666
+ if (this.workItemsMalformedBindingWarned.size >= WORKER_RUN_BIND_SKIP_LIMIT)
667
+ this.workItemsMalformedBindingWarned.clear();
668
+ this.workItemsMalformedBindingWarned.add(key);
669
+ this.ctx.logger.warn(`assistant-work-items: work item ${itemId} has a malformed ${error.detail ?? 'acceptance-bound row'}; its acceptance stays unbound and unsettled`);
670
+ return true;
671
+ }
672
+ workItemsBindWorkerRun(components, automations, candidate) {
673
+ const { item, automationId, occurrenceId, notBefore } = candidate;
674
+ if (item.authority?.mandate?.acceptance !== undefined)
675
+ return 'mandate-bound';
676
+ if (typeof automations.inspectOccurrenceAcceptance !== 'function')
677
+ return 'automations-unsupported';
678
+ const handle = automations.inspectOccurrenceAcceptance({ automationId, occurrenceId, workspace: item.scope.workspace, agentPreset: item.scope.preset });
679
+ if (handle === undefined)
680
+ return 'no-contract';
681
+ // The contract owner must be this item's owner on the live route; any
682
+ // drift since admission leaves the item unbound. Delivery's
683
+ // `missing-binding` (`owner-route-invalid`) is deterministic for this
684
+ // item: a revoked binding never becomes active again and every re-link or
685
+ // `/new` gets a strictly higher generation, so the generation frozen at
686
+ // admission cannot come back. Other Delivery failures
687
+ // (`owner-route-unavailable`) stay transient and are re-read.
688
+ const owner = this.workItemsAssertOwnerRoute(item);
689
+ const route = this.workItemsOwnerRouteBindingId(item, owner);
690
+ if (owner.principalVersion !== item.owner.principalVersion
691
+ || owner.bindingVersion !== item.owner.bindingVersion || owner.generation !== item.owner.generation)
692
+ return 'owner-changed';
693
+ // A Delivery without the method, or a route that moved between the two
694
+ // reads, is re-read on a later wake instead of parking the pair; only a
695
+ // malformed answer is final for this occurrence.
696
+ if (route.kind !== 'bound') {
697
+ return route.kind === 'unsupported' ? 'owner-route-binding-unsupported'
698
+ : route.kind === 'drift' ? 'owner-route-drift' : 'owner-route-binding-unknown';
699
+ }
700
+ // The run must have been frozen under exactly this owner route's binding.
701
+ const bindingId = route.bindingId;
702
+ if (typeof handle.bindingId !== 'string' || handle.bindingId !== bindingId
703
+ || handle.bindingVersion !== owner.bindingVersion || handle.bindingGeneration !== owner.generation)
704
+ return 'binding-mismatch';
705
+ const binding = resolveAcceptanceBinding(this.workItemsVerifier(), handle, owner, Date.now(), { taskRef: workerRunTaskRef(occurrenceId), notBefore });
706
+ if (typeof binding === 'string')
707
+ return binding;
708
+ return components.store.recordLateAcceptanceBinding({ itemId: item.id, occurrenceId, binding }).created ? 'bound' : 'already-bound';
709
+ }
710
+ /**
711
+ * The exact Delivery binding id behind `owner`, read through Delivery's
712
+ * Host-only `inspectOwnerRouteBinding`. Its embedded receipt must equal
713
+ * `owner` field for field, so the id belongs to the very route state just
714
+ * validated. Fails closed without a binding id when Delivery lacks the
715
+ * method (`unsupported`), when the embedded receipt differs from `owner`
716
+ * (`drift`: the route moved between the two reads) or when the answer is
717
+ * malformed; Delivery errors propagate like `validateOwnerRoute`'s.
718
+ */
719
+ workItemsOwnerRouteBindingId(item, owner) {
720
+ const delivery = this.workItemsDelivery();
721
+ if (typeof delivery?.inspectOwnerRouteBinding !== 'function') {
722
+ if (!this.workItemsOwnerRouteBindingWarned) {
723
+ this.workItemsOwnerRouteBindingWarned = true;
724
+ this.ctx.logger.warn('assistant-work-items: acceptance.bindWorkerRun needs assistant-delivery with inspectOwnerRouteBinding; worker runs stay unbound');
725
+ }
726
+ return { kind: 'unsupported' };
727
+ }
728
+ // Warn again if a later Delivery drops the method once more.
729
+ this.workItemsOwnerRouteBindingWarned = false;
730
+ const scope = item.scope;
731
+ let read;
732
+ try {
733
+ read = delivery.inspectOwnerRouteBinding({ authorityId: scope.ownerRouteId, principalId: scope.principalId,
734
+ workspace: scope.workspace, agentPreset: scope.preset });
735
+ }
736
+ catch (error) {
737
+ const missing = error !== null && typeof error === 'object' && 'code' in error && error.code === 'missing-binding';
738
+ fail(missing ? 'owner-route-invalid' : 'owner-route-unavailable', 'owner route binding read failed', error instanceof Error ? error.message : String(error));
739
+ }
740
+ const receipt = read?.receipt;
741
+ const keys = ['receiptVersion', 'authorityId', 'authorityHash', 'principalId',
742
+ 'principalRecordId', 'principalVersion', 'workspace', 'agentPreset', 'bindingVersion', 'generation'];
743
+ if (read === null || typeof read !== 'object' || read.protocol !== 'assistant-delivery/owner-route-binding/v1'
744
+ || receipt === null || typeof receipt !== 'object'
745
+ || typeof read.bindingId !== 'string' || read.bindingId === '' || read.bindingId.length > 256)
746
+ return { kind: 'malformed' };
747
+ // Only a well-formed receipt can drift; a missing or retyped field is a
748
+ // Delivery defect and must not be re-read forever.
749
+ if (!keys.every(key => typeof receipt[key] === typeof owner[key]))
750
+ return { kind: 'malformed' };
751
+ if (!keys.every(key => receipt[key] === owner[key]))
752
+ return { kind: 'drift' };
753
+ return { kind: 'bound', bindingId: read.bindingId };
754
+ }
755
+ /**
756
+ * WP21 slice 5: on the same native wake, read back at most `maxPerTick`
757
+ * executed, acceptance-bound items without a verdict through the Host-only
758
+ * `settleAcceptance`. Round-robin over an in-memory cursor so items still
759
+ * pending at the verifier cannot starve later ones. Anything short of a
760
+ * validated receipt writes nothing and is simply read again on a later wake.
761
+ */
762
+ workItemsAutoSettle(components) {
763
+ const autoSettle = this.workItemsConfig.acceptanceAutoSettle;
764
+ // A missing verifier peer cannot settle anything; skip the scan entirely.
765
+ if (autoSettle === undefined || this.workItemsClosed || this.workItemsVerifier() === undefined)
766
+ return;
767
+ const limit = autoSettle.maxPerTick;
768
+ const batch = [...components.store.listUnsettledAcceptanceItems(this.workItemsAutoSettleCursor, limit)];
769
+ if (batch.length < limit && this.workItemsAutoSettleCursor !== null) {
770
+ const seen = new Set(batch.map(item => item.id));
771
+ for (const item of components.store.listUnsettledAcceptanceItems(null, limit - batch.length))
772
+ if (!seen.has(item.id))
773
+ batch.push(item);
774
+ }
775
+ for (const item of batch) {
776
+ if (this.workItemsClosed)
777
+ break;
778
+ this.workItemsAutoSettleCursor = Object.freeze({ createdAt: item.createdAt, id: item.id });
779
+ try {
780
+ const result = this.settleAcceptance(item.id);
781
+ if (result.outcome !== 'verified' && result.outcome !== 'pending') {
782
+ this.ctx.logger.debug(`assistant-work-items: acceptance auto-settle for ${item.id}: ${result.outcome}`);
783
+ }
784
+ }
785
+ catch (error) {
786
+ if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
787
+ this.ctx.logger.debug(`assistant-work-items: acceptance auto-settle for ${item.id} not recorded: ${describe(error)}`);
788
+ }
789
+ }
790
+ }
791
+ }
792
+ /**
793
+ * WP21 slice 8 (`acceptance.holdReport`): the mandate binding, or else the
794
+ * latest late binding while it still belongs to the item's current
795
+ * occurrence. A malformed late row throws `storage`; callers decide whether
796
+ * to hold (fail closed) or treat the item as unbound.
797
+ */
798
+ workItemsAcceptanceBinding(components, item) {
799
+ const mandate = item.authority?.mandate?.acceptance;
800
+ if (mandate !== undefined)
801
+ return mandate;
802
+ const late = components.store.lateAcceptanceBinding(item.id);
803
+ return late !== undefined && late.occurrenceId === item.occurrenceId ? late.binding : undefined;
804
+ }
805
+ /**
806
+ * Whether this report must be held instead of dispatched: `holdReport` is
807
+ * on, it is the terminal `ready` self-report of an executed item, the status
808
+ * is notifiable, and the item already has an independent acceptance binding.
809
+ * An item that has not been bound yet (for example a run that reports before
810
+ * the wake that binds it) is reported normally, exactly as today.
811
+ *
812
+ * A verdict recorded for an earlier generation can never settle this report
813
+ * (the store keeps at most one verdict per item and excludes settled items
814
+ * from auto-settle), so holding it would just wait out `holdTimeoutMs` before
815
+ * releasing it unverified. It is dispatched now, exactly like flag-off.
816
+ * An unreadable verdict row fails closed and holds, like a malformed binding.
817
+ */
818
+ workItemsHoldsReport(components, item, report) {
819
+ if (this.workItemsConfig.acceptanceHoldReport === undefined || this.workItemsClosed
820
+ || report.status !== 'ready' || item.state !== 'ready' || item.observed)
821
+ return false;
822
+ if (!components.dispatch.shouldNotify(report))
823
+ return false;
824
+ if (this.workItemsAcceptanceBinding(components, item) === undefined)
825
+ return false;
826
+ let verdict;
827
+ try {
828
+ verdict = components.store.acceptanceVerdict(item.id);
829
+ }
830
+ catch {
831
+ return true;
832
+ }
833
+ return verdict === undefined || (verdict.attempt === item.attempt && verdict.occurrenceId === item.occurrenceId);
834
+ }
835
+ /**
836
+ * Whether this `ready` report is still held. Used to imply the verdict gate
837
+ * for held items even when `acceptance.verdictGate` itself is off: the gate
838
+ * must move the item before the held report can be replaced by the blocked
839
+ * one. A malformed binding row fails closed (treated as bound).
840
+ */
841
+ workItemsReportIsHeld(components, item) {
842
+ if (this.workItemsConfig.acceptanceHoldReport === undefined || item.state !== 'ready' || item.observed)
843
+ return false;
844
+ const report = components.store.lastReport(item.id);
845
+ if (report === undefined || report.status !== 'ready' || report.deliveryState !== null)
846
+ return false;
847
+ try {
848
+ return this.workItemsAcceptanceBinding(components, item) !== undefined;
849
+ }
850
+ catch {
851
+ return true;
852
+ }
853
+ }
854
+ /**
855
+ * WP21 slice 8: on the same native wake, release at most
856
+ * `HELD_REPORT_RELEASE_LIMIT` held reports through an in-memory
857
+ * `(recordedAt, itemId)` round-robin cursor. A full page advances the cursor
858
+ * past rows that are still waiting, so they cannot starve a releasable row
859
+ * behind them; a short page resets it and the next wake restarts from the
860
+ * oldest row. Achievement sends the ordinary report; any other settled
861
+ * verdict (or a verdict-gate `blocked` item) sends a blocked report instead;
862
+ * a still-pending item stays held, and one whose `holdTimeoutMs` elapsed is
863
+ * sent marked unverified. A row with no acceptance binding any more was never
864
+ * really held and is recovered as an ordinary report instead of stranded.
865
+ * One unreadable row (a malformed binding or verdict event) is warned about
866
+ * and skipped without aborting the remaining releases; the fail-closed hold
867
+ * itself is unchanged.
868
+ */
869
+ workItemsReleaseHeldReports(components) {
870
+ const hold = this.workItemsConfig.acceptanceHoldReport;
871
+ if (hold === undefined || this.workItemsClosed)
872
+ return;
873
+ const now = Date.now();
874
+ const page = components.store.listHeldAcceptanceReports(HELD_REPORT_RELEASE_LIMIT, this.workItemsHeldReportCursor ?? undefined);
875
+ // Only a full page proves there may be rows this wake did not examine; a
876
+ // short page ends the rotation so the next wake starts from the oldest.
877
+ if (page.length < HELD_REPORT_RELEASE_LIMIT)
878
+ this.workItemsHeldReportCursor = null;
879
+ else {
880
+ const last = page[page.length - 1];
881
+ this.workItemsHeldReportCursor = Object.freeze({ recordedAt: last.report.recordedAt, itemId: last.item.id });
882
+ }
883
+ for (const { item, report } of page) {
884
+ if (this.workItemsClosed)
885
+ break;
886
+ let release;
887
+ try {
888
+ let bound;
889
+ try {
890
+ bound = this.workItemsAcceptanceBinding(components, item) !== undefined;
891
+ }
892
+ catch (error) {
893
+ if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
894
+ this.ctx.logger.debug(`assistant-work-items: held report of ${item.id} cannot read its acceptance binding: ${describe(error)}`);
895
+ }
896
+ bound = true;
897
+ }
898
+ // No binding any more means the row was never actually held (the report
899
+ // committed before its pending delivery write, Delivery was missing,
900
+ // `notifyOn` excluded `ready`, or a re-arm lost the binding). Recovery
901
+ // sends the ordinary report instead of leaving it undelivered forever.
902
+ release = bound
903
+ ? this.workItemsHeldRelease(components, item, report, now, hold.holdTimeoutMs)
904
+ : Object.freeze({ kind: 'plain' });
905
+ }
906
+ catch (error) {
907
+ // One poison row (a malformed binding or verdict event) must not abort
908
+ // the scan: warn and keep releasing the remaining rows.
909
+ if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
910
+ this.ctx.logger.warn(`assistant-work-items: held report of ${item.id} cannot be decided: ${describe(error)}`);
911
+ }
912
+ continue;
913
+ }
914
+ if (release === undefined)
915
+ continue;
916
+ // A status the operator explicitly excluded from `notifyOn` cannot be
917
+ // sent; record the terminal no-send so every later wake stops rescanning it.
918
+ if (!this.workItemsConfig.notifyOn.includes(release.kind === 'blocked' ? 'blocked' : 'ready')) {
919
+ components.ledger.recordDelivery(item.id, report.revision, 'failed', null);
920
+ continue;
921
+ }
922
+ void components.dispatch.notifyHeld(item, report, release).catch(error => {
923
+ this.ctx.logger.warn(`assistant-work-items: held report release failed for ${item.id}: ${describe(error)}`);
924
+ });
925
+ }
926
+ }
927
+ /** Decide one held report without writing: release it, or wait for a later wake. */
928
+ workItemsHeldRelease(components, item, report, now, timeoutMs) {
929
+ if (item.state === 'blocked') {
930
+ // The store query only returns acceptance-gated blocked rows.
931
+ return { kind: 'blocked', reason: item.stateReason ?? 'acceptance-blocked' };
932
+ }
933
+ const verdict = components.store.acceptanceVerdict(item.id);
934
+ // Only the verdict of this executed generation decides this report; an
935
+ // older generation's verdict cannot be moved by the gate either, so the
936
+ // held report waits for this run's verdict or the timeout.
937
+ if (verdict !== undefined && verdict.attempt === item.attempt && verdict.occurrenceId === item.occurrenceId) {
938
+ return verdict.objectiveStatus === 'achieved'
939
+ ? { kind: 'achieved' }
940
+ : { kind: 'blocked', reason: `acceptance-${verdict.objectiveStatus}` };
941
+ }
942
+ // A released report is never held again; the owning revision records the send.
943
+ return now >= report.recordedAt + timeoutMs ? { kind: 'unverified', timeoutMs } : undefined;
944
+ }
945
+ get enabled() {
946
+ return this.workItemsConfig.enabled;
947
+ }
948
+ get mode() {
949
+ return this.workItemsConfig.mode;
950
+ }
951
+ get diagnosisEnabled() { return this.workItemsConfig.diagnosis !== undefined; }
952
+ get ownerContinuationEnabled() { return this.workItemsConfig.ownerContinuation !== undefined; }
953
+ /** Delivery's ownership fence for the single inbound intake observer. */
954
+ ownsInboundIntakeObserver = (observer) => !this.workItemsClosed && this.workItemsIntakeObserver !== undefined && observer === this.workItemsIntakeObserver;
955
+ ownsWorkItemReplyRegistration = (registration) => !this.workItemsClosed && replyConsumers.get(this.workItemsReplyKey)?.owns(registration) === true;
956
+ issueOwnerWorkItemFeedbackTarget = (input) => feedbackReaders.get(this.workItemsFeedbackKey)?.issue(input);
957
+ resolveOwnerWorkItemFeedbackTarget = (capability) => feedbackReaders.get(this.workItemsFeedbackKey)?.resolve(capability);
958
+ withOwnerWorkItemFeedbackTargetsFence = (capabilities, callback) => {
959
+ const reader = feedbackReaders.get(this.workItemsFeedbackKey);
960
+ if (this.workItemsClosed || !reader)
961
+ fail('missing-dependency', 'work-item feedback source fence is unavailable');
962
+ return reader.withTargetsFence(capabilities, callback);
963
+ };
964
+ issueOwnerWorkItemEngineeringTarget = (feedbackCapability) => engineeringReaders.get(this.workItemsFeedbackKey)?.issue(feedbackCapability);
965
+ resolveOwnerWorkItemEngineeringTarget = (capability) => engineeringReaders.get(this.workItemsFeedbackKey)?.resolve(capability);
966
+ withOwnerWorkItemEngineeringTargetsFence = (capabilities, callback) => {
967
+ const reader = engineeringReaders.get(this.workItemsFeedbackKey);
968
+ if (this.workItemsClosed || !reader)
969
+ fail('missing-dependency', 'work-item engineering source fence is unavailable');
970
+ return reader.withTargetsFence(capabilities, callback);
971
+ };
972
+ get ownerRecallEnabled() { return this.workItemsConfig.ownerRecall?.enabled === true; }
973
+ /** Foreground-only historical reads, independent of occurrence/worker authority. */
974
+ listForOwner = (execution, input) => this.workItemsOwnerReads.list(execution, input);
975
+ recallForOwner = (execution, input) => this.workItemsOwnerReads.recall(execution, input);
976
+ continueForOwner = (execution, input) => this.workItemsOwnerContinuations.continue(execution, input);
977
+ /** Bounded admission check; no work-item recovery, writes, dispatch or model. */
978
+ preauthorizeTool(execution) {
979
+ if (execution.name === 'work_item_continue')
980
+ return this.workItemsOwnerContinuations.preauthorize(execution);
981
+ if (execution.name === 'work_item_list' || execution.name === 'work_item_recall')
982
+ return this.workItemsOwnerReads.preauthorize(execution);
983
+ try {
984
+ if (execution.signal.aborted)
985
+ return false;
986
+ this.workItemsCheckToolExecution(execution, true);
987
+ return true;
988
+ }
989
+ catch {
990
+ return false;
991
+ }
992
+ }
993
+ /** Approval cannot extend the live occurrence/owner mandate at execution. */
994
+ assertToolExecution(execution) {
995
+ this.workItemsCheckToolExecution(execution, false);
996
+ }
997
+ workItemsCheckToolExecution(execution, requireModernMandate) {
998
+ const config = this.workItemsConfig;
999
+ if (execution.signal?.aborted)
1000
+ fail('invalid-state', 'the work-item tool execution was cancelled');
1001
+ if (this.workItemsClosed || !isActive(config))
1002
+ fail('disabled', 'active work-item intake is required');
1003
+ const components = this.workItemsComponents;
1004
+ if (components === undefined)
1005
+ fail('storage', 'the work-item ledger is unavailable');
1006
+ const occurrenceId = this.workItemsOccurrenceId(execution.agent);
1007
+ const item = components.store.findItemByOccurrence(occurrenceId);
1008
+ if (item === undefined)
1009
+ fail('not-found', 'no work item is bound to this automation occurrence');
1010
+ const scope = config.scope;
1011
+ const header = execution.agent?.session?.header;
1012
+ if (scope === null || header?.cwd !== scope.workspace || header.agentPreset !== scope.preset
1013
+ || item.scope.workspace !== scope.workspace || item.scope.preset !== scope.preset
1014
+ || item.scope.principalId !== scope.principalId || item.scope.ownerRouteId !== scope.ownerRouteId
1015
+ || item.owner.workspace !== scope.workspace || item.owner.agentPreset !== scope.preset) {
1016
+ fail('identity-mismatch', 'work-item tool scope does not match the current worker and admitted owner');
1017
+ }
1018
+ if (scope.expiresAt <= Date.now())
1019
+ fail('owner-route-expired', 'the configured work-item owner scope expired');
1020
+ const owner = this.workItemsAssertOwnerRoute(item);
1021
+ if (owner.principalVersion !== item.owner.principalVersion
1022
+ || owner.bindingVersion !== item.owner.bindingVersion || owner.generation !== item.owner.generation) {
1023
+ fail('owner-route-invalid', 'owner route generation changed since admission');
1024
+ }
1025
+ if (this.workItemsPolicy() === undefined)
1026
+ fail('missing-dependency', 'assistantPolicy is required for work-item tools');
1027
+ const authority = components.authority.check(item);
1028
+ if (authority.kind === 'revoked')
1029
+ fail('authority-revoked', 'admission mandate no longer permits this work-item tool');
1030
+ if (authority.kind === 'unavailable')
1031
+ fail('authority-unavailable', 'admission mandate could not be revalidated', authority.detail);
1032
+ if (requireModernMandate && authority.kind !== 'allow')
1033
+ fail('authority-unavailable', 'legacy work items require the ordinary approval path');
1034
+ this.workItemsAssertRuntime(components, occurrenceId, execution.name);
1035
+ const active = components.store.activeClaim(item.id);
1036
+ if (active !== undefined && active.runId !== occurrenceId && active.leasedUntil > Date.now())
1037
+ fail('claim-conflict', 'another occurrence owns this work item');
1038
+ const allowed = execution.name === 'work_item_report' ? ['status', 'summary', 'artifacts']
1039
+ : execution.name === 'work_item_read_report' ? ['revision', 'offset', 'max_bytes']
1040
+ : execution.name === 'work_item_read_context' ? ['kind', 'revision', 'offset', 'max_bytes']
1041
+ : execution.name === 'work_item_read_source' ? ['offset', 'max_bytes']
1042
+ : execution.name === 'work_item_claim' || execution.name === 'work_item_status' ? [] : undefined;
1043
+ if (allowed === undefined)
1044
+ fail('invalid-input', 'unknown work-item tool');
1045
+ const args = workItemToolArguments(execution.arguments, allowed);
1046
+ if (execution.name === 'work_item_read_context') {
1047
+ if (!['input', 'parent-brief', 'parent-report', 'parent-outcome'].includes(String(args.kind ?? 'input')))
1048
+ fail('invalid-input', 'invalid continuation context kind');
1049
+ this.readContext(execution.agent, { kind: (args.kind ?? 'input'),
1050
+ ...(args.revision === undefined ? {} : { revision: args.revision }),
1051
+ ...(args.offset === undefined ? {} : { offset: args.offset }),
1052
+ ...(args.max_bytes === undefined ? {} : { maxBytes: args.max_bytes }) });
1053
+ return;
1054
+ }
1055
+ if (execution.name === 'work_item_status')
1056
+ return;
1057
+ if (execution.name === 'work_item_read_source') {
1058
+ if (!config.diagnosis)
1059
+ fail('disabled', 'source recall requires diagnosis mode');
1060
+ const offset = args.offset ?? 0, maximum = args.max_bytes ?? 4096;
1061
+ if (!Number.isSafeInteger(offset) || Number(offset) < 0 || !Number.isSafeInteger(maximum)
1062
+ || Number(maximum) < 4 || Number(maximum) > 8192)
1063
+ fail('invalid-input', 'source byte window is invalid');
1064
+ return;
1065
+ }
1066
+ if (execution.name === 'work_item_read_report') {
1067
+ this.workItemsReadReportPage(components, item, {
1068
+ revision: args.revision,
1069
+ ...(args.offset === undefined ? {} : { offset: args.offset }),
1070
+ ...(args.max_bytes === undefined ? {} : { maxBytes: args.max_bytes }),
1071
+ });
1072
+ return;
1073
+ }
1074
+ const now = Date.now();
1075
+ const claim = components.store.findClaimByRun(occurrenceId);
1076
+ if (execution.name === 'work_item_claim') {
1077
+ if (item.observed)
1078
+ fail('invalid-state', 'observed work items cannot be claimed');
1079
+ if (claim?.itemId === item.id && claim.state === 'active' && claim.leasedUntil > now
1080
+ && item.leaseRunId === occurrenceId && item.leaseUntil !== null && item.leaseUntil > now)
1081
+ return;
1082
+ if (item.attempt >= item.maxAttempts)
1083
+ fail('retry-exhausted', 'work item exhausted its attempt budget');
1084
+ const recoverable = ['claimed', 'working'].includes(item.state) && item.leaseUntil !== null
1085
+ && item.leaseUntil <= now && (active === undefined || active.leasedUntil <= now);
1086
+ if (!['queued', 'waiting'].includes(item.state) && !recoverable)
1087
+ fail('claim-conflict', 'work item is not claimable');
1088
+ return;
1089
+ }
1090
+ if (typeof args.status !== 'string' || !['progress', 'waiting', 'ready', 'blocked', 'failed'].includes(args.status)
1091
+ || typeof args.summary !== 'string' || args.summary.trim().length === 0)
1092
+ fail('invalid-input', 'work-item report requires a valid status and non-empty summary');
1093
+ if (args.status === 'waiting' && components.store.getContinuation(item.id))
1094
+ fail('invalid-input', 'continuation attempts cannot schedule waiting retries');
1095
+ const artifacts = args.artifacts ?? [];
1096
+ if (!Array.isArray(artifacts) || artifacts.some(path => typeof path !== 'string'))
1097
+ fail('invalid-input', 'work-item report artifacts must be string paths');
1098
+ const normalizedArtifacts = components.workspace.validateArtifacts(item, artifacts);
1099
+ if (claim === undefined || claim.itemId !== item.id)
1100
+ fail('claim-conflict', 'claim this item before reporting');
1101
+ const reportCount = components.store.reportCount(item.id);
1102
+ if (reportCount > 100)
1103
+ fail('storage', 'work item exceeds the bounded report history');
1104
+ const key = `work-item-report:${workItemReportKey(args.status, args.summary, artifacts)}\0${item.id}\0${String(claim.claimId)}`;
1105
+ const summary = normalizeWorkItemReportSummary(args.summary, config.limits.maxReportBytes);
1106
+ const replay = components.store.reportReplayState({ itemId: item.id, idempotencyKey: key,
1107
+ status: args.status, ...summary, artifactPaths: normalizedArtifacts });
1108
+ if (replay === 'match')
1109
+ return;
1110
+ if (replay === 'conflict')
1111
+ fail('claim-conflict', 'idempotency key already belongs to a different persisted report');
1112
+ if (claim.state !== 'active' || claim.leasedUntil <= now || item.leaseRunId !== occurrenceId
1113
+ || item.leaseUntil === null || item.leaseUntil <= now)
1114
+ fail('claim-expired', 'report requires the current active claim');
1115
+ if (!['claimed', 'working'].includes(item.state))
1116
+ fail('invalid-state', 'work item is not reportable');
1117
+ if (reportCount >= config.limits.maxReportsPerItem)
1118
+ fail('report-limit', 'work item reached maxReportsPerItem');
1119
+ }
1120
+ /**
1121
+ * Admit one external work signal.
1122
+ *
1123
+ * Host-side entry point for a future external source plugin. It performs no
1124
+ * dispatch work beyond the same bounded wake the ledger does on recovery, so
1125
+ * observe mode stays free of external effects.
1126
+ */
1127
+ admit(input) {
1128
+ const components = this.workItemsAvailable();
1129
+ if (this.workItemsConfig.diagnosis)
1130
+ fail('invalid-input', 'diagnosis worker accepts only its private builtin source');
1131
+ const result = components.admission.admit(input);
1132
+ if (result.created && isActive(this.workItemsConfig)) {
1133
+ try {
1134
+ components.dispatch.wake(result.item);
1135
+ }
1136
+ catch (error) {
1137
+ // The item is durable evidence; a failed wake is recorded on the
1138
+ // dispatch row and retried by the next bounded recovery pass.
1139
+ this.ctx.logger.warn(`assistant-work-items: worker wake failed: ${describe(error)}`);
1140
+ }
1141
+ const dispatch = components.store.getDispatch(result.item.id);
1142
+ if (dispatch !== undefined)
1143
+ return Object.freeze({ ...result, dispatch });
1144
+ }
1145
+ return result;
1146
+ }
1147
+ /**
1148
+ * Admit one execute decision under a frozen mandate (Host-only, never a tool).
1149
+ *
1150
+ * Same wake semantics as `admit(...)`. A replayed decision id returns its one
1151
+ * existing item; a refusal is a value, not an error, and writes nothing.
1152
+ */
1153
+ admitDecision(decision) {
1154
+ const components = this.workItemsAvailable();
1155
+ if (this.workItemsConfig.diagnosis)
1156
+ fail('invalid-input', 'diagnosis worker accepts only its private builtin source');
1157
+ const result = components.admission.admitDecision(decision);
1158
+ if (result.outcome !== 'admitted')
1159
+ return result;
1160
+ try {
1161
+ components.dispatch.wake(result.item);
1162
+ }
1163
+ catch (error) {
1164
+ this.ctx.logger.warn(`assistant-work-items: worker wake failed: ${describe(error)}`);
1165
+ }
1166
+ const dispatch = components.store.getDispatch(result.item.id);
1167
+ return dispatch === undefined ? result : Object.freeze({ ...result, dispatch });
1168
+ }
1169
+ /**
1170
+ * Host-only (WP21): the Policy peer now runs under `policyVersion`.
1171
+ *
1172
+ * Never cancels, parks or wakes work. Each live item whose frozen mandate
1173
+ * basis the current Policy no longer upholds gets one durable notice per
1174
+ * version; replaying the same version (for example after a restart) writes
1175
+ * nothing new. Uses Policy `evaluate`, so no audit row or budget is spent.
1176
+ */
1177
+ noticePolicyChange(change) {
1178
+ const normalized = normalizePolicyChange(change);
1179
+ const components = this.workItemsAvailable();
1180
+ return noticePolicyChange(components.store, components.authority, normalized);
1181
+ }
1182
+ /**
1183
+ * Host-only (WP21 slice 4): read a bound item's verdict back from the
1184
+ * independent verifier. Takes no verdict input; a worker report, the
1185
+ * decision caller or this call's caller can never assert completion. Writes
1186
+ * one append-only verdict event once the verifier's receipt validates
1187
+ * against the frozen contract; anything short of that writes nothing.
1188
+ */
1189
+ settleAcceptance(itemId) {
1190
+ const components = this.workItemsAvailable();
1191
+ const item = components.store.findItemById(itemId);
1192
+ if (item === undefined)
1193
+ fail('not-found', 'work item does not exist');
1194
+ let binding = item.authority?.mandate?.acceptance;
1195
+ if (binding === undefined) {
1196
+ // WP21 slice 6: the latest late binding, only for the current occurrence;
1197
+ // an earlier generation's unsettled binding never settles re-armed work.
1198
+ const late = components.store.lateAcceptanceBinding(item.id);
1199
+ if (late !== undefined && late.occurrenceId !== item.occurrenceId)
1200
+ fail('invalid-state', 'acceptance binding is for an earlier occurrence');
1201
+ binding = late?.binding;
1202
+ }
1203
+ if (binding === undefined)
1204
+ fail('invalid-state', 'work item has no independent acceptance binding');
1205
+ // The stored mandate must still be intact for this exact item; checked
1206
+ // locally so a pull-based settle never writes Policy audit.
1207
+ if (!storedBasisHolds(item))
1208
+ fail('invalid-state', 'work item authority basis is no longer usable');
1209
+ // Only work that actually ran and reached `ready` can be accepted: never
1210
+ // claimed, parked, blocked, failed or dropped items settle nothing.
1211
+ if (item.attempt < 1 || item.state !== 'ready')
1212
+ fail('invalid-state', `work item is ${item.state}, not executed`);
1213
+ // `holdReport` implies the gate for a held report even when
1214
+ // `acceptance.verdictGate` is off, so a non-achieved verdict can move the
1215
+ // item before the held report is replaced by the blocked one.
1216
+ const gate = this.workItemsConfig.acceptanceVerdictGate === true || this.workItemsReportIsHeld(components, item)
1217
+ ? Object.freeze({ attempt: item.attempt, occurrenceId: item.occurrenceId }) : undefined;
1218
+ const result = settleAcceptance(components.store, this.workItemsVerifier(), item, binding, gate);
1219
+ // WP21 slice 7: the verifier's needs-attention writes no verdict, but with
1220
+ // the gate on it still moves this executed generation to `blocked`.
1221
+ if (gate !== undefined && result.outcome === 'needs-attention'
1222
+ && components.store.blockForAcceptance(item.id, gate, 'acceptance-needs-attention', { contractId: binding.contractId, verifierReason: result.reason })) {
1223
+ return Object.freeze({ ...result, blockedReason: 'acceptance-needs-attention' });
1224
+ }
1225
+ return result;
1226
+ }
1227
+ /** Host-only: the verifier verdict recorded for one item, or null. */
1228
+ acceptanceVerdict(itemId) {
1229
+ const components = this.workItemsAvailable();
1230
+ if (components.store.findItemById(itemId) === undefined)
1231
+ fail('not-found', 'work item does not exist');
1232
+ return components.store.acceptanceVerdict(itemId) ?? null;
1233
+ }
1234
+ /** Host-only: every Policy-change notice recorded for one item. */
1235
+ policyNotices(itemId) {
1236
+ const components = this.workItemsAvailable();
1237
+ if (components.store.findItemById(itemId) === undefined)
1238
+ fail('not-found', 'work item does not exist');
1239
+ return components.store.policyNotices(itemId);
1240
+ }
1241
+ /**
1242
+ * Host-only (WP21): a human follow-up on an item's work thread is owned by
1243
+ * that item while it is live. An ended item takes nothing (`item-ended`).
1244
+ */
1245
+ ownFollowUp(input) {
1246
+ const normalized = normalizeFollowUp(input);
1247
+ return this.workItemsAvailable().store.ownFollowUp(normalized);
1248
+ }
1249
+ /** Host-only: the live item processed one of its owned follow-ups. */
1250
+ markFollowUpProcessed(itemId, messageId) {
1251
+ return this.workItemsAvailable().store.markFollowUpProcessed(itemId, messageId);
1252
+ }
1253
+ /** Host-only: follow-ups owned by one item. */
1254
+ followUps(itemId) {
1255
+ return this.workItemsAvailable().store.followUps(itemId);
1256
+ }
1257
+ /**
1258
+ * Host-only: release every unprocessed follow-up of an ended item back to
1259
+ * judgment and return the newly released ones. No message is sent; the
1260
+ * caller decides what judgment does with them.
1261
+ */
1262
+ releaseFollowUps(limit) {
1263
+ return this.workItemsAvailable().store.releaseEndedFollowUps(limit);
1264
+ }
1265
+ /** Host-only: released follow-ups in release order (restart-safe re-read). */
1266
+ listReleasedFollowUps(options = {}) {
1267
+ return this.workItemsAvailable().store.listReleasedFollowUps(options);
1268
+ }
1269
+ /**
1270
+ * Record one inbound item's deterministic intake decision (WP20 slice 1).
1271
+ *
1272
+ * Host-only and metadata-only: the input carries normalized booleans and an
1273
+ * optional content digest, never a body. It works in observe mode, writes
1274
+ * only the intake ledger, and never admits, wakes or delivers anything. A
1275
+ * replay of the same `(source, sourceEventId)` returns the original row.
1276
+ */
1277
+ recordIntake(input, sourceText) {
1278
+ const ledger = this.workItemsConfig.intakeLedger;
1279
+ if (!this.workItemsConfig.enabled || ledger === undefined)
1280
+ fail('disabled', 'the work-item intake ledger is disabled');
1281
+ const candidate = classifyIntake(input, intakeSourcePolicy(ledger.sources, String(input?.source)));
1282
+ // A Host caller gets the same bound and digest check Delivery applies before forwarding a text.
1283
+ if (sourceText !== undefined && !authenticatesIntakeText(sourceText, candidate.contentDigest)) {
1284
+ fail('invalid-input', 'intake source text must be at most 65,536 UTF-16 units and match the content digest');
1285
+ }
1286
+ const result = this.workItemsAvailable().store.recordIntake(candidate);
1287
+ // `sourceText` is the authenticated text the content digest covers; with
1288
+ // evaluation off it is ignored, with it on it is held in memory only.
1289
+ if (sourceText !== undefined)
1290
+ this.workItemsIntakeEvaluation?.observe(result.record, sourceText);
1291
+ return result;
1292
+ }
1293
+ /**
1294
+ * Host-only: register the injectable WP20 inferred-evaluation supplier
1295
+ * (Policy snapshot + model call). Requires `intakeLedger.evaluation`.
1296
+ */
1297
+ registerIntakeEvaluator(registration) {
1298
+ const evaluation = this.workItemsIntakeEvaluation;
1299
+ if (evaluation === undefined || this.workItemsClosed)
1300
+ fail('disabled', 'intake evaluation is disabled');
1301
+ return evaluation.register(registration);
1302
+ }
1303
+ /** Host-only drive of one bounded evaluation run; the scheduler wake calls the same path. */
1304
+ async runIntakeEvaluation() {
1305
+ if (this.workItemsIntakeEvaluation === undefined || this.workItemsClosed)
1306
+ return [];
1307
+ return this.workItemsIntakeEvaluation.run();
1308
+ }
1309
+ /**
1310
+ * Host-only drive of one bounded admission pass over persisted accepting
1311
+ * evaluations (WP21); the scheduler wake calls it after each evaluation run.
1312
+ * Requires `intakeLedger.evaluation.admit`; never throws.
1313
+ */
1314
+ runIntakeAdmission() {
1315
+ if (this.workItemsIntakeAdmission === undefined || this.workItemsClosed)
1316
+ return [];
1317
+ return this.workItemsIntakeAdmission.run();
1318
+ }
1319
+ /** Durable admission outcomes (`would-admit`, `admitted`, `replayed`, `refused`) for one intake row. */
1320
+ intakeAdmissions(source, sourceEventId) {
1321
+ if (this.workItemsConfig.intakeLedger === undefined || !this.workItemsConfig.enabled)
1322
+ fail('disabled', 'the work-item intake ledger is disabled');
1323
+ return this.workItemsAvailable().store.listIntakeAdmissions(source, sourceEventId);
1324
+ }
1325
+ /**
1326
+ * Host-only, read-only (WP22): the WP20 intake judgment that admitted one
1327
+ * work item, for Evaluation's intake/attention agreement metric. Undefined
1328
+ * when the item does not exist or was not admitted from intake
1329
+ * (observe-mode `would-admit` never creates an item). Throws `disabled`
1330
+ * when the intake ledger is off, so "cannot tell" is never read as "not
1331
+ * intake". Carries the item's owner scope and no source text.
1332
+ */
1333
+ inspectIntakeProvenance(itemId) {
1334
+ if (this.workItemsConfig.intakeLedger === undefined || !this.workItemsConfig.enabled)
1335
+ fail('disabled', 'the work-item intake ledger is disabled');
1336
+ if (typeof itemId !== 'string' || itemId === '' || itemId.length > 256)
1337
+ fail('invalid-input', 'itemId is invalid');
1338
+ const store = this.workItemsAvailable().store;
1339
+ const found = store.intakeProvenance(itemId);
1340
+ if (found === undefined)
1341
+ return undefined;
1342
+ const item = store.findItemById(itemId);
1343
+ if (item === undefined)
1344
+ fail('storage', 'intake admission names a missing work item');
1345
+ const { admission, evaluation } = found;
1346
+ return Object.freeze({
1347
+ protocol: 'work-items/intake-provenance/v1',
1348
+ itemId,
1349
+ scope: Object.freeze({ workspace: item.scope.workspace, preset: item.scope.preset }),
1350
+ source: admission.source,
1351
+ sourceEventId: admission.sourceEventId,
1352
+ ruleId: admission.ruleId,
1353
+ policyVersion: admission.policyVersion,
1354
+ mode: admission.mode,
1355
+ level: evaluation.level,
1356
+ wouldAccept: evaluation.wouldAccept,
1357
+ admittedAt: admission.recordedAt,
1358
+ });
1359
+ }
1360
+ /** Persisted evaluation results for one intake row; replay reads these, never re-evaluates. */
1361
+ intakeEvaluations(source, sourceEventId) {
1362
+ if (this.workItemsConfig.intakeLedger === undefined || !this.workItemsConfig.enabled)
1363
+ fail('disabled', 'the work-item intake ledger is disabled');
1364
+ return this.workItemsAvailable().store.listIntakeEvaluations(source, sourceEventId);
1365
+ }
1366
+ intakeEvaluationHealth() {
1367
+ return this.workItemsIntakeEvaluation?.health();
1368
+ }
1369
+ /** Explicit Host-only reduction of a canonical work signal; never a model tool or a plan mint. */
1370
+ withdraw(input) {
1371
+ const result = this.workItemsAvailable().admission.withdraw(input);
1372
+ if (this.workItemsConfig.diagnosis) {
1373
+ const components = this.workItemsOpen();
1374
+ for (const item of result.items)
1375
+ if (components.store.readDiagnosisPlan(item.id))
1376
+ components.store.clearDiagnosisIntervention(item.id);
1377
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.retire(result.items.map(item => item.id));
1378
+ }
1379
+ continuationRuntimes.get(this.workItemsContinuationKey)?.retire(result.items.map(item => item.id));
1380
+ return result;
1381
+ }
1382
+ /**
1383
+ * Claim the item bound to the calling automation occurrence.
1384
+ *
1385
+ * Identity comes exclusively from the frozen, production automation
1386
+ * execution context installed by `assistant-automations`; a foreground
1387
+ * session, a preview run and another automation all fail closed.
1388
+ *
1389
+ * An armed signal source is swept here (bounded by the persisted
1390
+ * `sweepIntervalMs`) so ordinary use keeps producing work without a timer or
1391
+ * developer orchestration. A sweep failure never fails the claim.
1392
+ */
1393
+ claim(agent) {
1394
+ const components = this.workItemsRequired();
1395
+ this.workItemsSweepQuietly(components);
1396
+ this.workItemsSettleQuietly(components);
1397
+ const occurrenceId = this.workItemsOccurrenceId(agent);
1398
+ this.workItemsAssertRuntime(components, occurrenceId, 'work_item_claim');
1399
+ if (!this.workItemsConfig.diagnosis)
1400
+ components.ledger.recover();
1401
+ // Lease-expiry and waiting resume are generic recovery work, so ordinary
1402
+ // worker traffic is the lazy sweep that re-dispatches them. This is
1403
+ // best-effort: a recovery failure never blocks the caller's own claim.
1404
+ this.workItemsWakeQuietly(components);
1405
+ const item = components.store.findItemByOccurrence(occurrenceId);
1406
+ if (item === undefined)
1407
+ fail('not-found', 'no work item is bound to this automation occurrence');
1408
+ this.workItemsAssertOwnerRoute(item);
1409
+ this.workItemsRevalidateAuthority(components, item);
1410
+ const runId = occurrenceId;
1411
+ const result = components.ledger.claim({ itemId: item.id, runId, occurrenceId });
1412
+ // A claim can settle a prior window and unblock recovery, so it is a
1413
+ // transaction boundary that may have produced a new exportable revision.
1414
+ this.workItemsDrainEvaluation();
1415
+ // The resume brief is derived only after the claim committed, from the same
1416
+ // durable ledger; a missing row here means storage broke, so fail loudly
1417
+ // instead of handing the model an invented empty context.
1418
+ const resume = components.store.workItemResumeContext(result.item.id);
1419
+ if (resume === undefined)
1420
+ fail('storage', 'work item disappeared before its resume brief was derived');
1421
+ return Object.freeze({
1422
+ itemId: result.item.id,
1423
+ runId: result.claim.runId,
1424
+ state: result.item.state,
1425
+ kind: result.item.kind,
1426
+ externalId: result.item.externalId,
1427
+ title: result.item.title,
1428
+ brief: result.item.brief,
1429
+ briefTruncated: result.item.briefTruncated,
1430
+ acceptanceRef: result.item.acceptanceRef,
1431
+ workDir: result.item.workDir,
1432
+ attempt: result.item.attempt,
1433
+ maxAttempts: result.item.maxAttempts,
1434
+ leaseUntil: result.item.leaseUntil,
1435
+ replayed: result.replayed,
1436
+ resume,
1437
+ });
1438
+ }
1439
+ /**
1440
+ * Append one report revision under the caller's active claim.
1441
+ *
1442
+ * Reporting is the only worker write path. A terminal status additionally
1443
+ * enqueues one owner notification through `assistant-delivery`; a delivery
1444
+ * failure is recorded against the report instead of erasing report evidence.
1445
+ */
1446
+ async report(agent, input) {
1447
+ const components = this.workItemsRequired();
1448
+ const occurrenceId = this.workItemsOccurrenceId(agent);
1449
+ this.workItemsAssertRuntime(components, occurrenceId, 'work_item_report');
1450
+ if (!this.workItemsConfig.diagnosis)
1451
+ components.ledger.recover();
1452
+ const item = components.store.findItemByOccurrence(occurrenceId);
1453
+ if (item === undefined)
1454
+ fail('not-found', 'no work item is bound to this automation occurrence');
1455
+ if (input.status === 'waiting' && components.store.getContinuation(item.id))
1456
+ fail('invalid-input', 'continuation attempts cannot schedule waiting retries');
1457
+ const result = components.ledger.report({
1458
+ itemId: item.id,
1459
+ runId: occurrenceId,
1460
+ status: input.status,
1461
+ summary: input.summary,
1462
+ artifactPaths: input.artifactPaths ?? [],
1463
+ idempotencyKey: input.idempotencyKey,
1464
+ });
1465
+ if (!result.replayed) {
1466
+ if (components.store.getContinuation(item.id))
1467
+ continuationRuntimes.get(this.workItemsContinuationKey)?.reported(occurrenceId, result.report.status, result.report.revision);
1468
+ else
1469
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.reported(occurrenceId, result.report.status, result.report.revision);
1470
+ }
1471
+ let notified = false;
1472
+ if (!result.replayed) {
1473
+ try {
1474
+ // A held report is simply not dispatched this pass: the same scheduler
1475
+ // wake that settles the binding releases it in `workItemsReleaseHeldReports`.
1476
+ let held = false;
1477
+ try {
1478
+ held = this.workItemsHoldsReport(components, result.item, result.report);
1479
+ }
1480
+ catch (error) {
1481
+ // A malformed late binding fails closed: keep holding; the timeout
1482
+ // still releases it unverified rather than dropping the report.
1483
+ if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
1484
+ this.ctx.logger.debug(`assistant-work-items: report hold for ${item.id} cannot read its acceptance binding: ${describe(error)}`);
1485
+ }
1486
+ held = this.workItemsConfig.acceptanceHoldReport !== undefined && result.report.status === 'ready';
1487
+ }
1488
+ notified = held ? false : await components.dispatch.notify(result.item, result.report);
1489
+ }
1490
+ catch (error) {
1491
+ // `notify` already persisted the failed/unknown delivery state. Report
1492
+ // evidence must survive a notification failure, so surface it in the
1493
+ // log and keep the appended revision.
1494
+ this.ctx.logger.warn(`assistant-work-items: owner notification failed: ${describe(error)}`);
1495
+ }
1496
+ }
1497
+ const stored = components.store.reports(item.id).find(report => report.revision === result.report.revision);
1498
+ // A terminal report appends the settlement outcome synchronously, so this
1499
+ // is the main ordinary-use boundary that feeds the trusted lane.
1500
+ this.workItemsDrainEvaluation();
1501
+ return Object.freeze({
1502
+ itemId: result.item.id,
1503
+ revision: result.report.revision,
1504
+ status: result.report.status,
1505
+ state: result.item.state,
1506
+ truncated: result.report.truncated,
1507
+ artifactPaths: result.report.artifactPaths,
1508
+ delivery: (stored ?? result.report).deliveryState ?? 'pending',
1509
+ replayed: result.replayed,
1510
+ notified,
1511
+ });
1512
+ }
1513
+ /**
1514
+ * Register one additional automatic source and return an idempotent disposer.
1515
+ *
1516
+ * This is the public extension point for an external signal producer (a
1517
+ * repository watcher, a chat bridge, a scheduler): the producer supplies a
1518
+ * read-only `WorkItemSignalSource`, gets the same bounded sweep, provenance,
1519
+ * dedupe, cooldown and settled-item fence as the built-in adapter, and can be
1520
+ * withdrawn by disposing the registration. Registration fails closed when the
1521
+ * intake is disabled or already disposed, and never admits retroactively.
1522
+ */
1523
+ registerSignalSource(registration) {
1524
+ if (this.workItemsConfig.diagnosis)
1525
+ fail('invalid-input', 'diagnosis has an exclusive private builtin source; extension registration is unavailable');
1526
+ const components = this.workItemsAvailable();
1527
+ const dispose = components.signals.register(registration);
1528
+ return () => {
1529
+ dispose();
1530
+ };
1531
+ }
1532
+ /**
1533
+ * Register the only resolver for one followed-thread source.
1534
+ *
1535
+ * Host-only extension point. Registration is one-per-source, validated
1536
+ * synchronously and revoked by the returned disposer. A follow recorded while
1537
+ * a source is registered stores that source's generation, so replacing the
1538
+ * source later degrades the follow to `unresolved` instead of trusting a
1539
+ * stale closure. This adds resolution capability only: it is not authority to
1540
+ * end an item, and a closure never claims an objective was achieved.
1541
+ */
1542
+ registerThreadSource(registration) {
1543
+ if (!this.workItemsConfig.enabled || this.workItemsClosed)
1544
+ fail('disabled', 'the work-item intake is disabled');
1545
+ if (registration === null || typeof registration !== 'object' || Array.isArray(registration)) {
1546
+ fail('invalid-input', 'thread source registration must be an object');
1547
+ }
1548
+ const allowed = ['source', 'generation', 'resolve', 'validate', 'canonicalize'];
1549
+ for (const key of Object.keys(registration)) {
1550
+ if (!allowed.includes(key))
1551
+ fail('invalid-input', `thread source registration contains unknown field ${key}`);
1552
+ }
1553
+ const source = text(registration.source, 'thread source', 64);
1554
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(source))
1555
+ fail('invalid-input', 'thread source has an invalid shape');
1556
+ text(registration.generation, 'thread source generation', 200);
1557
+ if (typeof registration.resolve !== 'function')
1558
+ fail('invalid-input', 'thread source must expose a resolve function');
1559
+ if (registration.validate !== undefined && typeof registration.validate !== 'function')
1560
+ fail('invalid-input', 'thread source validate must be a function');
1561
+ if (registration.canonicalize !== undefined && typeof registration.canonicalize !== 'function')
1562
+ fail('invalid-input', 'thread source canonicalize must be a function');
1563
+ if (this.workItemsThreadSources.has(source))
1564
+ fail('invalid-input', `thread source ${source} is already registered`);
1565
+ const installed = registration;
1566
+ this.workItemsThreadSources.set(source, installed);
1567
+ return () => {
1568
+ // Only the disposer of the live registration may revoke it, so a stale
1569
+ // disposer from a replaced source cannot delete the current one.
1570
+ if (this.workItemsThreadSources.get(source) === installed)
1571
+ this.workItemsThreadSources.delete(source);
1572
+ };
1573
+ }
1574
+ /**
1575
+ * Trusted Evaluation producer generation (fourth trusted producer).
1576
+ *
1577
+ * `assistant-evaluation` reads this before registering a sink; returning a
1578
+ * fresh value on a new service instance is what makes a stale capability
1579
+ * fail closed instead of appending to the wrong host.
1580
+ */
1581
+ trustedWorkItemProducerGeneration() {
1582
+ return this.workItemsEvaluation.generation;
1583
+ }
1584
+ /**
1585
+ * Install the Evaluation-owned sink this plugin exports settled outcomes to.
1586
+ *
1587
+ * The registration is opaque: Evaluation passes its own capability and this
1588
+ * plugin only validates the protocol, generation and ownership before using
1589
+ * it. Exactly one sink is accepted; the returned disposer withdraws it.
1590
+ */
1591
+ registerTrustedWorkItemEvaluationSink(registration) {
1592
+ if (this.workItemsClosed)
1593
+ fail('storage', 'the work-item intake is disposed');
1594
+ // The exporter itself runs one bounded catch-up pass the moment the sink is
1595
+ // installed, so the service boundary must not queue a redundant one.
1596
+ const dispose = this.workItemsEvaluation.registerTrustedWorkItemEvaluationSink(registration);
1597
+ return () => {
1598
+ dispose();
1599
+ };
1600
+ }
1601
+ /**
1602
+ * Sweep every armed automatic source once and report what it did.
1603
+ *
1604
+ * Explicit host entry point; `force` ignores the persisted interval. The
1605
+ * disabled default and a missing optional peer both return an empty report,
1606
+ * so a caller never has to special-case activation.
1607
+ */
1608
+ sweepSignals(force = true) {
1609
+ if (!this.workItemsConfig.enabled || this.workItemsClosed)
1610
+ return Object.freeze({ sources: [] });
1611
+ try {
1612
+ if (this.workItemsConfig.diagnosis)
1613
+ return diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.sweep(force) ?? Object.freeze({ sources: [] });
1614
+ return this.workItemsOpen().signals.sweep(force);
1615
+ }
1616
+ catch (error) {
1617
+ this.ctx.logger.warn(`assistant-work-items: signal sweep unavailable: ${describe(error)}`);
1618
+ return Object.freeze({ sources: [] });
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Run one bounded settlement pass over the pending outcome windows.
1623
+ *
1624
+ * Host-only by design: there is deliberately no model tool for this, so a
1625
+ * worker can never settle or launder its own outcome. Returns a report instead
1626
+ * of throwing, and a disabled intake returns `skipped: 'disabled'`.
1627
+ */
1628
+ settleOutcomes(force = true) {
1629
+ if (!this.workItemsConfig.enabled || this.workItemsClosed || !this.workItemsConfig.settlement.enabled) {
1630
+ return Object.freeze({
1631
+ skipped: 'disabled',
1632
+ degraded: null,
1633
+ failed: false,
1634
+ scanned: 0,
1635
+ settled: 0,
1636
+ expired: 0,
1637
+ deferred: 0,
1638
+ closedByThread: 0,
1639
+ followDegraded: null,
1640
+ observedItemIds: Object.freeze([]),
1641
+ });
1642
+ }
1643
+ try {
1644
+ const result = this.workItemsSettlement(this.workItemsOpen(), force);
1645
+ this.workItemsDrainEvaluation();
1646
+ return result.report;
1647
+ }
1648
+ catch (error) {
1649
+ this.ctx.logger.warn(`assistant-work-items: settlement unavailable: ${describe(error)}`);
1650
+ // Only a genuinely absent dependency is reported as such; any other
1651
+ // failure is an undifferentiated degraded pass, never a silent success.
1652
+ const degraded = error instanceof WorkItemError && error.code === 'missing-dependency'
1653
+ ? 'missing-dependency'
1654
+ : null;
1655
+ return Object.freeze({
1656
+ skipped: null,
1657
+ degraded,
1658
+ failed: true,
1659
+ scanned: 0,
1660
+ settled: 0,
1661
+ expired: 0,
1662
+ deferred: 0,
1663
+ closedByThread: 0,
1664
+ followDegraded: null,
1665
+ observedItemIds: Object.freeze([]),
1666
+ });
1667
+ }
1668
+ }
1669
+ /**
1670
+ * Read the latest outcome revision per item, newest first.
1671
+ *
1672
+ * Host-only projection for status/health consumers; the disabled default
1673
+ * returns an empty list rather than opening the ledger.
1674
+ */
1675
+ outcomes(options = {}) {
1676
+ const limit = options.limit ?? 20;
1677
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
1678
+ fail('invalid-input', 'limit must be an integer between 1 and 100');
1679
+ if (!this.workItemsConfig.enabled || this.workItemsClosed)
1680
+ return Object.freeze([]);
1681
+ const components = this.workItemsOpen();
1682
+ return components.store.listOutcomes({
1683
+ limit,
1684
+ ...(options.evidenceState === undefined ? {} : { evidenceState: options.evidenceState }),
1685
+ });
1686
+ }
1687
+ /** Read-only status of the item bound to the calling occurrence. */
1688
+ status(agent) {
1689
+ const components = this.workItemsRequired();
1690
+ const occurrenceId = this.workItemsOccurrenceId(agent);
1691
+ this.workItemsAssertRuntime(components, occurrenceId, 'work_item_status');
1692
+ if (!this.workItemsConfig.diagnosis)
1693
+ components.ledger.recover();
1694
+ const item = components.store.findItemByOccurrence(occurrenceId);
1695
+ if (item === undefined)
1696
+ fail('not-found', 'no work item is bound to this automation occurrence');
1697
+ return Object.freeze({
1698
+ item,
1699
+ claim: components.store.activeClaim(item.id),
1700
+ reports: components.store.reports(item.id),
1701
+ openItems: components.store.countOpenItems(),
1702
+ outcome: components.store.outcomeView(item.id),
1703
+ });
1704
+ }
1705
+ /**
1706
+ * Recall one report belonging to the caller's exact production occurrence.
1707
+ * This path performs no recovery, parking, settlement or ledger mutation.
1708
+ */
1709
+ readReport(agent, input) {
1710
+ const components = this.workItemsRequired();
1711
+ const occurrenceId = this.workItemsOccurrenceId(agent);
1712
+ this.workItemsAssertRuntime(components, occurrenceId, 'work_item_read_report');
1713
+ const item = components.store.findItemByOccurrence(occurrenceId);
1714
+ if (item === undefined)
1715
+ fail('not-found', 'no work item is bound to this automation occurrence');
1716
+ const receipt = this.workItemsAssertOwnerRoute(item);
1717
+ if (receipt.principalVersion !== item.owner.principalVersion
1718
+ || receipt.bindingVersion !== item.owner.bindingVersion
1719
+ || receipt.generation !== item.owner.generation) {
1720
+ fail('owner-route-invalid', 'owner route generation changed since admission');
1721
+ }
1722
+ const authority = components.authority.check(item);
1723
+ if (authority.kind === 'revoked')
1724
+ fail('authority-revoked', 'admission mandate no longer permits report recall');
1725
+ if (authority.kind === 'unavailable')
1726
+ fail('authority-unavailable', 'admission mandate could not be revalidated', authority.detail);
1727
+ return this.workItemsReadReportPage(components, item, input);
1728
+ }
1729
+ readContext(agent, input) {
1730
+ this.workItemsRequired();
1731
+ const runtime = continuationRuntimes.get(this.workItemsContinuationKey);
1732
+ if (!runtime)
1733
+ fail('missing-dependency', 'continuation context runtime is unavailable');
1734
+ return runtime.readContext(this.workItemsOccurrenceId(agent), input);
1735
+ }
1736
+ workItemsAssertRuntime(components, occurrenceId, name) {
1737
+ const item = components.store.findItemByOccurrence(occurrenceId);
1738
+ if (item && components.store.getContinuation(item.id)) {
1739
+ const runtime = continuationRuntimes.get(this.workItemsContinuationKey);
1740
+ if (!runtime)
1741
+ fail('missing-dependency', 'continuation runtime is unavailable');
1742
+ runtime.assertTool(occurrenceId, name);
1743
+ }
1744
+ else if (this.workItemsConfig.diagnosis) {
1745
+ const runtime = diagnosisRuntimes.get(this.workItemsDiagnosisKey);
1746
+ if (!runtime)
1747
+ fail('missing-dependency', 'diagnosis runtime is unavailable');
1748
+ runtime.assertTool(occurrenceId, name);
1749
+ }
1750
+ else if (name === 'work_item_read_context')
1751
+ fail('invalid-state', 'continuation context requires its own invocation');
1752
+ }
1753
+ readSource(agent, input) {
1754
+ this.workItemsRequired();
1755
+ const runtime = diagnosisRuntimes.get(this.workItemsDiagnosisKey);
1756
+ if (!runtime || !this.workItemsConfig.diagnosis)
1757
+ fail('missing-dependency', 'diagnosis source runtime is unavailable');
1758
+ return runtime.readSource(this.workItemsOccurrenceId(agent), input);
1759
+ }
1760
+ /** Pure SELECT/UTF-8 projection shared by preflight and the read tool. */
1761
+ workItemsReadReportPage(components, item, input) {
1762
+ const offset = input.offset ?? 0;
1763
+ const maxBytes = input.maxBytes ?? 4_096;
1764
+ if (!Number.isSafeInteger(input.revision) || input.revision < 1)
1765
+ fail('invalid-input', 'revision must be a positive safe integer');
1766
+ if (!Number.isSafeInteger(offset) || offset < 0)
1767
+ fail('invalid-input', 'offset must be a non-negative UTF-8 byte offset');
1768
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > 8_192)
1769
+ fail('invalid-input', 'maxBytes must be an integer between 4 and 8192');
1770
+ const found = components.store.reportByRevision(item.id, input.revision);
1771
+ if (found === undefined)
1772
+ fail('not-found', 'no report with this revision belongs to the current work item');
1773
+ const bytes = Buffer.from(found.report.summary, 'utf8');
1774
+ if (offset > bytes.length || (offset < bytes.length && (bytes[offset] & 0xc0) === 0x80)) {
1775
+ fail('invalid-input', 'offset must be within the stored summary at a UTF-8 character boundary');
1776
+ }
1777
+ let end = Math.min(offset + maxBytes, bytes.length);
1778
+ while (end < bytes.length && (bytes[end] & 0xc0) === 0x80)
1779
+ end -= 1;
1780
+ return Object.freeze({
1781
+ itemId: item.id,
1782
+ revision: found.report.revision,
1783
+ status: found.report.status,
1784
+ summary: bytes.subarray(offset, end).toString('utf8'),
1785
+ offset,
1786
+ nextOffset: end < bytes.length ? end : null,
1787
+ summaryBytes: bytes.length,
1788
+ storedTruncated: found.report.truncated,
1789
+ artifactPaths: found.report.artifactPaths,
1790
+ recordedAt: found.report.recordedAt,
1791
+ previousRevision: found.previousRevision,
1792
+ nextRevision: found.nextRevision,
1793
+ });
1794
+ }
1795
+ /** Bounded health projection; never creates the ledger when disabled. */
1796
+ health() {
1797
+ const config = this.workItemsConfig;
1798
+ const empty = Object.freeze({
1799
+ enabled: config.enabled,
1800
+ mode: config.mode,
1801
+ openItems: 0,
1802
+ lastAdmitAt: null,
1803
+ lastDispatchAt: null,
1804
+ dispatchUnknown: 0,
1805
+ workerConnected: config.enabled && this.workItemsAutomations() !== undefined,
1806
+ ownerRouteValid: false,
1807
+ follows: Object.freeze({ open: 0, closed: 0, unresolved: 0, degraded: null }),
1808
+ authority: Object.freeze({ legacy: 0, parked: 0, lastDeniedAt: null, lastDeniedReason: null }),
1809
+ settlement: Object.freeze({
1810
+ enabled: config.enabled && config.settlement.enabled,
1811
+ pending: 0,
1812
+ observed: 0,
1813
+ unverified: 0,
1814
+ degraded: null,
1815
+ }),
1816
+ evaluation: Object.freeze({
1817
+ enabled: this.workItemsEvaluationEnabled(),
1818
+ connected: this.workItemsEvaluation.connected,
1819
+ pending: 0,
1820
+ exported: 0,
1821
+ skipped: 0,
1822
+ degraded: this.workItemsEvaluation.degraded,
1823
+ }),
1824
+ });
1825
+ const diagnosis = config.diagnosis || this.workItemsHasDiagnosisHistory ? { diagnosis: diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.health()
1826
+ ?? { enabled: true, ready: false, running: 0, lastError: this.workItemsDiagnosisError ?? (this.workItemsHasDiagnosisHistory ? 'diagnosis-disabled-with-history' : 'diagnosis-runtime-unavailable'),
1827
+ interventionRequired: this.workItemsComponents ? persistedDiagnosisIntervention(this.workItemsComponents.store, true) : true } } : {};
1828
+ const continuation = config.ownerContinuation ? { ownerContinuation: continuationRuntimes.get(this.workItemsContinuationKey)?.health()
1829
+ ?? { enabled: true, ready: false, running: 0, interventionRequired: true, lastError: 'continuation-activation-unavailable' } } : {};
1830
+ const replies = config.ownerReplyContinuation ? { ownerReplyContinuation: replyConsumers.get(this.workItemsReplyKey)?.health()
1831
+ ?? { enabled: true, registered: false, draining: false, lastError: 'reply-consumer-unavailable' } } : {};
1832
+ const feedback = config.ownerReportFeedback ? { ownerReportFeedback: { enabled: true,
1833
+ available: feedbackReaders.get(this.workItemsFeedbackKey)?.available() === true,
1834
+ registered: replyConsumers.get(this.workItemsReplyKey)?.isCurrent() === true } } : {};
1835
+ const schedulerWake = Object.freeze({ subscribed: this.workItemsSchedulerUnsubscribers.size > 0,
1836
+ lastWakeAt: this.workItemsSchedulerLastWakeAt, lastError: this.workItemsSchedulerLastError });
1837
+ if (!config.enabled || this.workItemsClosed)
1838
+ return Object.freeze({ ...empty, schedulerWake, ...diagnosis, ...continuation, ...replies, ...feedback });
1839
+ let components;
1840
+ try {
1841
+ components = this.workItemsOpen();
1842
+ }
1843
+ catch (error) {
1844
+ this.ctx.logger.warn(`assistant-work-items: ledger unavailable: ${describe(error)}`);
1845
+ return Object.freeze({ ...empty, schedulerWake, ...diagnosis, ...continuation, ...replies, ...feedback });
1846
+ }
1847
+ return Object.freeze({
1848
+ schedulerWake,
1849
+ ...diagnosis,
1850
+ ...continuation,
1851
+ ...replies,
1852
+ ...feedback,
1853
+ enabled: true,
1854
+ mode: config.mode,
1855
+ openItems: components.store.countOpenItems(),
1856
+ lastAdmitAt: components.store.lastAdmitAt(),
1857
+ lastDispatchAt: components.store.lastDispatchedAt(),
1858
+ dispatchUnknown: components.store.countDispatchUnknown(),
1859
+ workerConnected: workerIsConnected(this.workItemsAutomations()),
1860
+ ownerRouteValid: this.workItemsOwnerRouteValid(),
1861
+ follows: this.workItemsFollowHealth(components.store),
1862
+ authority: this.workItemsAuthorityHealth(components.store),
1863
+ settlement: Object.freeze({
1864
+ enabled: config.settlement.enabled,
1865
+ pending: components.store.countLatestOutcomes('pending'),
1866
+ observed: components.store.countLatestOutcomes('observed'),
1867
+ unverified: components.store.countLatestOutcomes('unverified'),
1868
+ degraded: this.workItemsSettlementDegraded(),
1869
+ }),
1870
+ evaluation: Object.freeze(this.workItemsEvaluationHealth(components)),
1871
+ });
1872
+ }
1873
+ /** Close the ledger exactly once; safe on an intake that never opened. */
1874
+ close() {
1875
+ if (this.workItemsClosed)
1876
+ return;
1877
+ this.workItemsClosed = true;
1878
+ engineeringReaders.get(this.workItemsFeedbackKey)?.close();
1879
+ engineeringReaders.delete(this.workItemsFeedbackKey);
1880
+ feedbackReaders.get(this.workItemsFeedbackKey)?.close();
1881
+ feedbackReaders.delete(this.workItemsFeedbackKey);
1882
+ replyConsumers.get(this.workItemsReplyKey)?.close();
1883
+ replyConsumers.delete(this.workItemsReplyKey);
1884
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.close();
1885
+ diagnosisRuntimes.delete(this.workItemsDiagnosisKey);
1886
+ continuationRuntimes.get(this.workItemsContinuationKey)?.close();
1887
+ continuationRuntimes.delete(this.workItemsContinuationKey);
1888
+ this.workItemsIntakeEvaluation?.close();
1889
+ for (const unregister of this.workItemsSchedulerUnsubscribers) {
1890
+ try {
1891
+ unregister();
1892
+ }
1893
+ catch (error) {
1894
+ this.ctx.logger.warn(`assistant-work-items: scheduler wake removal failed: ${describe(error)}`);
1895
+ }
1896
+ }
1897
+ this.workItemsEvaluation.dispose();
1898
+ const components = this.workItemsComponents;
1899
+ this.workItemsComponents = undefined;
1900
+ if (components === undefined)
1901
+ return;
1902
+ try {
1903
+ components.store.close();
1904
+ }
1905
+ catch (error) {
1906
+ this.ctx.logger.warn(`assistant-work-items: ledger close failed: ${describe(error)}`);
1907
+ }
1908
+ }
1909
+ /** Export is possible only from an active intake with settlement armed. */
1910
+ workItemsEvaluationEnabled() {
1911
+ return !this.workItemsClosed && isActive(this.workItemsConfig) && this.workItemsConfig.settlement.enabled;
1912
+ }
1913
+ /** One bounded counts read for the export projection. */
1914
+ workItemsEvaluationHealth(components) {
1915
+ const counts = components.store.countEvaluations();
1916
+ const enabled = this.workItemsEvaluationEnabled();
1917
+ return {
1918
+ enabled,
1919
+ connected: this.workItemsEvaluation.connected,
1920
+ pending: counts.pending,
1921
+ exported: counts.exported,
1922
+ skipped: counts.skipped,
1923
+ degraded: this.workItemsEvaluationDegraded(counts.pending),
1924
+ };
1925
+ }
1926
+ /**
1927
+ * Health degradation: a settled revision can never reach the trusted lane.
1928
+ *
1929
+ * A missing optional sink is only a *gap* once something actually awaits
1930
+ * export; a deployment that never installs Evaluation must not look degraded
1931
+ * while it has nothing to export.
1932
+ */
1933
+ workItemsEvaluationDegraded(pending) {
1934
+ if (!this.workItemsConfig.settlement.enabled)
1935
+ return null;
1936
+ if (this.workItemsConfig.scope === null)
1937
+ return 'owner-route-unavailable';
1938
+ if (!this.workItemsEvaluation.connected)
1939
+ return pending > 0 ? 'sink-unavailable' : null;
1940
+ return this.workItemsEvaluation.degraded;
1941
+ }
1942
+ /** Fire-and-forget bounded drain; export failures must never fail a caller. */
1943
+ workItemsDrainEvaluation() {
1944
+ void this.workItemsEvaluation.drain().catch(error => {
1945
+ this.ctx.logger.warn(`assistant-work-items: Evaluation export failed: ${describe(error)}`);
1946
+ });
1947
+ }
1948
+ workItemsAvailable() {
1949
+ if (!this.workItemsConfig.enabled)
1950
+ fail('disabled', 'the work-item intake is disabled');
1951
+ return this.workItemsOpen();
1952
+ }
1953
+ /** Best-effort bounded sweep; log and continue on any failure. */
1954
+ workItemsSweepQuietly(components) {
1955
+ if (!components.signals.armed())
1956
+ return;
1957
+ try {
1958
+ if (this.workItemsConfig.diagnosis)
1959
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.sweep(false);
1960
+ else
1961
+ components.signals.sweep();
1962
+ }
1963
+ catch (error) {
1964
+ this.ctx.logger.warn(`assistant-work-items: signal sweep failed: ${describe(error)}`);
1965
+ }
1966
+ }
1967
+ /** Best-effort bounded re-dispatch sweep behind ordinary claim activity. */
1968
+ workItemsWakeQuietly(components) {
1969
+ try {
1970
+ if (this.workItemsConfig.diagnosis)
1971
+ diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.wakePending();
1972
+ else
1973
+ components.dispatch.wakePending();
1974
+ replyConsumers.get(this.workItemsReplyKey)?.drain();
1975
+ continuationRuntimes.get(this.workItemsContinuationKey)?.wakePending();
1976
+ }
1977
+ catch (error) {
1978
+ this.ctx.logger.warn(`assistant-work-items: dispatch recovery failed: ${describe(error)}`);
1979
+ }
1980
+ }
1981
+ /** Best-effort settlement pass behind ordinary claim activity. */
1982
+ workItemsSettleQuietly(components) {
1983
+ if (!this.workItemsConfig.settlement.enabled)
1984
+ return;
1985
+ try {
1986
+ this.workItemsSettlement(components, false);
1987
+ }
1988
+ catch (error) {
1989
+ this.ctx.logger.warn(`assistant-work-items: settlement failed: ${describe(error)}`);
1990
+ }
1991
+ }
1992
+ /** Build the bounded settlement pass from config; never admits or wakes. */
1993
+ workItemsSettlement(components, force) {
1994
+ const config = this.workItemsConfig;
1995
+ const scope = config.scope;
1996
+ const result = runSettlement({
1997
+ store: components.store,
1998
+ delivery: this.workItemsDelivery(),
1999
+ scope: scope === null
2000
+ ? null
2001
+ : {
2002
+ ownerRouteId: scope.ownerRouteId,
2003
+ principalId: scope.principalId,
2004
+ workspace: scope.workspace,
2005
+ preset: scope.preset,
2006
+ },
2007
+ sweepIntervalMs: config.settlement.sweepIntervalMs,
2008
+ maxSourcesPerSweep: config.settlement.maxSourcesPerSweep,
2009
+ maxPendingPerSweep: config.settlement.maxPendingPerSweep,
2010
+ maxSettlementsPerSweep: config.settlement.maxSettlementsPerSweep,
2011
+ threads: follow => this.workItemsResolveThread(follow),
2012
+ warn: message => this.ctx.logger.warn(message),
2013
+ }, force);
2014
+ this.workItemsFollowDegraded = result.report.followDegraded;
2015
+ return result;
2016
+ }
2017
+ /**
2018
+ * Resolve one open follow against its registered source.
2019
+ *
2020
+ * Synchronous and read-only: no timer, no promise, no write. Every refusal
2021
+ * returns `unresolved` so the item stays parked and health can name the
2022
+ * degradation, rather than a missing source or a stale generation being read
2023
+ * as a closure.
2024
+ */
2025
+ workItemsResolveThread(follow) {
2026
+ const registration = this.workItemsThreadSources.get(follow.source);
2027
+ if (registration === undefined)
2028
+ return { kind: 'unresolved', reason: 'source-missing' };
2029
+ // A source that regenerated cannot vouch for a follow recorded under its
2030
+ // old generation; the closure may still be true, but it is not observed.
2031
+ if (registration.generation !== follow.sourceGeneration)
2032
+ return { kind: 'unresolved', reason: 'generation-mismatch' };
2033
+ let closure;
2034
+ try {
2035
+ closure = registration.resolve(follow.ref);
2036
+ }
2037
+ catch (error) {
2038
+ this.ctx.logger.warn(`assistant-work-items: thread resolution failed for ${follow.source}: ${describe(error)}`);
2039
+ return { kind: 'unresolved', reason: 'resolver-failed' };
2040
+ }
2041
+ if (closure === undefined)
2042
+ return { kind: 'unresolved', reason: 'resolver-failed' };
2043
+ if (closure === null || typeof closure !== 'object' || Array.isArray(closure)) {
2044
+ return { kind: 'unresolved', reason: 'resolver-failed' };
2045
+ }
2046
+ if (closure.state === 'open')
2047
+ return { kind: 'open' };
2048
+ if (closure.state !== 'closed')
2049
+ return { kind: 'unresolved', reason: 'resolver-failed' };
2050
+ if (!['succeeded', 'failed', 'cancelled', 'unknown'].includes(closure.outcome)
2051
+ || typeof closure.digest !== 'string' || closure.digest.trim().length === 0
2052
+ || (closure.observedAt !== undefined && (!Number.isSafeInteger(closure.observedAt) || closure.observedAt <= 0))) {
2053
+ // A malformed closure is never a closure: fail closed toward `waiting`.
2054
+ return { kind: 'unresolved', reason: 'resolver-failed' };
2055
+ }
2056
+ return { kind: 'closed', outcome: closure.outcome, digest: closure.digest };
2057
+ }
2058
+ /** Live follow counts and degradation; never resolves or writes anything. */
2059
+ workItemsFollowHealth(store) {
2060
+ const counts = store.countFollows();
2061
+ let unresolved = 0;
2062
+ let missingSource = false;
2063
+ let mismatched = false;
2064
+ for (const identity of store.openFollowIdentities()) {
2065
+ const registration = this.workItemsThreadSources.get(identity.source);
2066
+ if (registration === undefined) {
2067
+ unresolved += 1;
2068
+ missingSource = true;
2069
+ }
2070
+ else if (registration.generation !== identity.sourceGeneration) {
2071
+ unresolved += 1;
2072
+ mismatched = true;
2073
+ }
2074
+ }
2075
+ const degraded = missingSource
2076
+ ? 'source-missing'
2077
+ : mismatched ? 'generation-mismatch' : this.workItemsFollowDegraded;
2078
+ return Object.freeze({ open: counts.open, closed: counts.closed, unresolved, degraded });
2079
+ }
2080
+ /** Degradation visible in health without running a pass or touching SQLite. */
2081
+ workItemsSettlementDegraded() {
2082
+ if (!this.workItemsConfig.settlement.enabled)
2083
+ return null;
2084
+ if (this.workItemsConfig.scope === null)
2085
+ return 'owner-route-unavailable';
2086
+ return this.workItemsDelivery() === undefined ? 'missing-dependency' : null;
2087
+ }
2088
+ workItemsRequired() {
2089
+ const config = this.workItemsConfig;
2090
+ if (!config.enabled)
2091
+ fail('disabled', 'the work-item intake is disabled');
2092
+ if (!isActive(config))
2093
+ fail('observe-mode', 'the work-item intake is in observe mode; claim and report require mode: active');
2094
+ return this.workItemsOpen();
2095
+ }
2096
+ workItemsOpen() {
2097
+ const existing = this.workItemsComponents;
2098
+ if (existing !== undefined)
2099
+ return existing;
2100
+ if (this.workItemsClosed)
2101
+ fail('storage', 'the work-item intake is disposed');
2102
+ const config = this.workItemsConfig;
2103
+ const workspace = new WorkItemWorkspace(config.workRoot);
2104
+ workspace.prepare();
2105
+ const store = new WorkItemStore(config.databasePath);
2106
+ let diagnosisHistory;
2107
+ try {
2108
+ diagnosisHistory = store.readCanonicalMeta('diagnosis.activation/v1') !== undefined;
2109
+ if (diagnosisHistory && !config.diagnosis && isActive(config))
2110
+ fail('invalid-state', 'a diagnosis ledger cannot be silently activated as a generic worker');
2111
+ }
2112
+ catch (error) {
2113
+ store.close();
2114
+ throw error;
2115
+ }
2116
+ this.workItemsHasDiagnosisHistory = diagnosisHistory;
2117
+ const ledger = new WorkItemLedger({ store, workspace, limits: config.limits, ...(config.diagnosis || diagnosisHistory ? { diagnosis: true } : {}) });
2118
+ // Admission and dispatch both take the same ledger and the same resolved
2119
+ // peers, so one construction order keeps a single writer per durable file.
2120
+ const builders = new WeakMap();
2121
+ const admission = new WorkItemAdmission({
2122
+ config,
2123
+ store,
2124
+ ledger,
2125
+ workspace,
2126
+ policy: this.workItemsPolicy(),
2127
+ delivery: () => this.workItemsDelivery(),
2128
+ verifier: () => this.workItemsVerifier(),
2129
+ diagnosisPlanFor: input => builders.get(input),
2130
+ threadSource: kind => {
2131
+ const registration = this.workItemsThreadSources.get(kind);
2132
+ return registration === undefined
2133
+ ? undefined
2134
+ : Object.freeze({
2135
+ source: registration.source,
2136
+ generation: registration.generation,
2137
+ ...(registration.validate === undefined ? {} : { validate: (ref) => registration.validate?.(ref) === true }),
2138
+ ...(registration.canonicalize === undefined ? {} : { canonicalize: (ref) => registration.canonicalize?.(ref) }),
2139
+ });
2140
+ },
2141
+ });
2142
+ // One lazy policy resolver serves admission and revalidation: a peer that
2143
+ // registers after this plugin becomes visible to both without a restart.
2144
+ const authority = new WorkItemAuthority({ policy: () => this.workItemsPolicy() });
2145
+ const dispatch = new WorkItemDispatch({
2146
+ config,
2147
+ store,
2148
+ ledger,
2149
+ automations: () => this.workItemsAutomations(),
2150
+ delivery: () => this.workItemsDelivery(),
2151
+ validateWake: item => !(config.ownerReportFeedback && config.scope && config.scope.expiresAt <= Date.now())
2152
+ && (!config.diagnosis || diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.validateWake(item) === true),
2153
+ validateWakeLocal: () => !this.workItemsClosed && (!config.diagnosis || diagnosisRuntimes.get(this.workItemsDiagnosisKey)?.isWakeCurrent() === true),
2154
+ replyReportEligible: (item, report, notification) => replyConsumers.get(this.workItemsReplyKey)?.canEnqueue(item, report, notification) === true,
2155
+ authority,
2156
+ });
2157
+ // Signal sources are optional peers resolved lazily on every sweep, so a
2158
+ // deployment without `assistant-delivery` (or with the source disabled)
2159
+ // stays a no-op instead of failing activation or a claim.
2160
+ const signals = new WorkItemSignalIntake({
2161
+ config,
2162
+ store,
2163
+ admission,
2164
+ delivery: () => this.workItemsDelivery(),
2165
+ dispatch,
2166
+ warn: message => this.ctx.logger.warn(message),
2167
+ });
2168
+ const installed = { workspace, store, ledger, admission, authority, dispatch, signals };
2169
+ this.workItemsComponents = installed;
2170
+ diagnosisAdmissions.set(installed, (input, builder) => {
2171
+ if (!config.diagnosis || !isActive(config) || this.workItemsClosed)
2172
+ fail('disabled', 'private diagnosis admission is unavailable');
2173
+ builders.set(input, builder);
2174
+ try {
2175
+ return admission.admit(input);
2176
+ }
2177
+ finally {
2178
+ builders.delete(input);
2179
+ }
2180
+ });
2181
+ if (config.ownerContinuation) {
2182
+ const { diagnosis: _diagnosis, ...sharedConfig } = config;
2183
+ const continuationConfig = Object.freeze({ ...sharedConfig,
2184
+ workerAutomationId: config.ownerContinuation.automationId,
2185
+ limits: Object.freeze({ ...config.limits, maxAttempts: 1 }) });
2186
+ const continuationBuilders = new WeakMap();
2187
+ const continuationAdmission = new WorkItemAdmission({ config: continuationConfig, store, ledger, workspace,
2188
+ policy: this.workItemsPolicy(), delivery: () => this.workItemsDelivery(),
2189
+ continuationFor: input => continuationBuilders.get(input) });
2190
+ continuationAdmissions.set(installed, (input, continuation) => {
2191
+ if (this.workItemsClosed || !isActive(config))
2192
+ fail('disabled', 'continuation admission is unavailable');
2193
+ continuationBuilders.set(input, continuation);
2194
+ try {
2195
+ return continuationAdmission.admit(input);
2196
+ }
2197
+ finally {
2198
+ continuationBuilders.delete(input);
2199
+ }
2200
+ });
2201
+ continuationDispatches.set(installed, new WorkItemDispatch({ config: continuationConfig, store, ledger,
2202
+ automations: () => this.workItemsAutomations(), delivery: () => this.workItemsDelivery(), authority,
2203
+ validateWake: item => continuationRuntimes.get(this.workItemsContinuationKey)?.validateWake(item) === true,
2204
+ validateWakeLocal: item => !this.workItemsClosed && continuationRuntimes.get(this.workItemsContinuationKey)?.isWakeCurrent(item) === true }));
2205
+ }
2206
+ try {
2207
+ // Bounded startup reconcile: lease reclaim is lazy forever, so a host that
2208
+ // was down catches up on the first touch instead of on a timer.
2209
+ if (!config.diagnosis && !diagnosisHistory)
2210
+ installed.ledger.recover();
2211
+ if (isActive(config) && !config.diagnosis) {
2212
+ try {
2213
+ installed.dispatch.wakePending();
2214
+ }
2215
+ catch (error) {
2216
+ this.ctx.logger.warn(`assistant-work-items: pending wake failed: ${describe(error)}`);
2217
+ }
2218
+ }
2219
+ }
2220
+ catch (error) {
2221
+ this.workItemsComponents = undefined;
2222
+ store.close();
2223
+ throw error;
2224
+ }
2225
+ return installed;
2226
+ }
2227
+ /** Resolve the exact production automation occurrence for this agent. */
2228
+ workItemsOccurrenceId(agent) {
2229
+ if (agent === undefined)
2230
+ fail('identity-mismatch', 'work-item tools require the worker automation run');
2231
+ const execution = agent.ctx.get('assistantAutomationExecution');
2232
+ if (typeof execution !== 'object' || execution === null || Array.isArray(execution)
2233
+ || !Object.isFrozen(execution)
2234
+ || Object.keys(execution).sort().join(',') !== 'automationId,mode,occurrenceId') {
2235
+ fail('identity-mismatch', 'work-item tools require the immutable automation execution context');
2236
+ }
2237
+ const installed = execution;
2238
+ if (installed.mode !== 'production'
2239
+ || typeof installed.automationId !== 'string'
2240
+ || typeof installed.occurrenceId !== 'string') {
2241
+ fail('identity-mismatch', 'work-item tools are restricted to a production automation run');
2242
+ }
2243
+ const automationId = text(installed.automationId, 'automationId', 500);
2244
+ const occurrenceId = text(installed.occurrenceId, 'occurrenceId', 500);
2245
+ if (automationId !== this.workItemsConfig.workerAutomationId && automationId !== this.workItemsConfig.ownerContinuation?.automationId) {
2246
+ fail('identity-mismatch', 'this automation is not the configured work-item worker');
2247
+ }
2248
+ if (automationId === this.workItemsConfig.ownerContinuation?.automationId) {
2249
+ const item = this.workItemsComponents?.store.findItemByOccurrence(occurrenceId);
2250
+ if (!item || item.workerAutomationId !== automationId || !this.workItemsComponents?.store.getContinuation(item.id)) {
2251
+ fail('identity-mismatch', 'continuation occurrence does not own its persisted item');
2252
+ }
2253
+ }
2254
+ return occurrenceId;
2255
+ }
2256
+ /** Re-anchor a claim against the live owner route above the stored receipt. */
2257
+ workItemsAssertOwnerRoute(item) {
2258
+ const config = this.workItemsConfig;
2259
+ const delivery = this.workItemsDelivery();
2260
+ if (delivery === undefined)
2261
+ fail('owner-route-unavailable', 'assistantDelivery is required to validate the owner route');
2262
+ const scope = item.scope;
2263
+ if (scope.expiresAt <= Date.now())
2264
+ fail('owner-route-expired', 'the work-item owner scope expired');
2265
+ let receipt;
2266
+ try {
2267
+ receipt = delivery.validateOwnerRoute({
2268
+ authorityId: scope.ownerRouteId,
2269
+ principalId: scope.principalId,
2270
+ workspace: scope.workspace,
2271
+ agentPreset: scope.preset,
2272
+ });
2273
+ }
2274
+ catch (error) {
2275
+ const detail = error instanceof Error ? error.message : String(error);
2276
+ const missing = error !== null && typeof error === 'object' && 'code' in error && error.code === 'missing-binding';
2277
+ fail(missing ? 'owner-route-invalid' : 'owner-route-unavailable', 'owner route validation failed', detail);
2278
+ }
2279
+ if (receipt.receiptVersion !== 2
2280
+ || receipt.workspace !== scope.workspace
2281
+ || receipt.agentPreset !== scope.preset
2282
+ || receipt.principalId !== scope.principalId
2283
+ || receipt.authorityId !== scope.ownerRouteId) {
2284
+ fail('owner-route-invalid', 'owner route receipt does not match the admitted scope');
2285
+ }
2286
+ if (receipt.principalRecordId !== item.owner.principalRecordId) {
2287
+ fail('owner-route-invalid', 'owner principal changed since admission');
2288
+ }
2289
+ if (config.workerAutomationId !== item.workerAutomationId
2290
+ && (config.ownerContinuation?.automationId !== item.workerAutomationId || !this.workItemsComponents?.store.getContinuation(item.id))) {
2291
+ fail('identity-mismatch', 'item was dispatched to a different worker automation');
2292
+ }
2293
+ return receipt;
2294
+ }
2295
+ /**
2296
+ * Re-validate the stored admission mandate before this claim is allowed.
2297
+ *
2298
+ * Policy remains the only authority: the exact admission request is replayed
2299
+ * against the live policy under the same idempotency key. A denied, failing
2300
+ * or malformed decision parks the item in `waiting` with a typed reason and
2301
+ * fails the claim closed; the item is never claimed and never marked failed.
2302
+ * A later allow resumes it through the same wake path.
2303
+ */
2304
+ workItemsRevalidateAuthority(components, item) {
2305
+ // A terminal item is not claimable; the ledger refuses it. Revalidating
2306
+ // first would park and re-arm finished work into a new generation.
2307
+ if (TERMINAL_STATES.includes(item.state))
2308
+ return;
2309
+ const check = components.authority.check(item);
2310
+ if (check.kind === 'legacy')
2311
+ return;
2312
+ if (check.kind === 'allow') {
2313
+ components.ledger.restoreAuthority(item);
2314
+ return;
2315
+ }
2316
+ if (check.kind === 'revoked') {
2317
+ components.ledger.parkAuthority(item, 'authority-revoked', { reason: check.reasonCode, phase: 'claim' });
2318
+ components.ledger.rearmAuthority(item);
2319
+ fail('authority-revoked', `admission mandate no longer holds (${check.reasonCode})`);
2320
+ }
2321
+ components.ledger.parkAuthority(item, 'authority-unavailable', { reason: check.detail, phase: 'claim' });
2322
+ components.ledger.rearmAuthority(item);
2323
+ fail('authority-unavailable', 'admission mandate could not be revalidated', check.detail);
2324
+ }
2325
+ workItemsAuthorityHealth(store) {
2326
+ const denied = store.lastAuthorityDenial();
2327
+ return Object.freeze({
2328
+ legacy: store.countAuthorityLegacy(),
2329
+ parked: store.countAuthorityParked(),
2330
+ lastDeniedAt: denied === null ? null : denied.at,
2331
+ lastDeniedReason: denied === null ? null : denied.reason,
2332
+ });
2333
+ }
2334
+ workItemsOwnerRouteValid() {
2335
+ const config = this.workItemsConfig;
2336
+ const scope = config.scope;
2337
+ if (scope === null)
2338
+ return false;
2339
+ const delivery = this.workItemsDelivery();
2340
+ if (delivery === undefined)
2341
+ return false;
2342
+ if (scope.expiresAt <= Date.now())
2343
+ return false;
2344
+ try {
2345
+ const receipt = delivery.validateOwnerRoute({
2346
+ authorityId: scope.ownerRouteId,
2347
+ principalId: scope.principalId,
2348
+ workspace: scope.workspace,
2349
+ agentPreset: scope.preset,
2350
+ });
2351
+ return receipt.receiptVersion === 2 && receipt.workspace === scope.workspace;
2352
+ }
2353
+ catch {
2354
+ return false;
2355
+ }
2356
+ }
2357
+ workItemsPolicy() {
2358
+ return this.ctx.get('assistantPolicy');
2359
+ }
2360
+ /** Live owner scope for Event Triggers reads; throws when the owner route is unavailable. */
2361
+ workItemsEventFollowOwner(scope) {
2362
+ const delivery = this.workItemsDelivery();
2363
+ if (typeof delivery?.validateOwnerRoute !== 'function')
2364
+ fail('disabled', 'owner route is unavailable for event follows');
2365
+ const receipt = delivery.validateOwnerRoute({ authorityId: scope.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, agentPreset: scope.preset });
2366
+ if (receipt.principalId !== scope.principalId || receipt.workspace !== scope.workspace || receipt.agentPreset !== scope.preset) {
2367
+ fail('disabled', 'owner route does not match the configured scope');
2368
+ }
2369
+ return Object.freeze({ principalId: receipt.principalId, principalRecordId: receipt.principalRecordId,
2370
+ principalVersion: receipt.principalVersion, workspace: scope.workspace, preset: scope.preset });
2371
+ }
2372
+ workItemsDelivery() {
2373
+ return this.ctx.get('assistantDelivery');
2374
+ }
2375
+ workItemsVerifier() {
2376
+ return this.ctx.get('assistantVerifier');
2377
+ }
2378
+ workItemsAutomations() {
2379
+ return this.ctx.get('assistantAutomations');
2380
+ }
2381
+ }
2382
+ function workerIsConnected(automations) {
2383
+ return automations !== undefined;
2384
+ }
2385
+ function describe(error) {
2386
+ return error instanceof Error ? error.message : String(error);
2387
+ }
2388
+ //# sourceMappingURL=service.js.map