@dsh-enhanced/assistant-delivery 0.1.65 → 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 (101) hide show
  1. package/README.md +128 -4
  2. package/lib/agent-runtime.d.ts +6 -2
  3. package/lib/agent-runtime.d.ts.map +1 -1
  4. package/lib/agent-runtime.js +60 -14
  5. package/lib/agent-runtime.js.map +1 -1
  6. package/lib/attention-audit-store.d.ts +54 -0
  7. package/lib/attention-audit-store.d.ts.map +1 -0
  8. package/lib/attention-audit-store.js +213 -0
  9. package/lib/attention-audit-store.js.map +1 -0
  10. package/lib/attention-enforcement.d.ts +75 -0
  11. package/lib/attention-enforcement.d.ts.map +1 -0
  12. package/lib/attention-enforcement.js +84 -0
  13. package/lib/attention-enforcement.js.map +1 -0
  14. package/lib/attention.d.ts +203 -0
  15. package/lib/attention.d.ts.map +1 -0
  16. package/lib/attention.js +288 -0
  17. package/lib/attention.js.map +1 -0
  18. package/lib/coordinator.d.ts.map +1 -1
  19. package/lib/coordinator.js +6 -0
  20. package/lib/coordinator.js.map +1 -1
  21. package/lib/feedback-command.d.ts +23 -0
  22. package/lib/feedback-command.d.ts.map +1 -1
  23. package/lib/feedback-command.js +176 -0
  24. package/lib/feedback-command.js.map +1 -1
  25. package/lib/foreground-observation.d.ts +21 -0
  26. package/lib/foreground-observation.d.ts.map +1 -1
  27. package/lib/foreground-observation.js.map +1 -1
  28. package/lib/foreground-termination.d.ts +31 -0
  29. package/lib/foreground-termination.d.ts.map +1 -0
  30. package/lib/foreground-termination.js +42 -0
  31. package/lib/foreground-termination.js.map +1 -0
  32. package/lib/in-process-reload-runtime.d.ts +87 -0
  33. package/lib/in-process-reload-runtime.d.ts.map +1 -0
  34. package/lib/in-process-reload-runtime.js +319 -0
  35. package/lib/in-process-reload-runtime.js.map +1 -0
  36. package/lib/index.d.ts +20 -6
  37. package/lib/index.d.ts.map +1 -1
  38. package/lib/index.js +9 -3
  39. package/lib/index.js.map +1 -1
  40. package/lib/native-termination.d.ts +12 -0
  41. package/lib/native-termination.d.ts.map +1 -0
  42. package/lib/native-termination.js +57 -0
  43. package/lib/native-termination.js.map +1 -0
  44. package/lib/native-web-owner.d.ts +7 -0
  45. package/lib/native-web-owner.d.ts.map +1 -1
  46. package/lib/native-web-owner.js +40 -10
  47. package/lib/native-web-owner.js.map +1 -1
  48. package/lib/operator-snapshot.d.ts +31 -3
  49. package/lib/operator-snapshot.d.ts.map +1 -1
  50. package/lib/operator-snapshot.js +120 -47
  51. package/lib/operator-snapshot.js.map +1 -1
  52. package/lib/progress-attention.d.ts +55 -0
  53. package/lib/progress-attention.d.ts.map +1 -0
  54. package/lib/progress-attention.js +128 -0
  55. package/lib/progress-attention.js.map +1 -0
  56. package/lib/service.d.ts +515 -4
  57. package/lib/service.d.ts.map +1 -1
  58. package/lib/service.js +2183 -130
  59. package/lib/service.js.map +1 -1
  60. package/lib/sqlite.d.ts +1 -1
  61. package/lib/sqlite.d.ts.map +1 -1
  62. package/lib/sqlite.js +102 -2
  63. package/lib/sqlite.js.map +1 -1
  64. package/lib/store.d.ts +113 -2
  65. package/lib/store.d.ts.map +1 -1
  66. package/lib/store.js +450 -39
  67. package/lib/store.js.map +1 -1
  68. package/lib/types.d.ts +175 -0
  69. package/lib/types.d.ts.map +1 -1
  70. package/lib/types.js.map +1 -1
  71. package/lib/version.d.ts +1 -1
  72. package/lib/version.js +1 -1
  73. package/lib/work-item-engineering-context.d.ts +127 -0
  74. package/lib/work-item-engineering-context.d.ts.map +1 -0
  75. package/lib/work-item-engineering-context.js +151 -0
  76. package/lib/work-item-engineering-context.js.map +1 -0
  77. package/lib/work-item-feedback-schema.d.ts +2 -0
  78. package/lib/work-item-feedback-schema.d.ts.map +1 -0
  79. package/lib/work-item-feedback-schema.js +29 -0
  80. package/lib/work-item-feedback-schema.js.map +1 -0
  81. package/lib/work-item-feedback-store.d.ts +29 -0
  82. package/lib/work-item-feedback-store.d.ts.map +1 -0
  83. package/lib/work-item-feedback-store.js +111 -0
  84. package/lib/work-item-feedback-store.js.map +1 -0
  85. package/lib/work-item-feedback.d.ts +157 -0
  86. package/lib/work-item-feedback.d.ts.map +1 -0
  87. package/lib/work-item-feedback.js +2 -0
  88. package/lib/work-item-feedback.js.map +1 -0
  89. package/lib/work-item-replies-schema.d.ts +3 -0
  90. package/lib/work-item-replies-schema.d.ts.map +1 -0
  91. package/lib/work-item-replies-schema.js +45 -0
  92. package/lib/work-item-replies-schema.js.map +1 -0
  93. package/lib/work-item-replies-store.d.ts +43 -0
  94. package/lib/work-item-replies-store.d.ts.map +1 -0
  95. package/lib/work-item-replies-store.js +203 -0
  96. package/lib/work-item-replies-store.js.map +1 -0
  97. package/lib/work-item-replies.d.ts +93 -0
  98. package/lib/work-item-replies.d.ts.map +1 -0
  99. package/lib/work-item-replies.js +15 -0
  100. package/lib/work-item-replies.js.map +1 -0
  101. package/package.json +24 -24
package/lib/store.js CHANGED
@@ -1,13 +1,28 @@
1
+ import { WorkItemFeedbackStore } from './work-item-feedback-store.js';
2
+ import { AttentionAuditStore } from './attention-audit-store.js';
3
+ import { WorkItemRepliesStore } from './work-item-replies-store.js';
4
+ import { normalizeForegroundTerminationObservation } from './foreground-termination.js';
1
5
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
2
6
  import { isAbsolute } from 'node:path';
3
7
  import { acceptanceCanonicalJson } from '@dsh-enhanced/task-acceptance-contract';
4
8
  import { ASSISTANT_GROWTH_CONTRACT_VERSION, WORKFLOW_TRACE_SOURCE_ID, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowAutomationTemplateContent, validateWorkflowTraceRevision, workflowAutomationTemplateContentDigest, workflowArgumentShapeDigest, workflowScopeKey, workflowTraceRevisionDigest, WORKFLOW_OWNER_ANCHORED_PLACEHOLDER_SCHEDULE, } from '@dsh-enhanced/assistant-growth-contract';
5
9
  import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, canonicalPrincipal, canonicalTarget, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
6
10
  import { feedbackDispatchRecoveryCode, isFeedbackDeliveryCommand, isFeedbackDispatchRecoveryCode, isExactDeliveryCommand, isLearningDeliveryCommand, isLearningDispatchRecoveryCode, isLearningRetryableFailureCode, isPermissionDeliveryCommand, isWorkflowDeliveryCommand, isWorkflowDispatchRecoveryCode, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, permissionDispatchRecoveryFromFailureCode, workflowDispatchRecoveryCode, } from './session-commands.js';
7
- import { parseFeedbackCommand, parseNaturalObjectiveFeedback } from './feedback-command.js';
11
+ import { parseFeedbackCommand, parseNaturalOwnerReply } from './feedback-command.js';
8
12
  import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
9
13
  import { openDeliveryDatabase } from './sqlite.js';
10
14
  import { deriveDeterministicallyDeidentifiedWorkflowTemplate, getDeterministicallyDeidentifiedWorkflowTemplate, } from './workflow-auto-producer.js';
15
+ function foregroundTerminationFromBytes(value) {
16
+ if (value === null)
17
+ return undefined;
18
+ if (!(value instanceof Uint8Array) || value.length > 1024)
19
+ throw new Error('invalid termination bytes');
20
+ const raw = new TextDecoder('utf-8', { fatal: true }).decode(value);
21
+ const normalized = normalizeForegroundTerminationObservation(JSON.parse(raw));
22
+ if (JSON.stringify(normalized) !== raw || !Buffer.from(raw).equals(value))
23
+ throw new Error('noncanonical termination bytes');
24
+ return normalized;
25
+ }
11
26
  function foregroundSourceByteLimit(value) {
12
27
  if (value === undefined)
13
28
  return 16_384;
@@ -1026,7 +1041,7 @@ function canonicalMetadata(input, kind) {
1026
1041
  function canonicalIntent(input, binding, maxTextBytes) {
1027
1042
  const allowed = [
1028
1043
  'idempotencyKey', 'bindingId', 'target', 'text', 'format', 'approval', 'modelPicker', 'permissionPicker',
1029
- 'replyToEventId', 'metadata',
1044
+ 'replyToEventId', 'metadata', 'attachment',
1030
1045
  ];
1031
1046
  if (input === null || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !allowed.includes(key))) {
1032
1047
  throw new DeliveryStoreError('invalid-intent', 'outbound intent shape is invalid');
@@ -1109,12 +1124,28 @@ function canonicalIntent(input, binding, maxTextBytes) {
1109
1124
  }
1110
1125
  }
1111
1126
  const metadata = canonicalMetadata(input.metadata, 'intent');
1127
+ const attachment = canonicalTextAttachment(input.attachment, format, maxTextBytes);
1112
1128
  return { idempotencyKey, bindingId, target, text: input.text, format,
1113
1129
  ...(approval === undefined ? {} : { approval }),
1114
1130
  ...(modelPicker === undefined ? {} : { modelPicker }),
1115
1131
  ...(permissionPicker === undefined ? {} : { permissionPicker }),
1116
1132
  ...(replyToEventId === undefined ? {} : { replyToEventId }),
1117
- ...(metadata === undefined ? {} : { metadata }) };
1133
+ ...(metadata === undefined ? {} : { metadata }),
1134
+ ...(attachment === undefined ? {} : { attachment }) };
1135
+ }
1136
+ /** WP19 stage 2b text attachment: plain intents only, digest-bound, within the text byte budget. */
1137
+ function canonicalTextAttachment(input, format, maxTextBytes) {
1138
+ if (input === undefined)
1139
+ return undefined;
1140
+ if (format !== 'plain' || input === null || typeof input !== 'object' || Array.isArray(input)
1141
+ || Object.keys(input).length !== 5 || Object.keys(input).some(key => !['kind', 'name', 'mediaType', 'text', 'sha256'].includes(key))
1142
+ || input.kind !== 'text' || typeof input.name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(input.name)
1143
+ || (input.mediaType !== 'text/plain' && input.mediaType !== 'text/markdown')
1144
+ || typeof input.text !== 'string' || input.text.length === 0 || Buffer.byteLength(input.text, 'utf8') > maxTextBytes
1145
+ || typeof input.sha256 !== 'string' || input.sha256 !== digest(input.text)) {
1146
+ throw new DeliveryStoreError('invalid-intent', 'outbound text attachment is invalid');
1147
+ }
1148
+ return { kind: 'text', name: input.name, mediaType: input.mediaType, text: input.text, sha256: input.sha256 };
1118
1149
  }
1119
1150
  const ownerRouteReceiptKeys = [
1120
1151
  'dsh.route.authority',
@@ -1365,6 +1396,9 @@ function leaseTargetJson(target) {
1365
1396
  return JSON.stringify({ kind: 'construction', sessionId: target.sessionId, conversation: canonicalConversation(target.conversation), principal: canonicalPrincipal(target.principal), workspace: target.workspace, agentPreset: target.agentPreset, generation: target.generation, ...(target.previous === undefined ? {} : { previous: { id: target.previous.id, conversation: canonicalConversation(target.previous.conversation), principal: canonicalPrincipal(target.previous.principal), workspace: target.previous.workspace, agentPreset: target.previous.agentPreset, sessionId: target.previous.sessionId, generation: target.previous.generation, version: target.previous.version, status: 'active' } }) });
1366
1397
  }
1367
1398
  export class DeliveryStore {
1399
+ workItemReplies;
1400
+ workItemFeedback;
1401
+ attentionAudit;
1368
1402
  database;
1369
1403
  /** Scan position only; every candidate is authorized again before a provider retry. */
1370
1404
  progressRecoveryCursors = new Map();
@@ -1379,6 +1413,9 @@ export class DeliveryStore {
1379
1413
  this.now = options.now ?? Date.now;
1380
1414
  this.codeGenerator = options.codeGenerator ?? (() => randomBytes(5).toString('hex').toUpperCase());
1381
1415
  this.maxTextBytes = options.maxTextBytes ?? 65_536;
1416
+ this.workItemReplies = new WorkItemRepliesStore(this.database, operation => this.transaction(operation), id => this.getInbox(id), id => this.getBinding(id), id => this.getOutbox(id), binding => this.getPrincipal(binding.principal));
1417
+ this.workItemFeedback = new WorkItemFeedbackStore(this.database, operation => this.transaction(operation), id => this.getInbox(id));
1418
+ this.attentionAudit = new AttentionAuditStore(this.database, operation => this.writerTransactionActive ? operation() : this.transaction(operation), () => this.now());
1382
1419
  const instance = this.database.prepare(`
1383
1420
  SELECT instance_id FROM delivery_instance WHERE singleton = 1
1384
1421
  `).get();
@@ -1411,6 +1448,25 @@ export class DeliveryStore {
1411
1448
  this.closed = true;
1412
1449
  throw new DeliveryStoreError('conflict', 'workflow trace source authority is missing or stale');
1413
1450
  }
1451
+ this.detachLinksOfInactiveOwners(now);
1452
+ }
1453
+ /**
1454
+ * Idempotent startup repair for links written before revoking an owner
1455
+ * detached them: an active linked principal whose root is not the active
1456
+ * owner yields no alias receipt today, but a later reactivation of that
1457
+ * owner record would silently restore it. Detach exactly those rows, as
1458
+ * `revokePrincipal` now does. No schema change; a no-op once clean. Links
1459
+ * whose owner was already revoked and reactivated before this repair are
1460
+ * indistinguishable from fresh links and are not touched.
1461
+ */
1462
+ detachLinksOfInactiveOwners(now) {
1463
+ this.transaction(() => {
1464
+ this.database.prepare(`
1465
+ UPDATE delivery_principals SET linked_to_id = NULL, updated_at = ?, version = version + 1
1466
+ WHERE role = 'linked' AND status = 'active' AND linked_to_id IS NOT NULL
1467
+ AND linked_to_id NOT IN (SELECT id FROM delivery_principals WHERE role = 'owner' AND status = 'active')
1468
+ `).run(now);
1469
+ });
1414
1470
  }
1415
1471
  instanceId() {
1416
1472
  this.assertOpen();
@@ -1715,6 +1771,84 @@ export class DeliveryStore {
1715
1771
  .get(principalHash(input));
1716
1772
  return row?.status === 'active';
1717
1773
  }
1774
+ /** The single active owner, or `undefined` while owner authority is absent/ambiguous. */
1775
+ activeOwnerPrincipal() {
1776
+ this.assertOpen();
1777
+ const rows = this.database.prepare(`
1778
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1779
+ FROM delivery_principals WHERE role = 'owner' AND status = 'active' ORDER BY id LIMIT 2
1780
+ `).all();
1781
+ return rows.length === 1 ? principalFromRow(rows[0]) : undefined;
1782
+ }
1783
+ /**
1784
+ * Activate one explicitly allowlisted peer bot as a linked principal of the
1785
+ * current owner. This never creates an owner and never revives a revoked row:
1786
+ * the peer lane is an exact owner-configured authorization, not pairing or a
1787
+ * silent upgrade of an existing identity.
1788
+ */
1789
+ ensurePeerPrincipal(input, ownerPrincipalId, options = {}) {
1790
+ this.assertOpen();
1791
+ const principal = canonicalPrincipal(input);
1792
+ const json = JSON.stringify(principal);
1793
+ const hash = principalHash(principal);
1794
+ return this.transaction(() => {
1795
+ const owner = this.database.prepare(`
1796
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1797
+ FROM delivery_principals WHERE id = ?
1798
+ `).get(ownerPrincipalId);
1799
+ if (owner === undefined || owner.role !== 'owner' || owner.status !== 'active') {
1800
+ throw new DeliveryStoreError('unauthorized-principal', 'peer agent requires the single active owner');
1801
+ }
1802
+ const existing = this.database.prepare(`
1803
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1804
+ FROM delivery_principals WHERE key_hash = ?
1805
+ `).get(hash);
1806
+ if (existing === undefined) {
1807
+ const id = `principal_${randomUUID()}`;
1808
+ const now = this.now();
1809
+ this.database.prepare(`
1810
+ INSERT INTO delivery_principals (
1811
+ id, key_hash, principal_json, role, status, linked_to_id, created_at, updated_at, version
1812
+ ) VALUES (?, ?, ?, 'linked', 'active', ?, ?, ?, 1)
1813
+ `).run(id, hash, json, owner.id, now, now);
1814
+ return principalFromRow(this.database.prepare(`
1815
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1816
+ FROM delivery_principals WHERE id = ?
1817
+ `).get(id));
1818
+ }
1819
+ const reauthorize = options.reauthorize === true;
1820
+ if (existing.role !== 'linked' || (existing.linked_to_id !== owner.id && !reauthorize)) {
1821
+ throw new DeliveryStoreError('unauthorized-principal', 'peer agent identity is not linked to the active owner');
1822
+ }
1823
+ if (existing.status !== 'active') {
1824
+ if (!reauthorize) {
1825
+ throw new DeliveryStoreError('unauthorized-principal', 'peer agent identity was revoked; re-authorize it explicitly');
1826
+ }
1827
+ // The caller re-read the exact allowlisted tuple for this message. A
1828
+ // previous owner handoff may have revoked the link; re-link it to the
1829
+ // current owner rather than stranding the configured peer forever.
1830
+ this.database.prepare(`
1831
+ UPDATE delivery_principals
1832
+ SET status = 'active', linked_to_id = ?, updated_at = ?, version = version + 1
1833
+ WHERE id = ?
1834
+ `).run(owner.id, this.now(), existing.id);
1835
+ return principalFromRow(this.database.prepare(`
1836
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1837
+ FROM delivery_principals WHERE id = ?
1838
+ `).get(existing.id));
1839
+ }
1840
+ if (existing.linked_to_id !== owner.id) {
1841
+ this.database.prepare(`
1842
+ UPDATE delivery_principals SET linked_to_id = ?, updated_at = ?, version = version + 1 WHERE id = ?
1843
+ `).run(owner.id, this.now(), existing.id);
1844
+ return principalFromRow(this.database.prepare(`
1845
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
1846
+ FROM delivery_principals WHERE id = ?
1847
+ `).get(existing.id));
1848
+ }
1849
+ return principalFromRow(existing);
1850
+ });
1851
+ }
1718
1852
  getPrincipal(input) {
1719
1853
  this.assertOpen();
1720
1854
  const row = this.database.prepare(`
@@ -1841,6 +1975,75 @@ export class DeliveryStore {
1841
1975
  return this.database.prepare(`${bindingSelect} WHERE status = 'active' ORDER BY created_at, id`)
1842
1976
  .all().map(bindingFromRow);
1843
1977
  }
1978
+ /**
1979
+ * Read-only: newest inbound `received_at` per candidate binding, counting any
1980
+ * generation of the same conversation from the same principal (so a `/new`
1981
+ * rotation keeps its history). Bindings without inbound are absent. Callers
1982
+ * bound the candidate list.
1983
+ */
1984
+ latestInboundAtByBinding(bindingIds) {
1985
+ this.assertOpen();
1986
+ const rows = this.database.prepare(`SELECT c.value AS binding_id, (SELECT max(i.received_at) FROM inbox_messages AS i
1987
+ JOIN conversation_bindings AS ib ON ib.id = i.binding_id
1988
+ WHERE ib.conversation_hash = cb.conversation_hash AND ib.principal_id = cb.principal_id) AS received_at
1989
+ FROM json_each(?) AS c JOIN conversation_bindings AS cb ON cb.id = c.value`)
1990
+ .all(JSON.stringify(bindingIds));
1991
+ return new Map(rows.flatMap(row => row.received_at === null ? [] : [[row.binding_id, row.received_at]]));
1992
+ }
1993
+ /**
1994
+ * Read-only: the newest still-unsettled earlier report of one work item from
1995
+ * one source (any of `sourceIds`: canonical and as-sent forms), so a router can keep the next revision behind it on one lane.
1996
+ * Unsettled means the claim-order states (`pending`, `retry_wait`,
1997
+ * `attempting`, `unknown_after_send`) without an operator cancel; terminal
1998
+ * rows never match. Scoped by the origin binding's workspace and the
1999
+ * `outbox_claim` status index. A linked row is a Host-routed
2000
+ * `dsh.linked-report` v1 row of this origin with a lower `reportRevision`.
2001
+ * An origin row is queued on the origin binding and is either a typed report
2002
+ * (`delivery_work_item_reports` target with a lower `reportRevision`) or a
2003
+ * plain background notice whose WP19 audit row records `purpose: 'result'`
2004
+ * and this `workItemId`; that plain lane carries no revision, so any such
2005
+ * committed row of the item counts as earlier.
2006
+ */
2007
+ findUnsettledWorkItemReport(input) {
2008
+ this.assertOpen();
2009
+ const sourceIds = JSON.stringify([...new Set(input.sourceIds)]);
2010
+ const row = this.database.prepare(`
2011
+ SELECT o.id, o.binding_id, json_extract(o.intent_json, '$.metadata."dsh.linked-report"') IS NOT NULL AS linked
2012
+ FROM outbox_messages AS o
2013
+ JOIN conversation_bindings AS b ON b.id = o.binding_id AND b.workspace = ?
2014
+ WHERE o.status IN ('pending', 'retry_wait', 'attempting', 'unknown_after_send')
2015
+ AND NOT EXISTS (
2016
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2017
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = o.id
2018
+ AND resolution.attempt_count = o.attempt_count AND resolution.resolution = 'cancel'
2019
+ )
2020
+ AND ((
2021
+ o.idempotency_key GLOB 'linked-report:*'
2022
+ AND json_extract(o.intent_json, '$.metadata."dsh.linked-report"') = 'v1'
2023
+ AND json_extract(o.intent_json, '$.metadata."dsh.linked-report.sourceId"') IN (SELECT value FROM json_each(?))
2024
+ AND json_extract(o.intent_json, '$.metadata."dsh.linked-report.originBindingId"') = ?
2025
+ AND json_extract(o.intent_json, '$.metadata."dsh.linked-report.itemId"') = ?
2026
+ AND CAST(json_extract(o.intent_json, '$.metadata."dsh.linked-report.reportRevision"') AS INTEGER) < ?
2027
+ ) OR (
2028
+ o.binding_id = ?
2029
+ AND json_type(o.intent_json, '$.metadata."dsh.linked-report"') IS NULL
2030
+ AND (EXISTS (
2031
+ SELECT 1 FROM delivery_work_item_reports AS report
2032
+ WHERE report.outbox_id = o.id AND json_extract(report.target_json, '$.sourceId') IN (SELECT value FROM json_each(?))
2033
+ AND json_extract(report.target_json, '$.itemId') = ?
2034
+ AND json_extract(report.target_json, '$.reportRevision') < ?
2035
+ ) OR (NOT EXISTS (SELECT 1 FROM delivery_work_item_reports AS report WHERE report.outbox_id = o.id)
2036
+ AND EXISTS (
2037
+ SELECT 1 FROM delivery_attention_audit AS audit
2038
+ WHERE audit.outbox_id = o.id AND audit.lane = 'background' AND audit.purpose = 'result'
2039
+ AND audit.source_id IN (SELECT value FROM json_each(?)) AND audit.work_item_id = ?
2040
+ )))
2041
+ ))
2042
+ ORDER BY o.rowid DESC LIMIT 1
2043
+ `).get(input.workspace, sourceIds, input.originBindingId, input.itemId, input.beforeRevision, input.originBindingId, sourceIds, input.itemId, input.beforeRevision, sourceIds, input.itemId);
2044
+ return row === undefined ? undefined
2045
+ : Object.freeze({ outboxId: row.id, bindingId: row.binding_id, route: row.linked === 1 ? 'linked' : 'origin' });
2046
+ }
1844
2047
  /** Snapshot one exact active owner binding under the Delivery write fence. */
1845
2048
  resolveApprovalRouteByBinding(input) {
1846
2049
  this.assertOpen();
@@ -1957,7 +2160,7 @@ export class DeliveryStore {
1957
2160
  if (typeof input.sessionId !== 'string' || typeof input.workspace !== 'string'
1958
2161
  || typeof input.preset !== 'string')
1959
2162
  return undefined;
1960
- return this.transaction(() => {
2163
+ const read = () => {
1961
2164
  const binding = this.getBindingBySession(input.sessionId);
1962
2165
  const principal = binding === undefined ? undefined : this.getBindingPrincipal(binding.id);
1963
2166
  if (binding?.status !== 'active' || binding.sessionId !== input.sessionId
@@ -1966,7 +2169,9 @@ export class DeliveryStore {
1966
2169
  || JSON.stringify(principal.principal) !== JSON.stringify(binding.principal))
1967
2170
  return undefined;
1968
2171
  return Object.freeze({ binding, principal });
1969
- });
2172
+ };
2173
+ // A source writer fence already owns this connection's snapshot and lock.
2174
+ return this.writerTransactionActive ? read() : this.transaction(read);
1970
2175
  }
1971
2176
  getModelSelection(input) {
1972
2177
  this.assertOpen();
@@ -2483,6 +2688,37 @@ export class DeliveryStore {
2483
2688
  });
2484
2689
  return this.getInbox(inboxId);
2485
2690
  }
2691
+ /**
2692
+ * Finish an Inbox without dispatching it to the Agent. Slice-5 owner-question
2693
+ * replies are admitted, applied by their registered consumer, and only then
2694
+ * marked processed. Idempotent for an already-processed Inbox bound to the
2695
+ * same binding; any later lifecycle state is a conflict.
2696
+ */
2697
+ markInboxProcessed(inboxId, bindingId) {
2698
+ this.assertOpen();
2699
+ const record = this.getInbox(inboxId);
2700
+ const binding = this.getBinding(bindingId);
2701
+ if (record === undefined || binding === undefined || binding.status !== 'active') {
2702
+ throw new DeliveryStoreError('not-found', 'inbox or active binding was not found');
2703
+ }
2704
+ if (conversationHash(record.envelope.conversation) !== conversationHash(binding.conversation)
2705
+ || principalHash(record.envelope.principal) !== principalHash(binding.principal)) {
2706
+ throw new DeliveryStoreError('conflict', 'inbox envelope does not belong to the binding');
2707
+ }
2708
+ if (record.status === 'processed' && record.bindingId === bindingId)
2709
+ return record;
2710
+ if (record.status !== 'received' && record.status !== 'authorized') {
2711
+ throw new DeliveryStoreError('conflict', `cannot mark inbox processed in ${record.status}`);
2712
+ }
2713
+ const changed = this.database.prepare(`
2714
+ UPDATE inbox_messages SET status = 'processed', binding_id = ?, next_attempt_at = NULL,
2715
+ failure_code = NULL, claimed_by = NULL, fencing_token = NULL, lease_until = NULL, updated_at = ?
2716
+ WHERE id = ? AND status IN ('received', 'authorized')
2717
+ `).run(bindingId, this.now(), inboxId);
2718
+ if (changed.changes !== 1)
2719
+ throw new DeliveryStoreError('conflict', 'inbox process transition lost a race');
2720
+ return this.getInbox(inboxId);
2721
+ }
2486
2722
  /**
2487
2723
  * Establish a durable command boundary before `/stop` or `/new` is queued.
2488
2724
  *
@@ -2589,11 +2825,14 @@ export class DeliveryStore {
2589
2825
  if (!Number.isSafeInteger(cancelledCount)) {
2590
2826
  throw new DeliveryStoreError('conflict', 'inbox cancellation count is outside the safe integer range');
2591
2827
  }
2828
+ this.workItemFeedback.cancelBefore(bindingId, boundaryRow.admission_sequence);
2829
+ const replyChildItemIds = this.workItemReplies.cancelBefore(bindingId, boundaryRow.admission_sequence);
2592
2830
  return {
2593
2831
  cancelled: cancelledCount,
2594
2832
  dispatching: dispatchingRows.length,
2595
2833
  claimedInboxIds: claimedRows.map(row => row.id),
2596
2834
  dispatchingInboxIds: dispatchingRows.map(row => row.id),
2835
+ ...(replyChildItemIds.length === 0 ? {} : { replyChildItemIds }),
2597
2836
  };
2598
2837
  });
2599
2838
  }
@@ -3528,6 +3767,47 @@ export class DeliveryStore {
3528
3767
  scope: Object.freeze({ ...input.scope }), owner: Object.freeze({ ...input.owner }),
3529
3768
  binding: Object.freeze({ ...input.binding }), dispatchedAt: row.dispatched_at });
3530
3769
  }
3770
+ /** Internal Host read: the service supplies an actual unsettled-call task proof.
3771
+ * This pending lane does not weaken the separate terminal source reader. */
3772
+ readPendingForegroundToolInput(input) {
3773
+ this.assertOpen();
3774
+ const offset = input.offset ?? 0, limit = input.maxBytes ?? 4096;
3775
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 4 || limit > 8192) {
3776
+ throw new DeliveryStoreError('invalid-binding', 'foreground tool input byte page is invalid');
3777
+ }
3778
+ const read = () => {
3779
+ const task = this.inspectPendingForegroundToolTask(input.task);
3780
+ if (task === undefined || acceptanceCanonicalJson(task) !== acceptanceCanonicalJson(input.task))
3781
+ return undefined;
3782
+ const inbox = this.getInbox(task.inboxId);
3783
+ if (inbox === undefined || inbox.status !== 'claimed' || inbox.bindingId !== task.binding.id)
3784
+ return undefined;
3785
+ const model = this.database.prepare(`SELECT model_selection_state, model_provider, model_id, model_reasoning_effort
3786
+ FROM delivery_foreground_executions WHERE inbox_id = ? AND status = 'pending'`).get(task.inboxId);
3787
+ if (model === undefined || !['missing', 'frozen', 'inconsistent'].includes(model.model_selection_state)
3788
+ || model.model_selection_state === 'frozen' && (!model.model_provider || !model.model_id))
3789
+ return undefined;
3790
+ const modelSelectionState = model.model_selection_state;
3791
+ const modelSelection = modelSelectionState === 'frozen' ? Object.freeze({ provider: model.model_provider, model: model.model_id,
3792
+ ...(model.model_reasoning_effort === null ? {} : { reasoningEffort: model.model_reasoning_effort }) }) : undefined;
3793
+ const bytes = Buffer.from(inbox.envelope.text, 'utf8');
3794
+ if (offset > bytes.length || offset < bytes.length && (bytes[offset] & 0xc0) === 0x80) {
3795
+ throw new DeliveryStoreError('invalid-binding', 'foreground tool input offset is not a UTF-8 boundary');
3796
+ }
3797
+ let end = Math.min(bytes.length, offset + limit);
3798
+ while (end > offset && end < bytes.length && (bytes[end] & 0xc0) === 0x80)
3799
+ end--;
3800
+ const fullTextDigest = createHash('sha256').update(bytes).digest('hex');
3801
+ // Native call identity and page window are intentionally absent. Multiple
3802
+ // actual calls in the same claimed human task share one canonical source.
3803
+ const sourceDigest = digest(acceptanceCanonicalJson({ protocol: 'assistant-delivery/foreground-tool-input-source/v1',
3804
+ task, source: { envelopeHash: inbox.envelopeHash, envelope: inbox.envelope }, fullTextDigest, modelSelectionState,
3805
+ ...(modelSelection === undefined ? {} : { modelSelection }) }));
3806
+ return Object.freeze({ sourceDigest, modelSelectionState, ...(modelSelection === undefined ? {} : { modelSelection }), input: Object.freeze({ text: bytes.subarray(offset, end).toString('utf8'),
3807
+ fullTextDigest, fullBytes: bytes.length, returnedBytes: end - offset, offset, ...(end < bytes.length ? { nextOffset: end } : {}) }) });
3808
+ };
3809
+ return this.writerTransactionActive ? read() : this.transaction(read);
3810
+ }
3531
3811
  /** Immutable pre-prompt receipt for an authenticated owner foreground turn. */
3532
3812
  bindForegroundTaskExecution(input) {
3533
3813
  this.assertOpen();
@@ -3570,15 +3850,31 @@ export class DeliveryStore {
3570
3850
  || !Number.isSafeInteger(input.completedAt) || input.completedAt < 0) {
3571
3851
  throw new DeliveryStoreError('invalid-binding', 'foreground execution completion is invalid');
3572
3852
  }
3853
+ let termination;
3854
+ if (input.termination !== undefined) {
3855
+ try {
3856
+ termination = normalizeForegroundTerminationObservation(input.termination);
3857
+ }
3858
+ catch {
3859
+ throw new DeliveryStoreError('invalid-binding', 'foreground termination observation is invalid');
3860
+ }
3861
+ if (termination.kind === 'completed'
3862
+ ? input.status !== 'succeeded' || !input.quiescent || !termination.handleDisposed
3863
+ : input.status === 'succeeded')
3864
+ throw new DeliveryStoreError('invalid-binding', 'foreground termination contradicts completion');
3865
+ }
3866
+ const terminationJson = termination === undefined ? null : JSON.stringify(termination);
3573
3867
  const status = input.quiescent && input.status === 'succeeded' ? 'succeeded' : 'unknown';
3574
3868
  const quiescent = status === 'succeeded' ? 1 : 0;
3575
3869
  const changed = this.database.prepare(`UPDATE delivery_foreground_executions
3576
- SET status = ?, quiescent = ?, completed_at = ?, execution_ref = inbox_id
3577
- WHERE inbox_id = ? AND status = 'pending' AND dispatched_at <= ?`).run(status, quiescent, input.completedAt, input.inboxId, input.completedAt);
3870
+ SET status = ?, quiescent = ?, completed_at = ?, execution_ref = inbox_id, termination_json = ?
3871
+ WHERE inbox_id = ? AND status = 'pending' AND dispatched_at <= ?`).run(status, quiescent, input.completedAt, terminationJson, input.inboxId, input.completedAt);
3578
3872
  if (changed.changes !== 1) {
3579
- const current = this.database.prepare('SELECT status, quiescent, completed_at FROM delivery_foreground_executions WHERE inbox_id = ?')
3873
+ const current = this.database.prepare('SELECT status, quiescent, completed_at, CAST(termination_json AS BLOB) AS termination_bytes FROM delivery_foreground_executions WHERE inbox_id = ?')
3580
3874
  .get(input.inboxId);
3581
- if (current?.status === status && current.quiescent === quiescent && current.completed_at === input.completedAt)
3875
+ if (current?.status === status && current.quiescent === quiescent && current.completed_at === input.completedAt
3876
+ && (terminationJson === null ? current.termination_bytes === null
3877
+ : current.termination_bytes instanceof Uint8Array && Buffer.from(terminationJson).equals(current.termination_bytes)))
3582
3878
  return;
3583
3879
  throw new DeliveryStoreError('conflict', 'foreground execution is absent or its completion changed');
3584
3880
  }
@@ -3643,7 +3939,13 @@ export class DeliveryStore {
3643
3939
  return { authority, authorityHash, scopeKey };
3644
3940
  }
3645
3941
  foregroundSource(row, input, fence) {
3646
- const inbox = this.getInbox(row.inbox_id);
3942
+ let inbox;
3943
+ try {
3944
+ inbox = this.getInbox(row.inbox_id);
3945
+ }
3946
+ catch {
3947
+ return undefined;
3948
+ }
3647
3949
  const binding = this.getBinding(row.binding_id);
3648
3950
  if (!inbox || !binding || inbox.bindingId !== row.binding_id
3649
3951
  || row.workspace !== input.workspace || row.preset !== input.agentPreset
@@ -3658,8 +3960,15 @@ export class DeliveryStore {
3658
3960
  || !Number.isSafeInteger(row.completion_sequence) || row.completion_sequence < 1)
3659
3961
  return undefined;
3660
3962
  try {
3661
- const envelope = canonicalEnvelope(inbox.envelope, this.maxTextBytes);
3662
- if (digest(JSON.stringify(envelope)) !== inbox.envelopeHash
3963
+ const raw = this.database.prepare(`SELECT CAST(envelope_json AS BLOB) AS envelope_bytes,
3964
+ CAST(envelope_hash AS BLOB) AS hash_bytes FROM inbox_messages WHERE id = ?`).get(row.inbox_id);
3965
+ if (!raw || !(raw.envelope_bytes instanceof Uint8Array) || !(raw.hash_bytes instanceof Uint8Array))
3966
+ return undefined;
3967
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(raw.envelope_bytes);
3968
+ const envelope = canonicalEnvelope(JSON.parse(text), this.maxTextBytes);
3969
+ const canonical = JSON.stringify(envelope), envelopeHash = digest(canonical);
3970
+ if (!Buffer.from(canonical).equals(raw.envelope_bytes) || !Buffer.from(envelopeHash).equals(raw.hash_bytes)
3971
+ || envelopeHash !== inbox.envelopeHash
3663
3972
  || envelope.kind !== 'text' || parseDeliveryCommand({ ...envelope, kind: 'command' }) !== undefined
3664
3973
  || JSON.stringify(envelope.principal) !== JSON.stringify(binding.principal)
3665
3974
  || JSON.stringify(envelope.conversation) !== JSON.stringify(binding.conversation))
@@ -3668,6 +3977,17 @@ export class DeliveryStore {
3668
3977
  catch {
3669
3978
  return undefined;
3670
3979
  }
3980
+ let termination;
3981
+ try {
3982
+ termination = foregroundTerminationFromBytes(row.termination_bytes);
3983
+ if (termination !== undefined && (termination.kind === 'completed'
3984
+ ? row.status !== 'succeeded' || row.quiescent !== 1 || !termination.handleDisposed
3985
+ : row.status === 'succeeded'))
3986
+ return undefined;
3987
+ }
3988
+ catch {
3989
+ return undefined;
3990
+ }
3671
3991
  const execution = Object.freeze({ dispatchedAt: row.dispatched_at,
3672
3992
  status: row.status, quiescent: row.quiescent === 1, completedAt: row.completed_at,
3673
3993
  executionRef: row.execution_ref, modelSelectionState: row.model_selection_state,
@@ -3681,7 +4001,8 @@ export class DeliveryStore {
3681
4001
  owner: Object.freeze({ principalRecordId: row.principal_record_id, principalVersion: row.principal_version }),
3682
4002
  scope: Object.freeze({ workspace: row.workspace, preset: row.preset }),
3683
4003
  binding: Object.freeze({ id: row.binding_id, version: row.binding_version,
3684
- generation: row.binding_generation, sessionId: binding.sessionId }), execution };
4004
+ generation: row.binding_generation, sessionId: binding.sessionId }), execution,
4005
+ ...(termination === undefined ? {} : { termination }) };
3685
4006
  return Object.freeze({ ...content, sourceDigest: digest(acceptanceCanonicalJson({ ...content, envelopeHash: inbox.envelopeHash })) });
3686
4007
  }
3687
4008
  /** Host-only durable completion feed. Mutable delivery state never hides a completion. */
@@ -3701,7 +4022,7 @@ export class DeliveryStore {
3701
4022
  || !Number.isSafeInteger(after.sequence) || after.sequence < 0 || after.sequence > watermark)) {
3702
4023
  throw new DeliveryStoreError('invalid-binding', 'foreground source cursor is not bound to this owner and watermark');
3703
4024
  }
3704
- const rows = this.database.prepare(`SELECT * FROM delivery_foreground_executions
4025
+ const rows = this.database.prepare(`SELECT *, CAST(termination_json AS BLOB) AS termination_bytes FROM delivery_foreground_executions
3705
4026
  WHERE principal_record_id = ? AND principal_version = ? AND workspace = ? AND preset = ?
3706
4027
  AND status != 'pending' AND completion_sequence > ? AND completion_sequence <= ?
3707
4028
  ORDER BY completion_sequence LIMIT ?`).all(input.expectedOwner.principalRecordId, input.expectedOwner.principalVersion, input.workspace, input.agentPreset, after?.sequence ?? 0, watermark, limit + 1);
@@ -3725,7 +4046,7 @@ export class DeliveryStore {
3725
4046
  }
3726
4047
  const inspect = () => {
3727
4048
  const fence = this.foregroundSourceFence(input, authority);
3728
- const row = this.database.prepare(`SELECT * FROM delivery_foreground_executions
4049
+ const row = this.database.prepare(`SELECT *, CAST(termination_json AS BLOB) AS termination_bytes FROM delivery_foreground_executions
3729
4050
  WHERE inbox_id = ? AND status != 'pending'`).get(input.inboxId);
3730
4051
  return row === undefined ? undefined : this.foregroundSource(row, input, fence);
3731
4052
  };
@@ -3746,6 +4067,36 @@ export class DeliveryStore {
3746
4067
  // its owner check and snapshot without starting a nested BEGIN.
3747
4068
  return this.writerTransactionActive ? read() : this.transaction(read);
3748
4069
  }
4070
+ /** Original canonical input for new Host-observed terminal executions; never a replay grant. */
4071
+ readOwnerForegroundTaskInput(input, authority) {
4072
+ this.assertOpen();
4073
+ const offset = input.offset ?? 0, limit = input.maxBytes ?? 4096;
4074
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 4 || limit > 8192) {
4075
+ throw new DeliveryStoreError('invalid-binding', 'foreground input byte page is invalid');
4076
+ }
4077
+ const read = () => {
4078
+ const fence = this.foregroundSourceFence(input, authority);
4079
+ const row = this.database.prepare(`SELECT *, CAST(termination_json AS BLOB) AS termination_bytes FROM delivery_foreground_executions
4080
+ WHERE inbox_id = ? AND status != 'pending'`).get(input.inboxId);
4081
+ const source = row === undefined ? undefined : this.foregroundSource(row, input, fence);
4082
+ if (!source?.termination || source.sourceDigest !== input.expectedSourceDigest)
4083
+ return undefined;
4084
+ const bytes = Buffer.from(this.getInbox(source.inboxId).envelope.text, 'utf8');
4085
+ if (offset > bytes.length || offset < bytes.length && (bytes[offset] & 0xc0) === 0x80) {
4086
+ throw new DeliveryStoreError('invalid-binding', 'foreground input offset is not a UTF-8 boundary');
4087
+ }
4088
+ let end = Math.min(bytes.length, offset + limit);
4089
+ while (end > offset && end < bytes.length && (bytes[end] & 0xc0) === 0x80)
4090
+ end--;
4091
+ const fullTextDigest = createHash('sha256').update(bytes).digest('hex');
4092
+ const contentDigest = digest(acceptanceCanonicalJson({ protocol: 'assistant-delivery/owner-foreground-input/v1',
4093
+ sourceDigest: source.sourceDigest, inputDigest: fullTextDigest }));
4094
+ return Object.freeze({ source, sourceDigest: source.sourceDigest, contentDigest,
4095
+ input: Object.freeze({ text: bytes.subarray(offset, end).toString('utf8'), fullTextDigest,
4096
+ fullBytes: bytes.length, returnedBytes: end - offset, offset, ...(end < bytes.length ? { nextOffset: end } : {}) }) });
4097
+ };
4098
+ return this.writerTransactionActive ? read() : this.transaction(read);
4099
+ }
3749
4100
  /** Lock order: Delivery, then downstream Evaluation/Memory writer fences. */
3750
4101
  withOwnerForegroundTaskSourceFence(input, authority, callback) {
3751
4102
  if (typeof callback !== 'function')
@@ -3783,6 +4134,18 @@ export class DeliveryStore {
3783
4134
  return result;
3784
4135
  });
3785
4136
  }
4137
+ /** Internal Host composition seam. Keeps one Delivery transaction across mixed source locks. */
4138
+ withOwnerLearningSourceFence(input, authority, callback) {
4139
+ this.assertOpen();
4140
+ if (typeof callback !== 'function')
4141
+ throw new DeliveryStoreError('invalid-binding', 'learning source callback is invalid');
4142
+ return this.transaction(() => {
4143
+ this.foregroundSourceFence(input, authority);
4144
+ const result = callback();
4145
+ this.assertForegroundSourceSyncResult(result);
4146
+ return result;
4147
+ });
4148
+ }
3786
4149
  assertForegroundSourceSyncResult(result) {
3787
4150
  if (result !== null && (typeof result === 'object' || typeof result === 'function') && 'then' in result) {
3788
4151
  // The native method consumes a real rejected Promise without invoking an
@@ -3804,7 +4167,7 @@ export class DeliveryStore {
3804
4167
  });
3805
4168
  }
3806
4169
  foregroundSourceContent(input, fence, inputLimit, replyLimit) {
3807
- const row = this.database.prepare(`SELECT * FROM delivery_foreground_executions
4170
+ const row = this.database.prepare(`SELECT *, CAST(termination_json AS BLOB) AS termination_bytes FROM delivery_foreground_executions
3808
4171
  WHERE inbox_id = ? AND status != 'pending'`).get(input.inboxId);
3809
4172
  const source = row === undefined ? undefined : this.foregroundSource(row, input, fence);
3810
4173
  if (!source || source.sourceDigest !== input.expectedSourceDigest || source.execution.status !== 'succeeded'
@@ -4206,6 +4569,7 @@ export class DeliveryStore {
4206
4569
  text: input.text,
4207
4570
  format: input.format ?? 'plain',
4208
4571
  metadata: receiptMetadata,
4572
+ ...(input.attachment === undefined ? {} : { attachment: input.attachment }),
4209
4573
  }, binding, this.maxTextBytes);
4210
4574
  if (winner !== undefined) {
4211
4575
  if (winner.intentHash !== digest(JSON.stringify(expected))) {
@@ -4395,6 +4759,21 @@ export class DeliveryStore {
4395
4759
  .get(channel, account, providerMessageId);
4396
4760
  return row === undefined ? undefined : outboxFromRow(row);
4397
4761
  }
4762
+ /**
4763
+ * Exact provider-addressed lookup that refuses ambiguity. The schema's
4764
+ * unique index already prevents duplicates; this is defense in depth for a
4765
+ * reply binding that must never attach to the wrong message.
4766
+ */
4767
+ getUniqueOutboxByProviderMessage(channelInput, accountInput, providerMessageIdInput) {
4768
+ this.assertOpen();
4769
+ const channel = validateBindingText(channelInput, 'channel', 256);
4770
+ const account = validateBindingText(accountInput, 'account', 256);
4771
+ const providerMessageId = validateBindingText(providerMessageIdInput, 'providerMessageId', 512);
4772
+ const rows = this.database.prepare(`${outboxSelect}
4773
+ WHERE channel = ? AND account = ? AND provider_message_id = ? LIMIT 2`)
4774
+ .all(channel, account, providerMessageId);
4775
+ return rows.length === 1 ? outboxFromRow(rows[0]) : undefined;
4776
+ }
4398
4777
  getApprovalIntent(operationId, bindingId) {
4399
4778
  this.assertOpen();
4400
4779
  const operation = validateBindingText(operationId, 'operationId', 256);
@@ -5075,32 +5454,50 @@ export class DeliveryStore {
5075
5454
  SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
5076
5455
  FROM delivery_principals WHERE id = ?
5077
5456
  `).get(id);
5457
+ if (row.role === 'owner') {
5458
+ // A link is bound to the owner's activation epoch. Revoking the owner
5459
+ // detaches every still-active link rooted in it, so reactivating the
5460
+ // same owner record (a new row version) never silently restores an
5461
+ // alias: the owner must re-link. Detached principals stay active
5462
+ // ordinary principals, exactly like a freshly paired, unlinked one.
5463
+ this.database.prepare(`
5464
+ UPDATE delivery_principals SET linked_to_id = NULL, updated_at = ?, version = version + 1
5465
+ WHERE role = 'linked' AND status = 'active' AND linked_to_id = ?
5466
+ `).run(now, id);
5467
+ }
5078
5468
  return principalFromRow(row);
5079
5469
  });
5080
5470
  }
5081
5471
  linkPrincipal(input) {
5082
5472
  this.assertOpen();
5083
- const owner = this.getPrincipal(input.owner);
5084
- const linked = this.getPrincipal(input.linked);
5085
- if (owner?.status !== 'active' || owner.role !== 'owner') {
5086
- throw new DeliveryStoreError('unauthorized-principal', 'principal link requires an active owner');
5087
- }
5088
- if (linked?.status !== 'active' || linked.id === owner.id) {
5089
- throw new DeliveryStoreError('unauthorized-principal', 'linked principal must be a distinct active principal');
5090
- }
5091
- if (linked.version !== input.expectedLinkedVersion) {
5092
- throw new DeliveryStoreError('version-conflict', 'linked principal version changed');
5093
- }
5094
- if (linked.linkedToId === owner.id)
5095
- return linked;
5096
5473
  const now = this.now();
5097
- const changed = this.database.prepare(`
5098
- UPDATE delivery_principals SET role = 'linked', linked_to_id = ?, updated_at = ?, version = version + 1
5099
- WHERE id = ? AND version = ? AND status = 'active'
5100
- `).run(owner.id, now, linked.id, input.expectedLinkedVersion);
5101
- if (changed.changes !== 1)
5102
- throw new DeliveryStoreError('version-conflict', 'linked principal version changed');
5103
- return this.getPrincipal(input.linked);
5474
+ // One writer transaction: a concurrent owner revoke (possibly from another
5475
+ // process on the same database) must either detach this link or make the
5476
+ // link fail, never leave a link rooted in a revoked owner that a later
5477
+ // reactivation of the same record would silently restore.
5478
+ return this.transaction(() => {
5479
+ const owner = this.getPrincipal(input.owner);
5480
+ const linked = this.getPrincipal(input.linked);
5481
+ if (owner?.status !== 'active' || owner.role !== 'owner') {
5482
+ throw new DeliveryStoreError('unauthorized-principal', 'principal link requires an active owner');
5483
+ }
5484
+ if (linked?.status !== 'active' || linked.id === owner.id) {
5485
+ throw new DeliveryStoreError('unauthorized-principal', 'linked principal must be a distinct active principal');
5486
+ }
5487
+ if (linked.version !== input.expectedLinkedVersion) {
5488
+ throw new DeliveryStoreError('version-conflict', 'linked principal version changed');
5489
+ }
5490
+ if (linked.linkedToId === owner.id)
5491
+ return linked;
5492
+ const changed = this.database.prepare(`
5493
+ UPDATE delivery_principals SET role = 'linked', linked_to_id = ?, updated_at = ?, version = version + 1
5494
+ WHERE id = ? AND version = ? AND status = 'active'
5495
+ AND EXISTS (SELECT 1 FROM delivery_principals WHERE id = ? AND role = 'owner' AND status = 'active')
5496
+ `).run(owner.id, now, linked.id, input.expectedLinkedVersion, owner.id);
5497
+ if (changed.changes !== 1)
5498
+ throw new DeliveryStoreError('version-conflict', 'linked principal version changed');
5499
+ return this.getPrincipal(input.linked);
5500
+ });
5104
5501
  }
5105
5502
  resolveInbox(input) {
5106
5503
  this.assertOpen();
@@ -5696,6 +6093,20 @@ export class DeliveryStore {
5696
6093
  return Object.freeze({ revision, ...(template === undefined ? {} : { template }), replayed: false });
5697
6094
  });
5698
6095
  }
6096
+ /**
6097
+ * Atomically admit one authenticated owner work-item result and the explicit
6098
+ * continuation requested in the same reply. The result insert owns the single
6099
+ * Inbox handoff; the continuation insert reuses that processed Inbox in the
6100
+ * same transaction. A failure in either lane leaves neither row behind, so a
6101
+ * provider retry can attempt both again instead of losing one silently.
6102
+ */
6103
+ admitOwnerWorkItemFeedbackWithReply(input) {
6104
+ this.assertOpen();
6105
+ return this.transaction(() => Object.freeze({
6106
+ feedback: this.workItemFeedback.admitInTransaction(input.feedback, input.fence),
6107
+ reply: this.workItemReplies.admitInTransaction(input.feedback.inboxId, input.binding, input.report, { handoff: false }),
6108
+ }));
6109
+ }
5699
6110
  getNaturalObjectiveIntent(inboxId) {
5700
6111
  this.assertOpen();
5701
6112
  const row = this.database.prepare('SELECT * FROM delivery_natural_objective_intents WHERE inbox_id = ?')
@@ -5770,7 +6181,7 @@ export class DeliveryStore {
5770
6181
  || binding.conversation.kind !== 'dm' || owner?.status !== 'active' || owner.role !== 'owner'
5771
6182
  || inbox?.status !== 'claimed' || inbox.bindingId !== binding.id || inbox.envelope.kind !== 'text'
5772
6183
  || (inbox.envelope.attachments?.length ?? 0) !== 0
5773
- || parseNaturalObjectiveFeedback(inbox.envelope.text) !== input.selection
6184
+ || parseNaturalOwnerReply(inbox.envelope.text)?.status !== input.selection
5774
6185
  || !this.isVerifiedForegroundObjectiveTarget(binding, input.sourceInboxId, input.sourceOutboxId)
5775
6186
  || source === undefined || outbox === undefined
5776
6187
  || inbox.envelope.metadata?.replyToProviderMessageId !== outbox.providerMessageId
@@ -5904,7 +6315,7 @@ export class DeliveryStore {
5904
6315
  if (derived !== operationId)
5905
6316
  continue;
5906
6317
  const envelope = JSON.parse(candidate.envelope_json);
5907
- if (envelope.kind !== 'text' || parseNaturalObjectiveFeedback(envelope.text) === undefined)
6318
+ if (envelope.kind !== 'text' || parseNaturalOwnerReply(envelope.text) === undefined)
5908
6319
  continue;
5909
6320
  return Object.freeze({ inboxId: candidate.inbox_id, text: envelope.text.slice(0, 4096), truncated: envelope.text.length > 4096 });
5910
6321
  }
@@ -5971,7 +6382,7 @@ export class DeliveryStore {
5971
6382
  && naturalIntent.principalRecordId === owner.id && naturalIntent.principalVersion === owner.version
5972
6383
  && naturalIntent.admissionEpoch === feedbackInbox.admissionCursor.epoch
5973
6384
  && naturalIntent.admissionSequence === feedbackInbox.admissionCursor.sequence
5974
- && parseNaturalObjectiveFeedback(feedbackInbox.envelope.text) === naturalIntent.selection);
6385
+ && parseNaturalOwnerReply(feedbackInbox.envelope.text)?.status === naturalIntent.selection);
5975
6386
  if ((feedbackInbox?.status !== 'claimed' && !(naturalIntent !== undefined
5976
6387
  && (feedbackInbox?.status === 'processed' || feedbackInbox?.status === 'dead_letter')))
5977
6388
  || feedbackInbox?.bindingId !== binding.id || !naturalMatches