@hasna/todos 0.15.14 → 0.15.15
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/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/index.js +12103 -10728
- package/dist/contracts.js +1 -1
- package/dist/index.js +276 -41
- package/dist/mcp/index.js +2170 -798
- package/dist/mcp.js +1 -1
- package/dist/project-registration.js +1 -1
- package/dist/registry.js +1 -1
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +23 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +3 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +19650 -18278
- package/dist/server/openapi.d.ts +134 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +2 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/task-manifest/authority.d.ts +3 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +18 -1
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/http.d.ts +2 -1
- package/dist/task-manifest/http.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +1 -1
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +3 -1
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts +3 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts +2 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +4 -2
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +22 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +275 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12492,7 +12492,7 @@ var init_dispatches = __esm(() => {
|
|
|
12492
12492
|
// package.json
|
|
12493
12493
|
var package_default = {
|
|
12494
12494
|
name: "@hasna/todos",
|
|
12495
|
-
version: "0.15.
|
|
12495
|
+
version: "0.15.15",
|
|
12496
12496
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12497
12497
|
type: "module",
|
|
12498
12498
|
main: "dist/index.js",
|
|
@@ -39177,6 +39177,14 @@ var compensationSchema = exports_external.object({
|
|
|
39177
39177
|
idempotency_key: identifier,
|
|
39178
39178
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
39179
39179
|
}).strict();
|
|
39180
|
+
var bindingLookupSchema = exports_external.object({
|
|
39181
|
+
authority: exports_external.string().min(1).max(64),
|
|
39182
|
+
route: exports_external.string().min(1).max(128),
|
|
39183
|
+
schema_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
|
|
39184
|
+
tenant_id: identifier,
|
|
39185
|
+
plan_id: uuid2,
|
|
39186
|
+
max_items: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
39187
|
+
}).strict();
|
|
39180
39188
|
function parseTodosTaskManifest(input) {
|
|
39181
39189
|
const parsed = schema.safeParse(input);
|
|
39182
39190
|
if (!parsed.success) {
|
|
@@ -39242,6 +39250,36 @@ function parseTodosTaskManifestCompensation(input) {
|
|
|
39242
39250
|
}
|
|
39243
39251
|
return parsed.data;
|
|
39244
39252
|
}
|
|
39253
|
+
function parseTodosTaskManifestBindingLookup(input) {
|
|
39254
|
+
const parsed = bindingLookupSchema.safeParse(input);
|
|
39255
|
+
if (!parsed.success) {
|
|
39256
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", `Invalid task-manifest binding lookup: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, { issues: parsed.error.issues });
|
|
39257
|
+
}
|
|
39258
|
+
return parsed.data;
|
|
39259
|
+
}
|
|
39260
|
+
|
|
39261
|
+
// src/task-manifest/backend.ts
|
|
39262
|
+
var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
39263
|
+
function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
39264
|
+
if (rows.length === 0) {
|
|
39265
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BINDING_NOT_FOUND", `Managed task-manifest binding not found for plan: ${planId}`, { plan_id: planId });
|
|
39266
|
+
}
|
|
39267
|
+
if (rows.length !== 1) {
|
|
39268
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest plan lookup matched more than one binding", { plan_id: planId, matched_items: rows.length, max_items: 1 });
|
|
39269
|
+
}
|
|
39270
|
+
const row = rows[0];
|
|
39271
|
+
const bindingVersion = Number(row.binding_version);
|
|
39272
|
+
const state = row.state;
|
|
39273
|
+
if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
|
|
39274
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
39275
|
+
}
|
|
39276
|
+
return {
|
|
39277
|
+
plan_id: planId,
|
|
39278
|
+
apply_receipt_id: row.apply_receipt_id,
|
|
39279
|
+
binding_version: bindingVersion,
|
|
39280
|
+
state
|
|
39281
|
+
};
|
|
39282
|
+
}
|
|
39245
39283
|
|
|
39246
39284
|
// src/task-manifest/reference-guard.ts
|
|
39247
39285
|
function quoteSqliteIdentifier2(value) {
|
|
@@ -39331,10 +39369,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
|
|
|
39331
39369
|
}
|
|
39332
39370
|
|
|
39333
39371
|
// src/task-manifest/schema-sql.ts
|
|
39372
|
+
function sqlString(value) {
|
|
39373
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
39374
|
+
}
|
|
39334
39375
|
function sqliteTodosTaskManifestSchemaSql() {
|
|
39335
39376
|
return `
|
|
39336
39377
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39337
39378
|
receipt_id TEXT PRIMARY KEY,
|
|
39379
|
+
tenant_id TEXT NOT NULL,
|
|
39338
39380
|
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
39339
39381
|
route TEXT NOT NULL,
|
|
39340
39382
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
@@ -39352,6 +39394,7 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39352
39394
|
);
|
|
39353
39395
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39354
39396
|
operation_id TEXT PRIMARY KEY,
|
|
39397
|
+
tenant_id TEXT NOT NULL,
|
|
39355
39398
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
39356
39399
|
request_digest TEXT NOT NULL,
|
|
39357
39400
|
result_digest TEXT NOT NULL,
|
|
@@ -39387,10 +39430,37 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39387
39430
|
END;
|
|
39388
39431
|
`;
|
|
39389
39432
|
}
|
|
39390
|
-
function
|
|
39433
|
+
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
39434
|
+
const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
39435
|
+
return columns.some((column) => column.name === columnName);
|
|
39436
|
+
}
|
|
39437
|
+
function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
39438
|
+
db.exec(sqliteTodosTaskManifestSchemaSql());
|
|
39439
|
+
const tenantDefault = sqlString(tenantId);
|
|
39440
|
+
for (const tableName of [
|
|
39441
|
+
"todos_task_manifest_receipts",
|
|
39442
|
+
"todos_task_manifest_bindings"
|
|
39443
|
+
]) {
|
|
39444
|
+
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
39445
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
39446
|
+
}
|
|
39447
|
+
}
|
|
39448
|
+
db.exec(`
|
|
39449
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
39450
|
+
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
|
|
39451
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
|
|
39452
|
+
ON todos_task_manifest_bindings(
|
|
39453
|
+
tenant_id,
|
|
39454
|
+
json_extract(result_json, '$.graph.plan_id')
|
|
39455
|
+
);
|
|
39456
|
+
`);
|
|
39457
|
+
}
|
|
39458
|
+
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
39459
|
+
const tenantDefault = sqlString(tenantId);
|
|
39391
39460
|
return [
|
|
39392
39461
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39393
39462
|
receipt_id text PRIMARY KEY,
|
|
39463
|
+
tenant_id text NOT NULL,
|
|
39394
39464
|
authority text NOT NULL CHECK(authority = 'todos'),
|
|
39395
39465
|
route text NOT NULL,
|
|
39396
39466
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
@@ -39406,8 +39476,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39406
39476
|
created_at timestamptz NOT NULL,
|
|
39407
39477
|
UNIQUE(kind, idempotency_key)
|
|
39408
39478
|
)`,
|
|
39479
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
39480
|
+
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
39481
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
39482
|
+
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
39409
39483
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39410
39484
|
operation_id text PRIMARY KEY,
|
|
39485
|
+
tenant_id text NOT NULL,
|
|
39411
39486
|
idempotency_key text NOT NULL UNIQUE,
|
|
39412
39487
|
request_digest text NOT NULL,
|
|
39413
39488
|
result_digest text NOT NULL,
|
|
@@ -39420,6 +39495,10 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39420
39495
|
created_at timestamptz NOT NULL,
|
|
39421
39496
|
updated_at timestamptz NOT NULL
|
|
39422
39497
|
)`,
|
|
39498
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
39499
|
+
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
39500
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
39501
|
+
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
39423
39502
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
39424
39503
|
id text PRIMARY KEY,
|
|
39425
39504
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -39433,6 +39512,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39433
39512
|
)`,
|
|
39434
39513
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
39435
39514
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
39515
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
39516
|
+
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
39517
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
39518
|
+
ON todos_task_manifest_bindings(
|
|
39519
|
+
tenant_id,
|
|
39520
|
+
((result_json #>> '{graph,plan_id}'))
|
|
39521
|
+
)`,
|
|
39436
39522
|
`CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
|
|
39437
39523
|
RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
|
|
39438
39524
|
RAISE EXCEPTION 'todos task manifest receipts are immutable';
|
|
@@ -39456,10 +39542,12 @@ function parseApplyResult(value, duplicate) {
|
|
|
39456
39542
|
|
|
39457
39543
|
class SqliteTodosTaskManifestBackend {
|
|
39458
39544
|
db;
|
|
39545
|
+
tenantId;
|
|
39459
39546
|
kind = "sqlite";
|
|
39460
|
-
constructor(db) {
|
|
39547
|
+
constructor(db, tenantId = "default") {
|
|
39461
39548
|
this.db = db;
|
|
39462
|
-
|
|
39549
|
+
this.tenantId = tenantId;
|
|
39550
|
+
ensureSqliteTodosTaskManifestSchema(db, tenantId);
|
|
39463
39551
|
}
|
|
39464
39552
|
async serialized(run) {
|
|
39465
39553
|
const previous = sqliteTails.get(this.db) ?? Promise.resolve();
|
|
@@ -39489,7 +39577,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39489
39577
|
async apply(input, faults) {
|
|
39490
39578
|
return this.serialized(() => {
|
|
39491
39579
|
const { manifest } = input;
|
|
39492
|
-
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(manifest.operation_id);
|
|
39580
|
+
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, manifest.operation_id);
|
|
39493
39581
|
if (binding) {
|
|
39494
39582
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
39495
39583
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
|
|
@@ -39499,7 +39587,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39499
39587
|
}
|
|
39500
39588
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
39501
39589
|
}
|
|
39502
|
-
const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE idempotency_key = ? LIMIT 1").get(manifest.idempotency_key);
|
|
39590
|
+
const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE tenant_id = ? AND idempotency_key = ? LIMIT 1").get(this.tenantId, manifest.idempotency_key);
|
|
39503
39591
|
if (idempotency)
|
|
39504
39592
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
|
|
39505
39593
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -39589,9 +39677,9 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39589
39677
|
const resultJson = canonicalJson(result);
|
|
39590
39678
|
const manifestJson = canonicalJson(manifest);
|
|
39591
39679
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39592
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39680
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39593
39681
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
39594
|
-
) VALUES (?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, 1, NULL, ?, ?, ?)`).run(input.receipt_id, manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, manifestJson, resultJson, input.now);
|
|
39682
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, manifestJson, resultJson, input.now);
|
|
39595
39683
|
for (const entry2 of input.outbox) {
|
|
39596
39684
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
39597
39685
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -39599,24 +39687,55 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39599
39687
|
}
|
|
39600
39688
|
fault(faults, "after_outbox_write");
|
|
39601
39689
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
39602
|
-
operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
39690
|
+
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
39603
39691
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
39604
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
39692
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
|
|
39605
39693
|
fault(faults, "after_receipt_write");
|
|
39606
39694
|
return result;
|
|
39607
39695
|
});
|
|
39608
39696
|
}
|
|
39609
39697
|
async readExact(receiptId2) {
|
|
39610
|
-
const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(receiptId2);
|
|
39698
|
+
const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId2);
|
|
39611
39699
|
if (!row)
|
|
39612
39700
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
39613
39701
|
return parseApplyResult(row.result_json, false);
|
|
39614
39702
|
}
|
|
39703
|
+
async lookupBindingByPlanId(planId) {
|
|
39704
|
+
const rows = this.db.query(`
|
|
39705
|
+
SELECT
|
|
39706
|
+
b.apply_receipt_id AS apply_receipt_id,
|
|
39707
|
+
b.state AS state,
|
|
39708
|
+
b.version AS binding_version,
|
|
39709
|
+
b.tenant_id AS binding_tenant_id,
|
|
39710
|
+
b.operation_id AS binding_operation_id,
|
|
39711
|
+
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
39712
|
+
r.tenant_id AS receipt_tenant_id,
|
|
39713
|
+
r.authority AS receipt_authority,
|
|
39714
|
+
r.route AS receipt_route,
|
|
39715
|
+
r.schema_version AS receipt_schema_version,
|
|
39716
|
+
r.kind AS receipt_kind,
|
|
39717
|
+
r.operation_id AS receipt_operation_id,
|
|
39718
|
+
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
39719
|
+
FROM todos_task_manifest_bindings b
|
|
39720
|
+
LEFT JOIN todos_task_manifest_receipts r
|
|
39721
|
+
ON r.receipt_id = b.apply_receipt_id
|
|
39722
|
+
AND r.tenant_id = b.tenant_id
|
|
39723
|
+
WHERE b.tenant_id = ?
|
|
39724
|
+
AND json_extract(b.result_json, '$.graph.plan_id') = ?
|
|
39725
|
+
LIMIT 2
|
|
39726
|
+
`).all(this.tenantId, planId);
|
|
39727
|
+
return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
|
|
39728
|
+
}
|
|
39615
39729
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
39616
39730
|
await this.serialized(() => {
|
|
39617
39731
|
const result = this.db.query(`UPDATE todos_task_manifest_outbox
|
|
39618
39732
|
SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
|
|
39619
|
-
WHERE id = ? AND status = 'pending'
|
|
39733
|
+
WHERE id = ? AND status = 'pending'
|
|
39734
|
+
AND EXISTS (
|
|
39735
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39736
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39737
|
+
AND r.tenant_id = ?
|
|
39738
|
+
)`).run(deliveredAt, outboxId, this.tenantId);
|
|
39620
39739
|
if (result.changes !== 1) {
|
|
39621
39740
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
39622
39741
|
}
|
|
@@ -39625,23 +39744,28 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39625
39744
|
async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
|
|
39626
39745
|
return this.serialized(() => {
|
|
39627
39746
|
const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
39628
|
-
FROM todos_task_manifest_receipts
|
|
39747
|
+
FROM todos_task_manifest_receipts
|
|
39748
|
+
WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
|
|
39749
|
+
LIMIT 1`).get(this.tenantId, input.idempotency_key);
|
|
39629
39750
|
if (existing) {
|
|
39630
39751
|
if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
|
|
39631
39752
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
|
|
39632
39753
|
}
|
|
39633
39754
|
return { ...JSON.parse(existing.result_json), duplicate: true };
|
|
39634
39755
|
}
|
|
39635
|
-
const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(input.receipt_id);
|
|
39756
|
+
const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, input.receipt_id);
|
|
39636
39757
|
if (!row)
|
|
39637
39758
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
|
|
39638
|
-
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(String(row["operation_id"]));
|
|
39759
|
+
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, String(row["operation_id"]));
|
|
39639
39760
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
39640
39761
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
39641
39762
|
}
|
|
39642
39763
|
if (binding["state"] !== "applied")
|
|
39643
39764
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
39644
|
-
const delivered = this.db.query(
|
|
39765
|
+
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
39766
|
+
JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
|
|
39767
|
+
WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
|
|
39768
|
+
LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
39645
39769
|
if (delivered)
|
|
39646
39770
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
39647
39771
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
@@ -39654,7 +39778,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39654
39778
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
39655
39779
|
];
|
|
39656
39780
|
const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
39657
|
-
FROM todos_task_manifest_outbox
|
|
39781
|
+
FROM todos_task_manifest_outbox
|
|
39782
|
+
WHERE apply_receipt_id = ?
|
|
39783
|
+
AND EXISTS (
|
|
39784
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39785
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39786
|
+
AND r.tenant_id = ?
|
|
39787
|
+
)
|
|
39788
|
+
ORDER BY id`).all(input.receipt_id, this.tenantId);
|
|
39658
39789
|
if (storedOutbox.length !== expectedEffects.length) {
|
|
39659
39790
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
39660
39791
|
}
|
|
@@ -39742,7 +39873,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39742
39873
|
}
|
|
39743
39874
|
expectedVerificationIndex += 1;
|
|
39744
39875
|
}
|
|
39745
|
-
this.db.query(
|
|
39876
|
+
this.db.query(`UPDATE todos_task_manifest_outbox
|
|
39877
|
+
SET status = 'cancelled'
|
|
39878
|
+
WHERE apply_receipt_id = ? AND status = 'pending'
|
|
39879
|
+
AND EXISTS (
|
|
39880
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39881
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39882
|
+
AND r.tenant_id = ?
|
|
39883
|
+
)`).run(input.receipt_id, this.tenantId);
|
|
39746
39884
|
for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
|
|
39747
39885
|
const column = table === "task_dependencies" ? "task_id" : "task_id";
|
|
39748
39886
|
this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
|
|
@@ -39753,11 +39891,11 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39753
39891
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
39754
39892
|
const resultJson = canonicalJson(result);
|
|
39755
39893
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39756
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39894
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39757
39895
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
39758
|
-
) VALUES (?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, receipt.operation_id, input.idempotency_key, requestDigest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
|
|
39896
|
+
) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, input.idempotency_key, requestDigest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
|
|
39759
39897
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
39760
|
-
WHERE operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, receipt.operation_id, input.if_binding_version);
|
|
39898
|
+
WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, this.tenantId, receipt.operation_id, input.if_binding_version);
|
|
39761
39899
|
if (updated.changes !== 1) {
|
|
39762
39900
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
|
|
39763
39901
|
}
|
|
@@ -39896,17 +40034,19 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39896
40034
|
kind = "postgresql";
|
|
39897
40035
|
service;
|
|
39898
40036
|
tableName;
|
|
40037
|
+
tenantId;
|
|
39899
40038
|
schemaReady = null;
|
|
39900
40039
|
constructor(client, options = {}) {
|
|
39901
40040
|
this.client = client;
|
|
39902
40041
|
this.service = options.service ?? "todos";
|
|
39903
40042
|
this.tableName = safeIdentifier2(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
|
|
40043
|
+
this.tenantId = options.tenantId ?? "default";
|
|
39904
40044
|
}
|
|
39905
40045
|
async ensureSchema() {
|
|
39906
40046
|
this.schemaReady ??= (async () => {
|
|
39907
40047
|
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
39908
40048
|
await this.client.query(sql);
|
|
39909
|
-
for (const sql of postgresTodosTaskManifestSchemaSql())
|
|
40049
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
39910
40050
|
await this.client.query(sql);
|
|
39911
40051
|
})();
|
|
39912
40052
|
await this.schemaReady;
|
|
@@ -39928,7 +40068,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39928
40068
|
const { manifest } = input;
|
|
39929
40069
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
39930
40070
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
39931
|
-
const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE
|
|
40071
|
+
const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, manifest.operation_id]);
|
|
39932
40072
|
if (existing.rows[0]) {
|
|
39933
40073
|
const binding = existing.rows[0];
|
|
39934
40074
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
@@ -39939,7 +40079,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39939
40079
|
}
|
|
39940
40080
|
return { ...parseJson2(binding["result_json"]), duplicate: true };
|
|
39941
40081
|
}
|
|
39942
|
-
const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE
|
|
40082
|
+
const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND idempotency_key = $2 LIMIT 1", [this.tenantId, manifest.idempotency_key]);
|
|
39943
40083
|
if (reused.rows[0])
|
|
39944
40084
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
|
|
39945
40085
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -40049,10 +40189,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40049
40189
|
const manifestJson = canonicalJson(manifest);
|
|
40050
40190
|
const resultJson = canonicalJson(result);
|
|
40051
40191
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40052
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40192
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40053
40193
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
40054
|
-
) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'apply', $
|
|
40194
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
|
|
40055
40195
|
input.receipt_id,
|
|
40196
|
+
this.tenantId,
|
|
40056
40197
|
manifest.operation_id,
|
|
40057
40198
|
manifest.idempotency_key,
|
|
40058
40199
|
input.request_digest,
|
|
@@ -40075,10 +40216,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40075
40216
|
}
|
|
40076
40217
|
fault2(faults, "after_outbox_write");
|
|
40077
40218
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
40078
|
-
operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
40219
|
+
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
40079
40220
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
40080
|
-
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $
|
|
40221
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
|
|
40081
40222
|
manifest.operation_id,
|
|
40223
|
+
this.tenantId,
|
|
40082
40224
|
manifest.idempotency_key,
|
|
40083
40225
|
input.request_digest,
|
|
40084
40226
|
input.result_digest,
|
|
@@ -40093,17 +40235,50 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40093
40235
|
}
|
|
40094
40236
|
async readExact(receiptId2) {
|
|
40095
40237
|
await this.ensureSchema();
|
|
40096
|
-
const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE
|
|
40238
|
+
const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
|
|
40097
40239
|
if (!result.rows[0])
|
|
40098
40240
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40099
40241
|
return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
|
|
40100
40242
|
}
|
|
40243
|
+
async lookupBindingByPlanId(planId) {
|
|
40244
|
+
await this.ensureSchema();
|
|
40245
|
+
const result = await this.client.query(`
|
|
40246
|
+
SELECT
|
|
40247
|
+
b.apply_receipt_id AS apply_receipt_id,
|
|
40248
|
+
b.state AS state,
|
|
40249
|
+
b.version AS binding_version,
|
|
40250
|
+
b.tenant_id AS binding_tenant_id,
|
|
40251
|
+
b.operation_id AS binding_operation_id,
|
|
40252
|
+
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
40253
|
+
r.tenant_id AS receipt_tenant_id,
|
|
40254
|
+
r.authority AS receipt_authority,
|
|
40255
|
+
r.route AS receipt_route,
|
|
40256
|
+
r.schema_version AS receipt_schema_version,
|
|
40257
|
+
r.kind AS receipt_kind,
|
|
40258
|
+
r.operation_id AS receipt_operation_id,
|
|
40259
|
+
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
40260
|
+
FROM todos_task_manifest_bindings b
|
|
40261
|
+
LEFT JOIN todos_task_manifest_receipts r
|
|
40262
|
+
ON r.receipt_id = b.apply_receipt_id
|
|
40263
|
+
AND r.tenant_id = b.tenant_id
|
|
40264
|
+
WHERE b.tenant_id = $1
|
|
40265
|
+
AND b.result_json #>> '{graph,plan_id}' = $2
|
|
40266
|
+
LIMIT 2
|
|
40267
|
+
`, [this.tenantId, planId]);
|
|
40268
|
+
return validateTaskManifestBindingLookupRows(result.rows, this.tenantId, planId);
|
|
40269
|
+
}
|
|
40101
40270
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
40102
40271
|
await this.ensureSchema();
|
|
40103
40272
|
await this.client.transaction(async (tx) => {
|
|
40104
40273
|
const result = await tx.query(`UPDATE todos_task_manifest_outbox
|
|
40105
40274
|
SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
|
|
40106
|
-
WHERE id = $2 AND status = 'pending'
|
|
40275
|
+
WHERE id = $2 AND status = 'pending'
|
|
40276
|
+
AND EXISTS (
|
|
40277
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40278
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
40279
|
+
AND r.tenant_id = $3
|
|
40280
|
+
)
|
|
40281
|
+
RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
|
|
40107
40282
|
if (!result.rows[0])
|
|
40108
40283
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
40109
40284
|
});
|
|
@@ -40113,7 +40288,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40113
40288
|
return this.client.transaction(async (tx) => {
|
|
40114
40289
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
|
|
40115
40290
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fcompensation-idempotency\x1F${input.idempotency_key}`]);
|
|
40116
|
-
const previous = await tx.query(
|
|
40291
|
+
const previous = await tx.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
40292
|
+
FROM todos_task_manifest_receipts
|
|
40293
|
+
WHERE tenant_id = $1 AND kind = 'compensate' AND idempotency_key = $2
|
|
40294
|
+
LIMIT 1`, [this.tenantId, input.idempotency_key]);
|
|
40117
40295
|
if (previous.rows[0]) {
|
|
40118
40296
|
const row = previous.rows[0];
|
|
40119
40297
|
if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
|
|
@@ -40121,18 +40299,21 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40121
40299
|
}
|
|
40122
40300
|
return { ...parseJson2(row["result_json"]), duplicate: true };
|
|
40123
40301
|
}
|
|
40124
|
-
const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE
|
|
40302
|
+
const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, input.receipt_id]);
|
|
40125
40303
|
const applyRow = applyRows.rows[0];
|
|
40126
40304
|
if (!applyRow)
|
|
40127
40305
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
|
|
40128
|
-
const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE
|
|
40306
|
+
const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, receipt.operation_id]);
|
|
40129
40307
|
const binding = bindingRows.rows[0];
|
|
40130
40308
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40131
40309
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40132
40310
|
}
|
|
40133
40311
|
if (binding["state"] !== "applied")
|
|
40134
40312
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
40135
|
-
const delivered = await tx.query(
|
|
40313
|
+
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
40314
|
+
JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
|
|
40315
|
+
WHERE r.tenant_id = $1 AND o.apply_receipt_id = $2 AND o.status = 'delivered'
|
|
40316
|
+
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
40136
40317
|
if (delivered.rows[0])
|
|
40137
40318
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
40138
40319
|
const applyResult = parseJson2(applyRow["result_json"]);
|
|
@@ -40145,7 +40326,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40145
40326
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
40146
40327
|
];
|
|
40147
40328
|
const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
40148
|
-
FROM todos_task_manifest_outbox
|
|
40329
|
+
FROM todos_task_manifest_outbox
|
|
40330
|
+
WHERE apply_receipt_id = $1
|
|
40331
|
+
AND EXISTS (
|
|
40332
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40333
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
40334
|
+
AND r.tenant_id = $2
|
|
40335
|
+
)
|
|
40336
|
+
ORDER BY id`, [input.receipt_id, this.tenantId]);
|
|
40149
40337
|
if (outboxRows.rows.length !== expectedEffects.length) {
|
|
40150
40338
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
40151
40339
|
}
|
|
@@ -40245,7 +40433,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40245
40433
|
}
|
|
40246
40434
|
if (stored.rows.length !== managedIds.length)
|
|
40247
40435
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
|
|
40248
|
-
await tx.query(
|
|
40436
|
+
await tx.query(`UPDATE todos_task_manifest_outbox
|
|
40437
|
+
SET status = 'cancelled'
|
|
40438
|
+
WHERE apply_receipt_id = $1 AND status = 'pending'
|
|
40439
|
+
AND EXISTS (
|
|
40440
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40441
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
40442
|
+
AND r.tenant_id = $2
|
|
40443
|
+
)`, [input.receipt_id, this.tenantId]);
|
|
40249
40444
|
const typedIds = [
|
|
40250
40445
|
["dependencies", applyResult.graph.dependency_ids],
|
|
40251
40446
|
["comments", applyResult.graph.comment_ids],
|
|
@@ -40262,10 +40457,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40262
40457
|
const readback = await this.readback(tx, applyResult.graph);
|
|
40263
40458
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
40264
40459
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40265
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40460
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40266
40461
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
40267
|
-
) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $
|
|
40462
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
|
|
40268
40463
|
compensationReceiptId,
|
|
40464
|
+
this.tenantId,
|
|
40269
40465
|
receipt.operation_id,
|
|
40270
40466
|
input.idempotency_key,
|
|
40271
40467
|
requestDigest,
|
|
@@ -40277,10 +40473,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40277
40473
|
]);
|
|
40278
40474
|
const updated = await tx.query(`UPDATE todos_task_manifest_bindings
|
|
40279
40475
|
SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
|
|
40280
|
-
WHERE
|
|
40476
|
+
WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
|
|
40281
40477
|
receipt.binding_version,
|
|
40282
40478
|
compensationReceiptId,
|
|
40283
40479
|
now4,
|
|
40480
|
+
this.tenantId,
|
|
40284
40481
|
receipt.operation_id,
|
|
40285
40482
|
input.if_binding_version
|
|
40286
40483
|
]);
|
|
@@ -40320,6 +40517,13 @@ var FAULT_POINTS = [
|
|
|
40320
40517
|
"after_outbox_write",
|
|
40321
40518
|
"after_receipt_write"
|
|
40322
40519
|
];
|
|
40520
|
+
function resolveTenantId(value) {
|
|
40521
|
+
const tenantId = value ?? "default";
|
|
40522
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenantId)) {
|
|
40523
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
40524
|
+
}
|
|
40525
|
+
return tenantId;
|
|
40526
|
+
}
|
|
40323
40527
|
function normalize(input, now4) {
|
|
40324
40528
|
const parsed = parseTodosTaskManifest(input);
|
|
40325
40529
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
@@ -40407,15 +40611,18 @@ function sanitizeManifest(manifest) {
|
|
|
40407
40611
|
class PackageOwnedTodosTaskManifestAuthority {
|
|
40408
40612
|
backend;
|
|
40409
40613
|
options;
|
|
40614
|
+
tenantId;
|
|
40410
40615
|
constructor(backend, options = {}) {
|
|
40411
40616
|
this.backend = backend;
|
|
40412
40617
|
this.options = options;
|
|
40618
|
+
this.tenantId = resolveTenantId(options.tenantId);
|
|
40413
40619
|
}
|
|
40414
40620
|
async capability() {
|
|
40415
40621
|
return {
|
|
40416
40622
|
authority: "todos",
|
|
40417
40623
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
40418
40624
|
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
40625
|
+
tenant_id: this.tenantId,
|
|
40419
40626
|
backend: this.backend.kind,
|
|
40420
40627
|
deterministic_ids: true,
|
|
40421
40628
|
immutable_receipts: true,
|
|
@@ -40454,6 +40661,22 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
40454
40661
|
}
|
|
40455
40662
|
return this.backend.readExact(receiptId2).then((result) => this.bounded(result));
|
|
40456
40663
|
}
|
|
40664
|
+
async lookupBinding(input) {
|
|
40665
|
+
const request = parseTodosTaskManifestBindingLookup(input);
|
|
40666
|
+
if (request.max_items !== 1) {
|
|
40667
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", "max_items must be exactly 1 for task-manifest binding lookup", { max_items: request.max_items, max_items_limit: 1 });
|
|
40668
|
+
}
|
|
40669
|
+
if (request.authority !== "todos" || request.route !== TODOS_TASK_MANIFEST_ROUTE || request.schema_version !== TODOS_TASK_MANIFEST_SCHEMA_VERSION || request.tenant_id !== this.tenantId) {
|
|
40670
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAPABILITY_MISMATCH", "Task-manifest binding lookup does not match this authority identity");
|
|
40671
|
+
}
|
|
40672
|
+
return this.bounded({
|
|
40673
|
+
authority: "todos",
|
|
40674
|
+
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
40675
|
+
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
40676
|
+
tenant_id: this.tenantId,
|
|
40677
|
+
...await this.backend.lookupBindingByPlanId(request.plan_id)
|
|
40678
|
+
});
|
|
40679
|
+
}
|
|
40457
40680
|
markOutboxDelivered(outboxId) {
|
|
40458
40681
|
if (!outboxId || outboxId.length > 200) {
|
|
40459
40682
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
|
|
@@ -40493,13 +40716,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
|
|
|
40493
40716
|
if (!options?.database) {
|
|
40494
40717
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
|
|
40495
40718
|
}
|
|
40496
|
-
|
|
40719
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40720
|
+
return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
|
|
40497
40721
|
}
|
|
40498
40722
|
function createPostgresTodosTaskManifestAuthority(client, options = {}) {
|
|
40499
40723
|
if (!client || typeof client.transaction !== "function") {
|
|
40500
40724
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
|
|
40501
40725
|
}
|
|
40502
|
-
|
|
40726
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40727
|
+
return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
|
|
40503
40728
|
}
|
|
40504
40729
|
// src/task-manifest/http.ts
|
|
40505
40730
|
var JSON_HEADERS2 = { "Content-Type": "application/json" };
|
|
@@ -40513,6 +40738,7 @@ function status(error) {
|
|
|
40513
40738
|
case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
|
|
40514
40739
|
return 400;
|
|
40515
40740
|
case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
|
|
40741
|
+
case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
|
|
40516
40742
|
return 404;
|
|
40517
40743
|
case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
|
|
40518
40744
|
return 503;
|
|
@@ -40575,6 +40801,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
|
|
|
40575
40801
|
}
|
|
40576
40802
|
return json2({ result: await authority.readExact(input.receipt_id) });
|
|
40577
40803
|
}
|
|
40804
|
+
if (action === "bindings/lookup") {
|
|
40805
|
+
return json2({
|
|
40806
|
+
result: await authority.lookupBinding(await body(request))
|
|
40807
|
+
});
|
|
40808
|
+
}
|
|
40578
40809
|
if (action === "compensate") {
|
|
40579
40810
|
return json2({ result: await authority.compensate(await body(request)) }, 201);
|
|
40580
40811
|
}
|
|
@@ -40634,6 +40865,9 @@ class TodosTaskManifestHttpClient {
|
|
|
40634
40865
|
async readExact(receiptId2) {
|
|
40635
40866
|
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId2 }) })).result;
|
|
40636
40867
|
}
|
|
40868
|
+
async lookupBinding(input) {
|
|
40869
|
+
return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
|
|
40870
|
+
}
|
|
40637
40871
|
async markOutboxDelivered(outboxId) {
|
|
40638
40872
|
await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
|
|
40639
40873
|
}
|
|
@@ -61987,6 +62221,7 @@ export {
|
|
|
61987
62221
|
pauseFocusSession,
|
|
61988
62222
|
patrolTasks,
|
|
61989
62223
|
parseTodosTaskManifestCompensation,
|
|
62224
|
+
parseTodosTaskManifestBindingLookup,
|
|
61990
62225
|
parseTodosTaskManifest,
|
|
61991
62226
|
parseTmuxTarget,
|
|
61992
62227
|
parseStorageMode,
|