@hasna/todos 0.15.13 → 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 +12117 -10745
- package/dist/contracts.js +1 -1
- package/dist/index.js +287 -53
- package/dist/mcp/index.js +2181 -810
- package/dist/mcp.js +1 -1
- package/dist/project-registration.js +12 -13
- 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 +19661 -18290
- 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/storage.js +11 -12
- 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",
|
|
@@ -27580,21 +27580,21 @@ class PostgresJsonRecordStore {
|
|
|
27580
27580
|
), existing AS (
|
|
27581
27581
|
SELECT payload FROM ${this.tableName}
|
|
27582
27582
|
WHERE service = $1 AND object_type = 'plan_project_link_rollback_receipts'
|
|
27583
|
-
AND object_id = $
|
|
27583
|
+
AND object_id = $4 AND deleted_at IS NULL
|
|
27584
27584
|
FOR UPDATE
|
|
27585
27585
|
), checks AS (
|
|
27586
27586
|
SELECT
|
|
27587
27587
|
EXISTS (SELECT 1 FROM target_plan) AS plan_found,
|
|
27588
27588
|
COALESCE((SELECT payload->>'updated_at' = $3 FROM target_plan), false) AS plan_revision_ok,
|
|
27589
|
-
COALESCE((SELECT jsonb_object_agg(object_id, COALESCE(payload->'project_id', 'null'::jsonb) ORDER BY object_id) FROM member_tasks), '{}'::jsonb) = $
|
|
27590
|
-
AND COALESCE((SELECT jsonb_agg(object_id ORDER BY object_id) FROM member_tasks), '[]'::jsonb) = $
|
|
27589
|
+
COALESCE((SELECT jsonb_object_agg(object_id, COALESCE(payload->'project_id', 'null'::jsonb) ORDER BY object_id) FROM member_tasks), '{}'::jsonb) = $6::jsonb
|
|
27590
|
+
AND COALESCE((SELECT jsonb_agg(object_id ORDER BY object_id) FROM member_tasks), '[]'::jsonb) = $7::jsonb AS membership_ok,
|
|
27591
27591
|
EXISTS (SELECT 1 FROM existing) AS has_existing
|
|
27592
27592
|
), updated_plan AS (
|
|
27593
27593
|
UPDATE ${this.tableName} r SET
|
|
27594
|
-
payload = r.payload || jsonb_build_object('project_id', $
|
|
27595
|
-
updated_at = $
|
|
27594
|
+
payload = r.payload || jsonb_build_object('project_id', $8::jsonb, 'updated_at', $9::text),
|
|
27595
|
+
updated_at = $9::timestamptz,
|
|
27596
27596
|
version = COALESCE(r.version, 0) + 1,
|
|
27597
|
-
source_machine_id = COALESCE($
|
|
27597
|
+
source_machine_id = COALESCE($10, r.source_machine_id)
|
|
27598
27598
|
FROM checks
|
|
27599
27599
|
WHERE r.service = $1 AND r.object_type = 'plans' AND r.object_id = $2 AND r.deleted_at IS NULL
|
|
27600
27600
|
AND checks.plan_found AND checks.plan_revision_ok AND checks.membership_ok AND NOT checks.has_existing
|
|
@@ -27602,20 +27602,20 @@ class PostgresJsonRecordStore {
|
|
|
27602
27602
|
), updated_tasks AS (
|
|
27603
27603
|
UPDATE ${this.tableName} r SET
|
|
27604
27604
|
payload = r.payload || jsonb_build_object(
|
|
27605
|
-
'project_id', COALESCE($
|
|
27606
|
-
'updated_at', $
|
|
27605
|
+
'project_id', COALESCE($11::jsonb -> r.object_id, 'null'::jsonb),
|
|
27606
|
+
'updated_at', $9::text,
|
|
27607
27607
|
'version', COALESCE((r.payload->>'version')::int, 0) + 1
|
|
27608
27608
|
),
|
|
27609
|
-
updated_at = $
|
|
27609
|
+
updated_at = $9::timestamptz,
|
|
27610
27610
|
version = COALESCE(r.version, 0) + 1,
|
|
27611
|
-
source_machine_id = COALESCE($
|
|
27611
|
+
source_machine_id = COALESCE($10, r.source_machine_id)
|
|
27612
27612
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
27613
27613
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM updated_plan)
|
|
27614
27614
|
RETURNING 1
|
|
27615
27615
|
), task_gate AS (SELECT count(*) AS count FROM updated_tasks), inserted AS (
|
|
27616
27616
|
INSERT INTO ${this.tableName}
|
|
27617
27617
|
(service, object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version)
|
|
27618
|
-
SELECT $1, 'plan_project_link_rollback_receipts', $
|
|
27618
|
+
SELECT $1, 'plan_project_link_rollback_receipts', $4, $5::jsonb, $9::timestamptz, NULL, $10, 1
|
|
27619
27619
|
FROM checks, task_gate
|
|
27620
27620
|
WHERE NOT checks.has_existing AND EXISTS (SELECT 1 FROM updated_plan)
|
|
27621
27621
|
RETURNING payload
|
|
@@ -27629,7 +27629,6 @@ class PostgresJsonRecordStore {
|
|
|
27629
27629
|
this.service,
|
|
27630
27630
|
input.plan_id,
|
|
27631
27631
|
input.expected_plan_revision,
|
|
27632
|
-
input.receipt_id,
|
|
27633
27632
|
input.rollback_receipt_id,
|
|
27634
27633
|
jsonbParam(rollback),
|
|
27635
27634
|
jsonbParam(currentTaskProjects),
|
|
@@ -39178,6 +39177,14 @@ var compensationSchema = exports_external.object({
|
|
|
39178
39177
|
idempotency_key: identifier,
|
|
39179
39178
|
if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
|
|
39180
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();
|
|
39181
39188
|
function parseTodosTaskManifest(input) {
|
|
39182
39189
|
const parsed = schema.safeParse(input);
|
|
39183
39190
|
if (!parsed.success) {
|
|
@@ -39243,6 +39250,36 @@ function parseTodosTaskManifestCompensation(input) {
|
|
|
39243
39250
|
}
|
|
39244
39251
|
return parsed.data;
|
|
39245
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
|
+
}
|
|
39246
39283
|
|
|
39247
39284
|
// src/task-manifest/reference-guard.ts
|
|
39248
39285
|
function quoteSqliteIdentifier2(value) {
|
|
@@ -39332,10 +39369,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
|
|
|
39332
39369
|
}
|
|
39333
39370
|
|
|
39334
39371
|
// src/task-manifest/schema-sql.ts
|
|
39372
|
+
function sqlString(value) {
|
|
39373
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
39374
|
+
}
|
|
39335
39375
|
function sqliteTodosTaskManifestSchemaSql() {
|
|
39336
39376
|
return `
|
|
39337
39377
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39338
39378
|
receipt_id TEXT PRIMARY KEY,
|
|
39379
|
+
tenant_id TEXT NOT NULL,
|
|
39339
39380
|
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
39340
39381
|
route TEXT NOT NULL,
|
|
39341
39382
|
schema_version INTEGER NOT NULL CHECK(schema_version = 1),
|
|
@@ -39353,6 +39394,7 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39353
39394
|
);
|
|
39354
39395
|
CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39355
39396
|
operation_id TEXT PRIMARY KEY,
|
|
39397
|
+
tenant_id TEXT NOT NULL,
|
|
39356
39398
|
idempotency_key TEXT NOT NULL UNIQUE,
|
|
39357
39399
|
request_digest TEXT NOT NULL,
|
|
39358
39400
|
result_digest TEXT NOT NULL,
|
|
@@ -39388,10 +39430,37 @@ function sqliteTodosTaskManifestSchemaSql() {
|
|
|
39388
39430
|
END;
|
|
39389
39431
|
`;
|
|
39390
39432
|
}
|
|
39391
|
-
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);
|
|
39392
39460
|
return [
|
|
39393
39461
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
|
|
39394
39462
|
receipt_id text PRIMARY KEY,
|
|
39463
|
+
tenant_id text NOT NULL,
|
|
39395
39464
|
authority text NOT NULL CHECK(authority = 'todos'),
|
|
39396
39465
|
route text NOT NULL,
|
|
39397
39466
|
schema_version integer NOT NULL CHECK(schema_version = 1),
|
|
@@ -39407,8 +39476,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39407
39476
|
created_at timestamptz NOT NULL,
|
|
39408
39477
|
UNIQUE(kind, idempotency_key)
|
|
39409
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`,
|
|
39410
39483
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
|
|
39411
39484
|
operation_id text PRIMARY KEY,
|
|
39485
|
+
tenant_id text NOT NULL,
|
|
39412
39486
|
idempotency_key text NOT NULL UNIQUE,
|
|
39413
39487
|
request_digest text NOT NULL,
|
|
39414
39488
|
result_digest text NOT NULL,
|
|
@@ -39421,6 +39495,10 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39421
39495
|
created_at timestamptz NOT NULL,
|
|
39422
39496
|
updated_at timestamptz NOT NULL
|
|
39423
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`,
|
|
39424
39502
|
`CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
|
|
39425
39503
|
id text PRIMARY KEY,
|
|
39426
39504
|
apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
|
|
@@ -39434,6 +39512,13 @@ function postgresTodosTaskManifestSchemaSql() {
|
|
|
39434
39512
|
)`,
|
|
39435
39513
|
`CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
|
|
39436
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
|
+
)`,
|
|
39437
39522
|
`CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
|
|
39438
39523
|
RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
|
|
39439
39524
|
RAISE EXCEPTION 'todos task manifest receipts are immutable';
|
|
@@ -39457,10 +39542,12 @@ function parseApplyResult(value, duplicate) {
|
|
|
39457
39542
|
|
|
39458
39543
|
class SqliteTodosTaskManifestBackend {
|
|
39459
39544
|
db;
|
|
39545
|
+
tenantId;
|
|
39460
39546
|
kind = "sqlite";
|
|
39461
|
-
constructor(db) {
|
|
39547
|
+
constructor(db, tenantId = "default") {
|
|
39462
39548
|
this.db = db;
|
|
39463
|
-
|
|
39549
|
+
this.tenantId = tenantId;
|
|
39550
|
+
ensureSqliteTodosTaskManifestSchema(db, tenantId);
|
|
39464
39551
|
}
|
|
39465
39552
|
async serialized(run) {
|
|
39466
39553
|
const previous = sqliteTails.get(this.db) ?? Promise.resolve();
|
|
@@ -39490,7 +39577,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39490
39577
|
async apply(input, faults) {
|
|
39491
39578
|
return this.serialized(() => {
|
|
39492
39579
|
const { manifest } = input;
|
|
39493
|
-
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);
|
|
39494
39581
|
if (binding) {
|
|
39495
39582
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
39496
39583
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
|
|
@@ -39500,7 +39587,7 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39500
39587
|
}
|
|
39501
39588
|
return parseApplyResult(String(binding["result_json"]), true);
|
|
39502
39589
|
}
|
|
39503
|
-
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);
|
|
39504
39591
|
if (idempotency)
|
|
39505
39592
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
|
|
39506
39593
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -39590,9 +39677,9 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39590
39677
|
const resultJson = canonicalJson(result);
|
|
39591
39678
|
const manifestJson = canonicalJson(manifest);
|
|
39592
39679
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39593
|
-
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,
|
|
39594
39681
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
39595
|
-
) 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);
|
|
39596
39683
|
for (const entry2 of input.outbox) {
|
|
39597
39684
|
this.db.query(`INSERT INTO todos_task_manifest_outbox (
|
|
39598
39685
|
id, apply_receipt_id, topic, payload, payload_digest, status, created_at
|
|
@@ -39600,24 +39687,55 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39600
39687
|
}
|
|
39601
39688
|
fault(faults, "after_outbox_write");
|
|
39602
39689
|
this.db.query(`INSERT INTO todos_task_manifest_bindings (
|
|
39603
|
-
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,
|
|
39604
39691
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
39605
|
-
) 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);
|
|
39606
39693
|
fault(faults, "after_receipt_write");
|
|
39607
39694
|
return result;
|
|
39608
39695
|
});
|
|
39609
39696
|
}
|
|
39610
39697
|
async readExact(receiptId2) {
|
|
39611
|
-
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);
|
|
39612
39699
|
if (!row)
|
|
39613
39700
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
39614
39701
|
return parseApplyResult(row.result_json, false);
|
|
39615
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
|
+
}
|
|
39616
39729
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
39617
39730
|
await this.serialized(() => {
|
|
39618
39731
|
const result = this.db.query(`UPDATE todos_task_manifest_outbox
|
|
39619
39732
|
SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
|
|
39620
|
-
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);
|
|
39621
39739
|
if (result.changes !== 1) {
|
|
39622
39740
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
39623
39741
|
}
|
|
@@ -39626,23 +39744,28 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39626
39744
|
async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
|
|
39627
39745
|
return this.serialized(() => {
|
|
39628
39746
|
const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
|
|
39629
|
-
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);
|
|
39630
39750
|
if (existing) {
|
|
39631
39751
|
if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
|
|
39632
39752
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
|
|
39633
39753
|
}
|
|
39634
39754
|
return { ...JSON.parse(existing.result_json), duplicate: true };
|
|
39635
39755
|
}
|
|
39636
|
-
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);
|
|
39637
39757
|
if (!row)
|
|
39638
39758
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
|
|
39639
|
-
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"]));
|
|
39640
39760
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
39641
39761
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
39642
39762
|
}
|
|
39643
39763
|
if (binding["state"] !== "applied")
|
|
39644
39764
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
|
|
39645
|
-
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);
|
|
39646
39769
|
if (delivered)
|
|
39647
39770
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
39648
39771
|
const applyResult = parseApplyResult(String(row["result_json"]), false);
|
|
@@ -39655,7 +39778,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39655
39778
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
39656
39779
|
];
|
|
39657
39780
|
const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
39658
|
-
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);
|
|
39659
39789
|
if (storedOutbox.length !== expectedEffects.length) {
|
|
39660
39790
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
39661
39791
|
}
|
|
@@ -39743,7 +39873,14 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39743
39873
|
}
|
|
39744
39874
|
expectedVerificationIndex += 1;
|
|
39745
39875
|
}
|
|
39746
|
-
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);
|
|
39747
39884
|
for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
|
|
39748
39885
|
const column = table === "task_dependencies" ? "task_id" : "task_id";
|
|
39749
39886
|
this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
|
|
@@ -39754,11 +39891,11 @@ class SqliteTodosTaskManifestBackend {
|
|
|
39754
39891
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
39755
39892
|
const resultJson = canonicalJson(result);
|
|
39756
39893
|
this.db.query(`INSERT INTO todos_task_manifest_receipts (
|
|
39757
|
-
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,
|
|
39758
39895
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
39759
|
-
) 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);
|
|
39760
39897
|
const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
|
|
39761
|
-
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);
|
|
39762
39899
|
if (updated.changes !== 1) {
|
|
39763
39900
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
|
|
39764
39901
|
}
|
|
@@ -39897,17 +40034,19 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39897
40034
|
kind = "postgresql";
|
|
39898
40035
|
service;
|
|
39899
40036
|
tableName;
|
|
40037
|
+
tenantId;
|
|
39900
40038
|
schemaReady = null;
|
|
39901
40039
|
constructor(client, options = {}) {
|
|
39902
40040
|
this.client = client;
|
|
39903
40041
|
this.service = options.service ?? "todos";
|
|
39904
40042
|
this.tableName = safeIdentifier2(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
|
|
40043
|
+
this.tenantId = options.tenantId ?? "default";
|
|
39905
40044
|
}
|
|
39906
40045
|
async ensureSchema() {
|
|
39907
40046
|
this.schemaReady ??= (async () => {
|
|
39908
40047
|
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
39909
40048
|
await this.client.query(sql);
|
|
39910
|
-
for (const sql of postgresTodosTaskManifestSchemaSql())
|
|
40049
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
39911
40050
|
await this.client.query(sql);
|
|
39912
40051
|
})();
|
|
39913
40052
|
await this.schemaReady;
|
|
@@ -39929,7 +40068,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39929
40068
|
const { manifest } = input;
|
|
39930
40069
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
|
|
39931
40070
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
|
|
39932
|
-
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]);
|
|
39933
40072
|
if (existing.rows[0]) {
|
|
39934
40073
|
const binding = existing.rows[0];
|
|
39935
40074
|
if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
|
|
@@ -39940,7 +40079,7 @@ class PostgresTodosTaskManifestBackend {
|
|
|
39940
40079
|
}
|
|
39941
40080
|
return { ...parseJson2(binding["result_json"]), duplicate: true };
|
|
39942
40081
|
}
|
|
39943
|
-
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]);
|
|
39944
40083
|
if (reused.rows[0])
|
|
39945
40084
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
|
|
39946
40085
|
if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
|
|
@@ -40050,10 +40189,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40050
40189
|
const manifestJson = canonicalJson(manifest);
|
|
40051
40190
|
const resultJson = canonicalJson(result);
|
|
40052
40191
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40053
|
-
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,
|
|
40054
40193
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
40055
|
-
) 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)`, [
|
|
40056
40195
|
input.receipt_id,
|
|
40196
|
+
this.tenantId,
|
|
40057
40197
|
manifest.operation_id,
|
|
40058
40198
|
manifest.idempotency_key,
|
|
40059
40199
|
input.request_digest,
|
|
@@ -40076,10 +40216,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40076
40216
|
}
|
|
40077
40217
|
fault2(faults, "after_outbox_write");
|
|
40078
40218
|
await tx.query(`INSERT INTO todos_task_manifest_bindings (
|
|
40079
|
-
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,
|
|
40080
40220
|
manifest_json, result_json, state, version, created_at, updated_at
|
|
40081
|
-
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $
|
|
40221
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
|
|
40082
40222
|
manifest.operation_id,
|
|
40223
|
+
this.tenantId,
|
|
40083
40224
|
manifest.idempotency_key,
|
|
40084
40225
|
input.request_digest,
|
|
40085
40226
|
input.result_digest,
|
|
@@ -40094,17 +40235,50 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40094
40235
|
}
|
|
40095
40236
|
async readExact(receiptId2) {
|
|
40096
40237
|
await this.ensureSchema();
|
|
40097
|
-
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]);
|
|
40098
40239
|
if (!result.rows[0])
|
|
40099
40240
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
|
|
40100
40241
|
return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
|
|
40101
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
|
+
}
|
|
40102
40270
|
async markOutboxDelivered(outboxId, deliveredAt) {
|
|
40103
40271
|
await this.ensureSchema();
|
|
40104
40272
|
await this.client.transaction(async (tx) => {
|
|
40105
40273
|
const result = await tx.query(`UPDATE todos_task_manifest_outbox
|
|
40106
40274
|
SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
|
|
40107
|
-
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]);
|
|
40108
40282
|
if (!result.rows[0])
|
|
40109
40283
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
|
|
40110
40284
|
});
|
|
@@ -40114,7 +40288,10 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40114
40288
|
return this.client.transaction(async (tx) => {
|
|
40115
40289
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
|
|
40116
40290
|
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fcompensation-idempotency\x1F${input.idempotency_key}`]);
|
|
40117
|
-
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]);
|
|
40118
40295
|
if (previous.rows[0]) {
|
|
40119
40296
|
const row = previous.rows[0];
|
|
40120
40297
|
if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
|
|
@@ -40122,18 +40299,21 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40122
40299
|
}
|
|
40123
40300
|
return { ...parseJson2(row["result_json"]), duplicate: true };
|
|
40124
40301
|
}
|
|
40125
|
-
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]);
|
|
40126
40303
|
const applyRow = applyRows.rows[0];
|
|
40127
40304
|
if (!applyRow)
|
|
40128
40305
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
|
|
40129
|
-
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]);
|
|
40130
40307
|
const binding = bindingRows.rows[0];
|
|
40131
40308
|
if (!binding || Number(binding["version"]) !== input.if_binding_version) {
|
|
40132
40309
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
|
|
40133
40310
|
}
|
|
40134
40311
|
if (binding["state"] !== "applied")
|
|
40135
40312
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
|
|
40136
|
-
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]);
|
|
40137
40317
|
if (delivered.rows[0])
|
|
40138
40318
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
|
|
40139
40319
|
const applyResult = parseJson2(applyRow["result_json"]);
|
|
@@ -40146,7 +40326,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40146
40326
|
...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
|
|
40147
40327
|
];
|
|
40148
40328
|
const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
|
|
40149
|
-
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]);
|
|
40150
40337
|
if (outboxRows.rows.length !== expectedEffects.length) {
|
|
40151
40338
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
|
|
40152
40339
|
}
|
|
@@ -40246,7 +40433,14 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40246
40433
|
}
|
|
40247
40434
|
if (stored.rows.length !== managedIds.length)
|
|
40248
40435
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
|
|
40249
|
-
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]);
|
|
40250
40444
|
const typedIds = [
|
|
40251
40445
|
["dependencies", applyResult.graph.dependency_ids],
|
|
40252
40446
|
["comments", applyResult.graph.comment_ids],
|
|
@@ -40263,10 +40457,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40263
40457
|
const readback = await this.readback(tx, applyResult.graph);
|
|
40264
40458
|
const result = { duplicate: false, receipt, absent: true, readback };
|
|
40265
40459
|
await tx.query(`INSERT INTO todos_task_manifest_receipts (
|
|
40266
|
-
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,
|
|
40267
40461
|
request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
|
|
40268
|
-
) 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)`, [
|
|
40269
40463
|
compensationReceiptId,
|
|
40464
|
+
this.tenantId,
|
|
40270
40465
|
receipt.operation_id,
|
|
40271
40466
|
input.idempotency_key,
|
|
40272
40467
|
requestDigest,
|
|
@@ -40278,10 +40473,11 @@ class PostgresTodosTaskManifestBackend {
|
|
|
40278
40473
|
]);
|
|
40279
40474
|
const updated = await tx.query(`UPDATE todos_task_manifest_bindings
|
|
40280
40475
|
SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
|
|
40281
|
-
WHERE
|
|
40476
|
+
WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
|
|
40282
40477
|
receipt.binding_version,
|
|
40283
40478
|
compensationReceiptId,
|
|
40284
40479
|
now4,
|
|
40480
|
+
this.tenantId,
|
|
40285
40481
|
receipt.operation_id,
|
|
40286
40482
|
input.if_binding_version
|
|
40287
40483
|
]);
|
|
@@ -40321,6 +40517,13 @@ var FAULT_POINTS = [
|
|
|
40321
40517
|
"after_outbox_write",
|
|
40322
40518
|
"after_receipt_write"
|
|
40323
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
|
+
}
|
|
40324
40527
|
function normalize(input, now4) {
|
|
40325
40528
|
const parsed = parseTodosTaskManifest(input);
|
|
40326
40529
|
const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
|
|
@@ -40408,15 +40611,18 @@ function sanitizeManifest(manifest) {
|
|
|
40408
40611
|
class PackageOwnedTodosTaskManifestAuthority {
|
|
40409
40612
|
backend;
|
|
40410
40613
|
options;
|
|
40614
|
+
tenantId;
|
|
40411
40615
|
constructor(backend, options = {}) {
|
|
40412
40616
|
this.backend = backend;
|
|
40413
40617
|
this.options = options;
|
|
40618
|
+
this.tenantId = resolveTenantId(options.tenantId);
|
|
40414
40619
|
}
|
|
40415
40620
|
async capability() {
|
|
40416
40621
|
return {
|
|
40417
40622
|
authority: "todos",
|
|
40418
40623
|
route: TODOS_TASK_MANIFEST_ROUTE,
|
|
40419
40624
|
schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
40625
|
+
tenant_id: this.tenantId,
|
|
40420
40626
|
backend: this.backend.kind,
|
|
40421
40627
|
deterministic_ids: true,
|
|
40422
40628
|
immutable_receipts: true,
|
|
@@ -40455,6 +40661,22 @@ class PackageOwnedTodosTaskManifestAuthority {
|
|
|
40455
40661
|
}
|
|
40456
40662
|
return this.backend.readExact(receiptId2).then((result) => this.bounded(result));
|
|
40457
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
|
+
}
|
|
40458
40680
|
markOutboxDelivered(outboxId) {
|
|
40459
40681
|
if (!outboxId || outboxId.length > 200) {
|
|
40460
40682
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
|
|
@@ -40494,13 +40716,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
|
|
|
40494
40716
|
if (!options?.database) {
|
|
40495
40717
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
|
|
40496
40718
|
}
|
|
40497
|
-
|
|
40719
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40720
|
+
return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
|
|
40498
40721
|
}
|
|
40499
40722
|
function createPostgresTodosTaskManifestAuthority(client, options = {}) {
|
|
40500
40723
|
if (!client || typeof client.transaction !== "function") {
|
|
40501
40724
|
throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
|
|
40502
40725
|
}
|
|
40503
|
-
|
|
40726
|
+
const tenantId = resolveTenantId(options.tenantId);
|
|
40727
|
+
return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
|
|
40504
40728
|
}
|
|
40505
40729
|
// src/task-manifest/http.ts
|
|
40506
40730
|
var JSON_HEADERS2 = { "Content-Type": "application/json" };
|
|
@@ -40514,6 +40738,7 @@ function status(error) {
|
|
|
40514
40738
|
case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
|
|
40515
40739
|
return 400;
|
|
40516
40740
|
case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
|
|
40741
|
+
case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
|
|
40517
40742
|
return 404;
|
|
40518
40743
|
case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
|
|
40519
40744
|
return 503;
|
|
@@ -40576,6 +40801,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
|
|
|
40576
40801
|
}
|
|
40577
40802
|
return json2({ result: await authority.readExact(input.receipt_id) });
|
|
40578
40803
|
}
|
|
40804
|
+
if (action === "bindings/lookup") {
|
|
40805
|
+
return json2({
|
|
40806
|
+
result: await authority.lookupBinding(await body(request))
|
|
40807
|
+
});
|
|
40808
|
+
}
|
|
40579
40809
|
if (action === "compensate") {
|
|
40580
40810
|
return json2({ result: await authority.compensate(await body(request)) }, 201);
|
|
40581
40811
|
}
|
|
@@ -40635,6 +40865,9 @@ class TodosTaskManifestHttpClient {
|
|
|
40635
40865
|
async readExact(receiptId2) {
|
|
40636
40866
|
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId2 }) })).result;
|
|
40637
40867
|
}
|
|
40868
|
+
async lookupBinding(input) {
|
|
40869
|
+
return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
|
|
40870
|
+
}
|
|
40638
40871
|
async markOutboxDelivered(outboxId) {
|
|
40639
40872
|
await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
|
|
40640
40873
|
}
|
|
@@ -61988,6 +62221,7 @@ export {
|
|
|
61988
62221
|
pauseFocusSession,
|
|
61989
62222
|
patrolTasks,
|
|
61990
62223
|
parseTodosTaskManifestCompensation,
|
|
62224
|
+
parseTodosTaskManifestBindingLookup,
|
|
61991
62225
|
parseTodosTaskManifest,
|
|
61992
62226
|
parseTmuxTarget,
|
|
61993
62227
|
parseStorageMode,
|