@dsh-enhanced/assistant-automations 0.1.23 → 0.1.30

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 (51) hide show
  1. package/README.md +18 -5
  2. package/lib/acceptance.d.ts +52 -0
  3. package/lib/acceptance.d.ts.map +1 -0
  4. package/lib/acceptance.js +2 -0
  5. package/lib/acceptance.js.map +1 -0
  6. package/lib/coordinator.d.ts +5 -0
  7. package/lib/coordinator.d.ts.map +1 -1
  8. package/lib/coordinator.js +21 -0
  9. package/lib/coordinator.js.map +1 -1
  10. package/lib/external-event.d.ts +32 -0
  11. package/lib/external-event.d.ts.map +1 -0
  12. package/lib/external-event.js +114 -0
  13. package/lib/external-event.js.map +1 -0
  14. package/lib/growth.d.ts +10 -0
  15. package/lib/growth.d.ts.map +1 -1
  16. package/lib/growth.js +49 -16
  17. package/lib/growth.js.map +1 -1
  18. package/lib/index.d.ts +6 -1
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +2 -1
  21. package/lib/index.js.map +1 -1
  22. package/lib/operator.d.ts +68 -2
  23. package/lib/operator.d.ts.map +1 -1
  24. package/lib/operator.js +922 -6
  25. package/lib/operator.js.map +1 -1
  26. package/lib/preparation.d.ts +52 -0
  27. package/lib/preparation.d.ts.map +1 -0
  28. package/lib/preparation.js +191 -0
  29. package/lib/preparation.js.map +1 -0
  30. package/lib/runner.d.ts +29 -0
  31. package/lib/runner.d.ts.map +1 -1
  32. package/lib/runner.js +72 -2
  33. package/lib/runner.js.map +1 -1
  34. package/lib/service.d.ts +37 -0
  35. package/lib/service.d.ts.map +1 -1
  36. package/lib/service.js +269 -60
  37. package/lib/service.js.map +1 -1
  38. package/lib/sqlite.d.ts +1 -1
  39. package/lib/sqlite.d.ts.map +1 -1
  40. package/lib/sqlite.js +77 -3
  41. package/lib/sqlite.js.map +1 -1
  42. package/lib/store.d.ts +55 -0
  43. package/lib/store.d.ts.map +1 -1
  44. package/lib/store.js +233 -14
  45. package/lib/store.js.map +1 -1
  46. package/lib/types.d.ts +3 -0
  47. package/lib/types.d.ts.map +1 -1
  48. package/lib/types.js.map +1 -1
  49. package/lib/version.d.ts +1 -1
  50. package/lib/version.js +1 -1
  51. package/package.json +29 -23
package/lib/service.js CHANGED
@@ -10,8 +10,10 @@ import { GROWTH_AUTOMATION_OWNER, GrowthAutomationStore, } from './growth.js';
10
10
  import { AutomationCoordinator, } from './coordinator.js';
11
11
  import { AutomationProposalManager, AutomationProposalStore } from './proposals.js';
12
12
  import { DshAutomationRunner, HostAutomationRunner, RoutedAutomationRunner } from './runner.js';
13
+ import { AutomationPreparationRunner } from './preparation.js';
13
14
  import { AutomationStore } from './store.js';
14
15
  import { registerAutomationTools } from './tools.js';
16
+ import { parseExternalEventEnvelope } from './external-event.js';
15
17
  import { isHostAutomationDefinition } from './types.js';
16
18
  function registrationOwnedByEvaluation(registration) {
17
19
  const candidate = registration;
@@ -174,11 +176,16 @@ export class AssistantAutomationsService extends Service {
174
176
  growthStore;
175
177
  proposals;
176
178
  policy;
179
+ preparationRunner;
177
180
  coordinator;
178
181
  hostExecutors = new HostAutomationExecutorRegistry();
179
182
  config;
180
183
  evaluationProducerGeneration = `assistant-automations:${randomUUID()}`;
181
184
  presentationProducerGeneration = `assistant-automations-presentation:${randomUUID()}`;
185
+ acceptanceProducerGeneration = `assistant-automations-acceptance:${randomUUID()}`;
186
+ acceptanceSink;
187
+ /** Once a live verifier requires acceptance, a transient detach must not reopen prompt submission. */
188
+ acceptanceRequired = false;
182
189
  evaluationSink;
183
190
  presentationSink;
184
191
  approvalDelivery;
@@ -205,6 +212,7 @@ export class AssistantAutomationsService extends Service {
205
212
  throw new Error('assistant-automations: assistantPolicy service is required');
206
213
  this.config = config;
207
214
  this.policy = policy;
215
+ this.preparationRunner = new AutomationPreparationRunner(ctx, policy);
208
216
  const initialDelivery = ctx.get('assistantDelivery');
209
217
  this.approvalDelivery = initialDelivery;
210
218
  this.workflowTemplateResolver = initialDelivery;
@@ -216,10 +224,13 @@ export class AssistantAutomationsService extends Service {
216
224
  const artifacts = new AutomationArtifactStore({ rootPath: config.runsPath, maxBytes: config.maxArtifactBytes });
217
225
  const runner = new PolicyBoundRunner(policy, new RoutedAutomationRunner(new DshAutomationRunner(ctx, policy, {
218
226
  allowUnbudgetedExecution: config.allowUnbudgetedExecution,
227
+ prepareAcceptance: input => this.prepareTaskAcceptance(input),
228
+ acceptanceEnabled: () => this.acceptanceSink !== undefined || this.acceptanceRequired,
219
229
  }), new HostAutomationRunner(this.hostExecutors, policy, {
220
230
  allowUnbudgetedExecution: config.allowUnbudgetedExecution,
221
231
  })));
222
232
  this.coordinator = new AutomationCoordinator({
233
+ beforeDispatch: () => this.revalidatePromotedWorkflows(true),
223
234
  store: this.store,
224
235
  artifacts,
225
236
  runner,
@@ -252,9 +263,15 @@ export class AssistantAutomationsService extends Service {
252
263
  ctx.inject(['assistantEvaluation'], evaluationCtx => {
253
264
  const evaluation = evaluationCtx.get('assistantEvaluation');
254
265
  this.evaluation = evaluation;
266
+ const unsubscribe = evaluation.onTrustedTaskChange?.(() => this.revalidatePromotedWorkflows());
267
+ this.revalidatePromotedWorkflows();
255
268
  return () => {
256
- if (this.evaluation === evaluation)
269
+ unsubscribe?.();
270
+ if (this.evaluation === evaluation) {
257
271
  this.evaluation = undefined;
272
+ if (this.active)
273
+ this.revalidatePromotedWorkflows();
274
+ }
258
275
  };
259
276
  });
260
277
  // Optional: when a learning plugin is composed, finished runs become evidence.
@@ -289,6 +306,7 @@ export class AssistantAutomationsService extends Service {
289
306
  ctx.effect(() => async () => {
290
307
  this.active = false;
291
308
  this.evaluationSink = undefined;
309
+ this.acceptanceSink = undefined;
292
310
  this.presentationSink?.dispose();
293
311
  this.coordinator.setEvaluationRecorder(undefined);
294
312
  await this.coordinator.stop();
@@ -301,10 +319,86 @@ export class AssistantAutomationsService extends Service {
301
319
  this.assertActive();
302
320
  return this.evaluationProducerGeneration;
303
321
  }
322
+ /**
323
+ * Generate a bounded, unverified Proactive draft in a fresh Session. The
324
+ * caller owns the durable decision claim and result persistence.
325
+ */
326
+ runPreparation(input, signal, assertCurrent) {
327
+ this.assertActive();
328
+ return this.preparationRunner.run(input, signal, assertCurrent);
329
+ }
304
330
  trustedDeliveryPresentationProducerGeneration() {
305
331
  this.assertActive();
306
332
  return this.presentationProducerGeneration;
307
333
  }
334
+ /** Private verifier producer generation; invalidated with this service. */
335
+ trustedAcceptanceProducerGeneration() {
336
+ this.assertActive();
337
+ return this.acceptanceProducerGeneration;
338
+ }
339
+ registerTaskAcceptanceSink(registration) {
340
+ this.assertActive();
341
+ const verifier = this.ctx.get('assistantVerifier', false);
342
+ if (registration.protocol !== 'assistant-verifier/host-producer/v1'
343
+ || registration.generation !== this.acceptanceProducerGeneration
344
+ || typeof registration.prepare !== 'function' || typeof registration.completed !== 'function'
345
+ || typeof verifier?.ownsTaskAcceptanceRegistration !== 'function'
346
+ || !verifier.ownsTaskAcceptanceRegistration(registration)) {
347
+ throw new AssistantAutomationsError('runtime-conflict', 'task acceptance registration is invalid');
348
+ }
349
+ if (this.acceptanceSink !== undefined) {
350
+ if (this.acceptanceSink.registration === registration) {
351
+ const token = this.acceptanceSink.token;
352
+ return () => { if (this.acceptanceSink?.token === token)
353
+ this.acceptanceSink = undefined; };
354
+ }
355
+ throw new AssistantAutomationsError('runtime-conflict', 'task acceptance sink is already registered');
356
+ }
357
+ const token = Symbol('assistant-automations.task-acceptance');
358
+ this.acceptanceSink = Object.freeze({ token, registration });
359
+ if (registration.requiresAcceptance === true)
360
+ this.acceptanceRequired = true;
361
+ let live = true;
362
+ return () => {
363
+ if (!live)
364
+ return;
365
+ live = false;
366
+ if (this.acceptanceSink?.token === token)
367
+ this.acceptanceSink = undefined;
368
+ };
369
+ }
370
+ async inspectAcceptedExecution(contract) {
371
+ this.assertActive();
372
+ return this.store.inspectAcceptedExecution(contract);
373
+ }
374
+ prepareTaskAcceptance(input) {
375
+ const sink = this.acceptanceSink;
376
+ if (sink === undefined) {
377
+ if (this.acceptanceRequired) {
378
+ throw new AssistantAutomationsError('runtime-conflict', 'required task acceptance verifier is unavailable');
379
+ }
380
+ return;
381
+ }
382
+ if (input.owner === undefined || input.binding === undefined) {
383
+ if (this.acceptanceRequired) {
384
+ throw new AssistantAutomationsError('missing-approval-route', 'required task acceptance lacks an authenticated Delivery owner');
385
+ }
386
+ return;
387
+ }
388
+ const handle = sink.registration.prepare(Object.freeze({ scope: Object.freeze(input.scope), owner: input.owner,
389
+ task: Object.freeze({ kind: 'automation-run', ref: `run-${input.taskId}` }), objective: input.objective, }));
390
+ if (handle === null) {
391
+ if (this.acceptanceRequired) {
392
+ throw new AssistantAutomationsError('runtime-conflict', 'required task acceptance profile is unavailable');
393
+ }
394
+ return;
395
+ }
396
+ this.store.bindTaskAcceptance({ taskId: input.taskId, contractId: handle.contractId, contractDigest: handle.contractDigest,
397
+ scope: input.scope, owner: input.owner, bindingId: input.binding.id, bindingVersion: input.binding.version,
398
+ bindingGeneration: input.binding.generation,
399
+ dispatchedAt: Date.now(),
400
+ });
401
+ }
308
402
  /**
309
403
  * Delivery creates this publisher and can revoke it on either service's
310
404
  * reload. Automations retains it only as a private coordinator adapter; no
@@ -857,50 +951,31 @@ export class AssistantAutomationsService extends Service {
857
951
  this.assertActive();
858
952
  const input = validateGrowthCanaryInspectionRequest(inputValue);
859
953
  const replay = this.growthStore.beginOperation('canary-inspection', input);
860
- if (replay !== undefined)
861
- return validateGrowthCanaryInspectionReceipt(replay, input);
862
- const artifact = this.growthStore.requireArtifact(input);
954
+ const artifact = replay === undefined
955
+ ? this.growthStore.requireArtifact(input)
956
+ : this.growthStore.requireCanaryInspectionArtifact(input);
957
+ if (replay !== undefined) {
958
+ const receipt = validateGrowthCanaryInspectionReceipt(replay, input);
959
+ if (receipt.outcome === 'passed')
960
+ this.requireCurrentCanaryProof(artifact);
961
+ return receipt;
962
+ }
863
963
  const run = artifact.canaryRunId === undefined ? undefined : this.store.getRun(artifact.canaryRunId);
864
964
  if (artifact.canaryTaskId === undefined || artifact.canaryRunId === undefined || run === undefined
865
965
  || run.taskId !== artifact.canaryTaskId || run.executionMode !== 'production') {
866
966
  throw new AssistantAutomationsError('runtime-conflict', 'canary inspection has no exact exposure proof');
867
967
  }
868
- let proof;
869
- const evaluation = this.evaluation;
870
- if (evaluation !== undefined && run.status === 'succeeded') {
871
- const scope = canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset });
872
- const matches = evaluation.query({
873
- scope,
874
- situation: `automation:${artifact.automationId}`,
875
- objectiveStatus: 'achieved',
876
- trust: 'trusted',
877
- limit: 100,
878
- }).filter(outcome => outcome.evidence.some(entry => entry.kind === 'automation-run' && entry.ref === run.id));
879
- if (matches.length === 1) {
880
- proof = evaluation.getTrustedOutcome({ scope, outcomeId: matches[0].id });
881
- }
882
- }
883
- const passed = proof !== undefined && proof.trust === 'trusted'
884
- && proof.objectiveStatus === 'achieved'
885
- && proof.evidence.some(entry => entry.kind === 'automation-run' && entry.ref === run.id);
968
+ const proof = run.status === 'succeeded' ? this.currentCanaryProof(artifact) : undefined;
969
+ const passed = proof !== undefined;
886
970
  const failed = run.status !== 'succeeded';
887
- const evaluationDigest = !passed ? undefined : growthObjectDigest({
888
- contract: 'assistant-automations-growth-evaluation-proof/v1',
889
- id: proof.id,
890
- scopeKey: proof.scopeKey,
891
- situation: proof.situation,
892
- objectiveStatus: proof.objectiveStatus,
893
- trust: proof.trust,
894
- evidence: proof.evidence,
895
- occurredAt: proof.occurredAt,
896
- evaluator: proof.evaluator,
897
- });
971
+ const evaluationDigest = proof?.projection.digest;
898
972
  if (passed) {
899
973
  this.growthStore.recordCanaryEvaluation({
900
974
  artifactId: artifact.artifactId,
901
975
  runId: run.id,
902
- evaluationId: proof.id,
976
+ evaluationId: proof.triggerOutcomeId,
903
977
  evaluationDigest: evaluationDigest,
978
+ proof: proof,
904
979
  });
905
980
  }
906
981
  const receipt = withGrowthPortReceiptDigest({
@@ -919,6 +994,110 @@ export class AssistantAutomationsService extends Service {
919
994
  });
920
995
  return validateGrowthCanaryInspectionReceipt(this.growthStore.completeOperation('canary-inspection', input, receipt), input);
921
996
  }
997
+ currentCanaryProof(artifact) {
998
+ if (artifact.canaryRunId === undefined || this.evaluation === undefined
999
+ || typeof this.evaluation.getTrustedAutomationRunLearningProjection !== 'function')
1000
+ return undefined;
1001
+ const scope = canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset });
1002
+ const proof = this.evaluation.getTrustedAutomationRunLearningProjection({ scope, runId: artifact.canaryRunId });
1003
+ if (proof === undefined || proof.scope.workspace !== scope.workspace || proof.scope.preset !== scope.preset
1004
+ || proof.situation !== `automation:${artifact.automationId}`
1005
+ || proof.projection.subjectKind !== 'automation-run' || proof.projection.subjectRef !== artifact.canaryRunId
1006
+ || proof.projection.disposition !== 'upsert' || proof.execution?.status !== 'succeeded'
1007
+ || proof.objective?.status !== 'achieved')
1008
+ return undefined;
1009
+ return proof;
1010
+ }
1011
+ requireCurrentCanaryProof(artifact) {
1012
+ const current = this.currentCanaryProof(artifact);
1013
+ const saved = artifact.canaryEvaluationProof;
1014
+ // Legacy append-only proofs deliberately fail closed. Never upgrade an old
1015
+ // inspection receipt to a different canonical revision during promotion.
1016
+ if (current === undefined || saved === undefined
1017
+ || saved.scopeKey !== current.scopeKey
1018
+ || saved.projection.subjectKind !== current.projection.subjectKind
1019
+ || saved.projection.subjectRef !== current.projection.subjectRef
1020
+ || saved.projection.version !== current.projection.version
1021
+ || saved.projection.digest !== current.projection.digest
1022
+ || saved.projection.disposition !== 'upsert'
1023
+ || artifact.canaryEvaluationDigest !== current.projection.digest) {
1024
+ throw new AssistantAutomationsError('runtime-conflict', 'canonical canary evidence changed or is unavailable');
1025
+ }
1026
+ return current;
1027
+ }
1028
+ workflowInvalidationListeners = new Set();
1029
+ onWorkflowEvidenceInvalidated(listener) {
1030
+ this.workflowInvalidationListeners.add(listener);
1031
+ for (const artifact of this.growthStore.listDeployedArtifacts()) {
1032
+ const version = this.growthStore.rollbackVersion(artifact.experimentId);
1033
+ if (artifact.state === 'rolled-back' && version !== undefined)
1034
+ listener(artifact.experimentId, { id: artifact.artifactId, version, digest: artifact.definitionHash });
1035
+ }
1036
+ return () => { this.workflowInvalidationListeners.delete(listener); };
1037
+ }
1038
+ /** Revalidate deployed proof before scheduling, on authoritative writes, and after restart. */
1039
+ revalidatePromotedWorkflows(strict = false) {
1040
+ let unsafe = false;
1041
+ for (let artifact of this.growthStore.listDeployedArtifacts()) {
1042
+ try {
1043
+ if (artifact.state === 'rolled-back') {
1044
+ const version = this.growthStore.rollbackVersion(artifact.experimentId);
1045
+ if (version !== undefined)
1046
+ for (const listener of this.workflowInvalidationListeners)
1047
+ listener(artifact.experimentId, { id: artifact.artifactId, version, digest: artifact.definitionHash });
1048
+ continue;
1049
+ }
1050
+ if (artifact.state === 'canary-pending') {
1051
+ const live = this.store.get(artifact.automationId);
1052
+ if (live?.status !== 'active' || live.version !== artifact.definitionVersion + 1
1053
+ || live.owner !== GROWTH_AUTOMATION_OWNER || this.store.getDefinitionHash(live.id) !== artifact.definitionHash)
1054
+ continue;
1055
+ if (this.currentCanaryProof(artifact) !== undefined)
1056
+ continue;
1057
+ // Recover activation whose acknowledgement was lost before examining its proof.
1058
+ const recoveredRequest = {
1059
+ contractVersion: 1, operationId: `${artifact.experimentId}:promotion`,
1060
+ experimentId: artifact.experimentId, candidateId: artifact.candidateId,
1061
+ candidateRevision: artifact.candidateRevision, candidateDigest: artifact.candidateDigest,
1062
+ artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion, artifactDigest: artifact.definitionHash,
1063
+ };
1064
+ artifact = this.growthStore.completePromotion({ automation: live, request: recoveredRequest,
1065
+ receipt: withGrowthPortReceiptDigest({ ...growthIdentity(recoveredRequest),
1066
+ artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion,
1067
+ artifactDigest: artifact.definitionHash, outcome: 'promoted',
1068
+ resultingArtifactVersion: live.version, resultingArtifactDigest: artifact.definitionHash, }),
1069
+ });
1070
+ }
1071
+ if (artifact.state !== 'promoted')
1072
+ continue;
1073
+ // Deployed artifacts require current exact-run success, while a newer
1074
+ // positive owner judgement may strengthen an already successful proof.
1075
+ if (this.currentCanaryProof(artifact) !== undefined)
1076
+ continue;
1077
+ this.rollbackWorkflowAutomation({
1078
+ contractVersion: 1, operationId: `${artifact.experimentId}:rollback`,
1079
+ experimentId: artifact.experimentId, candidateId: artifact.candidateId,
1080
+ candidateRevision: artifact.candidateRevision, candidateDigest: artifact.candidateDigest,
1081
+ artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion,
1082
+ artifactDigest: artifact.definitionHash,
1083
+ });
1084
+ for (const listener of this.workflowInvalidationListeners)
1085
+ listener(artifact.experimentId, { id: artifact.artifactId, version: artifact.definitionVersion, digest: artifact.definitionHash });
1086
+ }
1087
+ catch {
1088
+ // Keep the durable artifact / pending rollback for retry. A different
1089
+ // deployed definition is unrelated and must not be paused or block it.
1090
+ const live = this.store.get(artifact.automationId);
1091
+ if (live?.status === 'active' && live.owner === GROWTH_AUTOMATION_OWNER
1092
+ && this.store.getDefinitionHash(live.id) === artifact.definitionHash
1093
+ && (live.version === artifact.definitionVersion
1094
+ || (artifact.state === 'canary-pending' && live.version === artifact.definitionVersion + 1)))
1095
+ unsafe = true;
1096
+ }
1097
+ }
1098
+ if (strict && unsafe)
1099
+ throw new AssistantAutomationsError('runtime-conflict', 'exact deployed canary rollback must finish before dispatch');
1100
+ }
922
1101
  promoteWorkflowAutomation(inputValue) {
923
1102
  this.assertActive();
924
1103
  const input = validateGrowthAutomationArtifactRequest(inputValue);
@@ -930,33 +1109,44 @@ export class AssistantAutomationsService extends Service {
930
1109
  || artifact.canaryEvaluationId === undefined || artifact.canaryEvaluationDigest === undefined) {
931
1110
  throw new AssistantAutomationsError('runtime-conflict', 'promotion requires the exact canary artifact');
932
1111
  }
933
- const definition = this.requireLiveGrowthArtifact(artifact);
934
- if (definition === undefined || definition.owner !== GROWTH_AUTOMATION_OWNER
935
- || this.store.getDefinitionHash(definition.id) !== input.artifactDigest
936
- || !((definition.status === 'paused' && definition.version === input.artifactVersion)
937
- || (definition.status === 'active' && definition.version === input.artifactVersion + 1))) {
938
- throw new AssistantAutomationsError('runtime-conflict', 'promotion artifact changed before exact CAS');
939
- }
940
- const promoted = definition.status === 'active'
941
- ? definition
942
- : this.reconcileSystem({
943
- owner: GROWTH_AUTOMATION_OWNER,
944
- automationId: definition.id,
945
- idempotencyKey: boundedGrowthKey('growth-promote', input),
946
- desiredStatus: 'active',
947
- definition: definition.definition,
1112
+ const proof = this.requireCurrentCanaryProof(artifact);
1113
+ const fenced = this.evaluation.withTrustedCanonicalLearningWriterFence({
1114
+ scope: canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset }),
1115
+ scopeWatermark: proof.scopeWatermark,
1116
+ evidence: [{ ...proof.projection, disposition: 'upsert' }],
1117
+ }, () => {
1118
+ const definition = this.requireLiveGrowthArtifact(artifact);
1119
+ if (definition === undefined || definition.owner !== GROWTH_AUTOMATION_OWNER
1120
+ || this.store.getDefinitionHash(definition.id) !== input.artifactDigest
1121
+ || !((definition.status === 'paused' && definition.version === input.artifactVersion)
1122
+ || (definition.status === 'active' && definition.version === input.artifactVersion + 1))) {
1123
+ throw new AssistantAutomationsError('runtime-conflict', 'promotion artifact changed before exact CAS');
1124
+ }
1125
+ const promoted = definition.status === 'active'
1126
+ ? definition
1127
+ : this.reconcileSystem({
1128
+ owner: GROWTH_AUTOMATION_OWNER,
1129
+ automationId: definition.id,
1130
+ idempotencyKey: boundedGrowthKey('growth-promote', input),
1131
+ desiredStatus: 'active',
1132
+ definition: definition.definition,
1133
+ });
1134
+ const receipt = withGrowthPortReceiptDigest({
1135
+ ...growthIdentity(input),
1136
+ artifactId: input.artifactId,
1137
+ artifactVersion: input.artifactVersion,
1138
+ artifactDigest: input.artifactDigest,
1139
+ outcome: 'promoted',
1140
+ resultingArtifactVersion: promoted.version,
1141
+ resultingArtifactDigest: input.artifactDigest,
948
1142
  });
949
- const stored = this.growthStore.completePromotion({ request: input, automation: promoted });
950
- const receipt = withGrowthPortReceiptDigest({
951
- ...growthIdentity(input),
952
- artifactId: input.artifactId,
953
- artifactVersion: input.artifactVersion,
954
- artifactDigest: input.artifactDigest,
955
- outcome: 'promoted',
956
- resultingArtifactVersion: stored.definitionVersion,
957
- resultingArtifactDigest: stored.definitionHash,
1143
+ this.growthStore.completePromotion({ request: input, automation: promoted, receipt });
1144
+ return validateGrowthPromotionReceipt(this.growthStore.completeOperation('promotion', input, receipt), input);
958
1145
  });
959
- return validateGrowthPromotionReceipt(this.growthStore.completeOperation('promotion', input, receipt), input);
1146
+ if (!fenced.matched) {
1147
+ throw new AssistantAutomationsError('runtime-conflict', `canonical canary evidence fence: ${fenced.reason}`);
1148
+ }
1149
+ return fenced.value;
960
1150
  }
961
1151
  rollbackWorkflowAutomation(inputValue) {
962
1152
  this.assertActive();
@@ -984,7 +1174,6 @@ export class AssistantAutomationsService extends Service {
984
1174
  });
985
1175
  automation = this.store.get(automation.id);
986
1176
  }
987
- this.growthStore.completeRollback({ request: input, automation });
988
1177
  const receipt = withGrowthPortReceiptDigest({
989
1178
  ...growthIdentity(input),
990
1179
  artifactId: input.artifactId,
@@ -992,6 +1181,7 @@ export class AssistantAutomationsService extends Service {
992
1181
  artifactDigest: input.artifactDigest,
993
1182
  outcome: 'rolled-back',
994
1183
  });
1184
+ this.growthStore.completeRollback({ request: input, automation, receipt });
995
1185
  return validateGrowthRollbackReceipt(this.growthStore.completeOperation('rollback', input, receipt), input);
996
1186
  }
997
1187
  propose(agent, input) {
@@ -1203,6 +1393,11 @@ export class AssistantAutomationsService extends Service {
1203
1393
  }
1204
1394
  ingestExternal(input) {
1205
1395
  this.assertActive();
1396
+ const envelope = input.envelope === undefined ? undefined : parseExternalEventEnvelope(input.envelope);
1397
+ if (envelope !== undefined && (envelope.source.id !== input.sourceId || envelope.event.id !== input.eventId
1398
+ || envelope.event.occurredAt !== input.occurredAt || envelope.target.automationId !== input.automationId)) {
1399
+ throw new AssistantAutomationsError('invalid-input', 'external event envelope does not bind this ingest input');
1400
+ }
1206
1401
  const automation = this.store.get(input.automationId);
1207
1402
  const decision = this.policy.authorize({
1208
1403
  subject: {
@@ -1220,6 +1415,7 @@ export class AssistantAutomationsService extends Service {
1220
1415
  automationId: input.automationId,
1221
1416
  externalEventId: `${input.sourceId}:${input.eventId}`,
1222
1417
  occurredAt: input.occurredAt,
1418
+ ...(envelope === undefined ? {} : { envelope }),
1223
1419
  });
1224
1420
  }
1225
1421
  reconcileSystem(input) {
@@ -1259,6 +1455,19 @@ export class AssistantAutomationsService extends Service {
1259
1455
  this.assertActive();
1260
1456
  return this.store.inspectSystemOwned(input);
1261
1457
  }
1458
+ /** Host-only binding metadata for exact restart reconciliation; no prompt or task content. */
1459
+ inspectSystemOwnedActivation = (input) => {
1460
+ this.assertActive();
1461
+ const current = this.store.get(input.automationId);
1462
+ if (!current)
1463
+ return undefined;
1464
+ if (current.owner !== input.owner || current.definition.execution?.kind !== 'host')
1465
+ throw new AssistantAutomationsError('not-found', 'exact system-owned Host automation was not found');
1466
+ const definitionHash = this.store.getDefinitionHash(input.automationId);
1467
+ if (!definitionHash)
1468
+ throw new AssistantAutomationsError('not-found', 'system-owned definition hash is unavailable');
1469
+ return Object.freeze({ definitionHash, activationNonce: current.definition.execution.activationNonce, ownerRouteId: current.definition.execution.ownerRouteId });
1470
+ };
1262
1471
  /** Host-only bounded identity inventory; definitions and scopes never leave the store. */
1263
1472
  listSystemOwned(input) {
1264
1473
  this.assertActive();