@hasna/todos 0.15.14 → 0.15.16
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/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +12470 -10747
- package/dist/contracts.js +1 -1
- package/dist/index.js +358 -47
- package/dist/lib/agent-adapter-docs.d.ts +10 -0
- package/dist/lib/agent-adapter-docs.d.ts.map +1 -1
- package/dist/mcp/index.js +2290 -803
- 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 +19806 -18319
- 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 +2 -2
- 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 +24 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +357 -46
- 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.16",
|
|
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",
|
|
@@ -39090,6 +39090,9 @@ init_types();
|
|
|
39090
39090
|
// src/task-manifest/types.ts
|
|
39091
39091
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
|
|
39092
39092
|
var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
|
|
39093
|
+
function supportsIdempotentOutboxDelivery(capability2) {
|
|
39094
|
+
return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
|
|
39095
|
+
}
|
|
39093
39096
|
|
|
39094
39097
|
class TodosTaskManifestError extends Error {
|
|
39095
39098
|
code;
|
|
@@ -39177,6 +39180,14 @@ var compensationSchema = exports_external.object({
|
|
|
39177
39180
|
idempotency_key: identifier,
|
|
39178
39181
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
39179
39182
|
}).strict();
|
|
39183
|
+
var bindingLookupSchema = exports_external.object({
|
|
39184
|
+
authority: exports_external.string().min(1).max(64),
|
|
39185
|
+
route: exports_external.string().min(1).max(128),
|
|
39186
|
+
schema_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
|
|
39187
|
+
tenant_id: identifier,
|
|
39188
|
+
plan_id: uuid2,
|
|
39189
|
+
max_items: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
39190
|
+
}).strict();
|
|
39180
39191
|
function parseTodosTaskManifest(input) {
|
|
39181
39192
|
const parsed = schema.safeParse(input);
|
|
39182
39193
|
if (!parsed.success) {
|
|
@@ -39242,6 +39253,36 @@ function parseTodosTaskManifestCompensation(input) {
|
|
|
39242
39253
|
}
|
|
39243
39254
|
return parsed.data;
|
|
39244
39255
|
}
|
|
39256
|
+
function parseTodosTaskManifestBindingLookup(input) {
|
|
39257
|
+
const parsed = bindingLookupSchema.safeParse(input);
|
|
39258
|
+
if (!parsed.success) {
|
|
39259
|
+
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 });
|
|
39260
|
+
}
|
|
39261
|
+
return parsed.data;
|
|
39262
|
+
}
|
|
39263
|
+
|
|
39264
|
+
// src/task-manifest/backend.ts
|
|
39265
|
+
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;
|
|
39266
|
+
function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
|
|
39267
|
+
if (rows.length === 0) {
|
|
39268
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BINDING_NOT_FOUND", `Managed task-manifest binding not found for plan: ${planId}`, { plan_id: planId });
|
|
39269
|
+
}
|
|
39270
|
+
if (rows.length !== 1) {
|
|
39271
|
+
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 });
|
|
39272
|
+
}
|
|
39273
|
+
const row = rows[0];
|
|
39274
|
+
const bindingVersion = Number(row.binding_version);
|
|
39275
|
+
const state = row.state;
|
|
39276
|
+
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") {
|
|
39277
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
|
|
39278
|
+
}
|
|
39279
|
+
return {
|
|
39280
|
+
plan_id: planId,
|
|
39281
|
+
apply_receipt_id: row.apply_receipt_id,
|
|
39282
|
+
binding_version: bindingVersion,
|
|
39283
|
+
state
|
|
39284
|
+
};
|
|
39285
|
+
}
|
|
39245
39286
|
|
|
39246
39287
|
// src/task-manifest/reference-guard.ts
|
|
39247
39288
|
function quoteSqliteIdentifier2(value) {
|
|
@@ -39331,10 +39372,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
|
|
|
39331
39372
|
}
|
|
39332
39373
|
|
|
39333
39374
|
// src/task-manifest/schema-sql.ts
|
|
39375
|
+
function sqlString(value) {
|
|
39376
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
39377
|
+
}
|
|
39334
39378
|
function sqliteTodosTaskManifestSchemaSql() {
|
|
39335
39379
|
return `
|
|
39336
39380
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39337
39381
|
receipt_id TEXT PRIMARY KEY,
|
|
39382
|
+
tenant_id TEXT NOT NULL,
|
|
39338
39383
|
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
39339
39384
|
route TEXT NOT NULL,
|
|
39340
39385
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
@@ -39352,6 +39397,7 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39352
39397
|
);
|
|
39353
39398
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39354
39399
|
operation_id TEXT PRIMARY KEY,
|
|
39400
|
+
tenant_id TEXT NOT NULL,
|
|
39355
39401
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
39356
39402
|
request_digest TEXT NOT NULL,
|
|
39357
39403
|
result_digest TEXT NOT NULL,
|
|
@@ -39387,10 +39433,37 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39387
39433
|
END;
|
|
39388
39434
|
`;
|
|
39389
39435
|
}
|
|
39390
|
-
function
|
|
39436
|
+
function sqliteTableHasColumn(db, tableName, columnName) {
|
|
39437
|
+
const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
39438
|
+
return columns.some((column) => column.name === columnName);
|
|
39439
|
+
}
|
|
39440
|
+
function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
|
|
39441
|
+
db.exec(sqliteTodosTaskManifestSchemaSql());
|
|
39442
|
+
const tenantDefault = sqlString(tenantId);
|
|
39443
|
+
for (const tableName of [
|
|
39444
|
+
"todos_task_manifest_receipts",
|
|
39445
|
+
"todos_task_manifest_bindings"
|
|
39446
|
+
]) {
|
|
39447
|
+
if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
|
|
39448
|
+
db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
|
|
39449
|
+
}
|
|
39450
|
+
}
|
|
39451
|
+
db.exec(`
|
|
39452
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
|
|
39453
|
+
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
|
|
39454
|
+
CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
|
|
39455
|
+
ON todos_task_manifest_bindings(
|
|
39456
|
+
tenant_id,
|
|
39457
|
+
json_extract(result_json, '$.graph.plan_id')
|
|
39458
|
+
);
|
|
39459
|
+
`);
|
|
39460
|
+
}
|
|
39461
|
+
function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
|
|
39462
|
+
const tenantDefault = sqlString(tenantId);
|
|
39391
39463
|
return [
|
|
39392
39464
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39393
39465
|
receipt_id text PRIMARY KEY,
|
|
39466
|
+
tenant_id text NOT NULL,
|
|
39394
39467
|
authority text NOT NULL CHECK(authority = 'todos'),
|
|
39395
39468
|
route text NOT NULL,
|
|
39396
39469
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
@@ -39406,8 +39479,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39406
39479
|
created_at timestamptz NOT NULL,
|
|
39407
39480
|
UNIQUE(kind, idempotency_key)
|
|
39408
39481
|
)`,
|
|
39482
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
39483
|
+
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
39484
|
+
`ALTER TABLE todos_task_manifest_receipts
|
|
39485
|
+
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
39409
39486
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39410
39487
|
operation_id text PRIMARY KEY,
|
|
39488
|
+
tenant_id text NOT NULL,
|
|
39411
39489
|
idempotency_key text NOT NULL UNIQUE,
|
|
39412
39490
|
request_digest text NOT NULL,
|
|
39413
39491
|
result_digest text NOT NULL,
|
|
@@ -39420,6 +39498,10 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39420
39498
|
created_at timestamptz NOT NULL,
|
|
39421
39499
|
updated_at timestamptz NOT NULL
|
|
39422
39500
|
)`,
|
|
39501
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
39502
|
+
ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
|
|
39503
|
+
`ALTER TABLE todos_task_manifest_bindings
|
|
39504
|
+
ALTER COLUMN tenant_id DROP DEFAULT`,
|
|
39423
39505
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
39424
39506
|
id text PRIMARY KEY,
|
|
39425
39507
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -39433,6 +39515,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39433
39515
|
)`,
|
|
39434
39516
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
39435
39517
|
ON todos_task_manifest_outbox(apply_receipt_id, status)`,
|
|
39518
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
|
|
39519
|
+
ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
|
|
39520
|
+
`CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
|
|
39521
|
+
ON todos_task_manifest_bindings(
|
|
39522
|
+
tenant_id,
|
|
39523
|
+
((result_json #>> '{graph,plan_id}'))
|
|
39524
|
+
)`,
|
|
39436
39525
|
`CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
|
|
39437
39526
|
RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
|
|
39438
39527
|
RAISE EXCEPTION 'todos task manifest receipts are immutable';
|
|
@@ -39456,10 +39545,12 @@ function parseApplyResult(value, duplicate) {
|
|
|
39456
39545
|
|
|
39457
39546
|
class SqliteTodosTaskManifestBackend {
|
|
39458
39547
|
db;
|
|
39548
|
+
tenantId;
|
|
39459
39549
|
kind = "sqlite";
|
|
39460
|
-
constructor(db) {
|
|
39550
|
+
constructor(db, tenantId = "default") {
|
|
39461
39551
|
this.db = db;
|
|
39462
|
-
|
|
39552
|
+
this.tenantId = tenantId;
|
|
39553
|
+
ensureSqliteTodosTaskManifestSchema(db, tenantId);
|
|
39463
39554
|
}
|
|
39464
39555
|
async serialized(run) {
|
|
39465
39556
|
const previous = sqliteTails.get(this.db) ?? Promise.resolve();
|
|
@@ -39489,7 +39580,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39489
39580
|
async apply(input, faults) {
|
|
39490
39581
|
return this.serialized(() => {
|
|
39491
39582
|
const { manifest } = input;
|
|
39492
|
-
const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(manifest.operation_id);
|
|
39583
|
+
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
39584
|
if (binding) {
|
|
39494
39585
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
39495
39586
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
|
|
@@ -39499,7 +39590,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39499
39590
|
}
|
|
39500
39591
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
39501
39592
|
}
|
|
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);
|
|
39593
|
+
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
39594
|
if (idempotency)
|
|
39504
39595
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
|
|
39505
39596
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -39589,9 +39680,9 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39589
39680
|
const resultJson = canonicalJson(result);
|
|
39590
39681
|
const manifestJson = canonicalJson(manifest);
|
|
39591
39682
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39592
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39683
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39593
39684
|
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);
|
|
39685
|
+
) 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
39686
|
for (const entry2 of input.outbox) {
|
|
39596
39687
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
39597
39688
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -39599,49 +39690,102 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39599
39690
|
}
|
|
39600
39691
|
fault(faults, "after_outbox_write");
|
|
39601
39692
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
39602
|
-
operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
39693
|
+
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
39603
39694
|
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);
|
|
39695
|
+
) 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
39696
|
fault(faults, "after_receipt_write");
|
|
39606
39697
|
return result;
|
|
39607
39698
|
});
|
|
39608
39699
|
}
|
|
39609
39700
|
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);
|
|
39701
|
+
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
39702
|
if (!row)
|
|
39612
39703
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
39613
39704
|
return parseApplyResult(row.result_json, false);
|
|
39614
39705
|
}
|
|
39706
|
+
async lookupBindingByPlanId(planId) {
|
|
39707
|
+
const rows = this.db.query(`
|
|
39708
|
+
SELECT
|
|
39709
|
+
b.apply_receipt_id AS apply_receipt_id,
|
|
39710
|
+
b.state AS state,
|
|
39711
|
+
b.version AS binding_version,
|
|
39712
|
+
b.tenant_id AS binding_tenant_id,
|
|
39713
|
+
b.operation_id AS binding_operation_id,
|
|
39714
|
+
json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
|
|
39715
|
+
r.tenant_id AS receipt_tenant_id,
|
|
39716
|
+
r.authority AS receipt_authority,
|
|
39717
|
+
r.route AS receipt_route,
|
|
39718
|
+
r.schema_version AS receipt_schema_version,
|
|
39719
|
+
r.kind AS receipt_kind,
|
|
39720
|
+
r.operation_id AS receipt_operation_id,
|
|
39721
|
+
json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
|
|
39722
|
+
FROM todos_task_manifest_bindings b
|
|
39723
|
+
LEFT JOIN todos_task_manifest_receipts r
|
|
39724
|
+
ON r.receipt_id = b.apply_receipt_id
|
|
39725
|
+
AND r.tenant_id = b.tenant_id
|
|
39726
|
+
WHERE b.tenant_id = ?
|
|
39727
|
+
AND json_extract(b.result_json, '$.graph.plan_id') = ?
|
|
39728
|
+
LIMIT 2
|
|
39729
|
+
`).all(this.tenantId, planId);
|
|
39730
|
+
return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
|
|
39731
|
+
}
|
|
39615
39732
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
39616
39733
|
await this.serialized(() => {
|
|
39617
39734
|
const result = this.db.query(`UPDATE todos_task_manifest_outbox
|
|
39618
39735
|
SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
|
|
39619
|
-
WHERE id = ? AND status = 'pending'
|
|
39620
|
-
|
|
39621
|
-
|
|
39622
|
-
|
|
39736
|
+
WHERE id = ? AND status = 'pending'
|
|
39737
|
+
AND EXISTS (
|
|
39738
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39739
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39740
|
+
AND r.tenant_id = ?
|
|
39741
|
+
AND r.authority = 'todos'
|
|
39742
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
39743
|
+
AND r.schema_version = 1
|
|
39744
|
+
AND r.kind = 'apply'
|
|
39745
|
+
)`).run(deliveredAt, outboxId, this.tenantId);
|
|
39746
|
+
if (result.changes === 1)
|
|
39747
|
+
return;
|
|
39748
|
+
const existing = this.db.query(`SELECT o.status
|
|
39749
|
+
FROM todos_task_manifest_outbox o
|
|
39750
|
+
JOIN todos_task_manifest_receipts r
|
|
39751
|
+
ON r.receipt_id = o.apply_receipt_id
|
|
39752
|
+
WHERE r.tenant_id = ?
|
|
39753
|
+
AND r.authority = 'todos'
|
|
39754
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
39755
|
+
AND r.schema_version = 1
|
|
39756
|
+
AND r.kind = 'apply'
|
|
39757
|
+
AND o.id = ?
|
|
39758
|
+
LIMIT 1`).get(this.tenantId, outboxId);
|
|
39759
|
+
if (existing?.status === "delivered")
|
|
39760
|
+
return;
|
|
39761
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
39623
39762
|
});
|
|
39624
39763
|
}
|
|
39625
39764
|
async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
|
|
39626
39765
|
return this.serialized(() => {
|
|
39627
39766
|
const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
39628
|
-
FROM todos_task_manifest_receipts
|
|
39767
|
+
FROM todos_task_manifest_receipts
|
|
39768
|
+
WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
|
|
39769
|
+
LIMIT 1`).get(this.tenantId, input.idempotency_key);
|
|
39629
39770
|
if (existing) {
|
|
39630
39771
|
if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
|
|
39631
39772
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
|
|
39632
39773
|
}
|
|
39633
39774
|
return { ...JSON.parse(existing.result_json), duplicate: true };
|
|
39634
39775
|
}
|
|
39635
|
-
const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(input.receipt_id);
|
|
39776
|
+
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
39777
|
if (!row)
|
|
39637
39778
|
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"]));
|
|
39779
|
+
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
39780
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
39640
39781
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
39641
39782
|
}
|
|
39642
39783
|
if (binding["state"] !== "applied")
|
|
39643
39784
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
39644
|
-
const delivered = this.db.query(
|
|
39785
|
+
const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
39786
|
+
JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
|
|
39787
|
+
WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
|
|
39788
|
+
LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
39645
39789
|
if (delivered)
|
|
39646
39790
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
39647
39791
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
@@ -39654,7 +39798,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39654
39798
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
39655
39799
|
];
|
|
39656
39800
|
const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
39657
|
-
FROM todos_task_manifest_outbox
|
|
39801
|
+
FROM todos_task_manifest_outbox
|
|
39802
|
+
WHERE apply_receipt_id = ?
|
|
39803
|
+
AND EXISTS (
|
|
39804
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39805
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39806
|
+
AND r.tenant_id = ?
|
|
39807
|
+
)
|
|
39808
|
+
ORDER BY id`).all(input.receipt_id, this.tenantId);
|
|
39658
39809
|
if (storedOutbox.length !== expectedEffects.length) {
|
|
39659
39810
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
39660
39811
|
}
|
|
@@ -39742,7 +39893,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39742
39893
|
}
|
|
39743
39894
|
expectedVerificationIndex += 1;
|
|
39744
39895
|
}
|
|
39745
|
-
this.db.query(
|
|
39896
|
+
this.db.query(`UPDATE todos_task_manifest_outbox
|
|
39897
|
+
SET status = 'cancelled'
|
|
39898
|
+
WHERE apply_receipt_id = ? AND status = 'pending'
|
|
39899
|
+
AND EXISTS (
|
|
39900
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
39901
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
39902
|
+
AND r.tenant_id = ?
|
|
39903
|
+
)`).run(input.receipt_id, this.tenantId);
|
|
39746
39904
|
for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
|
|
39747
39905
|
const column = table === "task_dependencies" ? "task_id" : "task_id";
|
|
39748
39906
|
this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
|
|
@@ -39753,11 +39911,11 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39753
39911
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
39754
39912
|
const resultJson = canonicalJson(result);
|
|
39755
39913
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39756
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39914
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
39757
39915
|
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);
|
|
39916
|
+
) 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
39917
|
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);
|
|
39918
|
+
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
39919
|
if (updated.changes !== 1) {
|
|
39762
39920
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
|
|
39763
39921
|
}
|
|
@@ -39896,17 +40054,19 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39896
40054
|
kind = "postgresql";
|
|
39897
40055
|
service;
|
|
39898
40056
|
tableName;
|
|
40057
|
+
tenantId;
|
|
39899
40058
|
schemaReady = null;
|
|
39900
40059
|
constructor(client, options = {}) {
|
|
39901
40060
|
this.client = client;
|
|
39902
40061
|
this.service = options.service ?? "todos";
|
|
39903
40062
|
this.tableName = safeIdentifier2(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
|
|
40063
|
+
this.tenantId = options.tenantId ?? "default";
|
|
39904
40064
|
}
|
|
39905
40065
|
async ensureSchema() {
|
|
39906
40066
|
this.schemaReady ??= (async () => {
|
|
39907
40067
|
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
39908
40068
|
await this.client.query(sql);
|
|
39909
|
-
for (const sql of postgresTodosTaskManifestSchemaSql())
|
|
40069
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
39910
40070
|
await this.client.query(sql);
|
|
39911
40071
|
})();
|
|
39912
40072
|
await this.schemaReady;
|
|
@@ -39928,7 +40088,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39928
40088
|
const { manifest } = input;
|
|
39929
40089
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
39930
40090
|
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
|
|
40091
|
+
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
40092
|
if (existing.rows[0]) {
|
|
39933
40093
|
const binding = existing.rows[0];
|
|
39934
40094
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
@@ -39939,7 +40099,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39939
40099
|
}
|
|
39940
40100
|
return { ...parseJson2(binding["result_json"]), duplicate: true };
|
|
39941
40101
|
}
|
|
39942
|
-
const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE
|
|
40102
|
+
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
40103
|
if (reused.rows[0])
|
|
39944
40104
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
|
|
39945
40105
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -40049,10 +40209,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40049
40209
|
const manifestJson = canonicalJson(manifest);
|
|
40050
40210
|
const resultJson = canonicalJson(result);
|
|
40051
40211
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40052
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40212
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40053
40213
|
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', $
|
|
40214
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
|
|
40055
40215
|
input.receipt_id,
|
|
40216
|
+
this.tenantId,
|
|
40056
40217
|
manifest.operation_id,
|
|
40057
40218
|
manifest.idempotency_key,
|
|
40058
40219
|
input.request_digest,
|
|
@@ -40075,10 +40236,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40075
40236
|
}
|
|
40076
40237
|
fault2(faults, "after_outbox_write");
|
|
40077
40238
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
40078
|
-
operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
40239
|
+
operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
|
|
40079
40240
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
40080
|
-
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $
|
|
40241
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
|
|
40081
40242
|
manifest.operation_id,
|
|
40243
|
+
this.tenantId,
|
|
40082
40244
|
manifest.idempotency_key,
|
|
40083
40245
|
input.request_digest,
|
|
40084
40246
|
input.result_digest,
|
|
@@ -40093,19 +40255,86 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40093
40255
|
}
|
|
40094
40256
|
async readExact(receiptId2) {
|
|
40095
40257
|
await this.ensureSchema();
|
|
40096
|
-
const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE
|
|
40258
|
+
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
40259
|
if (!result.rows[0])
|
|
40098
40260
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40099
40261
|
return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
|
|
40100
40262
|
}
|
|
40263
|
+
async lookupBindingByPlanId(planId) {
|
|
40264
|
+
await this.ensureSchema();
|
|
40265
|
+
const result = await this.client.query(`
|
|
40266
|
+
SELECT
|
|
40267
|
+
b.apply_receipt_id AS apply_receipt_id,
|
|
40268
|
+
b.state AS state,
|
|
40269
|
+
b.version AS binding_version,
|
|
40270
|
+
b.tenant_id AS binding_tenant_id,
|
|
40271
|
+
b.operation_id AS binding_operation_id,
|
|
40272
|
+
b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
|
|
40273
|
+
r.tenant_id AS receipt_tenant_id,
|
|
40274
|
+
r.authority AS receipt_authority,
|
|
40275
|
+
r.route AS receipt_route,
|
|
40276
|
+
r.schema_version AS receipt_schema_version,
|
|
40277
|
+
r.kind AS receipt_kind,
|
|
40278
|
+
r.operation_id AS receipt_operation_id,
|
|
40279
|
+
r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
|
|
40280
|
+
FROM todos_task_manifest_bindings b
|
|
40281
|
+
LEFT JOIN todos_task_manifest_receipts r
|
|
40282
|
+
ON r.receipt_id = b.apply_receipt_id
|
|
40283
|
+
AND r.tenant_id = b.tenant_id
|
|
40284
|
+
WHERE b.tenant_id = $1
|
|
40285
|
+
AND b.result_json #>> '{graph,plan_id}' = $2
|
|
40286
|
+
LIMIT 2
|
|
40287
|
+
`, [this.tenantId, planId]);
|
|
40288
|
+
return validateTaskManifestBindingLookupRows(result.rows, this.tenantId, planId);
|
|
40289
|
+
}
|
|
40101
40290
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
40102
40291
|
await this.ensureSchema();
|
|
40103
40292
|
await this.client.transaction(async (tx) => {
|
|
40293
|
+
const owned = await tx.query(`SELECT r.operation_id
|
|
40294
|
+
FROM todos_task_manifest_outbox o
|
|
40295
|
+
JOIN todos_task_manifest_receipts r
|
|
40296
|
+
ON r.receipt_id = o.apply_receipt_id
|
|
40297
|
+
WHERE r.tenant_id = $1
|
|
40298
|
+
AND r.authority = 'todos'
|
|
40299
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40300
|
+
AND r.schema_version = 1
|
|
40301
|
+
AND r.kind = 'apply'
|
|
40302
|
+
AND o.id = $2
|
|
40303
|
+
LIMIT 1`, [this.tenantId, outboxId]);
|
|
40304
|
+
const operationId = owned.rows[0]?.operation_id;
|
|
40305
|
+
if (operationId == null) {
|
|
40306
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
40307
|
+
}
|
|
40308
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${String(operationId)}`]);
|
|
40104
40309
|
const result = await tx.query(`UPDATE todos_task_manifest_outbox
|
|
40105
40310
|
SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
|
|
40106
|
-
WHERE id = $2 AND status = 'pending'
|
|
40107
|
-
|
|
40108
|
-
|
|
40311
|
+
WHERE id = $2 AND status = 'pending'
|
|
40312
|
+
AND EXISTS (
|
|
40313
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40314
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
40315
|
+
AND r.tenant_id = $3
|
|
40316
|
+
AND r.authority = 'todos'
|
|
40317
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40318
|
+
AND r.schema_version = 1
|
|
40319
|
+
AND r.kind = 'apply'
|
|
40320
|
+
)
|
|
40321
|
+
RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
|
|
40322
|
+
if (result.rows[0])
|
|
40323
|
+
return;
|
|
40324
|
+
const existing = await tx.query(`SELECT o.status
|
|
40325
|
+
FROM todos_task_manifest_outbox o
|
|
40326
|
+
JOIN todos_task_manifest_receipts r
|
|
40327
|
+
ON r.receipt_id = o.apply_receipt_id
|
|
40328
|
+
WHERE r.tenant_id = $1
|
|
40329
|
+
AND r.authority = 'todos'
|
|
40330
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40331
|
+
AND r.schema_version = 1
|
|
40332
|
+
AND r.kind = 'apply'
|
|
40333
|
+
AND o.id = $2
|
|
40334
|
+
LIMIT 1`, [this.tenantId, outboxId]);
|
|
40335
|
+
if (existing.rows[0]?.status === "delivered")
|
|
40336
|
+
return;
|
|
40337
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
40109
40338
|
});
|
|
40110
40339
|
}
|
|
40111
40340
|
async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
|
|
@@ -40113,7 +40342,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40113
40342
|
return this.client.transaction(async (tx) => {
|
|
40114
40343
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
|
|
40115
40344
|
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(
|
|
40345
|
+
const previous = await tx.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
40346
|
+
FROM todos_task_manifest_receipts
|
|
40347
|
+
WHERE tenant_id = $1 AND kind = 'compensate' AND idempotency_key = $2
|
|
40348
|
+
LIMIT 1`, [this.tenantId, input.idempotency_key]);
|
|
40117
40349
|
if (previous.rows[0]) {
|
|
40118
40350
|
const row = previous.rows[0];
|
|
40119
40351
|
if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
|
|
@@ -40121,18 +40353,27 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40121
40353
|
}
|
|
40122
40354
|
return { ...parseJson2(row["result_json"]), duplicate: true };
|
|
40123
40355
|
}
|
|
40124
|
-
const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE
|
|
40356
|
+
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
40357
|
const applyRow = applyRows.rows[0];
|
|
40126
40358
|
if (!applyRow)
|
|
40127
40359
|
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
|
|
40360
|
+
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
40361
|
const binding = bindingRows.rows[0];
|
|
40130
40362
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40131
40363
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40132
40364
|
}
|
|
40133
40365
|
if (binding["state"] !== "applied")
|
|
40134
40366
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
40135
|
-
const delivered = await tx.query(
|
|
40367
|
+
const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
|
|
40368
|
+
JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
|
|
40369
|
+
WHERE r.tenant_id = $1
|
|
40370
|
+
AND r.authority = 'todos'
|
|
40371
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40372
|
+
AND r.schema_version = 1
|
|
40373
|
+
AND r.kind = 'apply'
|
|
40374
|
+
AND o.apply_receipt_id = $2
|
|
40375
|
+
AND o.status = 'delivered'
|
|
40376
|
+
LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
40136
40377
|
if (delivered.rows[0])
|
|
40137
40378
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
40138
40379
|
const applyResult = parseJson2(applyRow["result_json"]);
|
|
@@ -40144,8 +40385,20 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40144
40385
|
},
|
|
40145
40386
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
40146
40387
|
];
|
|
40147
|
-
const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
40148
|
-
FROM todos_task_manifest_outbox
|
|
40388
|
+
const outboxRows = await tx.query(`SELECT o.id, o.topic, o.payload, o.payload_digest, o.status, o.attempts, o.delivered_at
|
|
40389
|
+
FROM todos_task_manifest_outbox o
|
|
40390
|
+
WHERE o.apply_receipt_id = $1
|
|
40391
|
+
AND EXISTS (
|
|
40392
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40393
|
+
WHERE r.receipt_id = o.apply_receipt_id
|
|
40394
|
+
AND r.tenant_id = $2
|
|
40395
|
+
AND r.authority = 'todos'
|
|
40396
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40397
|
+
AND r.schema_version = 1
|
|
40398
|
+
AND r.kind = 'apply'
|
|
40399
|
+
)
|
|
40400
|
+
ORDER BY o.id
|
|
40401
|
+
FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
|
|
40149
40402
|
if (outboxRows.rows.length !== expectedEffects.length) {
|
|
40150
40403
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
40151
40404
|
}
|
|
@@ -40245,7 +40498,23 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40245
40498
|
}
|
|
40246
40499
|
if (stored.rows.length !== managedIds.length)
|
|
40247
40500
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
|
|
40248
|
-
await tx.query(
|
|
40501
|
+
const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
|
|
40502
|
+
SET status = 'cancelled'
|
|
40503
|
+
WHERE apply_receipt_id = $1 AND status = 'pending'
|
|
40504
|
+
AND EXISTS (
|
|
40505
|
+
SELECT 1 FROM todos_task_manifest_receipts r
|
|
40506
|
+
WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
|
|
40507
|
+
AND r.tenant_id = $2
|
|
40508
|
+
AND r.authority = 'todos'
|
|
40509
|
+
AND r.route = 'todos.task-manifest.v1'
|
|
40510
|
+
AND r.schema_version = 1
|
|
40511
|
+
AND r.kind = 'apply'
|
|
40512
|
+
)
|
|
40513
|
+
RETURNING id`, [input.receipt_id, this.tenantId]);
|
|
40514
|
+
const cancelledIds = new Set(cancelled.rows.map((row) => String(row.id)));
|
|
40515
|
+
if (cancelledIds.size !== applyResult.outbox_ids.length || applyResult.outbox_ids.some((id) => !cancelledIds.has(id))) {
|
|
40516
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: failed to cancel every expected outbox row");
|
|
40517
|
+
}
|
|
40249
40518
|
const typedIds = [
|
|
40250
40519
|
["dependencies", applyResult.graph.dependency_ids],
|
|
40251
40520
|
["comments", applyResult.graph.comment_ids],
|
|
@@ -40262,10 +40531,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40262
40531
|
const readback = await this.readback(tx, applyResult.graph);
|
|
40263
40532
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
40264
40533
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40265
|
-
receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40534
|
+
receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
|
|
40266
40535
|
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', $
|
|
40536
|
+
) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
|
|
40268
40537
|
compensationReceiptId,
|
|
40538
|
+
this.tenantId,
|
|
40269
40539
|
receipt.operation_id,
|
|
40270
40540
|
input.idempotency_key,
|
|
40271
40541
|
requestDigest,
|
|
@@ -40277,10 +40547,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40277
40547
|
]);
|
|
40278
40548
|
const updated = await tx.query(`UPDATE todos_task_manifest_bindings
|
|
40279
40549
|
SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
|
|
40280
|
-
WHERE
|
|
40550
|
+
WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
|
|
40281
40551
|
receipt.binding_version,
|
|
40282
40552
|
compensationReceiptId,
|
|
40283
40553
|
now4,
|
|
40554
|
+
this.tenantId,
|
|
40284
40555
|
receipt.operation_id,
|
|
40285
40556
|
input.if_binding_version
|
|
40286
40557
|
]);
|
|
@@ -40320,6 +40591,13 @@ var FAULT_POINTS = [
|
|
|
40320
40591
|
"after_outbox_write",
|
|
40321
40592
|
"after_receipt_write"
|
|
40322
40593
|
];
|
|
40594
|
+
function resolveTenantId(value) {
|
|
40595
|
+
const tenantId = value ?? "default";
|
|
40596
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenantId)) {
|
|
40597
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
40598
|
+
}
|
|
40599
|
+
return tenantId;
|
|
40600
|
+
}
|
|
40323
40601
|
function normalize(input, now4) {
|
|
40324
40602
|
const parsed = parseTodosTaskManifest(input);
|
|
40325
40603
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
@@ -40407,19 +40685,23 @@ function sanitizeManifest(manifest) {
|
|
|
40407
40685
|
class PackageOwnedTodosTaskManifestAuthority {
|
|
40408
40686
|
backend;
|
|
40409
40687
|
options;
|
|
40688
|
+
tenantId;
|
|
40410
40689
|
constructor(backend, options = {}) {
|
|
40411
40690
|
this.backend = backend;
|
|
40412
40691
|
this.options = options;
|
|
40692
|
+
this.tenantId = resolveTenantId(options.tenantId);
|
|
40413
40693
|
}
|
|
40414
40694
|
async capability() {
|
|
40415
40695
|
return {
|
|
40416
40696
|
authority: "todos",
|
|
40417
40697
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
40418
40698
|
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
40699
|
+
tenant_id: this.tenantId,
|
|
40419
40700
|
backend: this.backend.kind,
|
|
40420
40701
|
deterministic_ids: true,
|
|
40421
40702
|
immutable_receipts: true,
|
|
40422
40703
|
transactional_outbox: true,
|
|
40704
|
+
idempotent_outbox_delivery: true,
|
|
40423
40705
|
exact_bounded_readback: true,
|
|
40424
40706
|
conditional_compensation: true,
|
|
40425
40707
|
transcript_safe: false,
|
|
@@ -40454,6 +40736,22 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
40454
40736
|
}
|
|
40455
40737
|
return this.backend.readExact(receiptId2).then((result) => this.bounded(result));
|
|
40456
40738
|
}
|
|
40739
|
+
async lookupBinding(input) {
|
|
40740
|
+
const request = parseTodosTaskManifestBindingLookup(input);
|
|
40741
|
+
if (request.max_items !== 1) {
|
|
40742
|
+
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 });
|
|
40743
|
+
}
|
|
40744
|
+
if (request.authority !== "todos" || request.route !== TODOS_TASK_MANIFEST_ROUTE || request.schema_version !== TODOS_TASK_MANIFEST_SCHEMA_VERSION || request.tenant_id !== this.tenantId) {
|
|
40745
|
+
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAPABILITY_MISMATCH", "Task-manifest binding lookup does not match this authority identity");
|
|
40746
|
+
}
|
|
40747
|
+
return this.bounded({
|
|
40748
|
+
authority: "todos",
|
|
40749
|
+
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
40750
|
+
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
40751
|
+
tenant_id: this.tenantId,
|
|
40752
|
+
...await this.backend.lookupBindingByPlanId(request.plan_id)
|
|
40753
|
+
});
|
|
40754
|
+
}
|
|
40457
40755
|
markOutboxDelivered(outboxId) {
|
|
40458
40756
|
if (!outboxId || outboxId.length > 200) {
|
|
40459
40757
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
|
|
@@ -40493,13 +40791,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
|
|
|
40493
40791
|
if (!options?.database) {
|
|
40494
40792
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
|
|
40495
40793
|
}
|
|
40496
|
-
|
|
40794
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40795
|
+
return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
|
|
40497
40796
|
}
|
|
40498
40797
|
function createPostgresTodosTaskManifestAuthority(client, options = {}) {
|
|
40499
40798
|
if (!client || typeof client.transaction !== "function") {
|
|
40500
40799
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
|
|
40501
40800
|
}
|
|
40502
|
-
|
|
40801
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40802
|
+
return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
|
|
40503
40803
|
}
|
|
40504
40804
|
// src/task-manifest/http.ts
|
|
40505
40805
|
var JSON_HEADERS2 = { "Content-Type": "application/json" };
|
|
@@ -40513,6 +40813,7 @@ function status(error) {
|
|
|
40513
40813
|
case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
|
|
40514
40814
|
return 400;
|
|
40515
40815
|
case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
|
|
40816
|
+
case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
|
|
40516
40817
|
return 404;
|
|
40517
40818
|
case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
|
|
40518
40819
|
return 503;
|
|
@@ -40575,6 +40876,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
|
|
|
40575
40876
|
}
|
|
40576
40877
|
return json2({ result: await authority.readExact(input.receipt_id) });
|
|
40577
40878
|
}
|
|
40879
|
+
if (action === "bindings/lookup") {
|
|
40880
|
+
return json2({
|
|
40881
|
+
result: await authority.lookupBinding(await body(request))
|
|
40882
|
+
});
|
|
40883
|
+
}
|
|
40578
40884
|
if (action === "compensate") {
|
|
40579
40885
|
return json2({ result: await authority.compensate(await body(request)) }, 201);
|
|
40580
40886
|
}
|
|
@@ -40634,6 +40940,9 @@ class TodosTaskManifestHttpClient {
|
|
|
40634
40940
|
async readExact(receiptId2) {
|
|
40635
40941
|
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId2 }) })).result;
|
|
40636
40942
|
}
|
|
40943
|
+
async lookupBinding(input) {
|
|
40944
|
+
return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
|
|
40945
|
+
}
|
|
40637
40946
|
async markOutboxDelivered(outboxId) {
|
|
40638
40947
|
await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
|
|
40639
40948
|
}
|
|
@@ -61774,6 +62083,7 @@ export {
|
|
|
61774
62083
|
syncWithAgents,
|
|
61775
62084
|
syncWithAgent,
|
|
61776
62085
|
syncKgEdges,
|
|
62086
|
+
supportsIdempotentOutboxDelivery,
|
|
61777
62087
|
supersedeDecisionRecord,
|
|
61778
62088
|
summarizeTaskRun,
|
|
61779
62089
|
summarizeRoadmap,
|
|
@@ -61987,6 +62297,7 @@ export {
|
|
|
61987
62297
|
pauseFocusSession,
|
|
61988
62298
|
patrolTasks,
|
|
61989
62299
|
parseTodosTaskManifestCompensation,
|
|
62300
|
+
parseTodosTaskManifestBindingLookup,
|
|
61990
62301
|
parseTodosTaskManifest,
|
|
61991
62302
|
parseTmuxTarget,
|
|
61992
62303
|
parseStorageMode,
|