@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.
- package/README.md +128 -4
- package/lib/agent-runtime.d.ts +6 -2
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +60 -14
- package/lib/agent-runtime.js.map +1 -1
- package/lib/attention-audit-store.d.ts +54 -0
- package/lib/attention-audit-store.d.ts.map +1 -0
- package/lib/attention-audit-store.js +213 -0
- package/lib/attention-audit-store.js.map +1 -0
- package/lib/attention-enforcement.d.ts +75 -0
- package/lib/attention-enforcement.d.ts.map +1 -0
- package/lib/attention-enforcement.js +84 -0
- package/lib/attention-enforcement.js.map +1 -0
- package/lib/attention.d.ts +203 -0
- package/lib/attention.d.ts.map +1 -0
- package/lib/attention.js +288 -0
- package/lib/attention.js.map +1 -0
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +6 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +23 -0
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +176 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/foreground-observation.d.ts +21 -0
- package/lib/foreground-observation.d.ts.map +1 -1
- package/lib/foreground-observation.js.map +1 -1
- package/lib/foreground-termination.d.ts +31 -0
- package/lib/foreground-termination.d.ts.map +1 -0
- package/lib/foreground-termination.js +42 -0
- package/lib/foreground-termination.js.map +1 -0
- package/lib/in-process-reload-runtime.d.ts +87 -0
- package/lib/in-process-reload-runtime.d.ts.map +1 -0
- package/lib/in-process-reload-runtime.js +319 -0
- package/lib/in-process-reload-runtime.js.map +1 -0
- package/lib/index.d.ts +20 -6
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -3
- package/lib/index.js.map +1 -1
- package/lib/native-termination.d.ts +12 -0
- package/lib/native-termination.d.ts.map +1 -0
- package/lib/native-termination.js +57 -0
- package/lib/native-termination.js.map +1 -0
- package/lib/native-web-owner.d.ts +7 -0
- package/lib/native-web-owner.d.ts.map +1 -1
- package/lib/native-web-owner.js +40 -10
- package/lib/native-web-owner.js.map +1 -1
- package/lib/operator-snapshot.d.ts +31 -3
- package/lib/operator-snapshot.d.ts.map +1 -1
- package/lib/operator-snapshot.js +120 -47
- package/lib/operator-snapshot.js.map +1 -1
- package/lib/progress-attention.d.ts +55 -0
- package/lib/progress-attention.d.ts.map +1 -0
- package/lib/progress-attention.js +128 -0
- package/lib/progress-attention.js.map +1 -0
- package/lib/service.d.ts +515 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +2183 -130
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +102 -2
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +113 -2
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +450 -39
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +175 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-engineering-context.d.ts +127 -0
- package/lib/work-item-engineering-context.d.ts.map +1 -0
- package/lib/work-item-engineering-context.js +151 -0
- package/lib/work-item-engineering-context.js.map +1 -0
- package/lib/work-item-feedback-schema.d.ts +2 -0
- package/lib/work-item-feedback-schema.d.ts.map +1 -0
- package/lib/work-item-feedback-schema.js +29 -0
- package/lib/work-item-feedback-schema.js.map +1 -0
- package/lib/work-item-feedback-store.d.ts +29 -0
- package/lib/work-item-feedback-store.d.ts.map +1 -0
- package/lib/work-item-feedback-store.js +111 -0
- package/lib/work-item-feedback-store.js.map +1 -0
- package/lib/work-item-feedback.d.ts +157 -0
- package/lib/work-item-feedback.d.ts.map +1 -0
- package/lib/work-item-feedback.js +2 -0
- package/lib/work-item-feedback.js.map +1 -0
- package/lib/work-item-replies-schema.d.ts +3 -0
- package/lib/work-item-replies-schema.d.ts.map +1 -0
- package/lib/work-item-replies-schema.js +45 -0
- package/lib/work-item-replies-schema.js.map +1 -0
- package/lib/work-item-replies-store.d.ts +43 -0
- package/lib/work-item-replies-store.d.ts.map +1 -0
- package/lib/work-item-replies-store.js +203 -0
- package/lib/work-item-replies-store.js.map +1 -0
- package/lib/work-item-replies.d.ts +93 -0
- package/lib/work-item-replies.d.ts.map +1 -0
- package/lib/work-item-replies.js +15 -0
- package/lib/work-item-replies.js.map +1 -0
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3662
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
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
|
-
||
|
|
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' ||
|
|
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
|
-
&&
|
|
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
|