@dsh-enhanced/assistant-delivery 0.1.24 → 0.1.31
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 +81 -10
- package/lib/acceptance.d.ts +46 -0
- package/lib/acceptance.d.ts.map +1 -0
- package/lib/acceptance.js +2 -0
- package/lib/acceptance.js.map +1 -0
- package/lib/agent-runtime.d.ts +33 -3
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +835 -62
- package/lib/agent-runtime.js.map +1 -1
- package/lib/coordinator.d.ts +4 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +9 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +21 -1
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +18 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/goal-wake-types.d.ts +93 -0
- package/lib/goal-wake-types.d.ts.map +1 -0
- package/lib/goal-wake-types.js +70 -0
- package/lib/goal-wake-types.js.map +1 -0
- package/lib/inbound-images.d.ts.map +1 -1
- package/lib/inbound-images.js +11 -9
- package/lib/inbound-images.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts +78 -0
- package/lib/native-web-owner.d.ts.map +1 -0
- package/lib/native-web-owner.js +291 -0
- package/lib/native-web-owner.js.map +1 -0
- package/lib/operator-snapshot.d.ts +80 -0
- package/lib/operator-snapshot.d.ts.map +1 -0
- package/lib/operator-snapshot.js +935 -0
- package/lib/operator-snapshot.js.map +1 -0
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +10 -0
- package/lib/operator.js.map +1 -1
- package/lib/service.d.ts +63 -13
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +917 -349
- package/lib/service.js.map +1 -1
- package/lib/session-lease-runtime.d.ts +69 -0
- package/lib/session-lease-runtime.d.ts.map +1 -0
- package/lib/session-lease-runtime.js +395 -0
- package/lib/session-lease-runtime.js.map +1 -0
- package/lib/session-lease-types.d.ts +27 -0
- package/lib/session-lease-types.d.ts.map +1 -0
- package/lib/session-lease-types.js +2 -0
- package/lib/session-lease-types.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +201 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +133 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +771 -43
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +9 -2
- 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/package.json +61 -44
package/lib/store.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { acceptanceCanonicalJson } from '@dsh-enhanced/task-acceptance-contract';
|
|
3
4
|
import { ASSISTANT_GROWTH_CONTRACT_VERSION, WORKFLOW_TRACE_SOURCE_ID, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowAutomationTemplateContent, validateWorkflowTraceRevision, workflowAutomationTemplateContentDigest, workflowArgumentShapeDigest, workflowScopeKey, workflowTraceRevisionDigest, } from '@dsh-enhanced/assistant-growth-contract';
|
|
4
5
|
import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, canonicalPrincipal, canonicalTarget, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
|
|
5
6
|
import { feedbackDispatchRecoveryCode, isFeedbackDeliveryCommand, isFeedbackDispatchRecoveryCode, isExactDeliveryCommand, isLearningDeliveryCommand, isLearningDispatchRecoveryCode, isLearningRetryableFailureCode, isPermissionDeliveryCommand, isWorkflowDeliveryCommand, isWorkflowDispatchRecoveryCode, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, permissionDispatchRecoveryFromFailureCode, workflowDispatchRecoveryCode, } from './session-commands.js';
|
|
6
7
|
import { parseFeedbackCommand } from './feedback-command.js';
|
|
8
|
+
import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
|
|
7
9
|
import { openDeliveryDatabase } from './sqlite.js';
|
|
8
10
|
import { deriveDeterministicallyDeidentifiedWorkflowTemplate, getDeterministicallyDeidentifiedWorkflowTemplate, } from './workflow-auto-producer.js';
|
|
9
11
|
export class DeliveryStoreError extends Error {
|
|
@@ -366,7 +368,7 @@ function canonicalImageRef(input) {
|
|
|
366
368
|
const value = input;
|
|
367
369
|
const required = ['attachmentId', 'mediaType', 'bytes', 'width', 'height'];
|
|
368
370
|
if (required.some(key => !Object.hasOwn(value, key))
|
|
369
|
-
|| Object.keys(value).some(key => ![...required, 'name'].includes(key))
|
|
371
|
+
|| Object.keys(value).some(key => ![...required, 'name', 'originalDimensions'].includes(key))
|
|
370
372
|
|| typeof value.attachmentId !== 'string'
|
|
371
373
|
|| value.attachmentId.length < 1 || value.attachmentId.length > 512
|
|
372
374
|
|| /\p{Cc}/u.test(value.attachmentId)
|
|
@@ -375,6 +377,15 @@ function canonicalImageRef(input) {
|
|
|
375
377
|
|| !Number.isSafeInteger(value.bytes) || value.bytes < 1
|
|
376
378
|
|| !Number.isSafeInteger(value.width) || value.width < 1
|
|
377
379
|
|| !Number.isSafeInteger(value.height) || value.height < 1
|
|
380
|
+
|| (value.originalDimensions !== undefined
|
|
381
|
+
&& (value.originalDimensions === null
|
|
382
|
+
|| typeof value.originalDimensions !== 'object'
|
|
383
|
+
|| Array.isArray(value.originalDimensions)
|
|
384
|
+
|| Object.keys(value.originalDimensions).some(key => key !== 'width' && key !== 'height')
|
|
385
|
+
|| !Number.isSafeInteger(value.originalDimensions.width)
|
|
386
|
+
|| value.originalDimensions.width < 1
|
|
387
|
+
|| !Number.isSafeInteger(value.originalDimensions.height)
|
|
388
|
+
|| value.originalDimensions.height < 1))
|
|
378
389
|
|| (value.name !== undefined && (typeof value.name !== 'string' || value.name.length < 1
|
|
379
390
|
|| value.name.length > 255 || value.name === '.' || value.name === '..' || /[\\/\p{Cc}]/u.test(value.name)))) {
|
|
380
391
|
invalidImageRef();
|
|
@@ -386,6 +397,12 @@ function canonicalImageRef(input) {
|
|
|
386
397
|
width: value.width,
|
|
387
398
|
height: value.height,
|
|
388
399
|
...(value.name === undefined ? {} : { name: value.name }),
|
|
400
|
+
...(value.originalDimensions === undefined ? {} : {
|
|
401
|
+
originalDimensions: {
|
|
402
|
+
width: value.originalDimensions.width,
|
|
403
|
+
height: value.originalDimensions.height,
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
389
406
|
};
|
|
390
407
|
}
|
|
391
408
|
function persistedImageRef(row) {
|
|
@@ -409,7 +426,9 @@ function sameImageRef(left, right) {
|
|
|
409
426
|
&& left.bytes === right.bytes
|
|
410
427
|
&& left.width === right.width
|
|
411
428
|
&& left.height === right.height
|
|
412
|
-
&& left.name === right.name
|
|
429
|
+
&& left.name === right.name
|
|
430
|
+
&& left.originalDimensions?.width === right.originalDimensions?.width
|
|
431
|
+
&& left.originalDimensions?.height === right.originalDimensions?.height;
|
|
413
432
|
}
|
|
414
433
|
function attachmentFromRow(row) {
|
|
415
434
|
const imageRef = row.status === 'ready' && row.resource_kind === 'image'
|
|
@@ -630,6 +649,32 @@ function approvalRouteFromRow(row) {
|
|
|
630
649
|
principalVersion: row.principal_version,
|
|
631
650
|
});
|
|
632
651
|
}
|
|
652
|
+
function goalOutcomeTargetFromRow(row) {
|
|
653
|
+
let locator;
|
|
654
|
+
let proof;
|
|
655
|
+
try {
|
|
656
|
+
locator = validateOwnerGoalOutcomeFeedbackLocator(JSON.parse(row.locator_json));
|
|
657
|
+
proof = validateOwnerGoalOutcomeFeedbackProof(JSON.parse(row.proof_json));
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
throw new DeliveryStoreError('invalid-intent', 'stored goal outcome target is invalid');
|
|
661
|
+
}
|
|
662
|
+
if (acceptanceCanonicalJson(proof.locator) !== acceptanceCanonicalJson(locator)
|
|
663
|
+
|| proof.proofDigest !== row.proof_digest || locator.ownerRouteId !== row.owner_route_id
|
|
664
|
+
|| locator.principalId !== row.principal_id || locator.principalRecordId !== row.principal_record_id
|
|
665
|
+
|| locator.principalVersion !== row.principal_version || locator.workspace !== row.workspace
|
|
666
|
+
|| locator.preset !== row.preset || locator.bindingId !== row.binding_id
|
|
667
|
+
|| locator.bindingVersion !== row.binding_version || locator.bindingGeneration !== row.binding_generation
|
|
668
|
+
|| locator.sessionId !== row.session_id || locator.goalId !== row.goal_id
|
|
669
|
+
|| locator.assessmentId !== row.assessment_id) {
|
|
670
|
+
throw new DeliveryStoreError('invalid-intent', 'stored goal outcome target columns do not match its proof');
|
|
671
|
+
}
|
|
672
|
+
return Object.freeze({ outboxId: row.outbox_id, locator, proof });
|
|
673
|
+
}
|
|
674
|
+
function sameGoalOutcomeTarget(left, locator, proof) {
|
|
675
|
+
return acceptanceCanonicalJson(left.locator) === acceptanceCanonicalJson(locator)
|
|
676
|
+
&& acceptanceCanonicalJson(left.proof) === acceptanceCanonicalJson(proof);
|
|
677
|
+
}
|
|
633
678
|
function sameApprovalRoute(left, right) {
|
|
634
679
|
return left.routeVersion === right.routeVersion
|
|
635
680
|
&& left.sourceId === right.sourceId
|
|
@@ -1263,6 +1308,28 @@ function getPairingStatement(database) {
|
|
|
1263
1308
|
FROM pairing_challenges WHERE id = ?
|
|
1264
1309
|
`);
|
|
1265
1310
|
}
|
|
1311
|
+
function sameLeaseBinding(left, right) {
|
|
1312
|
+
return left.id === right.id && left.sessionId === right.sessionId && left.generation === right.generation
|
|
1313
|
+
&& left.version === right.version && left.status === 'active' && right.status === 'active'
|
|
1314
|
+
&& left.workspace === right.workspace && left.agentPreset === right.agentPreset
|
|
1315
|
+
&& JSON.stringify(canonicalConversation(left.conversation)) === JSON.stringify(canonicalConversation(right.conversation))
|
|
1316
|
+
&& JSON.stringify(canonicalPrincipal(left.principal)) === JSON.stringify(canonicalPrincipal(right.principal));
|
|
1317
|
+
}
|
|
1318
|
+
function leaseSessionIdentity(target) {
|
|
1319
|
+
const value = target.kind === 'bound' ? target.binding : target;
|
|
1320
|
+
return JSON.stringify({ sessionId: value.sessionId, conversation: canonicalConversation(value.conversation),
|
|
1321
|
+
principal: canonicalPrincipal(value.principal), workspace: value.workspace,
|
|
1322
|
+
agentPreset: value.agentPreset, generation: value.generation });
|
|
1323
|
+
}
|
|
1324
|
+
function leaseTargetJson(target) {
|
|
1325
|
+
if (target?.kind !== 'bound' && target?.kind !== 'construction')
|
|
1326
|
+
throw new DeliveryStoreError('invalid-binding', 'invalid session lease target');
|
|
1327
|
+
if (target.kind === 'bound') {
|
|
1328
|
+
const binding = target.binding;
|
|
1329
|
+
return JSON.stringify({ kind: 'bound', binding: { id: binding.id, conversation: canonicalConversation(binding.conversation), principal: canonicalPrincipal(binding.principal), workspace: binding.workspace, agentPreset: binding.agentPreset, sessionId: binding.sessionId, generation: binding.generation, version: binding.version, status: 'active' } });
|
|
1330
|
+
}
|
|
1331
|
+
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' } }) });
|
|
1332
|
+
}
|
|
1266
1333
|
export class DeliveryStore {
|
|
1267
1334
|
database;
|
|
1268
1335
|
databaseInstanceId;
|
|
@@ -1564,6 +1631,47 @@ export class DeliveryStore {
|
|
|
1564
1631
|
return principalFromRow(result);
|
|
1565
1632
|
});
|
|
1566
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Bootstrap one empty Delivery database for a fixed local operator. This is
|
|
1636
|
+
* deliberately narrower than handoffOwner(): once any principal history
|
|
1637
|
+
* exists it can only return the exact, sole active owner without changing a
|
|
1638
|
+
* row, its bindings, or any linked authority.
|
|
1639
|
+
*/
|
|
1640
|
+
ensureOwner(input) {
|
|
1641
|
+
this.assertOpen();
|
|
1642
|
+
const principal = canonicalPrincipal(input);
|
|
1643
|
+
const json = principalJson(principal);
|
|
1644
|
+
const hash = digest(json);
|
|
1645
|
+
const now = this.now();
|
|
1646
|
+
return this.transaction(() => {
|
|
1647
|
+
const total = this.database.prepare('SELECT COUNT(*) AS count FROM delivery_principals')
|
|
1648
|
+
.get().count;
|
|
1649
|
+
if (total === 0) {
|
|
1650
|
+
const id = `principal_${randomUUID()}`;
|
|
1651
|
+
this.database.prepare(`
|
|
1652
|
+
INSERT INTO delivery_principals (
|
|
1653
|
+
id, key_hash, principal_json, role, status, linked_to_id, created_at, updated_at, version
|
|
1654
|
+
) VALUES (?, ?, ?, 'owner', 'active', NULL, ?, ?, 1)
|
|
1655
|
+
`).run(id, hash, json, now, now);
|
|
1656
|
+
return principalFromRow(this.database.prepare(`
|
|
1657
|
+
SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
|
|
1658
|
+
FROM delivery_principals WHERE id = ?
|
|
1659
|
+
`).get(id));
|
|
1660
|
+
}
|
|
1661
|
+
const exact = this.database.prepare(`
|
|
1662
|
+
SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
|
|
1663
|
+
FROM delivery_principals WHERE key_hash = ? AND principal_json = ?
|
|
1664
|
+
`).get(hash, json);
|
|
1665
|
+
const activeOwners = this.database.prepare(`
|
|
1666
|
+
SELECT id FROM delivery_principals WHERE role = 'owner' AND status = 'active'
|
|
1667
|
+
`).all();
|
|
1668
|
+
if (exact === undefined || exact.role !== 'owner' || exact.status !== 'active'
|
|
1669
|
+
|| exact.linked_to_id !== null || activeOwners.length !== 1 || activeOwners[0]?.id !== exact.id) {
|
|
1670
|
+
throw new DeliveryStoreError('unauthorized-principal', 'local owner initialization cannot alter existing authority');
|
|
1671
|
+
}
|
|
1672
|
+
return principalFromRow(exact);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1567
1675
|
isAuthorizedPrincipal(input) {
|
|
1568
1676
|
this.assertOpen();
|
|
1569
1677
|
const row = this.database.prepare('SELECT status FROM delivery_principals WHERE key_hash = ?')
|
|
@@ -1608,6 +1716,17 @@ export class DeliveryStore {
|
|
|
1608
1716
|
const now = this.now();
|
|
1609
1717
|
const id = `binding_${randomUUID()}`;
|
|
1610
1718
|
return this.transaction(() => {
|
|
1719
|
+
const construction = input.constructionLease === undefined ? undefined : this.sessionLeaseCurrent(input.constructionLease, true);
|
|
1720
|
+
if (input.constructionLease !== undefined) {
|
|
1721
|
+
const candidate = construction === undefined ? undefined : JSON.parse(construction.target_json);
|
|
1722
|
+
const expected = { kind: 'construction', sessionId, conversation: target.conversation,
|
|
1723
|
+
principal: target.principal, workspace, agentPreset, generation: input.expectedGeneration ?? 1 };
|
|
1724
|
+
if (construction === undefined || candidate?.kind !== 'construction' || candidate.previous !== undefined
|
|
1725
|
+
|| !['prepared', 'dispatched'].includes(construction.state)
|
|
1726
|
+
|| leaseSessionIdentity(candidate) !== leaseSessionIdentity(expected)) {
|
|
1727
|
+
throw new DeliveryStoreError('stale-fence', 'binding construction lease is no longer exact');
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1611
1730
|
const principalRow = this.database.prepare(`
|
|
1612
1731
|
SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
|
|
1613
1732
|
FROM delivery_principals
|
|
@@ -1622,6 +1741,8 @@ export class DeliveryStore {
|
|
|
1622
1741
|
WHERE conversation_hash = ? AND conversation_json = ? AND status = 'active'
|
|
1623
1742
|
`).get(hash, canonicalConversationJson);
|
|
1624
1743
|
if (existingRow !== undefined) {
|
|
1744
|
+
if (construction !== undefined)
|
|
1745
|
+
throw new DeliveryStoreError('conflict', 'construction already has a binding');
|
|
1625
1746
|
const existing = bindingFromRow(existingRow);
|
|
1626
1747
|
if (principalHash(existing.principal) !== canonicalPrincipalHash) {
|
|
1627
1748
|
throw new DeliveryStoreError('conflict', 'conversation is already bound to another principal');
|
|
@@ -1641,7 +1762,15 @@ export class DeliveryStore {
|
|
|
1641
1762
|
session_id, generation, policy_ref, status, created_at, updated_at, version
|
|
1642
1763
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, 1)
|
|
1643
1764
|
`).run(id, hash, canonicalConversationJson, principal.id, canonicalPrincipalJson, workspace, agentPreset, sessionId, generation, policyRef, now, now);
|
|
1644
|
-
|
|
1765
|
+
const binding = this.getBinding(id);
|
|
1766
|
+
if (construction !== undefined) {
|
|
1767
|
+
const promoted = this.database.prepare(`UPDATE delivery_session_leases SET target_json = ?
|
|
1768
|
+
WHERE session_id = ? AND holder_id = ? AND fencing_token = ? AND state IN ('prepared', 'dispatched')`)
|
|
1769
|
+
.run(leaseTargetJson({ kind: 'bound', binding }), construction.session_id, construction.holder_id, construction.fencing_token);
|
|
1770
|
+
if (Number(promoted.changes) !== 1)
|
|
1771
|
+
throw new DeliveryStoreError('stale-fence', 'construction binding promotion lost its lease');
|
|
1772
|
+
}
|
|
1773
|
+
return binding;
|
|
1645
1774
|
});
|
|
1646
1775
|
}
|
|
1647
1776
|
/**
|
|
@@ -2054,6 +2183,123 @@ export class DeliveryStore {
|
|
|
2054
2183
|
}
|
|
2055
2184
|
return { duplicate: false, record: this.getInbox(id) };
|
|
2056
2185
|
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Admit and claim one exact Host-native input under one SQLite write fence.
|
|
2188
|
+
*
|
|
2189
|
+
* The normal Delivery scheduler can only claim queued work. Publishing a
|
|
2190
|
+
* fresh native input directly as `claimed` therefore prevents it from
|
|
2191
|
+
* observing a moment where the input is in the ordinary queue. Replays are
|
|
2192
|
+
* read-only: a duplicate never renews, reclaims, or otherwise re-executes
|
|
2193
|
+
* its existing Inbox.
|
|
2194
|
+
*/
|
|
2195
|
+
claimNativeInbox(input) {
|
|
2196
|
+
this.assertOpen();
|
|
2197
|
+
const envelope = canonicalEnvelope(input.envelope, this.maxTextBytes);
|
|
2198
|
+
const json = JSON.stringify(envelope);
|
|
2199
|
+
const hash = digest(json);
|
|
2200
|
+
const ownerId = validateBindingText(input.ownerId, 'ownerId', 256);
|
|
2201
|
+
if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs < 1) {
|
|
2202
|
+
throw new DeliveryStoreError('conflict', 'invalid inbox lease');
|
|
2203
|
+
}
|
|
2204
|
+
let bindingId;
|
|
2205
|
+
let sessionId;
|
|
2206
|
+
let bindingConversationJson;
|
|
2207
|
+
let bindingPrincipalJson;
|
|
2208
|
+
try {
|
|
2209
|
+
bindingId = validateBindingText(input.binding.id, 'binding.id', 256);
|
|
2210
|
+
sessionId = validateBindingText(input.binding.sessionId, 'binding.sessionId', 512);
|
|
2211
|
+
bindingConversationJson = conversationJson(input.binding.conversation);
|
|
2212
|
+
bindingPrincipalJson = principalJson(input.binding.principal);
|
|
2213
|
+
}
|
|
2214
|
+
catch {
|
|
2215
|
+
throw new DeliveryStoreError('invalid-binding', 'native Inbox binding snapshot is invalid');
|
|
2216
|
+
}
|
|
2217
|
+
if (!Number.isSafeInteger(input.binding.version) || input.binding.version < 1
|
|
2218
|
+
|| !Number.isSafeInteger(input.binding.generation) || input.binding.generation < 1) {
|
|
2219
|
+
throw new DeliveryStoreError('invalid-binding', 'native Inbox binding snapshot is invalid');
|
|
2220
|
+
}
|
|
2221
|
+
if (conversationJson(envelope.conversation) !== bindingConversationJson
|
|
2222
|
+
|| principalJson(envelope.principal) !== bindingPrincipalJson) {
|
|
2223
|
+
throw new DeliveryStoreError('invalid-binding', 'native Inbox envelope does not belong to its binding');
|
|
2224
|
+
}
|
|
2225
|
+
const now = this.now();
|
|
2226
|
+
const id = `inbox_${randomUUID()}`;
|
|
2227
|
+
return this.transaction(() => {
|
|
2228
|
+
const existing = this.database.prepare(`${inboxSelect} WHERE channel = ? AND account = ? AND event_id = ?`)
|
|
2229
|
+
.get(envelope.channel, envelope.account, envelope.eventId);
|
|
2230
|
+
if (existing !== undefined) {
|
|
2231
|
+
if (existing.envelope_hash !== hash) {
|
|
2232
|
+
throw new DeliveryStoreError('idempotency-conflict', 'provider event id was reused with a different envelope');
|
|
2233
|
+
}
|
|
2234
|
+
return { duplicate: true, record: inboxFromRow(existing) };
|
|
2235
|
+
}
|
|
2236
|
+
const binding = this.database.prepare(`${bindingSelect} WHERE id = ?`).get(bindingId);
|
|
2237
|
+
const bindingPrincipal = this.database.prepare('SELECT principal_id FROM conversation_bindings WHERE id = ?').get(bindingId);
|
|
2238
|
+
const principal = this.database.prepare(`
|
|
2239
|
+
SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
|
|
2240
|
+
FROM delivery_principals WHERE key_hash = ? AND principal_json = ?
|
|
2241
|
+
`).get(principalHash(input.binding.principal), bindingPrincipalJson);
|
|
2242
|
+
if (binding === undefined || binding.status !== 'active'
|
|
2243
|
+
|| binding.version !== input.binding.version || binding.generation !== input.binding.generation
|
|
2244
|
+
|| binding.session_id !== sessionId || binding.conversation_json !== bindingConversationJson
|
|
2245
|
+
|| binding.principal_json !== bindingPrincipalJson
|
|
2246
|
+
|| principal === undefined || principal.id !== bindingPrincipal?.principal_id
|
|
2247
|
+
|| principal.id !== input.ownerLineage.principalRecordId || principal.version !== input.ownerLineage.principalVersion
|
|
2248
|
+
|| principal.status !== 'active' || principal.role !== 'owner') {
|
|
2249
|
+
throw new DeliveryStoreError('invalid-binding', 'native Inbox binding is no longer an active owner binding');
|
|
2250
|
+
}
|
|
2251
|
+
this.database.prepare(`
|
|
2252
|
+
INSERT INTO inbox_messages (
|
|
2253
|
+
id, channel, account, event_id, envelope_hash, envelope_json, status, binding_id,
|
|
2254
|
+
attempt_count, claimed_by, fencing_token, lease_until, received_at, updated_at
|
|
2255
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'received', ?, 0, NULL, NULL, NULL, ?, ?)
|
|
2256
|
+
`).run(id, envelope.channel, envelope.account, envelope.eventId, hash, json, bindingId, now, now);
|
|
2257
|
+
for (const [ordinal, attachment] of (envelope.attachments ?? []).entries()) {
|
|
2258
|
+
const descriptorHash = digest(JSON.stringify(attachment));
|
|
2259
|
+
this.database.prepare(`
|
|
2260
|
+
INSERT INTO delivery_attachments (
|
|
2261
|
+
id, owner_kind, owner_id, ordinal, media_type, size_bytes, sha256, spool_ref,
|
|
2262
|
+
resource_kind, provider_ref, file_name, status, expires_at, created_at
|
|
2263
|
+
) VALUES (?, 'inbox', ?, ?, ?, ?, ?, NULL, ?, ?, ?, 'metadata', NULL, ?)
|
|
2264
|
+
`).run(`attachment_${digest(`${id}:${ordinal}:${attachment.providerRef}`).slice(0, 40)}`, id, ordinal, attachment.mediaType ?? '', attachment.sizeBytes ?? 0, descriptorHash, attachment.resourceType, attachment.providerRef, attachment.fileName ?? null, now);
|
|
2265
|
+
}
|
|
2266
|
+
const blocked = this.database.prepare(`
|
|
2267
|
+
SELECT 1 FROM inbox_messages AS earlier
|
|
2268
|
+
LEFT JOIN conversation_bindings AS earlier_binding ON earlier_binding.id = earlier.binding_id
|
|
2269
|
+
JOIN delivery_inbox_admissions AS earlier_admission ON earlier_admission.inbox_id = earlier.id
|
|
2270
|
+
JOIN delivery_inbox_admissions AS admitted ON admitted.inbox_id = ?
|
|
2271
|
+
WHERE earlier.id <> ? AND earlier.status NOT IN ('processed', 'dead_letter')
|
|
2272
|
+
AND (
|
|
2273
|
+
(earlier_binding.workspace = ? AND earlier_binding.agent_preset = ?
|
|
2274
|
+
AND earlier_binding.principal_id = ?)
|
|
2275
|
+
OR (earlier.binding_id IS NULL
|
|
2276
|
+
AND json_extract(earlier.envelope_json, '$.conversation') = json(?)
|
|
2277
|
+
AND json_extract(earlier.envelope_json, '$.principal') = json(?))
|
|
2278
|
+
)
|
|
2279
|
+
AND (earlier_admission.admission_sequence < admitted.admission_sequence
|
|
2280
|
+
OR earlier.status = 'claimed')
|
|
2281
|
+
LIMIT 1
|
|
2282
|
+
`).get(id, id, binding.workspace, binding.agent_preset, principal.id, bindingConversationJson, bindingPrincipalJson);
|
|
2283
|
+
if (blocked !== undefined) {
|
|
2284
|
+
throw new DeliveryStoreError('conflict', 'native Inbox lane has unfinished earlier or active work');
|
|
2285
|
+
}
|
|
2286
|
+
const previousFence = this.database.prepare('SELECT COALESCE(MAX(fencing_token), 0) AS maximum FROM inbox_attempts WHERE inbox_id = ?').get(id);
|
|
2287
|
+
const fencingToken = previousFence.maximum + 1;
|
|
2288
|
+
const claimed = this.database.prepare(`
|
|
2289
|
+
UPDATE inbox_messages SET status = 'claimed', claimed_by = ?, fencing_token = ?, lease_until = ?,
|
|
2290
|
+
attempt_count = 1, failure_code = 'native-admission', updated_at = ?
|
|
2291
|
+
WHERE id = ? AND status = 'received' AND binding_id = ?
|
|
2292
|
+
`).run(ownerId, fencingToken, now + input.leaseMs, now, id, bindingId);
|
|
2293
|
+
if (claimed.changes !== 1) {
|
|
2294
|
+
throw new DeliveryStoreError('conflict', 'native Inbox claim lost its admission fence');
|
|
2295
|
+
}
|
|
2296
|
+
this.database.prepare(`
|
|
2297
|
+
INSERT INTO inbox_attempts (id, inbox_id, attempt_number, owner_id, fencing_token, status, created_at)
|
|
2298
|
+
VALUES (?, ?, 1, ?, ?, 'claimed', ?)
|
|
2299
|
+
`).run(`inbox_attempt_${randomUUID()}`, id, ownerId, fencingToken, now);
|
|
2300
|
+
return { duplicate: false, record: this.getInbox(id), fencingToken };
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2057
2303
|
listAttachments(input) {
|
|
2058
2304
|
this.assertOpen();
|
|
2059
2305
|
const rows = this.database.prepare(`
|
|
@@ -2175,6 +2421,16 @@ export class DeliveryStore {
|
|
|
2175
2421
|
}
|
|
2176
2422
|
const now = this.now();
|
|
2177
2423
|
this.transaction(() => {
|
|
2424
|
+
const current = this.getActiveBinding(record.envelope.conversation);
|
|
2425
|
+
const principal = this.getBindingPrincipal(binding.id);
|
|
2426
|
+
if (current?.id !== binding.id || current.version !== binding.version
|
|
2427
|
+
|| current.generation !== binding.generation || current.sessionId !== binding.sessionId) {
|
|
2428
|
+
throw new DeliveryStoreError('invalid-binding', 'inbox binding is no longer the exact active generation');
|
|
2429
|
+
}
|
|
2430
|
+
if (principal?.status !== 'active'
|
|
2431
|
+
|| JSON.stringify(principal.principal) !== JSON.stringify(binding.principal)) {
|
|
2432
|
+
throw new DeliveryStoreError('unauthorized-principal', 'inbox binding principal is not active');
|
|
2433
|
+
}
|
|
2178
2434
|
if (record.status === 'received') {
|
|
2179
2435
|
this.database.prepare(`
|
|
2180
2436
|
UPDATE inbox_messages SET status = 'authorized', binding_id = ?, updated_at = ?
|
|
@@ -2337,6 +2593,27 @@ export class DeliveryStore {
|
|
|
2337
2593
|
`).get(boundary.admission_sequence, conversationJson(target.conversation), principalJson(target.principal));
|
|
2338
2594
|
return row === undefined ? undefined : inboxFromRow(row);
|
|
2339
2595
|
}
|
|
2596
|
+
/**
|
|
2597
|
+
* List a bounded page of persisted admissions that have not reached the
|
|
2598
|
+
* worker queue yet. The admission cursor, rather than timestamps or rowid,
|
|
2599
|
+
* preserves the global order across restarts and database maintenance.
|
|
2600
|
+
*/
|
|
2601
|
+
listRecoverableInboundAdmissions(input = {}) {
|
|
2602
|
+
this.assertOpen();
|
|
2603
|
+
const afterSequence = input.afterSequence ?? 0;
|
|
2604
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
2605
|
+
throw new DeliveryStoreError('conflict', 'invalid inbound admission recovery cursor');
|
|
2606
|
+
}
|
|
2607
|
+
const limit = boundedMaintenanceLimit(input.limit, 100, 'inbound admission recovery');
|
|
2608
|
+
const rows = this.database.prepare(`${inboxSelect}
|
|
2609
|
+
JOIN delivery_inbox_admissions AS pending_admission
|
|
2610
|
+
ON pending_admission.inbox_id = inbox_messages.id
|
|
2611
|
+
WHERE inbox_messages.status IN ('received', 'authorized')
|
|
2612
|
+
AND pending_admission.admission_sequence > ?
|
|
2613
|
+
ORDER BY pending_admission.admission_sequence LIMIT ?
|
|
2614
|
+
`).all(afterSequence, limit);
|
|
2615
|
+
return rows.map(inboxFromRow);
|
|
2616
|
+
}
|
|
2340
2617
|
listInbox(input = {}) {
|
|
2341
2618
|
this.assertOpen();
|
|
2342
2619
|
const limit = Math.max(1, Math.min(100, input.limit ?? 20));
|
|
@@ -2569,7 +2846,9 @@ export class DeliveryStore {
|
|
|
2569
2846
|
`).all(now, input.maxAttempts, input.limit);
|
|
2570
2847
|
for (const candidate of candidates) {
|
|
2571
2848
|
const current = this.getInbox(candidate.id);
|
|
2572
|
-
const
|
|
2849
|
+
const previousFence = this.database.prepare('SELECT COALESCE(MAX(fencing_token), 0) AS maximum FROM inbox_attempts WHERE inbox_id = ?')
|
|
2850
|
+
.get(current.id);
|
|
2851
|
+
const fencingToken = previousFence.maximum + 1;
|
|
2573
2852
|
const changed = this.database.prepare(`
|
|
2574
2853
|
UPDATE inbox_messages SET status = 'claimed', claimed_by = ?, fencing_token = ?, lease_until = ?,
|
|
2575
2854
|
attempt_count = attempt_count + 1, next_attempt_at = NULL,
|
|
@@ -2616,6 +2895,49 @@ export class DeliveryStore {
|
|
|
2616
2895
|
});
|
|
2617
2896
|
return this.getInbox(input.inboxId);
|
|
2618
2897
|
}
|
|
2898
|
+
/** Return an undispatched Inbox to its queue while another live session holds the lane. */
|
|
2899
|
+
deferInboxForSessionLease(input) {
|
|
2900
|
+
this.assertOpen();
|
|
2901
|
+
let inboxId;
|
|
2902
|
+
let ownerId;
|
|
2903
|
+
try {
|
|
2904
|
+
inboxId = validateBindingText(input.inboxId, 'inboxId', 256);
|
|
2905
|
+
ownerId = validateBindingText(input.ownerId, 'ownerId', 256);
|
|
2906
|
+
}
|
|
2907
|
+
catch {
|
|
2908
|
+
return false;
|
|
2909
|
+
}
|
|
2910
|
+
const now = this.now();
|
|
2911
|
+
if (!Number.isSafeInteger(input.fencingToken) || input.fencingToken < 1
|
|
2912
|
+
|| !Number.isSafeInteger(input.retryAt) || input.retryAt < now)
|
|
2913
|
+
return false;
|
|
2914
|
+
return this.transaction(() => {
|
|
2915
|
+
const changed = this.database.prepare(`
|
|
2916
|
+
UPDATE inbox_messages AS inbox
|
|
2917
|
+
SET status = 'retry_wait', next_attempt_at = ?, attempt_count = attempt_count - 1,
|
|
2918
|
+
claimed_by = NULL, lease_until = NULL, failure_code = 'session-lease-busy', updated_at = ?
|
|
2919
|
+
WHERE inbox.id = ? AND inbox.status = 'claimed' AND inbox.claimed_by = ?
|
|
2920
|
+
AND inbox.fencing_token = ? AND inbox.lease_until > ? AND inbox.attempt_count >= 1
|
|
2921
|
+
AND inbox.failure_code IS NULL
|
|
2922
|
+
AND EXISTS (
|
|
2923
|
+
SELECT 1 FROM conversation_bindings AS binding
|
|
2924
|
+
JOIN delivery_session_leases AS lease ON lease.session_id = binding.session_id
|
|
2925
|
+
WHERE binding.id = inbox.binding_id AND lease.state IN ('prepared', 'dispatched')
|
|
2926
|
+
AND lease.lease_until > ?
|
|
2927
|
+
)
|
|
2928
|
+
`).run(input.retryAt, now, inboxId, ownerId, input.fencingToken, now, now);
|
|
2929
|
+
if (Number(changed.changes) !== 1)
|
|
2930
|
+
return false;
|
|
2931
|
+
const settled = this.database.prepare(`
|
|
2932
|
+
UPDATE inbox_attempts SET status = 'retry_wait', failure_code = 'session-lease-busy', finished_at = ?
|
|
2933
|
+
WHERE inbox_id = ? AND owner_id = ? AND fencing_token = ? AND status = 'claimed'
|
|
2934
|
+
`).run(now, inboxId, ownerId, input.fencingToken);
|
|
2935
|
+
if (Number(settled.changes) !== 1) {
|
|
2936
|
+
throw new DeliveryStoreError('conflict', 'session lease defer attempt settlement is missing');
|
|
2937
|
+
}
|
|
2938
|
+
return true;
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2619
2941
|
markInboxDispatching(input) {
|
|
2620
2942
|
this.assertOpen();
|
|
2621
2943
|
let bindingId;
|
|
@@ -2638,7 +2960,7 @@ export class DeliveryStore {
|
|
|
2638
2960
|
const now = this.now();
|
|
2639
2961
|
return this.transaction(() => {
|
|
2640
2962
|
const changed = this.database.prepare(`
|
|
2641
|
-
UPDATE inbox_messages AS inbox SET failure_code = CASE WHEN failure_code IN (
|
|
2963
|
+
UPDATE inbox_messages AS inbox SET failure_code = CASE WHEN failure_code = 'native-admission' THEN 'native-dispatch-started' WHEN failure_code IN (
|
|
2642
2964
|
'permission-dispatch-recovery',
|
|
2643
2965
|
'permission-cancelled-recovery',
|
|
2644
2966
|
'permission-failure-notice-recovery',
|
|
@@ -2675,6 +2997,99 @@ export class DeliveryStore {
|
|
|
2675
2997
|
throw new DeliveryStoreError('invalid-binding', 'inbox dispatch binding snapshot is no longer active');
|
|
2676
2998
|
});
|
|
2677
2999
|
}
|
|
3000
|
+
/** Immutable pre-prompt binding for a foreground acceptance contract. */
|
|
3001
|
+
bindForegroundTaskAcceptance(input) {
|
|
3002
|
+
this.assertOpen();
|
|
3003
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u.test(input.contractId)
|
|
3004
|
+
|| !/^[a-f0-9]{64}$/u.test(input.contractDigest)
|
|
3005
|
+
|| !Number.isSafeInteger(input.dispatchedAt) || input.dispatchedAt < 0
|
|
3006
|
+
|| !Number.isSafeInteger(input.owner.principalVersion) || input.owner.principalVersion < 1
|
|
3007
|
+
|| !Number.isSafeInteger(input.binding.version) || input.binding.version < 1
|
|
3008
|
+
|| !Number.isSafeInteger(input.binding.generation) || input.binding.generation < 1) {
|
|
3009
|
+
throw new DeliveryStoreError('invalid-binding', 'foreground acceptance binding is invalid');
|
|
3010
|
+
}
|
|
3011
|
+
const text = [input.inboxId, input.scope.workspace, input.scope.preset, input.owner.principalRecordId, input.binding.id];
|
|
3012
|
+
if (text.some(value => typeof value !== 'string' || value.trim() === '' || Buffer.byteLength(value, 'utf8') > 4_096)) {
|
|
3013
|
+
throw new DeliveryStoreError('invalid-binding', 'foreground acceptance binding text is invalid');
|
|
3014
|
+
}
|
|
3015
|
+
this.transaction(() => {
|
|
3016
|
+
const existing = this.database.prepare('SELECT * FROM delivery_task_acceptance_executions WHERE inbox_id = ?')
|
|
3017
|
+
.get(input.inboxId);
|
|
3018
|
+
if (existing !== undefined) {
|
|
3019
|
+
if (existing.contract_id !== input.contractId || existing.contract_digest !== input.contractDigest
|
|
3020
|
+
|| existing.workspace !== input.scope.workspace || existing.preset !== input.scope.preset
|
|
3021
|
+
|| existing.principal_record_id !== input.owner.principalRecordId || existing.principal_version !== input.owner.principalVersion
|
|
3022
|
+
|| existing.binding_id !== input.binding.id || existing.binding_version !== input.binding.version
|
|
3023
|
+
|| existing.binding_generation !== input.binding.generation) {
|
|
3024
|
+
throw new DeliveryStoreError('idempotency-conflict', 'foreground acceptance binding changed');
|
|
3025
|
+
}
|
|
3026
|
+
throw new DeliveryStoreError('idempotency-conflict', 'accepted foreground task has already crossed its dispatch boundary');
|
|
3027
|
+
}
|
|
3028
|
+
const admitted = this.database.prepare(`
|
|
3029
|
+
SELECT inbox.id FROM inbox_messages AS inbox
|
|
3030
|
+
JOIN conversation_bindings AS binding ON binding.id = inbox.binding_id
|
|
3031
|
+
JOIN delivery_principals AS principal ON principal.id = binding.principal_id
|
|
3032
|
+
WHERE inbox.id = ? AND inbox.status = 'claimed' AND inbox.binding_id = ?
|
|
3033
|
+
AND binding.status = 'active' AND binding.version = ? AND binding.generation = ?
|
|
3034
|
+
AND principal.status = 'active' AND principal.role = 'owner'
|
|
3035
|
+
AND principal.id = ? AND principal.version = ?
|
|
3036
|
+
AND binding.workspace = ? AND binding.agent_preset = ?
|
|
3037
|
+
`).get(input.inboxId, input.binding.id, input.binding.version, input.binding.generation, input.owner.principalRecordId, input.owner.principalVersion, input.scope.workspace, input.scope.preset);
|
|
3038
|
+
if (admitted === undefined)
|
|
3039
|
+
throw new DeliveryStoreError('invalid-binding', 'foreground acceptance is not bound to the claimed owner inbox');
|
|
3040
|
+
this.database.prepare(`INSERT INTO delivery_task_acceptance_executions(
|
|
3041
|
+
inbox_id, contract_id, contract_digest, workspace, preset, principal_record_id, principal_version,
|
|
3042
|
+
binding_id, binding_version, binding_generation, dispatched_at, status, quiescent
|
|
3043
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0)`).run(input.inboxId, input.contractId, input.contractDigest, input.scope.workspace, input.scope.preset, input.owner.principalRecordId, input.owner.principalVersion, input.binding.id, input.binding.version, input.binding.generation, input.dispatchedAt);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
finishForegroundTaskAcceptance(input) {
|
|
3047
|
+
this.assertOpen();
|
|
3048
|
+
if (!['succeeded', 'failed', 'timed-out', 'cancelled', 'unknown'].includes(input.status)
|
|
3049
|
+
|| !Number.isSafeInteger(input.completedAt) || input.completedAt < 0) {
|
|
3050
|
+
throw new DeliveryStoreError('invalid-binding', 'foreground acceptance completion is invalid');
|
|
3051
|
+
}
|
|
3052
|
+
const status = input.quiescent && input.status === 'succeeded' ? 'succeeded' : 'unknown';
|
|
3053
|
+
const quiescent = status === 'succeeded' ? 1 : 0;
|
|
3054
|
+
const changed = this.database.prepare(`UPDATE delivery_task_acceptance_executions
|
|
3055
|
+
SET status = ?, quiescent = ?, completed_at = ?, execution_ref = inbox_id
|
|
3056
|
+
WHERE contract_id = ? AND status = 'pending' AND dispatched_at <= ?`).run(status, quiescent, input.completedAt, input.contractId, input.completedAt);
|
|
3057
|
+
if (changed.changes !== 1) {
|
|
3058
|
+
const current = this.database.prepare('SELECT status, quiescent, completed_at FROM delivery_task_acceptance_executions WHERE contract_id = ?')
|
|
3059
|
+
.get(input.contractId);
|
|
3060
|
+
if (current?.status === status && current.quiescent === quiescent && current.completed_at === input.completedAt)
|
|
3061
|
+
return;
|
|
3062
|
+
throw new DeliveryStoreError('conflict', 'foreground acceptance binding is absent or its completion changed');
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
inspectForegroundAcceptedExecution(contract) {
|
|
3066
|
+
const row = this.database.prepare(`SELECT * FROM delivery_task_acceptance_executions
|
|
3067
|
+
WHERE contract_id = ?`).get(contract.id);
|
|
3068
|
+
if (row === undefined || row.contract_digest !== contract.digest || contract.task.kind !== 'foreground-turn'
|
|
3069
|
+
|| contract.task.ref !== row.inbox_id || row.workspace !== contract.scope.workspace || row.preset !== contract.scope.preset
|
|
3070
|
+
|| row.principal_record_id !== contract.owner.principalRecordId || row.principal_version !== contract.owner.principalVersion
|
|
3071
|
+
|| row.completed_at === null || row.execution_ref === null)
|
|
3072
|
+
return null;
|
|
3073
|
+
return Object.freeze({ contractId: row.contract_id, contractDigest: row.contract_digest,
|
|
3074
|
+
dispatchedAt: row.dispatched_at, status: row.status, quiescent: row.quiescent === 1,
|
|
3075
|
+
completedAt: row.completed_at, executionRef: row.execution_ref });
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Re-read the immutable acceptance fence for one exact owner reply target.
|
|
3079
|
+
* This deliberately exposes only the completed execution receipt: callers
|
|
3080
|
+
* cannot manufacture a foreground subject by naming an arbitrary Inbox.
|
|
3081
|
+
*/
|
|
3082
|
+
inspectForegroundAcceptedExecutionForOwner(input) {
|
|
3083
|
+
const row = this.database.prepare(`SELECT contract_id, contract_digest, dispatched_at, status, quiescent, completed_at, execution_ref
|
|
3084
|
+
FROM delivery_task_acceptance_executions
|
|
3085
|
+
WHERE inbox_id = ? AND workspace = ? AND preset = ? AND principal_record_id = ?
|
|
3086
|
+
AND principal_version = ? AND binding_id = ?`).get(input.inboxId, input.scope.workspace, input.scope.preset, input.owner.principalRecordId, input.owner.principalVersion, input.bindingId);
|
|
3087
|
+
if (row === undefined || row.completed_at === null || row.execution_ref !== input.inboxId)
|
|
3088
|
+
return null;
|
|
3089
|
+
return Object.freeze({ contractId: row.contract_id, contractDigest: row.contract_digest,
|
|
3090
|
+
dispatchedAt: row.dispatched_at, status: row.status, quiescent: row.quiescent === 1,
|
|
3091
|
+
completedAt: row.completed_at, executionRef: row.execution_ref });
|
|
3092
|
+
}
|
|
2678
3093
|
renewInboxClaim(input) {
|
|
2679
3094
|
this.assertOpen();
|
|
2680
3095
|
const ownerId = validateBindingText(input.ownerId, 'ownerId', 256);
|
|
@@ -2752,8 +3167,10 @@ export class DeliveryStore {
|
|
|
2752
3167
|
: existingPermissionRecovery === undefined
|
|
2753
3168
|
? undefined
|
|
2754
3169
|
: permissionDispatchRecoveryCode(existingPermissionRecovery);
|
|
2755
|
-
const
|
|
2756
|
-
|
|
3170
|
+
const acceptedDispatch = this.database.prepare('SELECT inbox_id FROM delivery_task_acceptance_executions WHERE inbox_id = ?').get(row.id) !== undefined;
|
|
3171
|
+
const ambiguous = acceptedDispatch || row.failure_code === 'native-admission' || row.failure_code === 'native-dispatch-started'
|
|
3172
|
+
|| (row.failure_code === 'dispatch-started'
|
|
3173
|
+
&& permissionRecovery === undefined && !feedbackRecovery && !learningRecovery && !workflowRecovery);
|
|
2757
3174
|
const exhausted = row.attempt_count >= input.maxAttempts
|
|
2758
3175
|
&& permissionRecovery === undefined && !feedbackRecovery && !learningRecovery && !workflowRecovery;
|
|
2759
3176
|
const changed = this.database.prepare(`
|
|
@@ -2891,6 +3308,65 @@ export class DeliveryStore {
|
|
|
2891
3308
|
return outbox;
|
|
2892
3309
|
});
|
|
2893
3310
|
}
|
|
3311
|
+
/** Atomically insert a typed whole-goal result and its immutable authority sidecar. */
|
|
3312
|
+
enqueueGoalOutcomeTarget(input) {
|
|
3313
|
+
this.assertOpen();
|
|
3314
|
+
let locator;
|
|
3315
|
+
let proof;
|
|
3316
|
+
try {
|
|
3317
|
+
locator = validateOwnerGoalOutcomeFeedbackLocator(input.locator);
|
|
3318
|
+
proof = validateOwnerGoalOutcomeFeedbackProof(input.proof);
|
|
3319
|
+
}
|
|
3320
|
+
catch {
|
|
3321
|
+
throw new DeliveryStoreError('invalid-intent', 'goal outcome target is invalid');
|
|
3322
|
+
}
|
|
3323
|
+
if (acceptanceCanonicalJson(locator) !== acceptanceCanonicalJson(proof.locator)) {
|
|
3324
|
+
throw new DeliveryStoreError('invalid-intent', 'goal outcome locator does not match its proof');
|
|
3325
|
+
}
|
|
3326
|
+
return this.transaction(() => {
|
|
3327
|
+
const binding = this.getBinding(locator.bindingId);
|
|
3328
|
+
const owner = binding === undefined ? undefined : this.getBindingPrincipal(binding.id);
|
|
3329
|
+
if (binding === undefined || binding.status !== 'active'
|
|
3330
|
+
|| binding.version !== locator.bindingVersion || binding.generation !== locator.bindingGeneration
|
|
3331
|
+
|| binding.sessionId !== locator.sessionId || binding.workspace !== locator.workspace
|
|
3332
|
+
|| binding.agentPreset !== locator.preset || externalPrincipalId(binding.principal) !== locator.principalId
|
|
3333
|
+
|| owner?.status !== 'active' || owner.role !== 'owner' || owner.id !== locator.principalRecordId
|
|
3334
|
+
|| owner.version !== locator.principalVersion || externalPrincipalId(owner.principal) !== locator.principalId) {
|
|
3335
|
+
throw new DeliveryStoreError('invalid-binding', 'goal outcome target exact owner fence changed');
|
|
3336
|
+
}
|
|
3337
|
+
const intent = canonicalIntent(input.intent, binding, this.maxTextBytes);
|
|
3338
|
+
const intentHash = digest(JSON.stringify(intent));
|
|
3339
|
+
const winnerRow = this.database.prepare(`${outboxSelect} WHERE idempotency_key = ?`)
|
|
3340
|
+
.get(intent.idempotencyKey);
|
|
3341
|
+
if (winnerRow !== undefined) {
|
|
3342
|
+
const sidecar = this.getGoalOutcomeTarget(winnerRow.id);
|
|
3343
|
+
if (winnerRow.intent_hash !== intentHash || sidecar === undefined
|
|
3344
|
+
|| !sameGoalOutcomeTarget(sidecar, locator, proof)) {
|
|
3345
|
+
throw new DeliveryStoreError('idempotency-conflict', 'goal outcome Outbox winner does not match the exact immutable target and intent');
|
|
3346
|
+
}
|
|
3347
|
+
return outboxFromRow(winnerRow);
|
|
3348
|
+
}
|
|
3349
|
+
const outbox = this.enqueueIntent(intent, false);
|
|
3350
|
+
this.database.prepare(`
|
|
3351
|
+
INSERT INTO delivery_goal_outcome_targets(
|
|
3352
|
+
outbox_id, locator_json, proof_json, proof_digest, owner_route_id, principal_id,
|
|
3353
|
+
principal_record_id, principal_version, workspace, preset, binding_id, binding_version,
|
|
3354
|
+
binding_generation, session_id, goal_id, assessment_id
|
|
3355
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3356
|
+
`).run(outbox.id, acceptanceCanonicalJson(locator), acceptanceCanonicalJson(proof), proof.proofDigest, locator.ownerRouteId, locator.principalId, locator.principalRecordId, locator.principalVersion, locator.workspace, locator.preset, locator.bindingId, locator.bindingVersion, locator.bindingGeneration, locator.sessionId, locator.goalId, locator.assessmentId);
|
|
3357
|
+
return outbox;
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
getGoalOutcomeTarget(outboxId) {
|
|
3361
|
+
this.assertOpen();
|
|
3362
|
+
const row = this.database.prepare(`
|
|
3363
|
+
SELECT outbox_id, locator_json, proof_json, proof_digest, owner_route_id, principal_id,
|
|
3364
|
+
principal_record_id, principal_version, workspace, preset, binding_id, binding_version,
|
|
3365
|
+
binding_generation, session_id, goal_id, assessment_id
|
|
3366
|
+
FROM delivery_goal_outcome_targets WHERE outbox_id = ?
|
|
3367
|
+
`).get(outboxId);
|
|
3368
|
+
return row === undefined ? undefined : goalOutcomeTargetFromRow(row);
|
|
3369
|
+
}
|
|
2894
3370
|
/**
|
|
2895
3371
|
* Resolve and enqueue one Host-owned stable owner route under the same
|
|
2896
3372
|
* `BEGIN IMMEDIATE` fence as Outbox insertion.
|
|
@@ -4456,10 +4932,26 @@ export class DeliveryStore {
|
|
|
4456
4932
|
* A normal durable reply alone is intentionally insufficient; the owner
|
|
4457
4933
|
* must have replied `/feedback achieved` to that exact provider message.
|
|
4458
4934
|
*/
|
|
4935
|
+
verifiedWorkflowObjectiveState(sourceOutboxId, lineage) {
|
|
4936
|
+
const row = this.database.prepare(`SELECT version, objective_status, principal_record_id, principal_version FROM delivery_owner_objective_commands
|
|
4937
|
+
WHERE source_outbox_id = ? AND json_extract(result_json, '$.error') IS NULL ORDER BY version DESC LIMIT 1`).get(sourceOutboxId);
|
|
4938
|
+
if (row !== undefined) {
|
|
4939
|
+
if (lineage !== undefined && (row.principal_record_id !== lineage.principalRecordId || row.principal_version !== lineage.principalVersion))
|
|
4940
|
+
return undefined;
|
|
4941
|
+
return { version: row.version, objectiveStatus: row.objective_status };
|
|
4942
|
+
}
|
|
4943
|
+
const initial = this.database.prepare(`SELECT objective_status, principal_record_id FROM workflow_verified_task_feedback WHERE source_outbox_id = ?`)
|
|
4944
|
+
.get(sourceOutboxId);
|
|
4945
|
+
// Legacy receipts predate principal-version audit. Only an unchanged first
|
|
4946
|
+
// principal incarnation can prove that no authority turnover occurred.
|
|
4947
|
+
if (lineage !== undefined && (lineage.principalVersion !== 1 || initial?.principal_record_id !== lineage.principalRecordId))
|
|
4948
|
+
return undefined;
|
|
4949
|
+
return initial === undefined ? undefined : { version: 1, objectiveStatus: initial.objective_status };
|
|
4950
|
+
}
|
|
4459
4951
|
commitVerifiedWorkflowTraceFeedback(input) {
|
|
4460
4952
|
this.assertOpen();
|
|
4461
4953
|
if (typeof input !== 'object' || input === null
|
|
4462
|
-
|| !['achieved', 'partial', 'not-achieved'].includes(input.objectiveStatus)
|
|
4954
|
+
|| !['achieved', 'partial', 'not-achieved', 'unknown'].includes(input.objectiveStatus)
|
|
4463
4955
|
|| typeof input.feedbackInboxId !== 'string' || typeof input.sourceInboxId !== 'string'
|
|
4464
4956
|
|| typeof input.sourceOutboxId !== 'string') {
|
|
4465
4957
|
throw new DeliveryStoreError('conflict', 'verified workflow feedback tuple is invalid');
|
|
@@ -4467,7 +4959,7 @@ export class DeliveryStore {
|
|
|
4467
4959
|
const feedbackInboxId = validateBindingText(input.feedbackInboxId, 'workflow feedback inbox id', 256);
|
|
4468
4960
|
const sourceInboxId = validateBindingText(input.sourceInboxId, 'workflow source inbox id', 256);
|
|
4469
4961
|
const sourceOutboxId = validateBindingText(input.sourceOutboxId, 'workflow source outbox id', 256);
|
|
4470
|
-
|
|
4962
|
+
const committed = this.transaction(() => {
|
|
4471
4963
|
const binding = this.getBinding(input.binding.id);
|
|
4472
4964
|
const owner = binding === undefined ? undefined : this.getPrincipal(binding.principal);
|
|
4473
4965
|
if (binding === undefined || binding.status !== 'active' || binding.conversation.kind !== 'dm'
|
|
@@ -4490,7 +4982,9 @@ export class DeliveryStore {
|
|
|
4490
4982
|
: parseFeedbackCommand(feedbackCommand.rawInput);
|
|
4491
4983
|
if (feedbackInbox?.status !== 'claimed' || feedbackInbox.bindingId !== binding.id
|
|
4492
4984
|
|| (feedbackInbox.envelope.attachments?.length ?? 0) !== 0
|
|
4493
|
-
|| feedback?.kind !== 'objective'
|
|
4985
|
+
|| (feedback?.kind !== 'objective' && feedback?.kind !== 'objective-revision')
|
|
4986
|
+
|| feedback.objectiveStatus !== input.objectiveStatus
|
|
4987
|
+
|| JSON.stringify(feedback.kind === 'objective-revision' ? feedback : undefined) !== JSON.stringify(input.command)
|
|
4494
4988
|
|| sourceInbox?.status !== 'processed' || sourceInbox.bindingId !== binding.id
|
|
4495
4989
|
|| sourceInbox.envelope.kind !== 'text' || (sourceInbox.envelope.attachments?.length ?? 0) !== 0
|
|
4496
4990
|
|| sourceOutbox === undefined || sourceOutbox.intent.bindingId !== binding.id
|
|
@@ -4512,6 +5006,58 @@ export class DeliveryStore {
|
|
|
4512
5006
|
|| JSON.stringify(sourceOutbox.intent.target.principal) !== JSON.stringify(binding.principal)) {
|
|
4513
5007
|
throw new DeliveryStoreError('conflict', 'verified workflow feedback does not target one completed owner turn');
|
|
4514
5008
|
}
|
|
5009
|
+
const replay = this.database.prepare(`SELECT result_json, principal_record_id, principal_version FROM delivery_owner_objective_commands WHERE inbox_id = ?`)
|
|
5010
|
+
.get(feedbackInbox.id);
|
|
5011
|
+
if (replay !== undefined) {
|
|
5012
|
+
if (replay.principal_record_id !== owner.id || replay.principal_version !== owner.version) {
|
|
5013
|
+
throw new DeliveryStoreError('unauthorized-principal', 'owner command lineage changed');
|
|
5014
|
+
}
|
|
5015
|
+
const receipt = JSON.parse(replay.result_json);
|
|
5016
|
+
if (receipt.error === 'version-conflict' || receipt.error === 'idempotency-conflict')
|
|
5017
|
+
return new DeliveryStoreError(receipt.error, 'feedback version changed; reply /feedback status and retry');
|
|
5018
|
+
return { ...receipt, replayed: true };
|
|
5019
|
+
}
|
|
5020
|
+
const currentState = this.verifiedWorkflowObjectiveState(sourceOutbox.id, { principalRecordId: owner.id, principalVersion: owner.version });
|
|
5021
|
+
const currentOwner = this.database.prepare(`SELECT principal_record_id, principal_version FROM delivery_owner_objective_commands
|
|
5022
|
+
WHERE source_outbox_id = ? AND json_extract(result_json, '$.error') IS NULL ORDER BY version DESC LIMIT 1`).get(sourceOutbox.id);
|
|
5023
|
+
if (currentOwner !== undefined && (currentOwner.principal_record_id !== owner.id || currentOwner.principal_version !== owner.version)) {
|
|
5024
|
+
throw new DeliveryStoreError('unauthorized-principal', 'owner lineage changed');
|
|
5025
|
+
}
|
|
5026
|
+
if (input.command !== undefined && (currentState === undefined
|
|
5027
|
+
|| currentState.version !== input.command.expectedVersion
|
|
5028
|
+
|| currentState.objectiveStatus !== input.command.previousStatus)) {
|
|
5029
|
+
this.database.prepare(`INSERT INTO delivery_owner_objective_commands VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
5030
|
+
.run(feedbackInbox.id, sourceOutbox.id, owner.id, owner.version, currentState?.version ?? 0, currentState?.objectiveStatus ?? 'unknown', JSON.stringify(feedback), JSON.stringify({ error: 'version-conflict' }));
|
|
5031
|
+
return new DeliveryStoreError('version-conflict', 'feedback version changed; reply /feedback status and retry');
|
|
5032
|
+
}
|
|
5033
|
+
const nextVersion = currentState === undefined ? 1 : currentState.objectiveStatus === input.objectiveStatus
|
|
5034
|
+
? currentState.version : currentState.version + 1;
|
|
5035
|
+
const finish = (result) => {
|
|
5036
|
+
const response = { ...result, ownerFeedbackState: { version: nextVersion, objectiveStatus: input.objectiveStatus } };
|
|
5037
|
+
this.database.prepare(`INSERT INTO delivery_owner_objective_commands VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
5038
|
+
.run(feedbackInbox.id, sourceOutbox.id, owner.id, owner.version, nextVersion, input.objectiveStatus, JSON.stringify(feedback), JSON.stringify(response));
|
|
5039
|
+
return response;
|
|
5040
|
+
};
|
|
5041
|
+
if (currentState !== undefined && (input.command !== undefined || currentState.version >= 1)
|
|
5042
|
+
&& (input.command === undefined || currentState.objectiveStatus === input.objectiveStatus)) {
|
|
5043
|
+
if (currentState.objectiveStatus !== input.objectiveStatus) {
|
|
5044
|
+
this.database.prepare(`INSERT INTO delivery_owner_objective_commands VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
5045
|
+
.run(feedbackInbox.id, sourceOutbox.id, owner.id, owner.version, currentState.version, currentState.objectiveStatus, JSON.stringify(feedback), JSON.stringify({ error: 'idempotency-conflict' }));
|
|
5046
|
+
return new DeliveryStoreError('idempotency-conflict', 'current owner judgement differs; use explicit correction');
|
|
5047
|
+
}
|
|
5048
|
+
const currentReceipt = this.database.prepare(`SELECT result_json FROM delivery_owner_objective_commands
|
|
5049
|
+
WHERE source_outbox_id = ? AND version = ? AND json_extract(result_json, '$.error') IS NULL LIMIT 1`)
|
|
5050
|
+
.get(sourceOutbox.id, currentState.version);
|
|
5051
|
+
if (currentReceipt !== undefined)
|
|
5052
|
+
return finish({ ...JSON.parse(currentReceipt.result_json), replayed: true });
|
|
5053
|
+
}
|
|
5054
|
+
const subjectRef = growthObjectDigest({
|
|
5055
|
+
contract: 'assistant-delivery-verified-workflow-subject/v1',
|
|
5056
|
+
bindingId: binding.id, sourceInboxId: sourceInbox.id, sourceOutboxId: sourceOutbox.id,
|
|
5057
|
+
});
|
|
5058
|
+
const traceHead = this.database.prepare(`SELECT version FROM workflow_trace_current WHERE subject_ref = ?`)
|
|
5059
|
+
.get(subjectRef);
|
|
5060
|
+
const traceVersion = (traceHead?.version ?? 0) + 1;
|
|
4515
5061
|
const taskRef = growthObjectDigest({
|
|
4516
5062
|
contract: 'assistant-delivery-verified-workflow-task-ref/v1',
|
|
4517
5063
|
bindingId: binding.id,
|
|
@@ -4546,7 +5092,7 @@ export class DeliveryStore {
|
|
|
4546
5092
|
const prior = this.database.prepare(`
|
|
4547
5093
|
SELECT * FROM workflow_verified_task_feedback WHERE source_outbox_id = ?
|
|
4548
5094
|
`).get(sourceOutbox.id);
|
|
4549
|
-
if (prior !== undefined) {
|
|
5095
|
+
if (prior !== undefined && input.command === undefined) {
|
|
4550
5096
|
return this.replayVerifiedWorkflowTraceFeedback({
|
|
4551
5097
|
row: prior,
|
|
4552
5098
|
binding,
|
|
@@ -4563,21 +5109,28 @@ export class DeliveryStore {
|
|
|
4563
5109
|
? deriveDeterministicallyDeidentifiedWorkflowTemplate(sourceInbox.envelope.text)
|
|
4564
5110
|
: undefined;
|
|
4565
5111
|
if (descriptor === undefined) {
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
5112
|
+
if (input.command !== undefined && traceHead !== undefined) {
|
|
5113
|
+
const payload = { source: this.workflowTraceSourceAttestation(),
|
|
5114
|
+
scope: { workspace: binding.workspace, preset: binding.agentPreset },
|
|
5115
|
+
subjectRef, version: traceVersion, disposition: 'retract' };
|
|
5116
|
+
this.insertWorkflowTraceRevisionInTransaction({ ...payload, digest: workflowTraceRevisionDigest(payload) });
|
|
5117
|
+
}
|
|
5118
|
+
if (input.command === undefined)
|
|
5119
|
+
this.insertVerifiedWorkflowTaskFeedback({
|
|
5120
|
+
sourceOutboxId: sourceOutbox.id,
|
|
5121
|
+
sourceInboxId: sourceInbox.id,
|
|
5122
|
+
feedbackInboxId: feedbackInbox.id,
|
|
5123
|
+
binding,
|
|
5124
|
+
ownerId: owner.id,
|
|
5125
|
+
objectiveStatus: input.objectiveStatus,
|
|
5126
|
+
taskRef,
|
|
5127
|
+
taskEvidenceDigest,
|
|
5128
|
+
});
|
|
5129
|
+
return finish(Object.freeze({
|
|
4577
5130
|
outcome: 'no-trace',
|
|
4578
5131
|
reason: input.objectiveStatus === 'achieved' ? 'privacy-abstained' : 'objective-not-achieved',
|
|
4579
5132
|
replayed: false,
|
|
4580
|
-
});
|
|
5133
|
+
}));
|
|
4581
5134
|
}
|
|
4582
5135
|
const content = validateWorkflowAutomationTemplateContent({
|
|
4583
5136
|
scope: { workspace: binding.workspace, preset: binding.agentPreset },
|
|
@@ -4641,17 +5194,11 @@ export class DeliveryStore {
|
|
|
4641
5194
|
'active', ?, ?, ?, ?, ?, 1)
|
|
4642
5195
|
`).run(template.templateRef, template.templateDigest, workflowScopeKey(content.scope), content.scope.workspace, content.scope.preset, binding.id, JSON.stringify(content), template.privacyAttestation.attestationId, template.privacyAttestation.attestationDigest, JSON.stringify(privacyReceipt), feedbackInbox.id, sourceInbox.id, sourceOutbox.id, now, now);
|
|
4643
5196
|
}
|
|
4644
|
-
const subjectRef = growthObjectDigest({
|
|
4645
|
-
contract: 'assistant-delivery-verified-workflow-subject/v1',
|
|
4646
|
-
bindingId: binding.id,
|
|
4647
|
-
sourceInboxId: sourceInbox.id,
|
|
4648
|
-
sourceOutboxId: sourceOutbox.id,
|
|
4649
|
-
});
|
|
4650
5197
|
const payload = Object.freeze({
|
|
4651
5198
|
source: this.workflowTraceSourceAttestation(),
|
|
4652
5199
|
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
4653
5200
|
subjectRef,
|
|
4654
|
-
version:
|
|
5201
|
+
version: traceVersion,
|
|
4655
5202
|
disposition: 'upsert',
|
|
4656
5203
|
evidence: Object.freeze({
|
|
4657
5204
|
occurredAt: feedbackInbox.receivedAt,
|
|
@@ -4675,20 +5222,24 @@ export class DeliveryStore {
|
|
|
4675
5222
|
if (recorded.replayed) {
|
|
4676
5223
|
throw new DeliveryStoreError('version-conflict', 'new verified workflow feedback unexpectedly replayed');
|
|
4677
5224
|
}
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
5225
|
+
if (input.command === undefined)
|
|
5226
|
+
this.insertVerifiedWorkflowTaskFeedback({
|
|
5227
|
+
sourceOutboxId: sourceOutbox.id,
|
|
5228
|
+
sourceInboxId: sourceInbox.id,
|
|
5229
|
+
feedbackInboxId: feedbackInbox.id,
|
|
5230
|
+
binding,
|
|
5231
|
+
ownerId: owner.id,
|
|
5232
|
+
objectiveStatus: 'achieved',
|
|
5233
|
+
taskRef,
|
|
5234
|
+
taskEvidenceDigest,
|
|
5235
|
+
revision,
|
|
5236
|
+
template,
|
|
5237
|
+
});
|
|
5238
|
+
return finish(Object.freeze({ outcome: 'trace-recorded', revision, template, replayed: false }));
|
|
4691
5239
|
});
|
|
5240
|
+
if (committed instanceof DeliveryStoreError)
|
|
5241
|
+
throw committed;
|
|
5242
|
+
return committed;
|
|
4692
5243
|
}
|
|
4693
5244
|
insertVerifiedWorkflowTaskFeedback(input) {
|
|
4694
5245
|
if ((input.revision === undefined) !== (input.template === undefined)) {
|
|
@@ -5508,6 +6059,183 @@ export class DeliveryStore {
|
|
|
5508
6059
|
throw new DeliveryStoreError('version-conflict', 'workflow template revocation lost its exact version');
|
|
5509
6060
|
}
|
|
5510
6061
|
}
|
|
6062
|
+
/** Ownership survives release, revocation and restart; it is not an active-lease query. */
|
|
6063
|
+
requiresSessionLease(sessionId) {
|
|
6064
|
+
this.assertOpen();
|
|
6065
|
+
sessionId = validateBindingText(sessionId, 'sessionId', 512);
|
|
6066
|
+
const row = this.database.prepare(`
|
|
6067
|
+
SELECT (EXISTS (SELECT 1 FROM conversation_bindings WHERE session_id = ?)
|
|
6068
|
+
OR EXISTS (SELECT 1 FROM delivery_session_leases WHERE session_id = ?)) AS managed
|
|
6069
|
+
`).get(sessionId, sessionId);
|
|
6070
|
+
return row.managed === 1;
|
|
6071
|
+
}
|
|
6072
|
+
claimSessionLease(target, holderId, leaseMs) {
|
|
6073
|
+
this.assertOpen();
|
|
6074
|
+
if (!Number.isSafeInteger(leaseMs) || leaseMs < 1 || leaseMs > 300_000)
|
|
6075
|
+
return { kind: 'denied' };
|
|
6076
|
+
let targetJson;
|
|
6077
|
+
let sessionId;
|
|
6078
|
+
try {
|
|
6079
|
+
targetJson = leaseTargetJson(target);
|
|
6080
|
+
sessionId = validateBindingText(target.kind === 'bound' ? target.binding.sessionId : target.sessionId, 'sessionId', 512);
|
|
6081
|
+
holderId = validateBindingText(holderId, 'holderId', 256);
|
|
6082
|
+
}
|
|
6083
|
+
catch {
|
|
6084
|
+
return { kind: 'denied' };
|
|
6085
|
+
}
|
|
6086
|
+
const now = this.now();
|
|
6087
|
+
const leaseUntil = now + leaseMs;
|
|
6088
|
+
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(leaseUntil))
|
|
6089
|
+
return { kind: 'denied' };
|
|
6090
|
+
return this.transaction(() => {
|
|
6091
|
+
const authority = this.sessionLeaseAuthority(target);
|
|
6092
|
+
if (authority === undefined)
|
|
6093
|
+
return { kind: 'denied' };
|
|
6094
|
+
const row = this.database.prepare('SELECT session_id, holder_id, fencing_token, lease_until, state, target_json, principal_record_id, principal_version FROM delivery_session_leases WHERE session_id = ?').get(sessionId);
|
|
6095
|
+
if (row === undefined) {
|
|
6096
|
+
this.database.prepare("INSERT INTO delivery_session_leases(session_id, holder_id, fencing_token, lease_until, state, target_json, principal_record_id, principal_version) VALUES (?, ?, 1, ?, 'prepared', ?, ?, ?)")
|
|
6097
|
+
.run(sessionId, holderId, leaseUntil, targetJson, authority.id, authority.version);
|
|
6098
|
+
return { kind: 'claimed', lease: Object.freeze({ sessionId, holderId, fencingToken: 1, leaseUntil }) };
|
|
6099
|
+
}
|
|
6100
|
+
// A released construction can outlive a crash before createBinding. Its Session
|
|
6101
|
+
// remains owned by the original identity, even though no binding exists yet.
|
|
6102
|
+
try {
|
|
6103
|
+
if (row.principal_record_id !== authority.id
|
|
6104
|
+
|| leaseSessionIdentity(JSON.parse(row.target_json)) !== leaseSessionIdentity(target))
|
|
6105
|
+
return { kind: 'denied' };
|
|
6106
|
+
}
|
|
6107
|
+
catch {
|
|
6108
|
+
return { kind: 'denied' };
|
|
6109
|
+
}
|
|
6110
|
+
if (row.state === 'unknown')
|
|
6111
|
+
return { kind: 'unknown' };
|
|
6112
|
+
if (row.state === 'dispatched') {
|
|
6113
|
+
if (row.lease_until > now)
|
|
6114
|
+
return { kind: 'busy' };
|
|
6115
|
+
this.database.prepare("UPDATE delivery_session_leases SET state = 'unknown' WHERE session_id = ? AND fencing_token = ? AND state = 'dispatched'")
|
|
6116
|
+
.run(sessionId, row.fencing_token);
|
|
6117
|
+
return { kind: 'unknown' };
|
|
6118
|
+
}
|
|
6119
|
+
if (row.state !== 'prepared' && row.state !== 'released')
|
|
6120
|
+
return { kind: 'unknown' };
|
|
6121
|
+
if (row.state === 'prepared' && row.lease_until > now)
|
|
6122
|
+
return { kind: 'busy' };
|
|
6123
|
+
const token = row.fencing_token + 1;
|
|
6124
|
+
if (!Number.isSafeInteger(token))
|
|
6125
|
+
return { kind: 'denied' };
|
|
6126
|
+
this.database.prepare("UPDATE delivery_session_leases SET holder_id = ?, fencing_token = ?, lease_until = ?, state = 'prepared', target_json = ?, principal_record_id = ?, principal_version = ? WHERE session_id = ? AND fencing_token = ?")
|
|
6127
|
+
.run(holderId, token, leaseUntil, targetJson, authority.id, authority.version, sessionId, row.fencing_token);
|
|
6128
|
+
return { kind: 'claimed', lease: Object.freeze({ sessionId, holderId, fencingToken: token, leaseUntil }) };
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
markSessionLeaseDispatched(lease) {
|
|
6132
|
+
this.assertOpen();
|
|
6133
|
+
return this.transaction(() => {
|
|
6134
|
+
const row = this.sessionLeaseCurrent(lease, true);
|
|
6135
|
+
if (row === undefined || row.state !== 'prepared')
|
|
6136
|
+
return false;
|
|
6137
|
+
return Number(this.database.prepare("UPDATE delivery_session_leases SET state = 'dispatched' WHERE session_id = ? AND holder_id = ? AND fencing_token = ? AND state = 'prepared'")
|
|
6138
|
+
.run(lease.sessionId, lease.holderId, lease.fencingToken).changes) === 1;
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
hasSessionLease(lease) {
|
|
6142
|
+
this.assertOpen();
|
|
6143
|
+
return this.transaction(() => {
|
|
6144
|
+
const row = this.sessionLeaseCurrent(lease, true);
|
|
6145
|
+
return row !== undefined && (row.state === 'prepared' || row.state === 'dispatched');
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
renewSessionLease(lease, leaseMs) {
|
|
6149
|
+
this.assertOpen();
|
|
6150
|
+
if (!Number.isSafeInteger(leaseMs) || leaseMs < 1 || leaseMs > 300_000)
|
|
6151
|
+
return false;
|
|
6152
|
+
return this.transaction(() => {
|
|
6153
|
+
const row = this.sessionLeaseCurrent(lease, true);
|
|
6154
|
+
if (row === undefined || (row.state !== 'prepared' && row.state !== 'dispatched'))
|
|
6155
|
+
return false;
|
|
6156
|
+
const until = this.now() + leaseMs;
|
|
6157
|
+
if (!Number.isSafeInteger(until))
|
|
6158
|
+
return false;
|
|
6159
|
+
return Number(this.database.prepare("UPDATE delivery_session_leases SET lease_until = ? WHERE session_id = ? AND holder_id = ? AND fencing_token = ? AND state IN ('prepared', 'dispatched')")
|
|
6160
|
+
.run(until, lease.sessionId, lease.holderId, lease.fencingToken).changes) === 1;
|
|
6161
|
+
});
|
|
6162
|
+
}
|
|
6163
|
+
finishSessionLease(lease, result) {
|
|
6164
|
+
this.assertOpen();
|
|
6165
|
+
if (!lease || typeof result?.quiescent !== 'boolean' || !this.sessionLeaseShape(lease))
|
|
6166
|
+
return false;
|
|
6167
|
+
return Number(this.database.prepare("UPDATE delivery_session_leases SET state = ? WHERE session_id = ? AND holder_id = ? AND fencing_token = ? AND state IN ('prepared', 'dispatched', 'unknown')")
|
|
6168
|
+
.run(result.quiescent ? 'released' : 'unknown', lease.sessionId, lease.holderId, lease.fencingToken).changes) === 1;
|
|
6169
|
+
}
|
|
6170
|
+
sessionLeaseShape(lease) {
|
|
6171
|
+
return lease !== null && typeof lease === 'object'
|
|
6172
|
+
&& typeof lease.sessionId === 'string' && typeof lease.holderId === 'string'
|
|
6173
|
+
&& Number.isSafeInteger(lease.fencingToken) && lease.fencingToken >= 1
|
|
6174
|
+
&& Number.isSafeInteger(lease.leaseUntil) && lease.leaseUntil >= 0;
|
|
6175
|
+
}
|
|
6176
|
+
/** Re-reads target authority; no runtime-supplied binding is trusted after claim. */
|
|
6177
|
+
sessionLeaseAuthority(target) {
|
|
6178
|
+
try {
|
|
6179
|
+
if (target?.kind !== 'bound' && target?.kind !== 'construction')
|
|
6180
|
+
return undefined;
|
|
6181
|
+
if (target.kind === 'bound') {
|
|
6182
|
+
const current = this.getBinding(target.binding.id);
|
|
6183
|
+
if (current === undefined || !sameLeaseBinding(current, target.binding))
|
|
6184
|
+
return undefined;
|
|
6185
|
+
const principal = this.getPrincipal(current.principal);
|
|
6186
|
+
return principal?.status === 'active' ? principal : undefined;
|
|
6187
|
+
}
|
|
6188
|
+
const conversation = canonicalConversation(target.conversation);
|
|
6189
|
+
const principalKey = canonicalPrincipal(target.principal);
|
|
6190
|
+
const sessionId = validateBindingText(target.sessionId, 'sessionId', 512);
|
|
6191
|
+
const workspace = validateBindingText(target.workspace, 'workspace', 4_096);
|
|
6192
|
+
const preset = validateBindingText(target.agentPreset, 'agentPreset', 128);
|
|
6193
|
+
if (!isAbsolute(workspace) || !Number.isSafeInteger(target.generation) || target.generation < 1)
|
|
6194
|
+
return undefined;
|
|
6195
|
+
const principal = this.getPrincipal(principalKey);
|
|
6196
|
+
if (principal?.status !== 'active')
|
|
6197
|
+
return undefined;
|
|
6198
|
+
const existingSession = this.database.prepare('SELECT id FROM conversation_bindings WHERE session_id = ?').get(sessionId);
|
|
6199
|
+
if (existingSession !== undefined)
|
|
6200
|
+
return undefined;
|
|
6201
|
+
const existing = this.getActiveBinding(conversation);
|
|
6202
|
+
if (target.previous !== undefined) {
|
|
6203
|
+
const previous = this.getBinding(target.previous.id);
|
|
6204
|
+
if (previous === undefined || !sameLeaseBinding(previous, target.previous)
|
|
6205
|
+
|| previous.generation + 1 !== target.generation
|
|
6206
|
+
|| JSON.stringify(canonicalConversation(previous.conversation)) !== JSON.stringify(conversation)
|
|
6207
|
+
|| JSON.stringify(canonicalPrincipal(previous.principal)) !== JSON.stringify(principalKey)
|
|
6208
|
+
|| previous.workspace !== workspace || previous.agentPreset !== preset
|
|
6209
|
+
|| existing === undefined || !sameLeaseBinding(existing, target.previous))
|
|
6210
|
+
return undefined;
|
|
6211
|
+
}
|
|
6212
|
+
else if (existing !== undefined || this.nextBindingGenerationByHash(conversationHash(conversation)) !== target.generation)
|
|
6213
|
+
return undefined;
|
|
6214
|
+
return principal;
|
|
6215
|
+
}
|
|
6216
|
+
catch {
|
|
6217
|
+
return undefined;
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
sessionLeaseCurrent(lease, requireLiveAuthority) {
|
|
6221
|
+
if (!this.sessionLeaseShape(lease))
|
|
6222
|
+
return undefined;
|
|
6223
|
+
const row = this.database.prepare('SELECT session_id, holder_id, fencing_token, lease_until, state, target_json, principal_record_id, principal_version FROM delivery_session_leases WHERE session_id = ? AND holder_id = ? AND fencing_token = ?')
|
|
6224
|
+
.get(lease.sessionId, lease.holderId, lease.fencingToken);
|
|
6225
|
+
if (row === undefined || row.lease_until <= this.now())
|
|
6226
|
+
return undefined;
|
|
6227
|
+
if (!requireLiveAuthority)
|
|
6228
|
+
return row;
|
|
6229
|
+
let target;
|
|
6230
|
+
try {
|
|
6231
|
+
target = JSON.parse(row.target_json);
|
|
6232
|
+
}
|
|
6233
|
+
catch {
|
|
6234
|
+
return undefined;
|
|
6235
|
+
}
|
|
6236
|
+
const principal = this.sessionLeaseAuthority(target);
|
|
6237
|
+
return principal !== undefined && principal.id === row.principal_record_id && principal.version === row.principal_version ? row : undefined;
|
|
6238
|
+
}
|
|
5511
6239
|
close() {
|
|
5512
6240
|
if (this.closed)
|
|
5513
6241
|
return;
|