@dsh-enhanced/assistant-work-items 0.0.0-stage → 0.1.67

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 +99 -0
  13. package/lib/authority.d.ts.map +1 -0
  14. package/lib/authority.js +233 -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 +268 -0
  107. package/lib/owner-replies.js.map +1 -0
  108. package/lib/peers.d.ts +624 -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 +632 -0
  121. package/lib/service.d.ts.map +1 -0
  122. package/lib/service.js +2447 -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 +791 -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
@@ -0,0 +1,607 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { acceptanceCanonicalJson, acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
3
+ import { OPEN_STATES } from './types.js';
4
+ import { continuationDigest, continuationSourceKey } from './continuation-record.js';
5
+ import { CONTINUATION_EXECUTOR, CONTINUATION_OWNER, CONTINUATION_PROMPT_VERSION, continuationCanonical, continuationConfigurationDigest, continuationDefinition, continuationDefinitionHash, continuationFail, continuationItemBasis, continuationOwnerLineage, continuationSchemas, createContinuationPlan, parseContinuationActivation, } from './continuation-contract.js';
6
+ import { DIAGNOSIS_OWNER } from './diagnosis-contract.js';
7
+ import { createReplyContinuationPlan, immutableReply, isReplyContinuationPlan, parseStoredContinuationPlan } from './reply-contract.js';
8
+ const ACTIVATION_KEY = 'continuation.activation/v1';
9
+ const PENDING_CURSOR_KEY = 'continuation.pending-cursor/v1';
10
+ /** A consumer of the existing Host execution service, with no additional loop or timer. */
11
+ export class WorkItemContinuation {
12
+ context;
13
+ config;
14
+ store;
15
+ hooks;
16
+ #controller = new AbortController();
17
+ #runs = new Map();
18
+ #activation;
19
+ #remove;
20
+ #closed = false;
21
+ #waking = false;
22
+ #lastError = null;
23
+ constructor(context, config, store, hooks) {
24
+ this.context = context;
25
+ this.config = config;
26
+ this.store = store;
27
+ this.hooks = hooks;
28
+ }
29
+ automations() {
30
+ if (this.#closed || this.#controller.signal.aborted || this.store.closed)
31
+ continuationFail('consumer disposed');
32
+ const peer = this.context.get('assistantAutomations');
33
+ if (!peer || peer.hostAgentAuthorizationVersion?.() !== 1 || typeof peer.runAgentForHostExecution !== 'function'
34
+ || typeof peer.registerHostExecutor !== 'function' || typeof peer.reconcileSystem !== 'function'
35
+ || typeof peer.inspectSystemOwned !== 'function' || typeof peer.inspectSystemOwnedActivation !== 'function'
36
+ || typeof peer.inspectProductionOccurrence !== 'function')
37
+ continuationFail('fenced Host peer unavailable');
38
+ return peer;
39
+ }
40
+ schemas() {
41
+ const allowed = this.config.ownerContinuation?.allowedTools;
42
+ if (!allowed)
43
+ continuationFail('configuration unavailable');
44
+ return continuationSchemas(this.context.tools.schemas().filter(schema => allowed.includes(schema.name)), this.config);
45
+ }
46
+ owner() {
47
+ const scope = this.config.scope, settings = this.config.ownerContinuation;
48
+ if (!scope || !settings || !this.config.enabled || this.config.mode !== 'active'
49
+ || Date.now() >= Math.min(scope.expiresAt, settings.expiresAt))
50
+ continuationFail('authorization expired or disabled');
51
+ const delivery = this.context.get('assistantDelivery');
52
+ if (!delivery?.validateOwnerRoute)
53
+ continuationFail('owner route unavailable');
54
+ const receipt = delivery.validateOwnerRoute({ authorityId: scope.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, agentPreset: scope.preset });
55
+ if (receipt.receiptVersion !== 2 || receipt.authorityId !== scope.ownerRouteId || receipt.principalId !== scope.principalId
56
+ || receipt.workspace !== scope.workspace || receipt.agentPreset !== scope.preset || !receipt.authorityHash
57
+ || !receipt.principalRecordId || !Number.isSafeInteger(receipt.principalVersion) || receipt.principalVersion < 1
58
+ || !Number.isSafeInteger(receipt.generation) || receipt.generation < 1 || !Number.isSafeInteger(receipt.bindingVersion) || receipt.bindingVersion < 1)
59
+ continuationFail('current owner differs');
60
+ return receipt;
61
+ }
62
+ start() {
63
+ if (this.#activation || this.#closed)
64
+ continuationFail('activation already started or disposed');
65
+ const settings = this.config.ownerContinuation, scope = this.config.scope;
66
+ if (!settings || !scope)
67
+ continuationFail('explicit configuration required');
68
+ const peer = this.automations(), receipt = this.owner(), schemas = this.schemas();
69
+ const policy = this.context.get('assistantPolicy');
70
+ if (policy?.authorize({ subject: { kind: 'background', id: this.config.intakeSourceId, workspace: scope.workspace, principal: scope.principalId },
71
+ action: 'admit', resource: { kind: 'automation', id: settings.automationId }, context: { initiator: 'background' } }, { idempotencyKey: `continuation-activation-admit:${this.config.intakeSourceId}:${settings.automationId}` }).effect !== 'allow')
72
+ continuationFail('activation admission denied');
73
+ const configurationDigest = continuationConfigurationDigest(this.config, schemas);
74
+ const catalogDigest = acceptanceDigest({ protocol: CONTINUATION_PROMPT_VERSION, schemas });
75
+ const prior = this.store.readCanonicalMeta(ACTIVATION_KEY);
76
+ let activation;
77
+ if (prior === undefined) {
78
+ const nonce = randomBytes(32).toString('hex');
79
+ activation = Object.freeze({ protocol: 'assistant-work-items/continuation-activation/v1', stage: 'pending', configurationDigest,
80
+ activationNonce: nonce, reconciliationKey: `continuation-${acceptanceDigest({ configurationDigest, nonce })}`,
81
+ definitionHash: continuationDefinitionHash(continuationDefinition(this.config, nonce, catalogDigest)), definitionVersion: 1,
82
+ catalogDigest, ownerLineage: continuationOwnerLineage(receipt) });
83
+ this.store.setMeta(ACTIVATION_KEY, continuationCanonical(activation));
84
+ }
85
+ else
86
+ activation = parseContinuationActivation(prior);
87
+ if (activation.configurationDigest !== configurationDigest || activation.catalogDigest !== catalogDigest
88
+ || acceptanceCanonicalJson(activation.ownerLineage) !== acceptanceCanonicalJson(continuationOwnerLineage(receipt))
89
+ || activation.definitionHash !== continuationDefinitionHash(continuationDefinition(this.config, activation.activationNonce, catalogDigest)))
90
+ continuationFail('activation configuration or owner changed');
91
+ this.#activation = activation;
92
+ this.#remove = peer.registerHostExecutor({ descriptor: { executorId: CONTINUATION_EXECUTOR, contractVersion: 1, catalogDigest },
93
+ accepts: spec => !this.#closed && spec.kind === 'host' && spec.executorId === CONTINUATION_EXECUTOR && spec.executorContractVersion === 1
94
+ && spec.runbookId === 'owner-continuation' && spec.runbookVersion === 1 && spec.catalogDigest === catalogDigest
95
+ && spec.activationNonce === activation.activationNonce && spec.ownerRouteId === scope.ownerRouteId
96
+ && spec.targetScope.workspace === scope.workspace && spec.targetScope.preset === scope.preset,
97
+ execute: input => this.execute(input) });
98
+ if (activation.stage === 'pending') {
99
+ let present = false;
100
+ try {
101
+ peer.inspectSystemOwned({ owner: CONTINUATION_OWNER, automationId: settings.automationId });
102
+ present = true;
103
+ }
104
+ catch (error) {
105
+ if (!(error && typeof error === 'object' && 'code' in error && error.code === 'not-found'))
106
+ throw error;
107
+ }
108
+ if (!present)
109
+ peer.reconcileSystem({ owner: CONTINUATION_OWNER, automationId: settings.automationId, idempotencyKey: activation.reconciliationKey,
110
+ definition: continuationDefinition(this.config, activation.activationNonce, catalogDigest), desiredStatus: 'active' });
111
+ this.assertActivation(activation);
112
+ activation = Object.freeze({ ...activation, stage: 'established' });
113
+ this.store.setMeta(ACTIVATION_KEY, continuationCanonical(activation));
114
+ this.#activation = activation;
115
+ }
116
+ else
117
+ this.assertActivation(activation);
118
+ this.wakePending();
119
+ }
120
+ assertActivation(expected = this.#activation) {
121
+ if (!expected)
122
+ continuationFail('activation unavailable');
123
+ const peer = this.automations(), receipt = this.owner(), current = parseContinuationActivation(this.store.readCanonicalMeta(ACTIVATION_KEY));
124
+ if (acceptanceCanonicalJson(current) !== acceptanceCanonicalJson(expected)
125
+ || current.configurationDigest !== continuationConfigurationDigest(this.config, this.schemas())
126
+ || acceptanceCanonicalJson(current.ownerLineage) !== acceptanceCanonicalJson(continuationOwnerLineage(receipt)))
127
+ continuationFail('activation receipt changed');
128
+ const input = { owner: CONTINUATION_OWNER, automationId: this.config.ownerContinuation.automationId };
129
+ const state = peer.inspectSystemOwned(input), binding = peer.inspectSystemOwnedActivation(input);
130
+ if (state.owner !== input.owner || state.automationId !== input.automationId || state.automationStatus !== 'active'
131
+ || state.definitionHash !== current.definitionHash || state.definitionVersion !== current.definitionVersion
132
+ || !binding || binding.definitionHash !== current.definitionHash || binding.activationNonce !== current.activationNonce
133
+ || binding.ownerRouteId !== this.config.scope.ownerRouteId)
134
+ continuationFail('worker paused, missing or changed');
135
+ return current;
136
+ }
137
+ proof(item, occurrenceId) {
138
+ const input = { automationId: item.workerAutomationId, occurrenceId, workspace: item.scope.workspace, agentPreset: item.scope.preset };
139
+ const proof = this.automations().inspectProductionOccurrence(input);
140
+ const continuation = this.store.getContinuation(item.id);
141
+ const budgetRequired = continuation !== undefined || item.workerAutomationId === this.config.ownerContinuation.automationId;
142
+ if (!proof || proof.protocol !== 'assistant-automations/production-occurrence/v1' || proof.automationId !== input.automationId
143
+ || proof.occurrenceId !== input.occurrenceId || proof.executionMode !== 'production' || !['succeeded', 'failed', 'cancelled', 'timed_out'].includes(proof.status)
144
+ || proof.quiescent !== true || proof.budgetSettlementState !== 'finalized' && (budgetRequired || proof.budgetSettlementState !== 'not-required')
145
+ || !Number.isSafeInteger(proof.completedAt) || proof.completedAt < 1
146
+ || proof.immutableContext.state !== 'verified' || proof.immutableContext.scope.workspace !== input.workspace
147
+ || proof.immutableContext.scope.agentPreset !== input.agentPreset)
148
+ continuationFail('exact production termination unproven');
149
+ if (continuation) {
150
+ const plan = parseStoredContinuationPlan(continuation.planJson);
151
+ if (acceptanceCanonicalJson(plan.item) !== acceptanceCanonicalJson(continuationItemBasis(item))
152
+ || plan.source.digest !== continuation.sourceDigest || plan.source.inputSha256 !== continuation.inputSha256
153
+ || plan.source.inputBytes !== continuation.inputBytes || plan.parent.itemId !== continuation.parentItemId
154
+ || plan.parent.rootItemId !== continuation.rootItemId || plan.parent.reportRevision !== continuation.parentReportRevision
155
+ || plan.parent.outcomeRevision !== continuation.parentOutcomeRevision || plan.activation.stage !== 'established'
156
+ || proof.immutableContext.definitionHash !== plan.activation.definitionHash
157
+ || proof.immutableContext.definitionVersion !== plan.activation.definitionVersion
158
+ || proof.immutableContext.owner !== CONTINUATION_OWNER)
159
+ continuationFail('production proof differs from frozen continuation');
160
+ }
161
+ return Object.freeze(JSON.parse(acceptanceCanonicalJson(proof)));
162
+ }
163
+ parent(itemId) {
164
+ const item = this.store.findItemById(itemId), owner = this.owner(), scope = this.config.scope;
165
+ if (!item || item.observed || OPEN_STATES.includes(item.state) || item.scope.principalId !== scope.principalId
166
+ || ![this.config.workerAutomationId, this.config.ownerContinuation.automationId].includes(item.workerAutomationId)
167
+ || item.scope.ownerRouteId !== scope.ownerRouteId || item.scope.workspace !== scope.workspace || item.scope.preset !== scope.preset
168
+ || item.owner.principalRecordId !== owner.principalRecordId || item.owner.principalVersion !== owner.principalVersion
169
+ || item.owner.generation > owner.generation || item.owner.generation === owner.generation && item.owner.bindingVersion !== owner.bindingVersion)
170
+ continuationFail('parent is not a terminal task of this owner');
171
+ const report = this.store.lastReport(itemId);
172
+ if (!report || !['ready', 'blocked', 'failed'].includes(report.status))
173
+ continuationFail('parent has no terminal report binding');
174
+ const binding = this.store.diagnosisReportOccurrence(itemId, report.revision);
175
+ const production = this.proof(item, binding.occurrenceId);
176
+ const inherited = this.store.getContinuation(itemId);
177
+ return Object.freeze({ itemId, rootItemId: inherited?.rootItemId ?? itemId, reportRevision: report.revision,
178
+ outcomeRevision: this.store.latestOutcome(itemId)?.revision ?? null, production });
179
+ }
180
+ prepare(parentItemId, source, assertCurrent) {
181
+ assertCurrent();
182
+ const activation = this.assertActivation(), parent = this.parent(parentItemId);
183
+ // Snapshot caller objects before any further peer callback can mutate them.
184
+ const frozenSource = JSON.parse(acceptanceCanonicalJson(source));
185
+ const sourceKey = continuationSourceKey(parentItemId, frozenSource.sourceDigest);
186
+ assertCurrent();
187
+ return Object.freeze({ parentItemId, sourceDigest: frozenSource.sourceDigest, sourceKey, create: (item) => {
188
+ assertCurrent();
189
+ const currentParent = this.parent(parentItemId);
190
+ if (acceptanceCanonicalJson(currentParent) !== acceptanceCanonicalJson(parent))
191
+ continuationFail('parent changed before admission');
192
+ const plan = createContinuationPlan(item, frozenSource, parent, this.config, this.assertActivation(activation), this.schemas());
193
+ const planJson = continuationCanonical(plan);
194
+ assertCurrent();
195
+ return Object.freeze({ itemId: item.id, parentItemId, rootItemId: parent.rootItemId, sourceKey, sourceDigest: frozenSource.sourceDigest,
196
+ inputText: frozenSource.inputText, inputSha256: continuationDigest(frozenSource.inputText), parentReportRevision: parent.reportRevision,
197
+ parentOutcomeRevision: parent.outcomeRevision, planJson, planDigest: continuationDigest(planJson), createdAt: item.createdAt });
198
+ } });
199
+ }
200
+ replySource(id, sourceDigest) {
201
+ if (!this.config.ownerReplyContinuation)
202
+ continuationFail('report reply continuation disabled');
203
+ const delivery = this.context.get('assistantDelivery');
204
+ if (delivery?.workItemReplyProtocolVersion?.() !== 1 || !delivery.readWorkItemReply)
205
+ continuationFail('reply source peer unavailable');
206
+ const source = delivery.readWorkItemReply({ id, expectedSourceDigest: sourceDigest }), owner = this.owner(), scope = this.config.scope;
207
+ immutableReply(source);
208
+ if (source.id !== id || source.sourceDigest !== sourceDigest || source.state === 'cancelled'
209
+ || source.scope.workspace !== scope.workspace || source.scope.preset !== scope.preset || source.principalId !== scope.principalId
210
+ || source.owner.principalRecordId !== owner.principalRecordId || source.owner.principalVersion !== owner.principalVersion
211
+ || source.binding.generation !== owner.generation || source.binding.version !== owner.bindingVersion)
212
+ continuationFail('reply source authority changed');
213
+ return source;
214
+ }
215
+ parentSupplier(parent) {
216
+ const item = this.store.findItemById(parent.itemId);
217
+ if (!item)
218
+ continuationFail('supplier parent missing');
219
+ const continuation = this.store.getContinuation(item.id);
220
+ let model, kind, planDigest;
221
+ if (continuation) {
222
+ const plan = parseStoredContinuationPlan(continuation.planJson);
223
+ // proof() binds this exact immutable production to the sealed plan.
224
+ const production = this.proof(item, parent.production.occurrenceId);
225
+ if (acceptanceCanonicalJson(production) !== acceptanceCanonicalJson(parent.production))
226
+ continuationFail('supplier production changed');
227
+ model = { provider: plan.contract.provider, model: plan.contract.model,
228
+ ...(Object.hasOwn(plan.contract, 'reasoningEffort') ? { reasoningEffort: plan.contract.reasoningEffort } : {}) };
229
+ if (acceptanceCanonicalJson(model) !== acceptanceCanonicalJson(plan.source.modelSelection))
230
+ continuationFail('supplier contract differs from source');
231
+ kind = 'continuation-plan';
232
+ planDigest = continuation.planDigest;
233
+ }
234
+ else {
235
+ const plan = this.store.readDiagnosisPlan(item.id);
236
+ if (!plan || plan.worker.owner !== DIAGNOSIS_OWNER || plan.worker.automationId !== item.workerAutomationId
237
+ || parent.production.budgetSettlementState !== 'finalized'
238
+ || parent.production.immutableContext.owner !== DIAGNOSIS_OWNER
239
+ || parent.production.immutableContext.definitionHash !== plan.worker.definitionHash
240
+ || parent.production.immutableContext.definitionVersion !== plan.worker.definitionVersion)
241
+ continuationFail('parent has no proven frozen diagnosis supplier');
242
+ model = { provider: plan.contract.provider, model: plan.contract.model,
243
+ ...(Object.hasOwn(plan.contract, 'reasoningEffort') ? { reasoningEffort: plan.contract.reasoningEffort } : {}) };
244
+ kind = 'diagnosis-plan';
245
+ planDigest = continuationDigest(acceptanceCanonicalJson(plan));
246
+ }
247
+ return Object.freeze({ modelSelection: Object.freeze(model), evidence: Object.freeze({ kind, itemId: item.id, planDigest, production: parent.production }) });
248
+ }
249
+ /** Local-only producer inspection. Delivery calls this while checking a
250
+ * sidecar, so it must never re-enter Delivery or depend on terminal proof. */
251
+ inspectReportStream(item) {
252
+ if (!this.config.ownerReplyContinuation || this.#closed || this.store.closed || item.observed)
253
+ return undefined;
254
+ const scope = this.config.scope;
255
+ if (!scope || item.scope.workspace !== scope.workspace || item.scope.preset !== scope.preset
256
+ || item.scope.principalId !== scope.principalId || item.scope.ownerRouteId !== scope.ownerRouteId
257
+ || ![this.config.workerAutomationId, this.config.ownerContinuation.automationId].includes(item.workerAutomationId))
258
+ return undefined;
259
+ const continuation = this.store.getContinuation(item.id);
260
+ if (continuation) {
261
+ const plan = parseStoredContinuationPlan(continuation.planJson);
262
+ if (acceptanceCanonicalJson(plan.item) !== acceptanceCanonicalJson(continuationItemBasis(item)))
263
+ return undefined;
264
+ return Object.freeze({ streamId: isReplyContinuationPlan(plan) ? plan.source.origin.reply.target.streamId : item.id, planDigest: continuation.planDigest });
265
+ }
266
+ const plan = this.store.readDiagnosisPlan(item.id);
267
+ if (!plan || plan.worker.owner !== DIAGNOSIS_OWNER || plan.worker.automationId !== item.workerAutomationId)
268
+ return undefined;
269
+ return Object.freeze({ streamId: item.id, planDigest: continuationDigest(acceptanceCanonicalJson(plan)) });
270
+ }
271
+ /** Pure local continuation eligibility; feedback metadata must not extend
272
+ * the original execution scope or its deadline. */
273
+ canAcceptReportReply() {
274
+ const scope = this.config.scope, settings = this.config.ownerContinuation;
275
+ return !this.#closed && !this.#controller.signal.aborted && !this.store.closed && this.#activation?.stage === 'established'
276
+ && this.config.ownerReplyContinuation === true && !!scope && !!settings && Date.now() < Math.min(scope.expiresAt, settings.expiresAt);
277
+ }
278
+ /** Admission only: the caller must durably ACK this child before any wake. */
279
+ prepareReply(id, sourceDigest, assertCurrent) {
280
+ assertCurrent();
281
+ const activation = this.assertActivation(), source = this.replySource(id, sourceDigest);
282
+ if (!source.executionParentItemId || source.state !== 'pending')
283
+ continuationFail('reply has no frozen parent selection');
284
+ const parent = this.parent(source.executionParentItemId), supplier = this.parentSupplier(parent);
285
+ const frozenSource = JSON.parse(acceptanceCanonicalJson(source));
286
+ const sourceKey = continuationSourceKey(parent.itemId, sourceDigest);
287
+ assertCurrent();
288
+ return Object.freeze({ parentItemId: parent.itemId, sourceDigest, sourceKey, create: (item) => {
289
+ assertCurrent();
290
+ const current = this.replySource(id, sourceDigest), currentParent = this.parent(parent.itemId), currentSupplier = this.parentSupplier(currentParent);
291
+ if (acceptanceCanonicalJson(immutableReply(current)) !== acceptanceCanonicalJson(immutableReply(frozenSource))
292
+ || current.executionParentItemId !== parent.itemId || current.state !== 'pending'
293
+ || acceptanceCanonicalJson(currentParent) !== acceptanceCanonicalJson(parent)
294
+ || acceptanceCanonicalJson(currentSupplier) !== acceptanceCanonicalJson(supplier))
295
+ continuationFail('reply changed before admission');
296
+ const plan = createReplyContinuationPlan(item, current, parent, supplier, this.config, this.assertActivation(activation), this.schemas());
297
+ const planJson = continuationCanonical(plan);
298
+ assertCurrent();
299
+ return Object.freeze({ itemId: item.id, parentItemId: parent.itemId, rootItemId: parent.rootItemId, sourceKey, sourceDigest,
300
+ inputText: current.inputText, inputSha256: current.inputSha256, parentReportRevision: parent.reportRevision,
301
+ parentOutcomeRevision: parent.outcomeRevision, planJson, planDigest: continuationDigest(planJson), createdAt: item.createdAt });
302
+ } });
303
+ }
304
+ currentPlan(item) {
305
+ const record = this.store.getContinuation(item.id);
306
+ if (!record)
307
+ continuationFail('frozen record unavailable');
308
+ const plan = parseStoredContinuationPlan(record.planJson), activation = this.assertActivation();
309
+ this.hooks.assertOwner(item);
310
+ this.hooks.assertMandate(item);
311
+ if (plan.parent.itemId !== record.parentItemId || plan.parent.rootItemId !== record.rootItemId
312
+ || plan.parent.reportRevision !== record.parentReportRevision || plan.parent.outcomeRevision !== record.parentOutcomeRevision
313
+ || plan.source.digest !== record.sourceDigest || plan.source.inputSha256 !== record.inputSha256 || plan.source.inputBytes !== record.inputBytes
314
+ || Date.now() >= plan.authorization.expiresAt)
315
+ continuationFail('frozen record association changed');
316
+ const parent = this.store.findItemById(record.parentItemId);
317
+ if (!parent || acceptanceCanonicalJson(this.proof(parent, plan.parent.production.occurrenceId)) !== acceptanceCanonicalJson(plan.parent.production))
318
+ continuationFail('parent proof changed');
319
+ const expected = isReplyContinuationPlan(plan) ? (() => {
320
+ const source = this.replySource(plan.source.origin.reply.id, record.sourceDigest);
321
+ if (source.state !== 'routed' || source.child?.itemId !== item.id || source.child.planDigest !== record.planDigest
322
+ || source.executionParentItemId !== record.parentItemId
323
+ || acceptanceCanonicalJson(immutableReply(source)) !== acceptanceCanonicalJson(plan.source.origin.reply))
324
+ continuationFail('reply child is not durably acknowledged');
325
+ return createReplyContinuationPlan(item, source, plan.parent, this.parentSupplier(plan.parent), this.config, activation, this.schemas());
326
+ })() : createContinuationPlan(item, { sourceDigest: record.sourceDigest, inputText: record.inputText, modelSelection: plan.source.modelSelection }, plan.parent, this.config, activation, this.schemas());
327
+ if (record.planJson !== continuationCanonical(expected))
328
+ continuationFail('frozen execution contract changed');
329
+ return plan;
330
+ }
331
+ slotAvailable() {
332
+ if (this.#runs.size > 0)
333
+ return false;
334
+ if (this.store.continuationDispatchUncertain(this.config.ownerContinuation.automationId))
335
+ return false;
336
+ const last = this.store.latestContinuationDispatch(this.config.ownerContinuation.automationId);
337
+ if (!last)
338
+ return true;
339
+ if (!last.dispatch.occurrenceId)
340
+ return false;
341
+ try {
342
+ const proof = this.proof(last.item, last.dispatch.occurrenceId), plan = parseStoredContinuationPlan(last.continuation.planJson);
343
+ return proof.immutableContext.definitionHash === plan.activation.definitionHash
344
+ && proof.immutableContext.definitionVersion === plan.activation.definitionVersion
345
+ && proof.immutableContext.owner === CONTINUATION_OWNER;
346
+ }
347
+ catch {
348
+ return false;
349
+ }
350
+ }
351
+ validateWake(item) {
352
+ try {
353
+ if (!this.slotAvailable() || item.observed || item.state !== 'queued' || item.attempt !== 0 || item.maxAttempts !== 1)
354
+ return false;
355
+ const dispatch = this.store.getDispatch(item.id);
356
+ if (!dispatch || dispatch.state !== 'pending' || dispatch.generation !== 0 || dispatch.automationId !== this.config.ownerContinuation.automationId)
357
+ return false;
358
+ this.currentPlan(item);
359
+ return this.isWakeCurrent(item);
360
+ }
361
+ catch {
362
+ return false;
363
+ }
364
+ }
365
+ /** No peer calls: closes disposal during the final foreign wake check. */
366
+ isWakeCurrent(item) {
367
+ const scope = this.config.scope, settings = this.config.ownerContinuation;
368
+ if (this.#closed || this.#controller.signal.aborted || this.store.closed || this.#activation?.stage !== 'established'
369
+ || this.#runs.size > 0 || !scope || !settings || Date.now() >= Math.min(scope.expiresAt, settings.expiresAt))
370
+ return false;
371
+ const record = this.store.getContinuation(item.id);
372
+ if (!record)
373
+ return false;
374
+ const plan = parseStoredContinuationPlan(record.planJson);
375
+ if (isReplyContinuationPlan(plan) && this.hooks.isReplyConsumerCurrent?.() !== true)
376
+ return false;
377
+ return !this.#closed && !this.#controller.signal.aborted && !this.store.closed;
378
+ }
379
+ wakePending() {
380
+ if (this.#closed || this.#waking)
381
+ return;
382
+ this.#waking = true;
383
+ try {
384
+ const automationId = this.config.ownerContinuation.automationId, stored = this.store.readCanonicalMeta(PENDING_CURSOR_KEY);
385
+ let after;
386
+ if (stored !== undefined) {
387
+ if (!stored || typeof stored !== 'object' || Array.isArray(stored))
388
+ continuationFail('pending cursor malformed');
389
+ const cursor = stored;
390
+ if (Object.keys(cursor).sort().join() !== 'after,automationId' || cursor.automationId !== automationId)
391
+ continuationFail('pending cursor scope differs');
392
+ if (cursor.after !== null) {
393
+ if (!cursor.after || typeof cursor.after !== 'object' || Array.isArray(cursor.after))
394
+ continuationFail('pending cursor malformed');
395
+ const value = cursor.after;
396
+ if (Object.keys(value).sort().join() !== 'createdAt,id' || !Number.isSafeInteger(value.createdAt) || Number(value.createdAt) < 1
397
+ || typeof value.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(value.id))
398
+ continuationFail('pending cursor position invalid');
399
+ after = { createdAt: Number(value.createdAt), id: value.id };
400
+ }
401
+ }
402
+ const page = this.store.listPendingContinuations(automationId, 10, after);
403
+ let sent = false;
404
+ for (const { item } of page) {
405
+ after = { createdAt: item.createdAt, id: item.id };
406
+ this.retireCancelledReply(item);
407
+ if (this.validateWake(item) && this.hooks.wake(item)) {
408
+ sent = true;
409
+ break;
410
+ }
411
+ }
412
+ // Each native wake examines at most ten rows. A durable keyset cursor
413
+ // reaches live rows behind stale owner generations and survives restarts.
414
+ this.store.setMeta(PENDING_CURSOR_KEY, continuationCanonical({ automationId, after: sent || page.length === 10 ? after : null }));
415
+ }
416
+ catch {
417
+ this.#lastError = 'continuation-wake-unavailable';
418
+ }
419
+ finally {
420
+ this.#waking = false;
421
+ }
422
+ }
423
+ retireCancelledReply(item) {
424
+ const record = this.store.getContinuation(item.id);
425
+ if (!record)
426
+ return;
427
+ const plan = parseStoredContinuationPlan(record.planJson);
428
+ if (!isReplyContinuationPlan(plan))
429
+ return;
430
+ try {
431
+ this.replySource(plan.source.origin.reply.id, record.sourceDigest);
432
+ }
433
+ catch (error) {
434
+ if (!(error && typeof error === 'object' && 'code' in error && error.code === 'work-item-reply-cancelled'))
435
+ return;
436
+ // A durable source cancellation only reduces local authority. It is not
437
+ // proof that any already-dispatched occurrence or effect has stopped.
438
+ const retired = this.store.withdrawByDedupKey(item.dedupKey, { sourceId: 'owner-report-reply', externalId: item.externalId, observedAt: Date.now() });
439
+ this.retire(retired.items.map(row => row.id));
440
+ }
441
+ }
442
+ assertRun(run) {
443
+ if (this.#closed || this.#controller.signal.aborted || run.input.signal.aborted || run.controller.signal.aborted
444
+ || this.#runs.get(run.input.occurrenceId) !== run)
445
+ continuationFail('invocation revoked');
446
+ const item = this.store.findItemById(run.itemId), dispatch = this.store.getDispatch(run.itemId);
447
+ if (!item || item.observed || item.state === 'dropped' || !dispatch || dispatch.state !== 'dispatched'
448
+ || dispatch.occurrenceId !== run.input.occurrenceId || this.currentPlan(item).digest !== run.plan.digest)
449
+ continuationFail('item or occurrence changed');
450
+ const claim = this.store.activeClaim(item.id), now = Date.now();
451
+ if (run.phase === 'before-claim') {
452
+ if (claim?.runId === run.input.occurrenceId && claim.leasedUntil > now)
453
+ run.phase = 'claimed';
454
+ else if (item.state !== 'queued' || item.attempt !== 0 || claim)
455
+ continuationFail('claim eligibility changed');
456
+ }
457
+ if (run.phase === 'claimed' && (!claim || claim.runId !== run.input.occurrenceId || claim.leasedUntil <= now
458
+ || item.leaseRunId !== run.input.occurrenceId || item.leaseUntil === null || item.leaseUntil <= now))
459
+ continuationFail('claim expired or changed');
460
+ if (run.phase === 'reported') {
461
+ const report = this.store.lastReport(item.id);
462
+ if (!report || report.revision !== run.reportRevision || report.status !== run.reportStatus || claim
463
+ || item.lastReportStatus !== run.reportStatus || !['ready', 'blocked', 'failed'].includes(item.state))
464
+ continuationFail('report ownership changed');
465
+ }
466
+ return item;
467
+ }
468
+ assertTool(occurrenceId, name) {
469
+ const run = this.#runs.get(occurrenceId);
470
+ if (!run)
471
+ continuationFail('tool requires live Host invocation');
472
+ this.assertRun(run);
473
+ if (!run.plan.contract.allowedTools.includes(name) || run.phase === 'reported')
474
+ continuationFail('tool outside current execution phase');
475
+ if (name !== 'work_item_claim' && run.phase !== 'claimed')
476
+ continuationFail('tool requires active claim');
477
+ }
478
+ reported(occurrenceId, status, revision) {
479
+ const run = this.#runs.get(occurrenceId);
480
+ if (!run || status === 'progress')
481
+ return;
482
+ if (!['ready', 'blocked', 'failed'].includes(status))
483
+ continuationFail('one-shot cannot wait or resume');
484
+ run.phase = 'reported';
485
+ run.reportStatus = status;
486
+ run.reportRevision = revision;
487
+ }
488
+ retire(itemIds) {
489
+ for (const run of this.#runs.values())
490
+ if (itemIds.includes(run.itemId))
491
+ run.controller.abort('continuation-withdrawn');
492
+ }
493
+ readContext(occurrenceId, input) {
494
+ this.assertTool(occurrenceId, 'work_item_read_context');
495
+ const run = this.#runs.get(occurrenceId), item = this.assertRun(run), record = this.store.getContinuation(item.id);
496
+ const kind = input.kind ?? 'input', offset = input.offset ?? 0, maxBytes = input.maxBytes ?? 4096;
497
+ if (!['input', 'parent-brief', 'parent-report', 'parent-outcome'].includes(kind)
498
+ || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > 8192
499
+ || input.revision !== undefined && (!Number.isSafeInteger(input.revision) || input.revision < 1)
500
+ || !['parent-report', 'parent-outcome'].includes(kind) && input.revision !== undefined)
501
+ continuationFail('context selector invalid');
502
+ let text, revision;
503
+ if (kind === 'input')
504
+ text = record.inputText;
505
+ else if (kind === 'parent-brief')
506
+ text = this.store.continuationParentBrief(item.id);
507
+ else {
508
+ const cutoff = kind === 'parent-report' ? record.parentReportRevision : record.parentOutcomeRevision;
509
+ revision = input.revision ?? cutoff ?? undefined;
510
+ if (revision === undefined || cutoff === null || revision > cutoff)
511
+ continuationFail('context history unavailable');
512
+ const row = kind === 'parent-report' ? this.store.continuationParentReports(item.id, revision - 1, 1)[0] : this.store.continuationParentOutcomes(item.id, revision - 1, 1)[0];
513
+ if (!row || row.revision !== revision)
514
+ continuationFail('context revision unavailable');
515
+ // No idempotency keys, delivery bindings or hidden authority are exposed.
516
+ text = kind === 'parent-report' ? ('summary' in row ? row.summary : continuationFail('report unavailable')) : acceptanceCanonicalJson({ revision: row.revision, ...('settledBy' in row ? {
517
+ settledBy: row.settledBy, executionStatus: row.executionStatus, objectiveStatus: row.objectiveStatus, evidenceState: row.evidenceState, evidenceReason: row.evidenceReason, settledAt: row.settledAt
518
+ } : {}) });
519
+ }
520
+ const bytes = Buffer.from(text, 'utf8');
521
+ if (offset > bytes.length || offset < bytes.length && (bytes[offset] & 0xc0) === 0x80)
522
+ continuationFail('context offset is not UTF-8 boundary');
523
+ let end = Math.min(bytes.length, offset + maxBytes);
524
+ while (end > offset && end < bytes.length && (bytes[end] & 0xc0) === 0x80)
525
+ end--;
526
+ this.assertRun(run);
527
+ return Object.freeze({ itemId: item.id, parentItemId: record.parentItemId, rootItemId: record.rootItemId, kind,
528
+ ...(revision === undefined ? {} : { revision }), parentReportRevision: record.parentReportRevision, parentOutcomeRevision: record.parentOutcomeRevision,
529
+ text: bytes.subarray(offset, end).toString('utf8'), fullTextDigest: continuationDigest(text), fullBytes: bytes.length, returnedBytes: end - offset, offset,
530
+ ...(end < bytes.length ? { nextOffset: end } : {}), ...(kind === 'input' ? { sourceDigest: record.sourceDigest } : {}),
531
+ ...(revision !== undefined && revision > 1 ? { previousRevision: revision - 1 } : {}),
532
+ ...(kind === 'parent-report' && revision !== undefined && record.parentReportRevision !== null && revision < record.parentReportRevision ? { nextRevision: revision + 1 } : {}),
533
+ ...(kind === 'parent-outcome' && revision !== undefined && record.parentOutcomeRevision !== null && revision < record.parentOutcomeRevision ? { nextRevision: revision + 1 } : {}) });
534
+ }
535
+ async execute(input) {
536
+ let started = false, run, itemId;
537
+ try {
538
+ const activation = this.assertActivation(), scope = this.config.scope, settings = this.config.ownerContinuation;
539
+ const item = this.store.findItemByOccurrence(input.occurrenceId), dispatch = item && this.store.getDispatch(item.id);
540
+ itemId = item?.id;
541
+ if (!item || !dispatch || item.state !== 'queued' || item.attempt !== 0 || item.observed || this.#runs.size > 0
542
+ || dispatch.state !== 'dispatched' || dispatch.generation !== 0 || input.executionMode !== 'production' || input.automationId !== settings.automationId
543
+ || input.definitionHash !== activation.definitionHash || input.activationNonce !== activation.activationNonce || input.catalogDigest !== activation.catalogDigest
544
+ || input.ownerRouteId !== scope.ownerRouteId || input.principal !== scope.principalId
545
+ || input.targetScope.workspace !== scope.workspace || input.targetScope.preset !== scope.preset)
546
+ continuationFail('Host execution identity differs');
547
+ const plan = this.currentPlan(item);
548
+ run = { input, itemId: item.id, plan, phase: 'before-claim', controller: new AbortController() };
549
+ this.#runs.set(input.occurrenceId, run);
550
+ this.assertRun(run);
551
+ started = true;
552
+ const result = await this.automations().runAgentForHostExecution(input, plan.contract, { signal: AbortSignal.any([this.#controller.signal, run.controller.signal]), assertCurrent: () => { this.assertRun(run); } });
553
+ this.assertRun(run);
554
+ if (result.outcome === 'succeeded' && result.quiescent === true && run.phase === 'reported')
555
+ return {
556
+ outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe'
557
+ };
558
+ this.store.appendEvent(item.id, 'continuation-unknown', 'continuation-runtime', { occurrenceId: input.occurrenceId,
559
+ reason: result.quiescent !== true ? 'teardown-unconfirmed' : run.phase !== 'reported' ? 'report-missing' : 'agent-unsuccessful' });
560
+ this.#lastError = 'continuation-awaits-intervention';
561
+ return { outcome: result.quiescent === true && result.outcome !== 'unknown' ? 'failed' : 'unknown', failureClass: 'execution', failurePhase: 'host-execution',
562
+ failureCode: 'continuation-agent-incomplete', sideEffectState: 'unknown', retryability: 'unsafe' };
563
+ }
564
+ catch {
565
+ this.#lastError = started ? 'continuation-awaits-intervention' : 'continuation-execution-refused';
566
+ if (itemId && !this.store.closed)
567
+ this.store.appendEvent(itemId, started ? 'continuation-unknown' : 'continuation-refused', 'continuation-runtime', { occurrenceId: input.occurrenceId });
568
+ return started ? { outcome: 'unknown', failureClass: 'unknown', failurePhase: 'host-execution', failureCode: 'continuation-execution-unknown', sideEffectState: 'unknown', retryability: 'unsafe' }
569
+ : { outcome: 'failed', failureClass: 'policy', failurePhase: 'preflight', failureCode: 'continuation-execution-refused', sideEffectState: 'none', retryability: 'after-intervention' };
570
+ }
571
+ finally {
572
+ if (run && this.#runs.get(input.occurrenceId) === run)
573
+ this.#runs.delete(input.occurrenceId);
574
+ }
575
+ }
576
+ health() {
577
+ let ready = !this.#closed && this.#activation?.stage === 'established';
578
+ if (ready) {
579
+ try {
580
+ this.assertActivation();
581
+ }
582
+ catch {
583
+ ready = false;
584
+ }
585
+ }
586
+ let interventionRequired = false;
587
+ if (this.#runs.size === 0) {
588
+ try {
589
+ interventionRequired = !this.slotAvailable();
590
+ }
591
+ catch {
592
+ interventionRequired = true;
593
+ }
594
+ }
595
+ return Object.freeze({ enabled: true, ready: ready && !interventionRequired, running: this.#runs.size,
596
+ interventionRequired, lastError: interventionRequired ? 'continuation-awaits-intervention' : !ready ? 'continuation-activation-unavailable' : this.#lastError });
597
+ }
598
+ close() {
599
+ if (this.#closed)
600
+ return;
601
+ this.#closed = true;
602
+ this.#controller.abort('continuation-consumer-disposed');
603
+ this.#remove?.();
604
+ this.#remove = undefined;
605
+ }
606
+ }
607
+ //# sourceMappingURL=continuation.js.map