@hasna/todos 0.15.35 → 0.15.41
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 +52 -18
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
- package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
- package/dist/cli/index.js +3299 -796
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +62 -54
- package/dist/db/plan-comments.d.ts +14 -0
- package/dist/db/plan-comments.d.ts.map +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-relations.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1787 -309
- package/dist/lib/comment-cursor.d.ts +19 -0
- package/dist/lib/comment-cursor.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/mcp/index.js +2273 -471
- package/dist/mcp.js +8 -4
- package/dist/project-registration.js +207 -63
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +69 -54
- 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 +35 -0
- package/dist/sdk/v1.generated.d.ts +141 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +12 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +5171 -8535
- package/dist/server/openapi.d.ts +2714 -1937
- 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/config.d.ts +12 -30
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +13 -2
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/timestamp-backfill.d.ts +62 -0
- package/dist/storage/timestamp-backfill.d.ts.map +1 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +199 -63
- package/dist/task-subtree-transfer/authority.d.ts +27 -0
- package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
- package/dist/task-subtree-transfer/backend.d.ts +9 -0
- package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
- package/dist/task-subtree-transfer/canonical.d.ts +14 -0
- package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
- package/dist/task-subtree-transfer/http.d.ts +16 -0
- package/dist/task-subtree-transfer/http.d.ts.map +1 -0
- package/dist/task-subtree-transfer/index.d.ts +6 -0
- package/dist/task-subtree-transfer/index.d.ts.map +1 -0
- package/dist/task-subtree-transfer/planner.d.ts +24 -0
- package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
- package/dist/task-subtree-transfer/postgres.d.ts +19 -0
- package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
- package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
- package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
- package/dist/task-subtree-transfer/types.d.ts +148 -0
- package/dist/task-subtree-transfer/types.d.ts.map +1 -0
- package/dist/task-subtree-transfer.d.ts +2 -0
- package/dist/task-subtree-transfer.d.ts.map +1 -0
- package/dist/task-subtree-transfer.js +1869 -0
- package/dist/testing.d.ts +41 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +44 -1
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -2676,6 +2676,20 @@ function ensureSchema(db) {
|
|
|
2676
2676
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
2677
2677
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
2678
2678
|
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
2679
|
+
ensureTable("plan_comments", `
|
|
2680
|
+
CREATE TABLE plan_comments (
|
|
2681
|
+
id TEXT PRIMARY KEY,
|
|
2682
|
+
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
2683
|
+
agent_id TEXT,
|
|
2684
|
+
session_id TEXT,
|
|
2685
|
+
content TEXT NOT NULL,
|
|
2686
|
+
type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
|
|
2687
|
+
progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
|
|
2688
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2689
|
+
machine_id TEXT,
|
|
2690
|
+
synced_at TEXT
|
|
2691
|
+
)`);
|
|
2692
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
|
|
2679
2693
|
ensureColumn("projects", "machine_id", "TEXT");
|
|
2680
2694
|
ensureColumn("projects", "synced_at", "TEXT");
|
|
2681
2695
|
ensureColumn("tasks", "machine_id", "TEXT");
|
|
@@ -3657,6 +3671,7 @@ var init_machines = __esm(() => {
|
|
|
3657
3671
|
"task_lists",
|
|
3658
3672
|
"plans",
|
|
3659
3673
|
"task_comments",
|
|
3674
|
+
"plan_comments",
|
|
3660
3675
|
"sessions",
|
|
3661
3676
|
"task_history",
|
|
3662
3677
|
"webhooks",
|
|
@@ -4160,20 +4175,16 @@ var init_sync_utils = __esm(() => {
|
|
|
4160
4175
|
// src/storage/config.ts
|
|
4161
4176
|
var exports_config = {};
|
|
4162
4177
|
__export(exports_config, {
|
|
4163
|
-
parseStorageMode: () => parseStorageMode,
|
|
4164
4178
|
parseStorageBackend: () => parseStorageBackend,
|
|
4165
4179
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
4166
4180
|
loadStorageConfig: () => loadStorageConfig,
|
|
4167
4181
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
4168
|
-
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
4169
4182
|
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
4170
4183
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
4171
|
-
getTodosStorageMode: () => getTodosStorageMode,
|
|
4172
4184
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
4173
4185
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
4174
4186
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
4175
4187
|
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
4176
|
-
getStorageMode: () => getStorageMode,
|
|
4177
4188
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
4178
4189
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
4179
4190
|
getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
|
|
@@ -4183,7 +4194,6 @@ __export(exports_config, {
|
|
|
4183
4194
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4184
4195
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4185
4196
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4186
|
-
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4187
4197
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4188
4198
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4189
4199
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4238,9 +4248,6 @@ function loadStorageConfig(env = process.env) {
|
|
|
4238
4248
|
function isTodosPostgresBackend(config) {
|
|
4239
4249
|
return config.mode === "postgres";
|
|
4240
4250
|
}
|
|
4241
|
-
function isTodosRemoteStorageEnabled(config) {
|
|
4242
|
-
return isTodosPostgresBackend(config);
|
|
4243
|
-
}
|
|
4244
4251
|
function assertTodosRemoteStorageConfig(config) {
|
|
4245
4252
|
if (!isTodosPostgresBackend(config))
|
|
4246
4253
|
return;
|
|
@@ -4256,32 +4263,15 @@ function parseStorageBackend(value) {
|
|
|
4256
4263
|
return "sqlite";
|
|
4257
4264
|
if (normalized === "postgres" || normalized === "postgresql")
|
|
4258
4265
|
return "postgres";
|
|
4259
|
-
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4260
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
|
|
4261
|
-
}
|
|
4262
4266
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4263
4267
|
}
|
|
4264
|
-
function parseStorageMode(value) {
|
|
4265
|
-
return parseStorageBackend(value);
|
|
4266
|
-
}
|
|
4267
4268
|
function getTodosStorageBackend(env = process.env) {
|
|
4268
|
-
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4269
|
-
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4270
|
-
throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
|
|
4271
|
-
}
|
|
4272
|
-
}
|
|
4273
4269
|
if (!getTodosStorageDatabaseUrl(env))
|
|
4274
4270
|
return "sqlite";
|
|
4275
4271
|
if (isTodosShadowEnabled(env))
|
|
4276
4272
|
return "sqlite";
|
|
4277
4273
|
return "postgres";
|
|
4278
4274
|
}
|
|
4279
|
-
function getTodosStorageMode(env = process.env) {
|
|
4280
|
-
return getTodosStorageBackend(env);
|
|
4281
|
-
}
|
|
4282
|
-
function getStorageMode(env = process.env) {
|
|
4283
|
-
return getTodosStorageBackend(env);
|
|
4284
|
-
}
|
|
4285
4275
|
function isTodosShadowEnabled(env = process.env) {
|
|
4286
4276
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
4287
4277
|
}
|
|
@@ -4348,7 +4338,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
4348
4338
|
}
|
|
4349
4339
|
return parsed;
|
|
4350
4340
|
}
|
|
4351
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV,
|
|
4341
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
4352
4342
|
var init_config = __esm(() => {
|
|
4353
4343
|
TODOS_STORAGE_TABLES = [
|
|
4354
4344
|
"todos_sync_records",
|
|
@@ -4387,12 +4377,6 @@ var init_config = __esm(() => {
|
|
|
4387
4377
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
4388
4378
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
4389
4379
|
};
|
|
4390
|
-
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
4391
|
-
"HASNA_TODOS_STORAGE_MODE",
|
|
4392
|
-
"HASNA_TODOS_MODE",
|
|
4393
|
-
"TODOS_STORAGE_MODE",
|
|
4394
|
-
"TODOS_MODE"
|
|
4395
|
-
];
|
|
4396
4380
|
});
|
|
4397
4381
|
|
|
4398
4382
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -6857,22 +6841,31 @@ function updateTaskList(id, input, db) {
|
|
|
6857
6841
|
throw new TaskListNotFoundError(id);
|
|
6858
6842
|
const sets = ["updated_at = ?"];
|
|
6859
6843
|
const params = [now()];
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6844
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
|
|
6845
|
+
const scopeChanged = projectId !== existing.project_id;
|
|
6846
|
+
const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
|
|
6847
|
+
if (input.slug !== undefined && !effectiveSlug) {
|
|
6848
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
6849
|
+
}
|
|
6850
|
+
if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
|
|
6851
|
+
const duplicate = projectId ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(projectId, effectiveSlug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(effectiveSlug, id);
|
|
6865
6852
|
if (duplicate) {
|
|
6866
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
6853
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
6867
6854
|
}
|
|
6868
|
-
if (
|
|
6855
|
+
if (scopeChanged || effectiveSlug !== existing.slug) {
|
|
6869
6856
|
releaseCanonicalSlugClaims("task_list", id, d);
|
|
6870
|
-
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(
|
|
6871
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
6857
|
+
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
|
|
6858
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
6872
6859
|
}
|
|
6873
6860
|
}
|
|
6861
|
+
}
|
|
6862
|
+
if (input.slug !== undefined) {
|
|
6874
6863
|
sets.push("slug = ?");
|
|
6875
|
-
params.push(
|
|
6864
|
+
params.push(effectiveSlug);
|
|
6865
|
+
}
|
|
6866
|
+
if (projectId !== existing.project_id) {
|
|
6867
|
+
sets.push("project_id = ?");
|
|
6868
|
+
params.push(projectId);
|
|
6876
6869
|
}
|
|
6877
6870
|
if (input.name !== undefined) {
|
|
6878
6871
|
sets.push("name = ?");
|
|
@@ -9321,8 +9314,8 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
9321
9314
|
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
9322
9315
|
const timestamp2 = now();
|
|
9323
9316
|
const failTx = d.transaction(() => {
|
|
9324
|
-
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
9325
|
-
WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
9317
|
+
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, completed_at = COALESCE(completed_at, ?), metadata = ?, version = version + 1, updated_at = ?
|
|
9318
|
+
WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
9326
9319
|
if (res.changes === 0) {
|
|
9327
9320
|
const current = getTask(id, d);
|
|
9328
9321
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -9334,6 +9327,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
9334
9327
|
status: "failed",
|
|
9335
9328
|
locked_by: null,
|
|
9336
9329
|
locked_at: null,
|
|
9330
|
+
completed_at: task.completed_at ?? timestamp2,
|
|
9337
9331
|
reason: safeReason,
|
|
9338
9332
|
metadata: safeMeta,
|
|
9339
9333
|
version: task.version + 1,
|
|
@@ -9982,13 +9976,20 @@ function updateTaskStored(id, input, db) {
|
|
|
9982
9976
|
if (isTerminalStatus(input.status)) {
|
|
9983
9977
|
sets.push("locked_by = NULL");
|
|
9984
9978
|
sets.push("locked_at = NULL");
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9979
|
+
if (input.status === "completed") {
|
|
9980
|
+
sets.push("completed_at = ?");
|
|
9981
|
+
params.push(completionTimestamp);
|
|
9982
|
+
} else {
|
|
9983
|
+
sets.push("completed_at = COALESCE(completed_at, ?)");
|
|
9984
|
+
params.push(completionTimestamp);
|
|
9985
|
+
}
|
|
9989
9986
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
9990
9987
|
sets.push("completed_at = NULL");
|
|
9991
9988
|
}
|
|
9989
|
+
if (input.status === "in_progress") {
|
|
9990
|
+
sets.push("started_at = COALESCE(started_at, ?)");
|
|
9991
|
+
params.push(timestamp2);
|
|
9992
|
+
}
|
|
9992
9993
|
}
|
|
9993
9994
|
if (input.priority !== undefined) {
|
|
9994
9995
|
sets.push("priority = ?");
|
|
@@ -10126,7 +10127,6 @@ function updateTaskStored(id, input, db) {
|
|
|
10126
10127
|
if (input.approved_by !== undefined)
|
|
10127
10128
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10128
10129
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
10129
|
-
const completedNow = input.status === "completed";
|
|
10130
10130
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
10131
10131
|
const updatedTask = {
|
|
10132
10132
|
...task,
|
|
@@ -10137,7 +10137,8 @@ function updateTaskStored(id, input, db) {
|
|
|
10137
10137
|
updated_at: timestamp2,
|
|
10138
10138
|
locked_by: terminalNow ? null : task.locked_by,
|
|
10139
10139
|
locked_at: terminalNow ? null : task.locked_at,
|
|
10140
|
-
completed_at:
|
|
10140
|
+
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
|
|
10141
|
+
started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
|
|
10141
10142
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
10142
10143
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
10143
10144
|
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
@@ -10198,6 +10199,9 @@ function deleteTask(id, db) {
|
|
|
10198
10199
|
version: row.version
|
|
10199
10200
|
}, d);
|
|
10200
10201
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
10202
|
+
if (result.changes > 0) {
|
|
10203
|
+
d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
|
|
10204
|
+
}
|
|
10201
10205
|
return result.changes > 0;
|
|
10202
10206
|
}
|
|
10203
10207
|
var init_task_crud = __esm(() => {
|
|
@@ -12882,10 +12886,10 @@ function cancelDispatch(id, db) {
|
|
|
12882
12886
|
_db2.run("UPDATE dispatches SET status = 'cancelled' WHERE id = ?", [id]);
|
|
12883
12887
|
return getDispatch(id, _db2);
|
|
12884
12888
|
}
|
|
12885
|
-
function updateDispatchStatus(id,
|
|
12889
|
+
function updateDispatchStatus(id, status3, opts = {}, db) {
|
|
12886
12890
|
const _db2 = db ?? getDatabase();
|
|
12887
12891
|
const error = opts.error ? sanitizePreWriteText(opts.error, "dispatch.error") : null;
|
|
12888
|
-
_db2.run("UPDATE dispatches SET status = ?, error = ?, sent_at = ? WHERE id = ?", [
|
|
12892
|
+
_db2.run("UPDATE dispatches SET status = ?, error = ?, sent_at = ? WHERE id = ?", [status3, error, opts.sent_at ?? null, id]);
|
|
12889
12893
|
}
|
|
12890
12894
|
function createDispatchLog(log, db) {
|
|
12891
12895
|
const _db2 = db ?? getDatabase();
|
|
@@ -12918,7 +12922,7 @@ var init_dispatches = __esm(() => {
|
|
|
12918
12922
|
// package.json
|
|
12919
12923
|
var package_default = {
|
|
12920
12924
|
name: "@hasna/todos",
|
|
12921
|
-
version: "0.15.
|
|
12925
|
+
version: "0.15.41",
|
|
12922
12926
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12923
12927
|
type: "module",
|
|
12924
12928
|
main: "dist/index.js",
|
|
@@ -12964,6 +12968,10 @@ var package_default = {
|
|
|
12964
12968
|
"./task-manifest": {
|
|
12965
12969
|
types: "./dist/task-manifest.d.ts",
|
|
12966
12970
|
import: "./dist/task-manifest.js"
|
|
12971
|
+
},
|
|
12972
|
+
"./task-subtree-transfer": {
|
|
12973
|
+
types: "./dist/task-subtree-transfer.d.ts",
|
|
12974
|
+
import: "./dist/task-subtree-transfer.js"
|
|
12967
12975
|
}
|
|
12968
12976
|
},
|
|
12969
12977
|
workspaces: [
|
|
@@ -12977,8 +12985,8 @@ var package_default = {
|
|
|
12977
12985
|
"README.md"
|
|
12978
12986
|
],
|
|
12979
12987
|
scripts: {
|
|
12980
|
-
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
12981
|
-
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
12988
|
+
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
12989
|
+
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
12982
12990
|
migrate: "bun run src/server/index.ts migrate",
|
|
12983
12991
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
12984
12992
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -13032,7 +13040,7 @@ var package_default = {
|
|
|
13032
13040
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
13033
13041
|
license: "Apache-2.0",
|
|
13034
13042
|
dependencies: {
|
|
13035
|
-
"@hasna/contracts": "0.
|
|
13043
|
+
"@hasna/contracts": "0.13.3",
|
|
13036
13044
|
"@hasna/events": "^0.1.11",
|
|
13037
13045
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13038
13046
|
chalk: "^5.4.1",
|
|
@@ -25218,6 +25226,13 @@ var TODOS_PACKAGE_EXPORTS = [
|
|
|
25218
25226
|
types: "./dist/task-manifest.d.ts",
|
|
25219
25227
|
description: "Transactional package-owned authority for deterministic plan and task graph manifests.",
|
|
25220
25228
|
stability: "stable"
|
|
25229
|
+
},
|
|
25230
|
+
{
|
|
25231
|
+
subpath: "./task-subtree-transfer",
|
|
25232
|
+
import: "./dist/task-subtree-transfer.js",
|
|
25233
|
+
types: "./dist/task-subtree-transfer.d.ts",
|
|
25234
|
+
description: "Atomic package-owned authority for existing task-subtree project transfers and rollback.",
|
|
25235
|
+
stability: "stable"
|
|
25221
25236
|
}
|
|
25222
25237
|
];
|
|
25223
25238
|
function source8(version) {
|
|
@@ -25410,6 +25425,40 @@ init_tasks();
|
|
|
25410
25425
|
init_projects();
|
|
25411
25426
|
init_plans();
|
|
25412
25427
|
|
|
25428
|
+
// src/db/plan-comments.ts
|
|
25429
|
+
init_types();
|
|
25430
|
+
init_database();
|
|
25431
|
+
init_plans();
|
|
25432
|
+
init_prewrite_secrets();
|
|
25433
|
+
function addPlanComment(input, db) {
|
|
25434
|
+
const d = db || getDatabase();
|
|
25435
|
+
if (!getPlan(input.plan_id, d)) {
|
|
25436
|
+
throw new PlanNotFoundError(input.plan_id);
|
|
25437
|
+
}
|
|
25438
|
+
const id = uuid();
|
|
25439
|
+
const timestamp3 = now();
|
|
25440
|
+
d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
25441
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
25442
|
+
id,
|
|
25443
|
+
input.plan_id,
|
|
25444
|
+
input.agent_id || null,
|
|
25445
|
+
input.session_id || null,
|
|
25446
|
+
sanitizePreWriteText(input.content, "comment.content"),
|
|
25447
|
+
input.type || "comment",
|
|
25448
|
+
input.progress_pct ?? null,
|
|
25449
|
+
timestamp3
|
|
25450
|
+
]);
|
|
25451
|
+
return getPlanComment(id, d);
|
|
25452
|
+
}
|
|
25453
|
+
function getPlanComment(id, db) {
|
|
25454
|
+
const d = db || getDatabase();
|
|
25455
|
+
return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
|
|
25456
|
+
}
|
|
25457
|
+
function listPlanComments(planId, db) {
|
|
25458
|
+
const d = db || getDatabase();
|
|
25459
|
+
return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
|
|
25460
|
+
}
|
|
25461
|
+
|
|
25413
25462
|
// src/db/agents.ts
|
|
25414
25463
|
init_types();
|
|
25415
25464
|
init_database();
|
|
@@ -26067,6 +26116,20 @@ init_task_lists();
|
|
|
26067
26116
|
init_templates();
|
|
26068
26117
|
init_audit();
|
|
26069
26118
|
init_comments();
|
|
26119
|
+
|
|
26120
|
+
// src/lib/comment-cursor.ts
|
|
26121
|
+
function isStrictlyOlder(comment, before) {
|
|
26122
|
+
return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
|
|
26123
|
+
}
|
|
26124
|
+
function compareCommentKeyset(left, right) {
|
|
26125
|
+
if (isStrictlyOlder(left, right))
|
|
26126
|
+
return -1;
|
|
26127
|
+
if (isStrictlyOlder(right, left))
|
|
26128
|
+
return 1;
|
|
26129
|
+
return 0;
|
|
26130
|
+
}
|
|
26131
|
+
|
|
26132
|
+
// src/storage/local-sqlite.ts
|
|
26070
26133
|
init_database();
|
|
26071
26134
|
|
|
26072
26135
|
// src/db/integrity.ts
|
|
@@ -27163,7 +27226,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
27163
27226
|
list: (projectId) => listPlans(projectId, database()),
|
|
27164
27227
|
update: (id, input) => updatePlan(id, input, database()),
|
|
27165
27228
|
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
27166
|
-
delete: (id) => deletePlan(id, database())
|
|
27229
|
+
delete: (id) => deletePlan(id, database()),
|
|
27230
|
+
addComment: (input) => addPlanComment(input, database()),
|
|
27231
|
+
getComments: (planId) => listPlanComments(planId, database()),
|
|
27232
|
+
getCommentsPage: (planId, options2) => {
|
|
27233
|
+
if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
|
|
27234
|
+
throw new Error("Plan comment limit must be an integer between 1 and 1001");
|
|
27235
|
+
}
|
|
27236
|
+
let comments = listPlanComments(planId, database());
|
|
27237
|
+
comments = comments.sort(compareCommentKeyset);
|
|
27238
|
+
if (options2?.before) {
|
|
27239
|
+
const before = options2.before;
|
|
27240
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
27241
|
+
}
|
|
27242
|
+
const limit = options2?.limit ?? 100;
|
|
27243
|
+
return comments.slice(-limit);
|
|
27244
|
+
}
|
|
27167
27245
|
},
|
|
27168
27246
|
planProjectLinks: {
|
|
27169
27247
|
apply: (input) => applyPlanProjectLinkSqlite(input, database()),
|
|
@@ -27204,10 +27282,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
27204
27282
|
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
27205
27283
|
}
|
|
27206
27284
|
let comments = listComments(taskId, database());
|
|
27207
|
-
comments = comments.sort(
|
|
27285
|
+
comments = comments.sort(compareCommentKeyset);
|
|
27208
27286
|
if (options2?.before) {
|
|
27209
27287
|
const before = options2.before;
|
|
27210
|
-
comments = comments.filter((comment) => comment
|
|
27288
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
27211
27289
|
}
|
|
27212
27290
|
if (options2?.limit !== undefined)
|
|
27213
27291
|
comments = comments.slice(-options2.limit);
|
|
@@ -27737,7 +27815,26 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27737
27815
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
27738
27816
|
update: (id, input) => updatePlan2(id, input, store),
|
|
27739
27817
|
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
27740
|
-
delete: (id, context) => store.deletePlan(id, context)
|
|
27818
|
+
delete: (id, context) => store.deletePlan(id, context),
|
|
27819
|
+
addComment: (input, context) => addPlanComment2(input, store, context),
|
|
27820
|
+
getComments: async (planId) => {
|
|
27821
|
+
const pages = [];
|
|
27822
|
+
let before;
|
|
27823
|
+
while (true) {
|
|
27824
|
+
const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
|
|
27825
|
+
if (page.length === 0)
|
|
27826
|
+
break;
|
|
27827
|
+
pages.unshift(page);
|
|
27828
|
+
if (page.length < 1000)
|
|
27829
|
+
break;
|
|
27830
|
+
const oldest = page[0];
|
|
27831
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
27832
|
+
}
|
|
27833
|
+
return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27834
|
+
},
|
|
27835
|
+
getCommentsPage: async (planId, options2) => {
|
|
27836
|
+
return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27837
|
+
}
|
|
27741
27838
|
},
|
|
27742
27839
|
planProjectLinks: {
|
|
27743
27840
|
apply: (input, context) => store.applyPlanProjectLink(input, context),
|
|
@@ -27955,6 +28052,27 @@ class PostgresJsonRecordStore {
|
|
|
27955
28052
|
LIMIT $${params.length}`, params);
|
|
27956
28053
|
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
27957
28054
|
}
|
|
28055
|
+
async listPlanComments(planId, options = {}) {
|
|
28056
|
+
await this.ensureSchema();
|
|
28057
|
+
const limit = options.limit ?? 100;
|
|
28058
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
28059
|
+
throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
|
|
28060
|
+
}
|
|
28061
|
+
const params = [this.service, planId];
|
|
28062
|
+
let cursorPredicate = "";
|
|
28063
|
+
if (options.before) {
|
|
28064
|
+
params.push(options.before.created_at, options.before.id);
|
|
28065
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
28066
|
+
}
|
|
28067
|
+
params.push(limit);
|
|
28068
|
+
const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
|
|
28069
|
+
WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
|
|
28070
|
+
AND payload->>'plan_id' = $2
|
|
28071
|
+
${cursorPredicate}
|
|
28072
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
28073
|
+
LIMIT $${params.length}`, params);
|
|
28074
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
28075
|
+
}
|
|
27958
28076
|
async listRecords(type) {
|
|
27959
28077
|
await this.ensureSchema();
|
|
27960
28078
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -29116,6 +29234,15 @@ class PostgresJsonRecordStore {
|
|
|
29116
29234
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
29117
29235
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29118
29236
|
RETURNING 1
|
|
29237
|
+
), deleted_plan_comments AS (
|
|
29238
|
+
UPDATE ${this.tableName} r SET
|
|
29239
|
+
deleted_at = $3::timestamptz,
|
|
29240
|
+
updated_at = $3::timestamptz,
|
|
29241
|
+
source_machine_id = COALESCE($4, r.source_machine_id),
|
|
29242
|
+
version = COALESCE(r.version, 0) + 1
|
|
29243
|
+
WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
|
|
29244
|
+
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29245
|
+
RETURNING 1
|
|
29119
29246
|
), deleted_plan AS (
|
|
29120
29247
|
UPDATE ${this.tableName} r SET
|
|
29121
29248
|
deleted_at = $3::timestamptz,
|
|
@@ -29291,7 +29418,8 @@ async function updateTask2(id, input, store, context) {
|
|
|
29291
29418
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
29292
29419
|
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
29293
29420
|
created_by: existing.created_by,
|
|
29294
|
-
completed_at:
|
|
29421
|
+
completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
|
|
29422
|
+
started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
|
|
29295
29423
|
};
|
|
29296
29424
|
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
29297
29425
|
operation: "update",
|
|
@@ -29336,7 +29464,8 @@ async function failTask2(id, agentId, reason, options, store) {
|
|
|
29336
29464
|
status: "failed",
|
|
29337
29465
|
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
29338
29466
|
reason: reason ?? task2.reason,
|
|
29339
|
-
retry_after: options?.retry_after ?? task2.retry_after
|
|
29467
|
+
retry_after: options?.retry_after ?? task2.retry_after,
|
|
29468
|
+
completed_at: task2.completed_at ?? new Date().toISOString()
|
|
29340
29469
|
}, store);
|
|
29341
29470
|
if (!options?.retry)
|
|
29342
29471
|
return { task: failed };
|
|
@@ -29784,16 +29913,22 @@ async function createTaskList2(input, store, context) {
|
|
|
29784
29913
|
async function updateTaskList2(id, input, store) {
|
|
29785
29914
|
const list = await requireRecord("task_lists", id, store);
|
|
29786
29915
|
const patch = definedPatch(input);
|
|
29916
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
|
|
29787
29917
|
if (input.slug !== undefined) {
|
|
29788
29918
|
const slug = slugifyRaw(input.slug);
|
|
29789
29919
|
if (!slug)
|
|
29790
29920
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
29791
|
-
|
|
29921
|
+
patch.slug = slug;
|
|
29922
|
+
}
|
|
29923
|
+
const effectiveSlug = patch.slug ?? list.slug;
|
|
29924
|
+
if (projectId !== list.project_id || effectiveSlug !== list.slug) {
|
|
29925
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
|
|
29792
29926
|
if (duplicate) {
|
|
29793
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
29927
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
29794
29928
|
}
|
|
29795
|
-
patch.slug = slug;
|
|
29796
29929
|
}
|
|
29930
|
+
if (projectId !== list.project_id)
|
|
29931
|
+
patch.project_id = projectId;
|
|
29797
29932
|
return store.upsert("task_lists", {
|
|
29798
29933
|
...list,
|
|
29799
29934
|
...patch,
|
|
@@ -29886,6 +30021,22 @@ async function addComment2(input, store, context) {
|
|
|
29886
30021
|
function redactComment(comment) {
|
|
29887
30022
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
29888
30023
|
}
|
|
30024
|
+
async function addPlanComment2(input, store, context) {
|
|
30025
|
+
const comment = {
|
|
30026
|
+
id: randomUUID3(),
|
|
30027
|
+
plan_id: input.plan_id,
|
|
30028
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
30029
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
30030
|
+
content: redactEvidenceText(input.content),
|
|
30031
|
+
type: input.type ?? "comment",
|
|
30032
|
+
progress_pct: input.progress_pct ?? null,
|
|
30033
|
+
created_at: new Date().toISOString()
|
|
30034
|
+
};
|
|
30035
|
+
return store.upsert("plan_comments", comment, context);
|
|
30036
|
+
}
|
|
30037
|
+
function redactPlanComment(comment) {
|
|
30038
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
30039
|
+
}
|
|
29889
30040
|
async function exportSnapshot(store) {
|
|
29890
30041
|
return {
|
|
29891
30042
|
exportedAt: new Date().toISOString(),
|
|
@@ -43624,6 +43775,1314 @@ class TodosTaskManifestHttpClient {
|
|
|
43624
43775
|
function createTodosTaskManifestHttpClient(options) {
|
|
43625
43776
|
return new TodosTaskManifestHttpClient(options);
|
|
43626
43777
|
}
|
|
43778
|
+
// src/task-subtree-transfer/types.ts
|
|
43779
|
+
var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1";
|
|
43780
|
+
var TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1;
|
|
43781
|
+
|
|
43782
|
+
class TodosTaskSubtreeTransferError extends Error {
|
|
43783
|
+
code;
|
|
43784
|
+
details;
|
|
43785
|
+
constructor(code, message, details = {}) {
|
|
43786
|
+
super(message);
|
|
43787
|
+
this.code = code;
|
|
43788
|
+
this.details = details;
|
|
43789
|
+
this.name = "TodosTaskSubtreeTransferError";
|
|
43790
|
+
}
|
|
43791
|
+
}
|
|
43792
|
+
|
|
43793
|
+
// src/task-subtree-transfer/canonical.ts
|
|
43794
|
+
function taskSubtreeTransferRequestDigest(input) {
|
|
43795
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43796
|
+
return canonicalDigest(request);
|
|
43797
|
+
}
|
|
43798
|
+
function taskSubtreeTransferRollbackRequestDigest(input) {
|
|
43799
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43800
|
+
return canonicalDigest(request);
|
|
43801
|
+
}
|
|
43802
|
+
function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
|
|
43803
|
+
return `tstk_${canonicalDigest({
|
|
43804
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
43805
|
+
...input
|
|
43806
|
+
}).slice(0, 48)}`;
|
|
43807
|
+
}
|
|
43808
|
+
|
|
43809
|
+
// src/task-subtree-transfer/planner.ts
|
|
43810
|
+
function sortedTaskImage(task3) {
|
|
43811
|
+
return {
|
|
43812
|
+
task_id: task3.task_id,
|
|
43813
|
+
project_id: task3.project_id,
|
|
43814
|
+
parent_id: task3.parent_id,
|
|
43815
|
+
plan_id: task3.plan_id,
|
|
43816
|
+
task_list_id: task3.task_list_id,
|
|
43817
|
+
version: task3.version,
|
|
43818
|
+
updated_at: task3.updated_at
|
|
43819
|
+
};
|
|
43820
|
+
}
|
|
43821
|
+
function sourcePopulationDigest(tasks) {
|
|
43822
|
+
return canonicalDigest(tasks.map((task3) => ({
|
|
43823
|
+
...sortedTaskImage(task3),
|
|
43824
|
+
archived_at: task3.archived_at ?? null
|
|
43825
|
+
})).sort((left, right) => left.task_id.localeCompare(right.task_id)));
|
|
43826
|
+
}
|
|
43827
|
+
function closure(tasks, rootTaskId) {
|
|
43828
|
+
const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
|
|
43829
|
+
if (!byId.has(rootTaskId)) {
|
|
43830
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
|
|
43831
|
+
}
|
|
43832
|
+
const children = new Map;
|
|
43833
|
+
for (const task3 of tasks) {
|
|
43834
|
+
if (!task3.parent_id)
|
|
43835
|
+
continue;
|
|
43836
|
+
const siblings = children.get(task3.parent_id) ?? [];
|
|
43837
|
+
siblings.push(task3.task_id);
|
|
43838
|
+
children.set(task3.parent_id, siblings);
|
|
43839
|
+
}
|
|
43840
|
+
for (const ids2 of children.values())
|
|
43841
|
+
ids2.sort();
|
|
43842
|
+
const visiting = new Set;
|
|
43843
|
+
const visited = new Set;
|
|
43844
|
+
const result = [];
|
|
43845
|
+
const visit = (id) => {
|
|
43846
|
+
if (visiting.has(id)) {
|
|
43847
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
|
|
43848
|
+
}
|
|
43849
|
+
if (visited.has(id))
|
|
43850
|
+
return;
|
|
43851
|
+
const task3 = byId.get(id);
|
|
43852
|
+
if (!task3) {
|
|
43853
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
|
|
43854
|
+
}
|
|
43855
|
+
visiting.add(id);
|
|
43856
|
+
result.push(task3);
|
|
43857
|
+
for (const child of children.get(id) ?? [])
|
|
43858
|
+
visit(child);
|
|
43859
|
+
visiting.delete(id);
|
|
43860
|
+
visited.add(id);
|
|
43861
|
+
};
|
|
43862
|
+
visit(rootTaskId);
|
|
43863
|
+
return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
43864
|
+
}
|
|
43865
|
+
function prepareTransfer(snapshot, input) {
|
|
43866
|
+
if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
|
|
43867
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
|
|
43868
|
+
}
|
|
43869
|
+
const moved = closure(snapshot.source_tasks, input.root_task_id);
|
|
43870
|
+
const movedIds = new Set(moved.map((task3) => task3.task_id));
|
|
43871
|
+
if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
|
|
43872
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
|
|
43873
|
+
}
|
|
43874
|
+
const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
|
|
43875
|
+
const allPlanTasks = new Map;
|
|
43876
|
+
for (const task3 of snapshot.plan_tasks) {
|
|
43877
|
+
if (!task3.plan_id)
|
|
43878
|
+
continue;
|
|
43879
|
+
const rows = allPlanTasks.get(task3.plan_id) ?? [];
|
|
43880
|
+
rows.push(task3);
|
|
43881
|
+
allPlanTasks.set(task3.plan_id, rows);
|
|
43882
|
+
}
|
|
43883
|
+
const contained = [];
|
|
43884
|
+
const shared = [];
|
|
43885
|
+
const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
|
|
43886
|
+
for (const planId of planIds) {
|
|
43887
|
+
const plan = planById.get(planId);
|
|
43888
|
+
if (!plan || plan.project_id !== input.source_project_id) {
|
|
43889
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
|
|
43890
|
+
}
|
|
43891
|
+
const members = allPlanTasks.get(planId) ?? [];
|
|
43892
|
+
if (members.length === 0) {
|
|
43893
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
|
|
43894
|
+
}
|
|
43895
|
+
if (members.every((task3) => movedIds.has(task3.task_id)))
|
|
43896
|
+
contained.push(planId);
|
|
43897
|
+
else
|
|
43898
|
+
shared.push(planId);
|
|
43899
|
+
}
|
|
43900
|
+
const inspection = {
|
|
43901
|
+
source_project_id: input.source_project_id,
|
|
43902
|
+
destination_project_id: input.destination_project_id,
|
|
43903
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
43904
|
+
root_task_id: input.root_task_id,
|
|
43905
|
+
destination_parent_id: input.destination_parent_id,
|
|
43906
|
+
expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
|
|
43907
|
+
source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
|
|
43908
|
+
expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
|
|
43909
|
+
contained_plan_ids: contained,
|
|
43910
|
+
shared_plan_ids: shared,
|
|
43911
|
+
complete: true
|
|
43912
|
+
};
|
|
43913
|
+
return {
|
|
43914
|
+
inspection,
|
|
43915
|
+
prior_tasks: moved.map(sortedTaskImage),
|
|
43916
|
+
prior_plans: contained.map((id) => planById.get(id)),
|
|
43917
|
+
task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
|
|
43918
|
+
};
|
|
43919
|
+
}
|
|
43920
|
+
function validateApplySnapshot(prepared, request, plans) {
|
|
43921
|
+
if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
|
|
43922
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
|
|
43923
|
+
}
|
|
43924
|
+
if (request.source_population_digest !== prepared.inspection.source_population_digest) {
|
|
43925
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
|
|
43926
|
+
}
|
|
43927
|
+
const expected = request.expected_tasks.map((task3) => ({ task_id: task3.task_id, version: task3.version })).sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
43928
|
+
if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
|
|
43929
|
+
const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
|
|
43930
|
+
const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
|
|
43931
|
+
throw new TodosTaskSubtreeTransferError(sameIds ? "TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT" : "TODOS_TASK_SUBTREE_TRANSFER_CLOSURE_DRIFT", sameIds ? "At least one exact task version changed since inspection" : "Exact descendant closure changed since inspection");
|
|
43932
|
+
}
|
|
43933
|
+
const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
|
|
43934
|
+
if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
|
|
43935
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
|
|
43936
|
+
expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
|
|
43937
|
+
received_shared_plan_ids: requestedSources
|
|
43938
|
+
});
|
|
43939
|
+
}
|
|
43940
|
+
const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
|
|
43941
|
+
const destinationIds = new Set;
|
|
43942
|
+
for (const mapping of request.shared_plan_splits) {
|
|
43943
|
+
if (destinationIds.has(mapping.destination_plan_id)) {
|
|
43944
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
|
|
43945
|
+
}
|
|
43946
|
+
destinationIds.add(mapping.destination_plan_id);
|
|
43947
|
+
const destination = planById.get(mapping.destination_plan_id);
|
|
43948
|
+
if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
|
|
43949
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Shared-plan destination does not match the exact destination project and task list", { destination_plan_id: mapping.destination_plan_id });
|
|
43950
|
+
}
|
|
43951
|
+
for (const [taskId, planId] of prepared.task_plan_targets) {
|
|
43952
|
+
if (planId === mapping.source_plan_id) {
|
|
43953
|
+
prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
|
|
43954
|
+
}
|
|
43955
|
+
}
|
|
43956
|
+
}
|
|
43957
|
+
}
|
|
43958
|
+
|
|
43959
|
+
// src/task-subtree-transfer/schema-sql.ts
|
|
43960
|
+
function sqliteTodosTaskSubtreeTransferSchemaSql() {
|
|
43961
|
+
return `
|
|
43962
|
+
CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
43963
|
+
receipt_id TEXT PRIMARY KEY,
|
|
43964
|
+
tenant_id TEXT NOT NULL,
|
|
43965
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
43966
|
+
operation_id TEXT NOT NULL,
|
|
43967
|
+
step_id TEXT NOT NULL,
|
|
43968
|
+
idempotency_key TEXT NOT NULL,
|
|
43969
|
+
request_digest TEXT NOT NULL,
|
|
43970
|
+
precondition_digest TEXT NOT NULL,
|
|
43971
|
+
result_digest TEXT NOT NULL,
|
|
43972
|
+
apply_receipt_id TEXT,
|
|
43973
|
+
request_json TEXT NOT NULL,
|
|
43974
|
+
result_json TEXT NOT NULL,
|
|
43975
|
+
created_at TEXT NOT NULL,
|
|
43976
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
43977
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
43978
|
+
);
|
|
43979
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
43980
|
+
BEFORE UPDATE ON todos_task_subtree_transfer_receipts BEGIN
|
|
43981
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
43982
|
+
END;
|
|
43983
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_delete
|
|
43984
|
+
BEFORE DELETE ON todos_task_subtree_transfer_receipts BEGIN
|
|
43985
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
43986
|
+
END;
|
|
43987
|
+
`;
|
|
43988
|
+
}
|
|
43989
|
+
function ensureSqliteTodosTaskSubtreeTransferSchema(db) {
|
|
43990
|
+
db.exec(sqliteTodosTaskSubtreeTransferSchemaSql());
|
|
43991
|
+
}
|
|
43992
|
+
function postgresTodosTaskSubtreeTransferSchemaSql() {
|
|
43993
|
+
return [
|
|
43994
|
+
`CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
43995
|
+
receipt_id text PRIMARY KEY,
|
|
43996
|
+
tenant_id text NOT NULL,
|
|
43997
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
43998
|
+
operation_id text NOT NULL,
|
|
43999
|
+
step_id text NOT NULL,
|
|
44000
|
+
idempotency_key text NOT NULL,
|
|
44001
|
+
request_digest text NOT NULL,
|
|
44002
|
+
precondition_digest text NOT NULL,
|
|
44003
|
+
result_digest text NOT NULL,
|
|
44004
|
+
apply_receipt_id text,
|
|
44005
|
+
request_json jsonb NOT NULL,
|
|
44006
|
+
result_json jsonb NOT NULL,
|
|
44007
|
+
created_at timestamptz NOT NULL,
|
|
44008
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
44009
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
44010
|
+
)`,
|
|
44011
|
+
`CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
|
|
44012
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
44013
|
+
BEGIN
|
|
44014
|
+
RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
|
|
44015
|
+
END $$`,
|
|
44016
|
+
`DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
44017
|
+
ON todos_task_subtree_transfer_receipts`,
|
|
44018
|
+
`CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
|
|
44019
|
+
BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
|
|
44020
|
+
FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
|
|
44021
|
+
];
|
|
44022
|
+
}
|
|
44023
|
+
|
|
44024
|
+
// src/task-subtree-transfer/postgres.ts
|
|
44025
|
+
function safeIdentifier3(value, fallback) {
|
|
44026
|
+
const candidate = value ?? fallback;
|
|
44027
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
|
|
44028
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
|
|
44029
|
+
}
|
|
44030
|
+
return candidate;
|
|
44031
|
+
}
|
|
44032
|
+
function timestamp4(value) {
|
|
44033
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
44034
|
+
}
|
|
44035
|
+
function payloadOf(row) {
|
|
44036
|
+
return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
|
|
44037
|
+
}
|
|
44038
|
+
function taskRecord(row) {
|
|
44039
|
+
const payload = payloadOf(row);
|
|
44040
|
+
return {
|
|
44041
|
+
task_id: String(payload.id ?? row.object_id),
|
|
44042
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44043
|
+
parent_id: payload.parent_id == null ? null : String(payload.parent_id),
|
|
44044
|
+
plan_id: payload.plan_id == null ? null : String(payload.plan_id),
|
|
44045
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44046
|
+
version: Number(payload.version ?? row.version ?? 1),
|
|
44047
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at),
|
|
44048
|
+
archived_at: payload.archived_at == null ? null : String(payload.archived_at)
|
|
44049
|
+
};
|
|
44050
|
+
}
|
|
44051
|
+
function planRecord(row) {
|
|
44052
|
+
const payload = payloadOf(row);
|
|
44053
|
+
return {
|
|
44054
|
+
plan_id: String(payload.id ?? row.object_id),
|
|
44055
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44056
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44057
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at)
|
|
44058
|
+
};
|
|
44059
|
+
}
|
|
44060
|
+
|
|
44061
|
+
class PostgresTodosTaskSubtreeTransferBackend {
|
|
44062
|
+
client;
|
|
44063
|
+
kind = "postgresql";
|
|
44064
|
+
service;
|
|
44065
|
+
tableName;
|
|
44066
|
+
tenantId;
|
|
44067
|
+
schemaReady = null;
|
|
44068
|
+
constructor(client, options = {}) {
|
|
44069
|
+
this.client = client;
|
|
44070
|
+
this.service = options.service ?? "todos";
|
|
44071
|
+
this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
|
|
44072
|
+
this.tenantId = options.tenantId ?? "default";
|
|
44073
|
+
}
|
|
44074
|
+
async ensureSchema() {
|
|
44075
|
+
this.schemaReady ??= (async () => {
|
|
44076
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
44077
|
+
await this.client.query(sql);
|
|
44078
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
44079
|
+
await this.client.query(sql);
|
|
44080
|
+
})();
|
|
44081
|
+
await this.schemaReady;
|
|
44082
|
+
}
|
|
44083
|
+
async snapshot(client, input, forUpdate = false) {
|
|
44084
|
+
const lock = forUpdate ? " FOR UPDATE" : "";
|
|
44085
|
+
const source9 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44086
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
|
|
44087
|
+
AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
|
|
44088
|
+
const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44089
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44090
|
+
const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44091
|
+
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44092
|
+
const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44093
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
44094
|
+
AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
|
|
44095
|
+
const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44096
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
44097
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
|
|
44098
|
+
const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44099
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44100
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_parent_id, input.destination_project_id])).rows.length === 1;
|
|
44101
|
+
return {
|
|
44102
|
+
source_tasks: source9.rows.map(taskRecord),
|
|
44103
|
+
plan_tasks: allTasks.rows.map(taskRecord),
|
|
44104
|
+
plans: plans.rows.map(planRecord),
|
|
44105
|
+
destination_project_found: destinationProject.rows.length === 1,
|
|
44106
|
+
destination_task_list_found: destinationList.rows.length === 1,
|
|
44107
|
+
destination_parent_found: destinationParent
|
|
44108
|
+
};
|
|
44109
|
+
}
|
|
44110
|
+
async inspect(input) {
|
|
44111
|
+
await this.ensureSchema();
|
|
44112
|
+
return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
|
|
44113
|
+
}
|
|
44114
|
+
async insertPayload(client, task3, update, now4) {
|
|
44115
|
+
const current = await client.query(`SELECT payload FROM ${this.tableName}
|
|
44116
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44117
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
44118
|
+
const row = current.rows[0];
|
|
44119
|
+
if (!row)
|
|
44120
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
|
|
44121
|
+
const payload = payloadOf(row);
|
|
44122
|
+
if (Number(payload.version ?? 1) !== task3.version) {
|
|
44123
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
|
|
44124
|
+
}
|
|
44125
|
+
const next = {
|
|
44126
|
+
...payload,
|
|
44127
|
+
...update,
|
|
44128
|
+
version: task3.version + 1,
|
|
44129
|
+
updated_at: now4
|
|
44130
|
+
};
|
|
44131
|
+
const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44132
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
|
|
44133
|
+
AND deleted_at IS NULL AND version = $6
|
|
44134
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
|
|
44135
|
+
if (result.rows.length !== 1) {
|
|
44136
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
|
|
44137
|
+
}
|
|
44138
|
+
}
|
|
44139
|
+
async apply(input, options) {
|
|
44140
|
+
await this.ensureSchema();
|
|
44141
|
+
if (!this.client.transaction) {
|
|
44142
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
|
|
44143
|
+
}
|
|
44144
|
+
return this.client.transaction(async (tx) => {
|
|
44145
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44146
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44147
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44148
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44149
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
44150
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44151
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44152
|
+
if (existing.rows[0]) {
|
|
44153
|
+
const row = existing.rows[0];
|
|
44154
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
|
|
44155
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
|
|
44156
|
+
}
|
|
44157
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
|
|
44158
|
+
}
|
|
44159
|
+
const snapshot = await this.snapshot(tx, input, true);
|
|
44160
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44161
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44162
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44163
|
+
const priorImage = {
|
|
44164
|
+
tasks: prepared.prior_tasks,
|
|
44165
|
+
plans: prepared.prior_plans
|
|
44166
|
+
};
|
|
44167
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44168
|
+
await this.insertPayload(tx, task3, {
|
|
44169
|
+
project_id: input.destination_project_id,
|
|
44170
|
+
task_list_id: input.destination_task_list_id,
|
|
44171
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44172
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
|
|
44173
|
+
}, now4);
|
|
44174
|
+
}
|
|
44175
|
+
if (options.faultInjector && await options.faultInjector("after_task_writes"))
|
|
44176
|
+
throw new Error("Injected task-subtree-transfer fault at after_task_writes");
|
|
44177
|
+
for (const plan of prepared.prior_plans) {
|
|
44178
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
44179
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
44180
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
|
|
44181
|
+
const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44182
|
+
if (!current || String(current.project_id ?? "") !== String(plan.project_id ?? "") || (current.task_list_id ?? null) !== plan.task_list_id || timestamp4(current.updated_at ?? row.rows[0]?.updated_at) !== plan.updated_at) {
|
|
44183
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
|
|
44184
|
+
}
|
|
44185
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = jsonb_set(jsonb_set(jsonb_set(payload, '{project_id}', to_jsonb($1::text)), '{task_list_id}', to_jsonb($2::text)), '{updated_at}', to_jsonb($3::text)), updated_at = $3
|
|
44186
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44187
|
+
RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
|
|
44188
|
+
if (result2.rows.length !== 1) {
|
|
44189
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44190
|
+
}
|
|
44191
|
+
}
|
|
44192
|
+
if (options.faultInjector && await options.faultInjector("after_plan_writes"))
|
|
44193
|
+
throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
|
|
44194
|
+
const postImage = {
|
|
44195
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44196
|
+
...task3,
|
|
44197
|
+
project_id: input.destination_project_id,
|
|
44198
|
+
task_list_id: input.destination_task_list_id,
|
|
44199
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44200
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44201
|
+
version: task3.version + 1,
|
|
44202
|
+
updated_at: now4
|
|
44203
|
+
})),
|
|
44204
|
+
plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
|
|
44205
|
+
};
|
|
44206
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
|
|
44207
|
+
const receipt = {
|
|
44208
|
+
receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
|
|
44209
|
+
authority: "todos",
|
|
44210
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44211
|
+
schema_version: 1,
|
|
44212
|
+
kind: "apply",
|
|
44213
|
+
operation_id: input.operation_id,
|
|
44214
|
+
step_id: input.step_id,
|
|
44215
|
+
idempotency_key: input.idempotency_key,
|
|
44216
|
+
request_digest: requestDigest,
|
|
44217
|
+
precondition_digest: input.precondition_digest,
|
|
44218
|
+
result_digest: resultDigest,
|
|
44219
|
+
apply_receipt_id: null,
|
|
44220
|
+
source_project_id: input.source_project_id,
|
|
44221
|
+
destination_project_id: input.destination_project_id,
|
|
44222
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44223
|
+
root_task_id: input.root_task_id,
|
|
44224
|
+
source_population_digest: input.source_population_digest,
|
|
44225
|
+
prior_image: priorImage,
|
|
44226
|
+
post_image: postImage,
|
|
44227
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44228
|
+
created_at: now4
|
|
44229
|
+
};
|
|
44230
|
+
const result = { duplicate: false, receipt, moved_task_ids: prepared.prior_tasks.map((task3) => task3.task_id), moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id), complete: true };
|
|
44231
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44232
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44233
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44234
|
+
request_json, result_json, created_at
|
|
44235
|
+
) VALUES ($1, $2, 'apply', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10::jsonb, $11)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4]);
|
|
44236
|
+
if (options.faultInjector && await options.faultInjector("after_receipt_write"))
|
|
44237
|
+
throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
|
|
44238
|
+
return result;
|
|
44239
|
+
});
|
|
44240
|
+
}
|
|
44241
|
+
async readExact(receiptId2) {
|
|
44242
|
+
await this.ensureSchema();
|
|
44243
|
+
const row = await this.client.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND receipt_id = $2 LIMIT 1`, [this.tenantId, receiptId2]);
|
|
44244
|
+
if (!row.rows[0])
|
|
44245
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
|
|
44246
|
+
return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
|
|
44247
|
+
}
|
|
44248
|
+
async rollback(input, options) {
|
|
44249
|
+
await this.ensureSchema();
|
|
44250
|
+
if (!this.client.transaction)
|
|
44251
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
|
|
44252
|
+
return this.client.transaction(async (tx) => {
|
|
44253
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44254
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44255
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44256
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44257
|
+
WHERE tenant_id = $1 AND kind = 'rollback'
|
|
44258
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44259
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44260
|
+
if (existing.rows[0]) {
|
|
44261
|
+
const row = existing.rows[0];
|
|
44262
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
|
|
44263
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
|
|
44264
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
|
|
44265
|
+
}
|
|
44266
|
+
const applyRow = await tx.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND kind = 'apply' AND receipt_id = $2 LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
44267
|
+
if (!applyRow.rows[0])
|
|
44268
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44269
|
+
const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
|
|
44270
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44271
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44272
|
+
}
|
|
44273
|
+
const expectedPrecondition = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", operation_id: input.operation_id, step_id: input.step_id, apply_receipt_id: input.receipt_id, apply_result_digest: applied.receipt.result_digest });
|
|
44274
|
+
if (input.precondition_digest !== expectedPrecondition)
|
|
44275
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44276
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44277
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44278
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
44279
|
+
if (!row.rows[0])
|
|
44280
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
|
|
44281
|
+
const payload = payloadOf(row.rows[0]);
|
|
44282
|
+
if (Number(payload.version ?? 1) !== task3.version || String(payload.project_id ?? "") !== String(task3.project_id ?? "") || (payload.parent_id ?? null) !== task3.parent_id || (payload.plan_id ?? null) !== task3.plan_id || (payload.task_list_id ?? null) !== task3.task_list_id)
|
|
44283
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
|
|
44284
|
+
}
|
|
44285
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44286
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL`, [this.service, task3.task_id]);
|
|
44287
|
+
const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44288
|
+
if (!payload)
|
|
44289
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
|
|
44290
|
+
const next = { ...payload, project_id: task3.project_id, task_list_id: task3.task_list_id, plan_id: task3.plan_id, parent_id: task3.parent_id, version: task3.version + 2, updated_at: now4 };
|
|
44291
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44292
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
|
|
44293
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
|
|
44294
|
+
if (result2.rows.length !== 1) {
|
|
44295
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
|
|
44296
|
+
}
|
|
44297
|
+
}
|
|
44298
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
|
|
44299
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
|
|
44300
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44301
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44302
|
+
const result2 = await tx.query(`UPDATE ${this.tableName}
|
|
44303
|
+
SET payload = jsonb_set(
|
|
44304
|
+
jsonb_set(
|
|
44305
|
+
jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
|
|
44306
|
+
'{task_list_id}',
|
|
44307
|
+
COALESCE(to_jsonb($2::text), 'null'::jsonb)
|
|
44308
|
+
),
|
|
44309
|
+
'{updated_at}',
|
|
44310
|
+
to_jsonb($3::text)
|
|
44311
|
+
), updated_at = $3
|
|
44312
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44313
|
+
AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
|
|
44314
|
+
RETURNING object_id`, [plan.project_id, plan.task_list_id, now4, this.service, plan.plan_id, post.project_id, post.task_list_id, post.updated_at]);
|
|
44315
|
+
if (result2.rows.length !== 1) {
|
|
44316
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44317
|
+
}
|
|
44318
|
+
}
|
|
44319
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
|
|
44320
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
|
|
44321
|
+
const restored = {
|
|
44322
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
|
|
44323
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44324
|
+
};
|
|
44325
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored });
|
|
44326
|
+
const receipt = { ...applied.receipt, receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id), kind: "rollback", operation_id: input.operation_id, step_id: input.step_id, idempotency_key: input.idempotency_key, request_digest: requestDigest, precondition_digest: input.precondition_digest, result_digest: resultDigest, apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored, created_at: now4 };
|
|
44327
|
+
const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
|
|
44328
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44329
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44330
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44331
|
+
request_json, result_json, created_at
|
|
44332
|
+
) VALUES ($1, $2, 'rollback', $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4]);
|
|
44333
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
|
|
44334
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
|
|
44335
|
+
return result;
|
|
44336
|
+
});
|
|
44337
|
+
}
|
|
44338
|
+
}
|
|
44339
|
+
|
|
44340
|
+
// src/task-subtree-transfer/sqlite.ts
|
|
44341
|
+
var sqliteTails2 = new WeakMap;
|
|
44342
|
+
async function fault3(options, point) {
|
|
44343
|
+
if (options.faultInjector && await options.faultInjector(point) === true) {
|
|
44344
|
+
throw new Error(`Injected task-subtree-transfer fault at ${point}`);
|
|
44345
|
+
}
|
|
44346
|
+
}
|
|
44347
|
+
function parseResult(value, duplicate) {
|
|
44348
|
+
return { ...JSON.parse(value), duplicate };
|
|
44349
|
+
}
|
|
44350
|
+
function rowImage(row) {
|
|
44351
|
+
return {
|
|
44352
|
+
task_id: String(row.id),
|
|
44353
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44354
|
+
parent_id: row.parent_id == null ? null : String(row.parent_id),
|
|
44355
|
+
plan_id: row.plan_id == null ? null : String(row.plan_id),
|
|
44356
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44357
|
+
version: Number(row.version),
|
|
44358
|
+
updated_at: String(row.updated_at),
|
|
44359
|
+
archived_at: row.archived_at == null ? null : String(row.archived_at)
|
|
44360
|
+
};
|
|
44361
|
+
}
|
|
44362
|
+
function planImage(row) {
|
|
44363
|
+
return {
|
|
44364
|
+
plan_id: String(row.id),
|
|
44365
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44366
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44367
|
+
updated_at: String(row.updated_at)
|
|
44368
|
+
};
|
|
44369
|
+
}
|
|
44370
|
+
function imageFromTaskRow(row) {
|
|
44371
|
+
const image = rowImage(row);
|
|
44372
|
+
const { archived_at: _archivedAt, ...withoutArchive } = image;
|
|
44373
|
+
return withoutArchive;
|
|
44374
|
+
}
|
|
44375
|
+
function imageFromPlanRow(row) {
|
|
44376
|
+
return planImage(row);
|
|
44377
|
+
}
|
|
44378
|
+
function receiptId2(input) {
|
|
44379
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, taskSubtreeTransferRequestDigest(input));
|
|
44380
|
+
}
|
|
44381
|
+
function rollbackReceiptId(input) {
|
|
44382
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id);
|
|
44383
|
+
}
|
|
44384
|
+
function taskWhere(db, taskId) {
|
|
44385
|
+
const row = db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44386
|
+
FROM tasks WHERE id = ? LIMIT 1`).get(taskId);
|
|
44387
|
+
if (!row) {
|
|
44388
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${taskId}`, { task_id: taskId });
|
|
44389
|
+
}
|
|
44390
|
+
return row;
|
|
44391
|
+
}
|
|
44392
|
+
|
|
44393
|
+
class SqliteTodosTaskSubtreeTransferBackend {
|
|
44394
|
+
db;
|
|
44395
|
+
tenantId;
|
|
44396
|
+
kind = "sqlite";
|
|
44397
|
+
constructor(db, tenantId = "default") {
|
|
44398
|
+
this.db = db;
|
|
44399
|
+
this.tenantId = tenantId;
|
|
44400
|
+
ensureSqliteTodosTaskSubtreeTransferSchema(db);
|
|
44401
|
+
}
|
|
44402
|
+
async serialized(run) {
|
|
44403
|
+
const previous = sqliteTails2.get(this.db) ?? Promise.resolve();
|
|
44404
|
+
let release;
|
|
44405
|
+
const current = new Promise((resolve10) => {
|
|
44406
|
+
release = resolve10;
|
|
44407
|
+
});
|
|
44408
|
+
const tail = previous.then(() => current);
|
|
44409
|
+
sqliteTails2.set(this.db, tail);
|
|
44410
|
+
await previous;
|
|
44411
|
+
try {
|
|
44412
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
44413
|
+
try {
|
|
44414
|
+
const result = await run();
|
|
44415
|
+
this.db.exec("COMMIT");
|
|
44416
|
+
return result;
|
|
44417
|
+
} catch (error) {
|
|
44418
|
+
this.db.exec("ROLLBACK");
|
|
44419
|
+
throw error;
|
|
44420
|
+
}
|
|
44421
|
+
} finally {
|
|
44422
|
+
release();
|
|
44423
|
+
if (sqliteTails2.get(this.db) === tail)
|
|
44424
|
+
sqliteTails2.delete(this.db);
|
|
44425
|
+
}
|
|
44426
|
+
}
|
|
44427
|
+
snapshot(input) {
|
|
44428
|
+
const sourceTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44429
|
+
FROM tasks WHERE project_id = ? ORDER BY id`).all(input.source_project_id).map(rowImage);
|
|
44430
|
+
const allTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44431
|
+
FROM tasks ORDER BY id`).all().map(rowImage);
|
|
44432
|
+
const plans = this.db.query(`SELECT id, project_id, task_list_id, updated_at
|
|
44433
|
+
FROM plans ORDER BY id`).all().map(planImage);
|
|
44434
|
+
const destinationProjectFound = Boolean(this.db.query("SELECT id FROM projects WHERE id = ? LIMIT 1").get(input.destination_project_id));
|
|
44435
|
+
const destinationTaskList = this.db.query("SELECT id FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_task_list_id, input.destination_project_id);
|
|
44436
|
+
const destinationParentFound = input.destination_parent_id === null || Boolean(this.db.query("SELECT id FROM tasks WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_parent_id, input.destination_project_id));
|
|
44437
|
+
return {
|
|
44438
|
+
source_tasks: sourceTasks,
|
|
44439
|
+
plan_tasks: allTasks,
|
|
44440
|
+
plans,
|
|
44441
|
+
destination_project_found: destinationProjectFound,
|
|
44442
|
+
destination_task_list_found: Boolean(destinationTaskList),
|
|
44443
|
+
destination_parent_found: destinationParentFound
|
|
44444
|
+
};
|
|
44445
|
+
}
|
|
44446
|
+
async inspect(input) {
|
|
44447
|
+
return this.serialized(() => prepareTransfer(this.snapshot(input), input).inspection);
|
|
44448
|
+
}
|
|
44449
|
+
async apply(input, options) {
|
|
44450
|
+
return this.serialized(async () => {
|
|
44451
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44452
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44453
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44454
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
44455
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44456
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44457
|
+
if (existing) {
|
|
44458
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44459
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation/step or idempotency key was already accepted for a different request");
|
|
44460
|
+
}
|
|
44461
|
+
return parseResult(String(existing.result_json), true);
|
|
44462
|
+
}
|
|
44463
|
+
const snapshot = this.snapshot(input);
|
|
44464
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44465
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44466
|
+
const priorImage = {
|
|
44467
|
+
tasks: prepared.prior_tasks,
|
|
44468
|
+
plans: prepared.prior_plans
|
|
44469
|
+
};
|
|
44470
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44471
|
+
const movedTaskIds = prepared.prior_tasks.map((task3) => task3.task_id);
|
|
44472
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44473
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44474
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44475
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task changed after inspection", { task_id: task3.task_id });
|
|
44476
|
+
}
|
|
44477
|
+
}
|
|
44478
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44479
|
+
const targetPlan = prepared.task_plan_targets.get(task3.task_id) ?? null;
|
|
44480
|
+
const parentId = task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id;
|
|
44481
|
+
const result2 = this.db.query(`UPDATE tasks
|
|
44482
|
+
SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?, version = version + 1, updated_at = ?
|
|
44483
|
+
WHERE id = ? AND version = ?`).run(input.destination_project_id, input.destination_task_list_id, targetPlan, parentId, now4, task3.task_id, task3.version);
|
|
44484
|
+
if (result2.changes !== 1) {
|
|
44485
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task version changed during the atomic transfer", { task_id: task3.task_id });
|
|
44486
|
+
}
|
|
44487
|
+
}
|
|
44488
|
+
await fault3(options, "after_task_writes");
|
|
44489
|
+
for (const plan of prepared.prior_plans) {
|
|
44490
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44491
|
+
WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(input.destination_project_id, input.destination_task_list_id, now4, plan.plan_id, plan.project_id, plan.task_list_id, plan.updated_at);
|
|
44492
|
+
if (result2.changes !== 1) {
|
|
44493
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A contained plan changed during the atomic transfer", { plan_id: plan.plan_id });
|
|
44494
|
+
}
|
|
44495
|
+
}
|
|
44496
|
+
await fault3(options, "after_plan_writes");
|
|
44497
|
+
const postImage = {
|
|
44498
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44499
|
+
...task3,
|
|
44500
|
+
project_id: input.destination_project_id,
|
|
44501
|
+
task_list_id: input.destination_task_list_id,
|
|
44502
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44503
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44504
|
+
version: task3.version + 1,
|
|
44505
|
+
updated_at: now4
|
|
44506
|
+
})),
|
|
44507
|
+
plans: prepared.prior_plans.map((plan) => ({
|
|
44508
|
+
...plan,
|
|
44509
|
+
project_id: input.destination_project_id,
|
|
44510
|
+
task_list_id: input.destination_task_list_id,
|
|
44511
|
+
updated_at: now4
|
|
44512
|
+
}))
|
|
44513
|
+
};
|
|
44514
|
+
const resultDigest = canonicalDigest({
|
|
44515
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44516
|
+
request_digest: requestDigest,
|
|
44517
|
+
prior_image: priorImage,
|
|
44518
|
+
post_image: postImage
|
|
44519
|
+
});
|
|
44520
|
+
const receipt = {
|
|
44521
|
+
receipt_id: receiptId2(input),
|
|
44522
|
+
authority: "todos",
|
|
44523
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44524
|
+
schema_version: 1,
|
|
44525
|
+
kind: "apply",
|
|
44526
|
+
operation_id: input.operation_id,
|
|
44527
|
+
step_id: input.step_id,
|
|
44528
|
+
idempotency_key: input.idempotency_key,
|
|
44529
|
+
request_digest: requestDigest,
|
|
44530
|
+
precondition_digest: input.precondition_digest,
|
|
44531
|
+
result_digest: resultDigest,
|
|
44532
|
+
apply_receipt_id: null,
|
|
44533
|
+
source_project_id: input.source_project_id,
|
|
44534
|
+
destination_project_id: input.destination_project_id,
|
|
44535
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44536
|
+
root_task_id: input.root_task_id,
|
|
44537
|
+
source_population_digest: input.source_population_digest,
|
|
44538
|
+
prior_image: priorImage,
|
|
44539
|
+
post_image: postImage,
|
|
44540
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44541
|
+
created_at: now4
|
|
44542
|
+
};
|
|
44543
|
+
const result = {
|
|
44544
|
+
duplicate: false,
|
|
44545
|
+
receipt,
|
|
44546
|
+
moved_task_ids: movedTaskIds,
|
|
44547
|
+
moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id),
|
|
44548
|
+
complete: true
|
|
44549
|
+
};
|
|
44550
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44551
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44552
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44553
|
+
request_json, result_json, created_at
|
|
44554
|
+
) VALUES (?, ?, 'apply', ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4);
|
|
44555
|
+
await fault3(options, "after_receipt_write");
|
|
44556
|
+
return result;
|
|
44557
|
+
});
|
|
44558
|
+
}
|
|
44559
|
+
async readExact(receiptIdValue) {
|
|
44560
|
+
const row = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44561
|
+
WHERE tenant_id = ? AND receipt_id = ? LIMIT 1`).get(this.tenantId, receiptIdValue);
|
|
44562
|
+
if (!row) {
|
|
44563
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptIdValue}`);
|
|
44564
|
+
}
|
|
44565
|
+
return parseResult(String(row.result_json), false);
|
|
44566
|
+
}
|
|
44567
|
+
async rollback(input, options) {
|
|
44568
|
+
return this.serialized(async () => {
|
|
44569
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44570
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44571
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44572
|
+
WHERE tenant_id = ? AND kind = 'rollback'
|
|
44573
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44574
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44575
|
+
if (existing) {
|
|
44576
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44577
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation/step or idempotency key was already accepted for a different request");
|
|
44578
|
+
}
|
|
44579
|
+
return parseResult(String(existing.result_json), true);
|
|
44580
|
+
}
|
|
44581
|
+
const applyRow = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44582
|
+
WHERE tenant_id = ? AND kind = 'apply' AND receipt_id = ? LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
44583
|
+
if (!applyRow) {
|
|
44584
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44585
|
+
}
|
|
44586
|
+
const applied = parseResult(String(applyRow.result_json), false);
|
|
44587
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44588
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44589
|
+
}
|
|
44590
|
+
const expectedPrecondition = canonicalDigest({
|
|
44591
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44592
|
+
direction: "rollback",
|
|
44593
|
+
operation_id: input.operation_id,
|
|
44594
|
+
step_id: input.step_id,
|
|
44595
|
+
apply_receipt_id: input.receipt_id,
|
|
44596
|
+
apply_result_digest: applied.receipt.result_digest
|
|
44597
|
+
});
|
|
44598
|
+
if (input.precondition_digest !== expectedPrecondition) {
|
|
44599
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44600
|
+
}
|
|
44601
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44602
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44603
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44604
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44605
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred task changed after apply", { task_id: task3.task_id });
|
|
44606
|
+
}
|
|
44607
|
+
}
|
|
44608
|
+
for (const plan of applied.receipt.post_image.plans) {
|
|
44609
|
+
const row = this.db.query("SELECT id, project_id, task_list_id, updated_at FROM plans WHERE id = ? LIMIT 1").get(plan.plan_id);
|
|
44610
|
+
if (!row || canonicalJson(imageFromPlanRow(row)) !== canonicalJson(plan)) {
|
|
44611
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred plan changed after apply", { plan_id: plan.plan_id });
|
|
44612
|
+
}
|
|
44613
|
+
}
|
|
44614
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44615
|
+
const result2 = this.db.query(`UPDATE tasks SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?,
|
|
44616
|
+
version = version + 1, updated_at = ?
|
|
44617
|
+
WHERE id = ? AND version = ?`).run(task3.project_id, task3.task_list_id, task3.plan_id, task3.parent_id, now4, task3.task_id, applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version);
|
|
44618
|
+
if (result2.changes !== 1) {
|
|
44619
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback task CAS failed", { task_id: task3.task_id });
|
|
44620
|
+
}
|
|
44621
|
+
}
|
|
44622
|
+
await fault3(options, "after_rollback_task_writes");
|
|
44623
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44624
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44625
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44626
|
+
WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(plan.project_id, plan.task_list_id, now4, plan.plan_id, post.project_id, post.task_list_id, post.updated_at);
|
|
44627
|
+
if (result2.changes !== 1) {
|
|
44628
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback plan CAS failed", { plan_id: plan.plan_id });
|
|
44629
|
+
}
|
|
44630
|
+
}
|
|
44631
|
+
await fault3(options, "after_rollback_plan_writes");
|
|
44632
|
+
const restored = {
|
|
44633
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({
|
|
44634
|
+
...task3,
|
|
44635
|
+
version: applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version + 1,
|
|
44636
|
+
updated_at: now4
|
|
44637
|
+
})),
|
|
44638
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44639
|
+
};
|
|
44640
|
+
const resultDigest = canonicalDigest({
|
|
44641
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44642
|
+
direction: "rollback",
|
|
44643
|
+
apply_receipt_id: input.receipt_id,
|
|
44644
|
+
prior_image: applied.receipt.post_image,
|
|
44645
|
+
post_image: restored
|
|
44646
|
+
});
|
|
44647
|
+
const receipt = {
|
|
44648
|
+
...applied.receipt,
|
|
44649
|
+
receipt_id: rollbackReceiptId(input),
|
|
44650
|
+
kind: "rollback",
|
|
44651
|
+
step_id: input.step_id,
|
|
44652
|
+
idempotency_key: input.idempotency_key,
|
|
44653
|
+
request_digest: requestDigest,
|
|
44654
|
+
precondition_digest: input.precondition_digest,
|
|
44655
|
+
result_digest: resultDigest,
|
|
44656
|
+
apply_receipt_id: input.receipt_id,
|
|
44657
|
+
prior_image: applied.receipt.post_image,
|
|
44658
|
+
post_image: restored,
|
|
44659
|
+
created_at: now4
|
|
44660
|
+
};
|
|
44661
|
+
const result = {
|
|
44662
|
+
duplicate: false,
|
|
44663
|
+
receipt,
|
|
44664
|
+
moved_task_ids: applied.moved_task_ids,
|
|
44665
|
+
moved_plan_ids: applied.moved_plan_ids,
|
|
44666
|
+
complete: true
|
|
44667
|
+
};
|
|
44668
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44669
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44670
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44671
|
+
request_json, result_json, created_at
|
|
44672
|
+
) VALUES (?, ?, 'rollback', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4);
|
|
44673
|
+
await fault3(options, "after_rollback_receipt_write");
|
|
44674
|
+
return result;
|
|
44675
|
+
});
|
|
44676
|
+
}
|
|
44677
|
+
}
|
|
44678
|
+
|
|
44679
|
+
// src/task-subtree-transfer/authority.ts
|
|
44680
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
44681
|
+
var DIGEST = /^[0-9a-f]{64}$/i;
|
|
44682
|
+
function boundedString(value, field2, max = 200) {
|
|
44683
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max) {
|
|
44684
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a non-empty bounded string`);
|
|
44685
|
+
}
|
|
44686
|
+
return value;
|
|
44687
|
+
}
|
|
44688
|
+
function uuid3(value, field2) {
|
|
44689
|
+
const parsed = boundedString(value, field2, 80);
|
|
44690
|
+
if (!UUID.test(parsed)) {
|
|
44691
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a UUID`);
|
|
44692
|
+
}
|
|
44693
|
+
return parsed;
|
|
44694
|
+
}
|
|
44695
|
+
function digest2(value, field2) {
|
|
44696
|
+
const parsed = boundedString(value, field2, 64);
|
|
44697
|
+
if (!DIGEST.test(parsed)) {
|
|
44698
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a SHA-256 digest`);
|
|
44699
|
+
}
|
|
44700
|
+
return parsed.toLowerCase();
|
|
44701
|
+
}
|
|
44702
|
+
function nullableUuid(value, field2) {
|
|
44703
|
+
if (value === null)
|
|
44704
|
+
return null;
|
|
44705
|
+
return uuid3(value, field2);
|
|
44706
|
+
}
|
|
44707
|
+
function strictObject(input, fields) {
|
|
44708
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
44709
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
|
|
44710
|
+
}
|
|
44711
|
+
const object = input;
|
|
44712
|
+
const allowed = new Set(fields);
|
|
44713
|
+
for (const key2 of Object.keys(object)) {
|
|
44714
|
+
if (!allowed.has(key2)) {
|
|
44715
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
|
|
44716
|
+
}
|
|
44717
|
+
}
|
|
44718
|
+
return object;
|
|
44719
|
+
}
|
|
44720
|
+
var INSPECT_FIELDS = [
|
|
44721
|
+
"source_project_id",
|
|
44722
|
+
"destination_project_id",
|
|
44723
|
+
"destination_task_list_id",
|
|
44724
|
+
"root_task_id",
|
|
44725
|
+
"destination_parent_id"
|
|
44726
|
+
];
|
|
44727
|
+
function parseTodosTaskSubtreeTransferInspect(input) {
|
|
44728
|
+
const value = strictObject(input, INSPECT_FIELDS);
|
|
44729
|
+
const request = {
|
|
44730
|
+
source_project_id: uuid3(value.source_project_id, "source_project_id"),
|
|
44731
|
+
destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
|
|
44732
|
+
destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
|
|
44733
|
+
root_task_id: uuid3(value.root_task_id, "root_task_id"),
|
|
44734
|
+
destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
|
|
44735
|
+
};
|
|
44736
|
+
if (request.source_project_id === request.destination_project_id) {
|
|
44737
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
|
|
44738
|
+
}
|
|
44739
|
+
return request;
|
|
44740
|
+
}
|
|
44741
|
+
var APPLY_FIELDS = [
|
|
44742
|
+
...INSPECT_FIELDS,
|
|
44743
|
+
"version",
|
|
44744
|
+
"operation_id",
|
|
44745
|
+
"step_id",
|
|
44746
|
+
"idempotency_key",
|
|
44747
|
+
"precondition_digest",
|
|
44748
|
+
"expected_root_parent_id",
|
|
44749
|
+
"source_population_digest",
|
|
44750
|
+
"expected_tasks",
|
|
44751
|
+
"shared_plan_splits"
|
|
44752
|
+
];
|
|
44753
|
+
function parseTodosTaskSubtreeTransferApply(input) {
|
|
44754
|
+
const value = strictObject(input, APPLY_FIELDS);
|
|
44755
|
+
if (value.version !== 1) {
|
|
44756
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
|
|
44757
|
+
}
|
|
44758
|
+
if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
|
|
44759
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
|
|
44760
|
+
}
|
|
44761
|
+
const expected_tasks = value.expected_tasks.map((item, index) => {
|
|
44762
|
+
const row = strictObject(item, ["task_id", "version"]);
|
|
44763
|
+
const version = row.version;
|
|
44764
|
+
if (!Number.isSafeInteger(version) || Number(version) < 1) {
|
|
44765
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
|
|
44766
|
+
}
|
|
44767
|
+
return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
|
|
44768
|
+
});
|
|
44769
|
+
const ids2 = new Set;
|
|
44770
|
+
for (const row of expected_tasks) {
|
|
44771
|
+
if (ids2.has(row.task_id)) {
|
|
44772
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
|
|
44773
|
+
}
|
|
44774
|
+
ids2.add(row.task_id);
|
|
44775
|
+
}
|
|
44776
|
+
if (!Array.isArray(value.shared_plan_splits)) {
|
|
44777
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
|
|
44778
|
+
}
|
|
44779
|
+
const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
|
|
44780
|
+
const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
|
|
44781
|
+
return {
|
|
44782
|
+
source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
|
|
44783
|
+
destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
|
|
44784
|
+
};
|
|
44785
|
+
});
|
|
44786
|
+
return {
|
|
44787
|
+
...parseTodosTaskSubtreeTransferInspect({
|
|
44788
|
+
source_project_id: value.source_project_id,
|
|
44789
|
+
destination_project_id: value.destination_project_id,
|
|
44790
|
+
destination_task_list_id: value.destination_task_list_id,
|
|
44791
|
+
root_task_id: value.root_task_id,
|
|
44792
|
+
destination_parent_id: value.destination_parent_id
|
|
44793
|
+
}),
|
|
44794
|
+
version: 1,
|
|
44795
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44796
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44797
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44798
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
|
|
44799
|
+
expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
|
|
44800
|
+
source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
|
|
44801
|
+
expected_tasks,
|
|
44802
|
+
shared_plan_splits
|
|
44803
|
+
};
|
|
44804
|
+
}
|
|
44805
|
+
function parseTodosTaskSubtreeTransferRollback(input) {
|
|
44806
|
+
const value = strictObject(input, [
|
|
44807
|
+
"receipt_id",
|
|
44808
|
+
"operation_id",
|
|
44809
|
+
"step_id",
|
|
44810
|
+
"idempotency_key",
|
|
44811
|
+
"precondition_digest"
|
|
44812
|
+
]);
|
|
44813
|
+
return {
|
|
44814
|
+
receipt_id: uuid3(value.receipt_id, "receipt_id"),
|
|
44815
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44816
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44817
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44818
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest")
|
|
44819
|
+
};
|
|
44820
|
+
}
|
|
44821
|
+
function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
|
|
44822
|
+
const {
|
|
44823
|
+
source_project_id,
|
|
44824
|
+
destination_project_id,
|
|
44825
|
+
destination_task_list_id,
|
|
44826
|
+
root_task_id,
|
|
44827
|
+
expected_root_parent_id,
|
|
44828
|
+
destination_parent_id,
|
|
44829
|
+
source_population_digest,
|
|
44830
|
+
expected_tasks,
|
|
44831
|
+
shared_plan_splits
|
|
44832
|
+
} = input;
|
|
44833
|
+
return canonicalDigest({
|
|
44834
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44835
|
+
direction: "apply",
|
|
44836
|
+
source_project_id,
|
|
44837
|
+
destination_project_id,
|
|
44838
|
+
destination_task_list_id,
|
|
44839
|
+
root_task_id,
|
|
44840
|
+
expected_root_parent_id,
|
|
44841
|
+
destination_parent_id,
|
|
44842
|
+
source_population_digest,
|
|
44843
|
+
expected_tasks,
|
|
44844
|
+
shared_plan_splits
|
|
44845
|
+
});
|
|
44846
|
+
}
|
|
44847
|
+
function deriveTodosTaskSubtreeTransferRollbackPreconditionDigest(input) {
|
|
44848
|
+
return canonicalDigest({
|
|
44849
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44850
|
+
direction: "rollback",
|
|
44851
|
+
apply_receipt_id: input.receipt_id,
|
|
44852
|
+
operation_id: input.operation_id,
|
|
44853
|
+
step_id: input.step_id,
|
|
44854
|
+
apply_result_digest: input.apply_result_digest
|
|
44855
|
+
});
|
|
44856
|
+
}
|
|
44857
|
+
function resolveTenantId2(value) {
|
|
44858
|
+
const tenant = value ?? "default";
|
|
44859
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
|
|
44860
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
44861
|
+
}
|
|
44862
|
+
return tenant;
|
|
44863
|
+
}
|
|
44864
|
+
|
|
44865
|
+
class PackageOwnedTodosTaskSubtreeTransferAuthority {
|
|
44866
|
+
backend;
|
|
44867
|
+
tenantId;
|
|
44868
|
+
backendOptions;
|
|
44869
|
+
constructor(backend, options = {}) {
|
|
44870
|
+
this.backend = backend;
|
|
44871
|
+
this.tenantId = resolveTenantId2(options.tenantId);
|
|
44872
|
+
this.backendOptions = { ...options, tenantId: this.tenantId };
|
|
44873
|
+
}
|
|
44874
|
+
async capability() {
|
|
44875
|
+
return {
|
|
44876
|
+
authority: "todos",
|
|
44877
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44878
|
+
schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
44879
|
+
tenant_id: this.tenantId,
|
|
44880
|
+
backend: this.backend.kind,
|
|
44881
|
+
exact_descendant_closure: true,
|
|
44882
|
+
complete_source_population_digest: true,
|
|
44883
|
+
per_task_version_cas: true,
|
|
44884
|
+
explicit_shared_plan_splits: true,
|
|
44885
|
+
atomic_apply: true,
|
|
44886
|
+
immutable_forward_inverse_receipts: true,
|
|
44887
|
+
prior_image_receipts: true,
|
|
44888
|
+
cas_protected_rollback: true,
|
|
44889
|
+
preserves_descendant_parent_ids: true,
|
|
44890
|
+
preserves_task_and_relation_identities: true
|
|
44891
|
+
};
|
|
44892
|
+
}
|
|
44893
|
+
async inspect(input) {
|
|
44894
|
+
return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
|
|
44895
|
+
}
|
|
44896
|
+
async apply(input) {
|
|
44897
|
+
const request = parseTodosTaskSubtreeTransferApply(input);
|
|
44898
|
+
const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
|
|
44899
|
+
if (request.precondition_digest !== expected) {
|
|
44900
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "precondition_digest does not match the exact transfer target and source snapshot", { expected_precondition_digest: expected });
|
|
44901
|
+
}
|
|
44902
|
+
const requestDigest = taskSubtreeTransferRequestDigest(request);
|
|
44903
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
44904
|
+
operation_id: request.operation_id,
|
|
44905
|
+
step_id: request.step_id,
|
|
44906
|
+
direction: "apply",
|
|
44907
|
+
target_selector: request.root_task_id,
|
|
44908
|
+
request_digest: requestDigest,
|
|
44909
|
+
precondition_digest: request.precondition_digest
|
|
44910
|
+
});
|
|
44911
|
+
if (request.idempotency_key !== expectedKey) {
|
|
44912
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
|
|
44913
|
+
}
|
|
44914
|
+
return this.backend.apply(request, this.backendOptions);
|
|
44915
|
+
}
|
|
44916
|
+
async readExact(receiptIdValue) {
|
|
44917
|
+
return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
|
|
44918
|
+
}
|
|
44919
|
+
async rollback(input) {
|
|
44920
|
+
const request = parseTodosTaskSubtreeTransferRollback(input);
|
|
44921
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
|
|
44922
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
44923
|
+
operation_id: request.operation_id,
|
|
44924
|
+
step_id: request.step_id,
|
|
44925
|
+
direction: "rollback",
|
|
44926
|
+
target_selector: request.receipt_id,
|
|
44927
|
+
request_digest: requestDigest,
|
|
44928
|
+
precondition_digest: request.precondition_digest
|
|
44929
|
+
});
|
|
44930
|
+
if (request.idempotency_key !== expectedKey) {
|
|
44931
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
|
|
44932
|
+
}
|
|
44933
|
+
return this.backend.rollback(request, this.backendOptions);
|
|
44934
|
+
}
|
|
44935
|
+
}
|
|
44936
|
+
function createSqliteTodosTaskSubtreeTransferAuthority(options) {
|
|
44937
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new SqliteTodosTaskSubtreeTransferBackend(options.database, resolveTenantId2(options.tenantId)), options);
|
|
44938
|
+
}
|
|
44939
|
+
function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
|
|
44940
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
|
|
44941
|
+
}
|
|
44942
|
+
// src/task-subtree-transfer/http.ts
|
|
44943
|
+
var JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
44944
|
+
var REQUEST_BYTES = 2 * 1024 * 1024;
|
|
44945
|
+
var RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
44946
|
+
function json3(body2, status2 = 200) {
|
|
44947
|
+
return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
|
|
44948
|
+
}
|
|
44949
|
+
function status2(error) {
|
|
44950
|
+
switch (error.code) {
|
|
44951
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
|
|
44952
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
|
|
44953
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
|
|
44954
|
+
return 400;
|
|
44955
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
|
|
44956
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
|
|
44957
|
+
return 404;
|
|
44958
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
|
|
44959
|
+
return 503;
|
|
44960
|
+
default:
|
|
44961
|
+
return 409;
|
|
44962
|
+
}
|
|
44963
|
+
}
|
|
44964
|
+
async function boundedText2(message, limit) {
|
|
44965
|
+
if (!message.body)
|
|
44966
|
+
return "";
|
|
44967
|
+
const reader = message.body.getReader();
|
|
44968
|
+
const decoder = new TextDecoder;
|
|
44969
|
+
let bytes = 0;
|
|
44970
|
+
let text = "";
|
|
44971
|
+
try {
|
|
44972
|
+
while (true) {
|
|
44973
|
+
const { done, value } = await reader.read();
|
|
44974
|
+
if (done)
|
|
44975
|
+
break;
|
|
44976
|
+
bytes += value.byteLength;
|
|
44977
|
+
if (bytes > limit) {
|
|
44978
|
+
await reader.cancel();
|
|
44979
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
|
|
44980
|
+
}
|
|
44981
|
+
text += decoder.decode(value, { stream: true });
|
|
44982
|
+
}
|
|
44983
|
+
return text + decoder.decode();
|
|
44984
|
+
} finally {
|
|
44985
|
+
reader.releaseLock();
|
|
44986
|
+
}
|
|
44987
|
+
}
|
|
44988
|
+
async function readBody(request) {
|
|
44989
|
+
const declaredLength = Number(request.headers.get("content-length") ?? 0);
|
|
44990
|
+
if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
|
|
44991
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
|
|
44992
|
+
}
|
|
44993
|
+
const text = await boundedText2(request, REQUEST_BYTES);
|
|
44994
|
+
try {
|
|
44995
|
+
return JSON.parse(text);
|
|
44996
|
+
} catch {
|
|
44997
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
|
|
44998
|
+
}
|
|
44999
|
+
}
|
|
45000
|
+
async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
|
|
45001
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
|
|
45002
|
+
return null;
|
|
45003
|
+
const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
45004
|
+
try {
|
|
45005
|
+
if ((action === "" || action === "capability") && request.method === "GET") {
|
|
45006
|
+
return json3({ capability: await authority.capability() });
|
|
45007
|
+
}
|
|
45008
|
+
if (request.method !== "POST")
|
|
45009
|
+
return json3({ error: "method not allowed" }, 405);
|
|
45010
|
+
if (action === "inspect")
|
|
45011
|
+
return json3({ inspection: await authority.inspect(await readBody(request)) });
|
|
45012
|
+
if (action === "apply")
|
|
45013
|
+
return json3({ result: await authority.apply(await readBody(request)) }, 201);
|
|
45014
|
+
if (action === "rollback")
|
|
45015
|
+
return json3({ result: await authority.rollback(await readBody(request)) }, 201);
|
|
45016
|
+
if (action === "read-exact") {
|
|
45017
|
+
const input = await readBody(request);
|
|
45018
|
+
if (!input || typeof input.receipt_id !== "string") {
|
|
45019
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
|
|
45020
|
+
}
|
|
45021
|
+
return json3({ result: await authority.readExact(input.receipt_id) });
|
|
45022
|
+
}
|
|
45023
|
+
return json3({ error: "unknown task-subtree-transfer route" }, 404);
|
|
45024
|
+
} catch (cause) {
|
|
45025
|
+
if (cause instanceof TodosTaskSubtreeTransferError) {
|
|
45026
|
+
return json3({
|
|
45027
|
+
error: cause.message,
|
|
45028
|
+
code: cause.code,
|
|
45029
|
+
details: cause.details,
|
|
45030
|
+
authoritative: true
|
|
45031
|
+
}, status2(cause));
|
|
45032
|
+
}
|
|
45033
|
+
return json3({ error: "internal task-subtree-transfer error" }, 500);
|
|
45034
|
+
}
|
|
45035
|
+
}
|
|
45036
|
+
|
|
45037
|
+
class TodosTaskSubtreeTransferHttpClient {
|
|
45038
|
+
baseUrl;
|
|
45039
|
+
fetchImpl;
|
|
45040
|
+
headers;
|
|
45041
|
+
constructor(options) {
|
|
45042
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
45043
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
45044
|
+
this.headers = {
|
|
45045
|
+
...options.headers,
|
|
45046
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
45047
|
+
"Content-Type": "application/json"
|
|
45048
|
+
};
|
|
45049
|
+
}
|
|
45050
|
+
async request(action, init = {}) {
|
|
45051
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/task-subtree-transfer${action}`, {
|
|
45052
|
+
...init,
|
|
45053
|
+
headers: { ...this.headers, ...init.headers ?? {} }
|
|
45054
|
+
});
|
|
45055
|
+
const text = await boundedText2(response, RESPONSE_BYTES);
|
|
45056
|
+
let payload;
|
|
45057
|
+
try {
|
|
45058
|
+
payload = JSON.parse(text);
|
|
45059
|
+
} catch {
|
|
45060
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", `Task-subtree-transfer HTTP ${response.status} returned invalid JSON`);
|
|
45061
|
+
}
|
|
45062
|
+
if (!response.ok) {
|
|
45063
|
+
throw new TodosTaskSubtreeTransferError(typeof payload.code === "string" ? payload.code : "TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", typeof payload.error === "string" ? payload.error : `Task-subtree-transfer HTTP ${response.status}`, payload.details && typeof payload.details === "object" ? payload.details : {});
|
|
45064
|
+
}
|
|
45065
|
+
return payload;
|
|
45066
|
+
}
|
|
45067
|
+
async capability() {
|
|
45068
|
+
return (await this.request("/capability")).capability;
|
|
45069
|
+
}
|
|
45070
|
+
async inspect(input) {
|
|
45071
|
+
return (await this.request("/inspect", { method: "POST", body: JSON.stringify(input) })).inspection;
|
|
45072
|
+
}
|
|
45073
|
+
async apply(input) {
|
|
45074
|
+
return (await this.request("/apply", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45075
|
+
}
|
|
45076
|
+
async readExact(receiptId3) {
|
|
45077
|
+
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId3 }) })).result;
|
|
45078
|
+
}
|
|
45079
|
+
async rollback(input) {
|
|
45080
|
+
return (await this.request("/rollback", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45081
|
+
}
|
|
45082
|
+
}
|
|
45083
|
+
function createTodosTaskSubtreeTransferHttpClient(options) {
|
|
45084
|
+
return new TodosTaskSubtreeTransferHttpClient(options);
|
|
45085
|
+
}
|
|
43627
45086
|
// src/ai-tools.ts
|
|
43628
45087
|
import { createHash as createHash16 } from "crypto";
|
|
43629
45088
|
// src/cli/cloud-router.ts
|
|
@@ -43635,26 +45094,26 @@ var retryCapabilityCache = new Map;
|
|
|
43635
45094
|
var taskCreatorCapabilityCache = new Map;
|
|
43636
45095
|
var gitRefCapabilityCache = new Map;
|
|
43637
45096
|
var remoteCommandCapabilityCache = new Map;
|
|
43638
|
-
var
|
|
43639
|
-
|
|
43640
|
-
|
|
43641
|
-
|
|
43642
|
-
|
|
43643
|
-
|
|
43644
|
-
|
|
43645
|
-
|
|
43646
|
-
|
|
43647
|
-
|
|
43648
|
-
|
|
43649
|
-
|
|
45097
|
+
var todosLocalFallbackNoticeEmitted = false;
|
|
45098
|
+
function emitTodosLocalFallbackNotice(env) {
|
|
45099
|
+
if (todosLocalFallbackNoticeEmitted)
|
|
45100
|
+
return;
|
|
45101
|
+
todosLocalFallbackNoticeEmitted = true;
|
|
45102
|
+
const notice = {
|
|
45103
|
+
event: "todos-local-fallback",
|
|
45104
|
+
transport: "sqlite",
|
|
45105
|
+
source: "default",
|
|
45106
|
+
apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
45107
|
+
apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
45108
|
+
notice: "No hosted API config (HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY) is present; " + "using local SQLite. Hosted todos are NOT visible in this output."
|
|
45109
|
+
};
|
|
45110
|
+
console.error(JSON.stringify(notice));
|
|
43650
45111
|
}
|
|
43651
|
-
function
|
|
43652
|
-
assertNoLegacyStorageMode(env);
|
|
45112
|
+
function resolveTodosCliTransport(env = process.env) {
|
|
43653
45113
|
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
43654
45114
|
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
43655
45115
|
if (urlValue && keyValue) {
|
|
43656
45116
|
return {
|
|
43657
|
-
mode: "http",
|
|
43658
45117
|
transport: "http",
|
|
43659
45118
|
selected: true,
|
|
43660
45119
|
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
@@ -43666,15 +45125,15 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
43666
45125
|
if (keyValue) {
|
|
43667
45126
|
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
43668
45127
|
}
|
|
45128
|
+
emitTodosLocalFallbackNotice(env);
|
|
43669
45129
|
return {
|
|
43670
|
-
mode: "sqlite",
|
|
43671
45130
|
transport: "sqlite",
|
|
43672
45131
|
selected: false,
|
|
43673
45132
|
source: "default"
|
|
43674
45133
|
};
|
|
43675
45134
|
}
|
|
43676
45135
|
function requestedTransport(env) {
|
|
43677
|
-
return
|
|
45136
|
+
return resolveTodosCliTransport(env).transport;
|
|
43678
45137
|
}
|
|
43679
45138
|
function normalizeRemoteAuthorityUrl(value) {
|
|
43680
45139
|
const trimmed = value?.trim();
|
|
@@ -43708,13 +45167,13 @@ function normalizeRemoteAuthorityUrl(value) {
|
|
|
43708
45167
|
function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
43709
45168
|
let resolution;
|
|
43710
45169
|
try {
|
|
43711
|
-
resolution =
|
|
45170
|
+
resolution = resolveTodosCliTransport(env);
|
|
43712
45171
|
} catch (error) {
|
|
43713
45172
|
const issue = error instanceof Error ? error.message : String(error);
|
|
43714
45173
|
return {
|
|
43715
45174
|
selected: true,
|
|
43716
45175
|
ok: false,
|
|
43717
|
-
|
|
45176
|
+
transport: "invalid",
|
|
43718
45177
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
43719
45178
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
43720
45179
|
v1_base_url: null,
|
|
@@ -43722,12 +45181,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43722
45181
|
local_fallback: false
|
|
43723
45182
|
};
|
|
43724
45183
|
}
|
|
43725
|
-
const {
|
|
45184
|
+
const { transport, selected } = resolution;
|
|
43726
45185
|
if (!selected) {
|
|
43727
45186
|
return {
|
|
43728
45187
|
selected: false,
|
|
43729
45188
|
ok: true,
|
|
43730
|
-
|
|
45189
|
+
transport,
|
|
43731
45190
|
api_url_configured: false,
|
|
43732
45191
|
api_key_configured: false,
|
|
43733
45192
|
v1_base_url: null,
|
|
@@ -43752,7 +45211,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43752
45211
|
return {
|
|
43753
45212
|
selected: true,
|
|
43754
45213
|
ok: issues.length === 0,
|
|
43755
|
-
|
|
45214
|
+
transport,
|
|
43756
45215
|
api_url_configured: apiUrl !== null,
|
|
43757
45216
|
api_key_configured: apiKeyConfigured,
|
|
43758
45217
|
v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
|
|
@@ -43761,24 +45220,24 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43761
45220
|
};
|
|
43762
45221
|
}
|
|
43763
45222
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
43764
|
-
const
|
|
43765
|
-
if (!
|
|
43766
|
-
throw new Error(
|
|
45223
|
+
const status3 = getTodosRemoteAuthorityConfigStatus(env);
|
|
45224
|
+
if (!status3.ok)
|
|
45225
|
+
throw new Error(status3.issues[0]);
|
|
43767
45226
|
return {
|
|
43768
45227
|
...env,
|
|
43769
|
-
HASNA_TODOS_API_URL:
|
|
45228
|
+
HASNA_TODOS_API_URL: status3.v1_base_url.replace(/\/v1$/, ""),
|
|
43770
45229
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
43771
45230
|
};
|
|
43772
45231
|
}
|
|
43773
45232
|
function classifyRemoteRequestError(baseUrl, route, error) {
|
|
43774
|
-
const
|
|
43775
|
-
if (
|
|
45233
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45234
|
+
if (status3 === 401) {
|
|
43776
45235
|
throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43777
45236
|
}
|
|
43778
|
-
if (
|
|
45237
|
+
if (status3 === 403) {
|
|
43779
45238
|
throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
|
|
43780
45239
|
}
|
|
43781
|
-
if (
|
|
45240
|
+
if (status3 === 409) {
|
|
43782
45241
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43783
45242
|
if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
|
|
43784
45243
|
const remoteError = body2;
|
|
@@ -43788,17 +45247,18 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
43788
45247
|
}
|
|
43789
45248
|
throw error;
|
|
43790
45249
|
}
|
|
43791
|
-
if (typeof
|
|
45250
|
+
if (typeof status3 === "number" && status3 >= 300 && status3 < 400) {
|
|
43792
45251
|
throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
|
|
43793
45252
|
}
|
|
43794
|
-
if (typeof
|
|
43795
|
-
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${
|
|
45253
|
+
if (typeof status3 === "number" && status3 >= 500) {
|
|
45254
|
+
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${status3} for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43796
45255
|
}
|
|
43797
45256
|
const message = error instanceof Error ? error.message : String(error);
|
|
43798
45257
|
if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
|
|
43799
|
-
|
|
45258
|
+
const unboundedNote = route === "/tasks" ? "; this is likely an UNBOUNDED task read timing out (bounded task reads from this authority return in ~1 s) \u2014 retry with a bounded/count-specific shape such as todos list --limit; local SQLite fallback is disabled" : "; local SQLite fallback is disabled";
|
|
45259
|
+
throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
|
|
43800
45260
|
}
|
|
43801
|
-
if (
|
|
45261
|
+
if (status3 === undefined) {
|
|
43802
45262
|
throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43803
45263
|
}
|
|
43804
45264
|
throw error;
|
|
@@ -43866,8 +45326,8 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
43866
45326
|
try {
|
|
43867
45327
|
return await request();
|
|
43868
45328
|
} catch (error) {
|
|
43869
|
-
const
|
|
43870
|
-
if (
|
|
45329
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45330
|
+
if (status3 === 404 || status3 === 405) {
|
|
43871
45331
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43872
45332
|
const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
|
|
43873
45333
|
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
@@ -43884,11 +45344,10 @@ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUES
|
|
|
43884
45344
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
43885
45345
|
timeoutMs: requestTimeoutMs
|
|
43886
45346
|
});
|
|
43887
|
-
if (resolved.transport === "cloud-http")
|
|
43888
|
-
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
43889
45347
|
const transportName = resolved.transport;
|
|
43890
|
-
if (transportName === "http")
|
|
45348
|
+
if (transportName === "cloud-http" || transportName === "http") {
|
|
43891
45349
|
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
45350
|
+
}
|
|
43892
45351
|
return null;
|
|
43893
45352
|
}
|
|
43894
45353
|
function unwrapTask(raw) {
|
|
@@ -44039,9 +45498,9 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
44039
45498
|
const { status: _status, limit, offset, ...baseFilter } = filter;
|
|
44040
45499
|
const start = offset ?? 0;
|
|
44041
45500
|
const windowEnd = typeof limit === "number" ? start + limit : undefined;
|
|
44042
|
-
const pages = await Promise.all(statuses.map((
|
|
45501
|
+
const pages = await Promise.all(statuses.map((status3) => requestCloudTaskPage(client, {
|
|
44043
45502
|
...baseFilter,
|
|
44044
|
-
status:
|
|
45503
|
+
status: status3,
|
|
44045
45504
|
...windowEnd === undefined ? {} : { limit: windowEnd }
|
|
44046
45505
|
})));
|
|
44047
45506
|
const seen = new Set;
|
|
@@ -44319,11 +45778,11 @@ init_audit();
|
|
|
44319
45778
|
init_database();
|
|
44320
45779
|
function upsertCheckpoint(task_id, step, updates, db) {
|
|
44321
45780
|
const d = db || getDatabase();
|
|
44322
|
-
const
|
|
45781
|
+
const timestamp5 = now();
|
|
44323
45782
|
const existing = d.query("SELECT id FROM task_checkpoints WHERE task_id = ? AND step = ?").get(task_id, step);
|
|
44324
45783
|
const id = existing?.id ?? uuid();
|
|
44325
45784
|
const agentId = updates.agent_id ?? null;
|
|
44326
|
-
const
|
|
45785
|
+
const status3 = updates.status ?? "pending";
|
|
44327
45786
|
const data = updates.data ? JSON.stringify(updates.data) : JSON.stringify({});
|
|
44328
45787
|
const error = updates.error ?? null;
|
|
44329
45788
|
const attempt = updates.attempt ?? 1;
|
|
@@ -44337,22 +45796,22 @@ function upsertCheckpoint(task_id, step, updates, db) {
|
|
|
44337
45796
|
task_id,
|
|
44338
45797
|
agentId,
|
|
44339
45798
|
step,
|
|
44340
|
-
|
|
45799
|
+
status3,
|
|
44341
45800
|
data,
|
|
44342
45801
|
error,
|
|
44343
45802
|
attempt,
|
|
44344
45803
|
maxAttempts,
|
|
44345
45804
|
startedAt,
|
|
44346
45805
|
completedAt2,
|
|
44347
|
-
|
|
44348
|
-
|
|
45806
|
+
timestamp5,
|
|
45807
|
+
status3,
|
|
44349
45808
|
data,
|
|
44350
45809
|
error,
|
|
44351
45810
|
attempt,
|
|
44352
45811
|
maxAttempts,
|
|
44353
45812
|
startedAt,
|
|
44354
45813
|
completedAt2,
|
|
44355
|
-
|
|
45814
|
+
timestamp5
|
|
44356
45815
|
]);
|
|
44357
45816
|
return rowToCheckpoint(d.query("SELECT * FROM task_checkpoints WHERE id = ?").get(id));
|
|
44358
45817
|
}
|
|
@@ -44415,10 +45874,10 @@ function stepForGate(gate) {
|
|
|
44415
45874
|
throw new Error("Approval gate name is required");
|
|
44416
45875
|
return `approval:${trimmed}`;
|
|
44417
45876
|
}
|
|
44418
|
-
function approvalStatusToCheckpointStatus(
|
|
44419
|
-
if (
|
|
45877
|
+
function approvalStatusToCheckpointStatus(status3) {
|
|
45878
|
+
if (status3 === "approved")
|
|
44420
45879
|
return "completed";
|
|
44421
|
-
if (
|
|
45880
|
+
if (status3 === "pending")
|
|
44422
45881
|
return "pending";
|
|
44423
45882
|
return "failed";
|
|
44424
45883
|
}
|
|
@@ -44440,12 +45899,12 @@ function isExpired(expiresAt, at = new Date) {
|
|
|
44440
45899
|
return Boolean(expiresAt && new Date(expiresAt).getTime() <= at.getTime());
|
|
44441
45900
|
}
|
|
44442
45901
|
function gateFromCheckpoint(checkpoint) {
|
|
44443
|
-
const
|
|
45902
|
+
const status3 = checkpointStatusToApprovalStatus(checkpoint);
|
|
44444
45903
|
return {
|
|
44445
45904
|
id: checkpoint.id,
|
|
44446
45905
|
task_id: checkpoint.task_id,
|
|
44447
45906
|
gate: String(checkpoint.data["approval_gate_name"] || checkpoint.step.replace(/^approval:/, "")),
|
|
44448
|
-
status:
|
|
45907
|
+
status: status3,
|
|
44449
45908
|
reviewer: typeof checkpoint.data["reviewer"] === "string" ? checkpoint.data["reviewer"] : null,
|
|
44450
45909
|
requester: typeof checkpoint.data["requester"] === "string" ? checkpoint.data["requester"] : null,
|
|
44451
45910
|
reason: typeof checkpoint.data["reason"] === "string" ? checkpoint.data["reason"] : null,
|
|
@@ -44490,13 +45949,13 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
|
|
|
44490
45949
|
});
|
|
44491
45950
|
}
|
|
44492
45951
|
}
|
|
44493
|
-
function writeGate(input,
|
|
45952
|
+
function writeGate(input, status3, decision, db) {
|
|
44494
45953
|
const d = db || getDatabase();
|
|
44495
45954
|
ensureTask(input.task_id, d);
|
|
44496
45955
|
const step = stepForGate(input.gate);
|
|
44497
45956
|
const existing = getCheckpoint(input.task_id, step, d);
|
|
44498
45957
|
const existingData = existing?.data || {};
|
|
44499
|
-
const
|
|
45958
|
+
const timestamp5 = decision?.decided_at || now();
|
|
44500
45959
|
const existingRunId = typeof existingData["run_id"] === "string" ? existingData["run_id"] : undefined;
|
|
44501
45960
|
const runId = input.run_id ? resolveTaskRunId(input.run_id, d) : existingRunId;
|
|
44502
45961
|
const data = {
|
|
@@ -44504,7 +45963,7 @@ function writeGate(input, status2, decision, db) {
|
|
|
44504
45963
|
...input.metadata || {},
|
|
44505
45964
|
approval_gate: true,
|
|
44506
45965
|
approval_gate_name: input.gate.trim(),
|
|
44507
|
-
approval_status:
|
|
45966
|
+
approval_status: status3,
|
|
44508
45967
|
requester: input.requester ?? existingData["requester"] ?? null,
|
|
44509
45968
|
reviewer: input.reviewer ?? decision?.decided_by ?? existingData["reviewer"] ?? null,
|
|
44510
45969
|
reason: decision?.reason ?? input.reason ?? existingData["reason"] ?? null,
|
|
@@ -44517,11 +45976,11 @@ function writeGate(input, status2, decision, db) {
|
|
|
44517
45976
|
};
|
|
44518
45977
|
const checkpoint = upsertCheckpoint(input.task_id, step, {
|
|
44519
45978
|
agent_id: decision?.decided_by || input.requester || input.reviewer,
|
|
44520
|
-
status: approvalStatusToCheckpointStatus(
|
|
45979
|
+
status: approvalStatusToCheckpointStatus(status3),
|
|
44521
45980
|
data,
|
|
44522
|
-
error:
|
|
44523
|
-
started_at: existing?.started_at ||
|
|
44524
|
-
completed_at:
|
|
45981
|
+
error: status3 === "rejected" || status3 === "expired" ? String(data.reason || status3) : null,
|
|
45982
|
+
started_at: existing?.started_at || timestamp5,
|
|
45983
|
+
completed_at: status3 === "pending" ? null : timestamp5
|
|
44525
45984
|
}, d);
|
|
44526
45985
|
return gateFromCheckpoint(checkpoint);
|
|
44527
45986
|
}
|
|
@@ -45369,7 +46828,7 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45369
46828
|
const planId = boundedOptionalString(record, "plan_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45370
46829
|
const taskListId = boundedOptionalString(record, "task_list_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45371
46830
|
const assignedTo = boundedOptionalString(record, "assigned_to", TODOS_AI_READ_TOOL_LIMITS.max_filter_string_bytes);
|
|
45372
|
-
const
|
|
46831
|
+
const status3 = boundedEnum(record, "status", TASK_STATUSES);
|
|
45373
46832
|
const priority = boundedEnum(record, "priority", TASK_PRIORITIES);
|
|
45374
46833
|
const tags = boundedTags(record);
|
|
45375
46834
|
if (projectId !== undefined)
|
|
@@ -45380,8 +46839,8 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45380
46839
|
filter.task_list_id = taskListId;
|
|
45381
46840
|
if (assignedTo !== undefined)
|
|
45382
46841
|
filter.assigned_to = assignedTo;
|
|
45383
|
-
if (
|
|
45384
|
-
filter.status =
|
|
46842
|
+
if (status3 !== undefined)
|
|
46843
|
+
filter.status = status3;
|
|
45385
46844
|
if (priority !== undefined)
|
|
45386
46845
|
filter.priority = priority;
|
|
45387
46846
|
if (tags !== undefined)
|
|
@@ -45723,29 +47182,29 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
45723
47182
|
};
|
|
45724
47183
|
}
|
|
45725
47184
|
function getNativeStorageSyncPlan(env = process.env, options = {}) {
|
|
45726
|
-
const
|
|
47185
|
+
const status3 = getNativeStorageStatus(env);
|
|
45727
47186
|
const steps = [
|
|
45728
47187
|
"Read local SQLite snapshot",
|
|
45729
|
-
|
|
45730
|
-
|
|
47188
|
+
status3.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
|
|
47189
|
+
status3.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
|
|
45731
47190
|
"Report planned changes without opening network connections"
|
|
45732
47191
|
];
|
|
45733
47192
|
return {
|
|
45734
|
-
ok:
|
|
47193
|
+
ok: status3.ok,
|
|
45735
47194
|
service: "todos",
|
|
45736
47195
|
dry_run: true,
|
|
45737
47196
|
no_network: true,
|
|
45738
|
-
status:
|
|
47197
|
+
status: status3,
|
|
45739
47198
|
postgres: {
|
|
45740
|
-
required:
|
|
45741
|
-
configured:
|
|
47199
|
+
required: status3.remote_enabled,
|
|
47200
|
+
configured: status3.database.configured,
|
|
45742
47201
|
schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
|
|
45743
47202
|
},
|
|
45744
47203
|
object_storage: {
|
|
45745
|
-
required:
|
|
45746
|
-
configured:
|
|
45747
|
-
bucket:
|
|
45748
|
-
prefix:
|
|
47204
|
+
required: status3.remote_enabled,
|
|
47205
|
+
configured: status3.object_storage.configured,
|
|
47206
|
+
bucket: status3.object_storage.bucket,
|
|
47207
|
+
prefix: status3.object_storage.prefix
|
|
45749
47208
|
},
|
|
45750
47209
|
steps
|
|
45751
47210
|
};
|
|
@@ -45851,8 +47310,8 @@ function resolveTaskList(task3, project, db) {
|
|
|
45851
47310
|
}
|
|
45852
47311
|
return null;
|
|
45853
47312
|
}
|
|
45854
|
-
function isTerminal2(
|
|
45855
|
-
return
|
|
47313
|
+
function isTerminal2(status3) {
|
|
47314
|
+
return status3 === "completed" || status3 === "cancelled" || status3 === "failed";
|
|
45856
47315
|
}
|
|
45857
47316
|
function routeConcurrencyKey(task3, project, taskList, projectPath) {
|
|
45858
47317
|
if (project?.id)
|
|
@@ -46033,7 +47492,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
46033
47492
|
latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
|
|
46034
47493
|
workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
|
|
46035
47494
|
});
|
|
46036
|
-
const
|
|
47495
|
+
const timestamp5 = now();
|
|
46037
47496
|
const {
|
|
46038
47497
|
current_workflow_invocation_id,
|
|
46039
47498
|
current_run_id,
|
|
@@ -46049,7 +47508,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
46049
47508
|
workflow_invocation: {
|
|
46050
47509
|
schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
|
|
46051
47510
|
...next,
|
|
46052
|
-
updated_at:
|
|
47511
|
+
updated_at: timestamp5,
|
|
46053
47512
|
updated_by: input.actor ?? null
|
|
46054
47513
|
}
|
|
46055
47514
|
};
|
|
@@ -46083,8 +47542,8 @@ function normalizePath3(input) {
|
|
|
46083
47542
|
return resolve10(input);
|
|
46084
47543
|
}
|
|
46085
47544
|
function sourceStoreId(sourceDbPath) {
|
|
46086
|
-
const
|
|
46087
|
-
return `sqlite:${
|
|
47545
|
+
const digest3 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
47546
|
+
return `sqlite:${digest3}`;
|
|
46088
47547
|
}
|
|
46089
47548
|
function inferSourceRepoPath(sourceDbPath) {
|
|
46090
47549
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46855,7 +48314,7 @@ function rowToSnapshot(row) {
|
|
|
46855
48314
|
function saveSnapshot(input, db) {
|
|
46856
48315
|
const d = db || getDatabase();
|
|
46857
48316
|
const id = uuid();
|
|
46858
|
-
const
|
|
48317
|
+
const timestamp5 = now();
|
|
46859
48318
|
d.run(`INSERT INTO context_snapshots (id, agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps, metadata, created_at)
|
|
46860
48319
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
46861
48320
|
id,
|
|
@@ -46869,7 +48328,7 @@ function saveSnapshot(input, db) {
|
|
|
46869
48328
|
JSON.stringify(input.blockers || []),
|
|
46870
48329
|
input.next_steps || null,
|
|
46871
48330
|
JSON.stringify(input.metadata || {}),
|
|
46872
|
-
|
|
48331
|
+
timestamp5
|
|
46873
48332
|
]);
|
|
46874
48333
|
return {
|
|
46875
48334
|
id,
|
|
@@ -46883,7 +48342,7 @@ function saveSnapshot(input, db) {
|
|
|
46883
48342
|
blockers: input.blockers || [],
|
|
46884
48343
|
next_steps: input.next_steps || null,
|
|
46885
48344
|
metadata: input.metadata || {},
|
|
46886
|
-
created_at:
|
|
48345
|
+
created_at: timestamp5
|
|
46887
48346
|
};
|
|
46888
48347
|
}
|
|
46889
48348
|
function getLatestSnapshot(agentId, taskId, db) {
|
|
@@ -46999,7 +48458,7 @@ function createKnowledgeRecord(input, db) {
|
|
|
46999
48458
|
if (!title)
|
|
47000
48459
|
throw new Error("Knowledge record title is required");
|
|
47001
48460
|
const d = db || getDatabase();
|
|
47002
|
-
const
|
|
48461
|
+
const timestamp5 = now();
|
|
47003
48462
|
const id = uuid();
|
|
47004
48463
|
d.run(`INSERT INTO project_knowledge_records (
|
|
47005
48464
|
id, record_type, title, content, decision, rationale, alternatives,
|
|
@@ -47019,8 +48478,8 @@ function createKnowledgeRecord(input, db) {
|
|
|
47019
48478
|
input.snapshot_id || null,
|
|
47020
48479
|
JSON.stringify(normalizeTags(input.tags)),
|
|
47021
48480
|
JSON.stringify(input.metadata || {}),
|
|
47022
|
-
|
|
47023
|
-
|
|
48481
|
+
timestamp5,
|
|
48482
|
+
timestamp5
|
|
47024
48483
|
]);
|
|
47025
48484
|
return getKnowledgeRecord(id, d);
|
|
47026
48485
|
}
|
|
@@ -47211,9 +48670,9 @@ function normalizeText4(value) {
|
|
|
47211
48670
|
const trimmed = value.trim();
|
|
47212
48671
|
return trimmed.length > 0 ? trimmed : null;
|
|
47213
48672
|
}
|
|
47214
|
-
function assertStatus(
|
|
47215
|
-
if (!VALID_STATUSES.has(
|
|
47216
|
-
throw new Error(`Invalid risk status: ${
|
|
48673
|
+
function assertStatus(status3) {
|
|
48674
|
+
if (!VALID_STATUSES.has(status3))
|
|
48675
|
+
throw new Error(`Invalid risk status: ${status3}`);
|
|
47217
48676
|
}
|
|
47218
48677
|
function assertSeverity(severity) {
|
|
47219
48678
|
if (!VALID_SEVERITIES2.has(severity))
|
|
@@ -47258,16 +48717,16 @@ function createRisk(input, db) {
|
|
|
47258
48717
|
const title = input.title.trim();
|
|
47259
48718
|
if (!title)
|
|
47260
48719
|
throw new Error("Risk title is required");
|
|
47261
|
-
const
|
|
48720
|
+
const status3 = input.status || "open";
|
|
47262
48721
|
const severity = input.severity || "medium";
|
|
47263
48722
|
const probability = input.probability || "medium";
|
|
47264
|
-
assertStatus(
|
|
48723
|
+
assertStatus(status3);
|
|
47265
48724
|
assertSeverity(severity);
|
|
47266
48725
|
assertProbability(probability);
|
|
47267
48726
|
const d = db || getDatabase();
|
|
47268
|
-
const
|
|
48727
|
+
const timestamp5 = now();
|
|
47269
48728
|
const id = uuid();
|
|
47270
|
-
const closedAt =
|
|
48729
|
+
const closedAt = status3 === "resolved" || status3 === "accepted" ? timestamp5 : null;
|
|
47271
48730
|
d.run(`INSERT INTO project_risks (
|
|
47272
48731
|
id, title, description, status, severity, probability, owner, mitigation, due_at,
|
|
47273
48732
|
project_id, plan_id, task_id, tags, metadata, created_at, updated_at, closed_at
|
|
@@ -47275,7 +48734,7 @@ function createRisk(input, db) {
|
|
|
47275
48734
|
id,
|
|
47276
48735
|
title,
|
|
47277
48736
|
normalizeText4(input.description),
|
|
47278
|
-
|
|
48737
|
+
status3,
|
|
47279
48738
|
severity,
|
|
47280
48739
|
probability,
|
|
47281
48740
|
normalizeText4(input.owner),
|
|
@@ -47286,8 +48745,8 @@ function createRisk(input, db) {
|
|
|
47286
48745
|
resolveKnownId2("tasks", input.task_id, d),
|
|
47287
48746
|
JSON.stringify(normalizeTags2(input.tags)),
|
|
47288
48747
|
JSON.stringify(input.metadata || {}),
|
|
47289
|
-
|
|
47290
|
-
|
|
48748
|
+
timestamp5,
|
|
48749
|
+
timestamp5,
|
|
47291
48750
|
closedAt
|
|
47292
48751
|
]);
|
|
47293
48752
|
return getRisk(id, d);
|
|
@@ -47370,8 +48829,8 @@ function updateRisk(id, input, db) {
|
|
|
47370
48829
|
d.run(`UPDATE project_risks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
47371
48830
|
return getRisk(resolved, d);
|
|
47372
48831
|
}
|
|
47373
|
-
function closeRisk(id,
|
|
47374
|
-
return updateRisk(id, { status:
|
|
48832
|
+
function closeRisk(id, status3 = "resolved", db) {
|
|
48833
|
+
return updateRisk(id, { status: status3 }, db);
|
|
47375
48834
|
}
|
|
47376
48835
|
function buildRiskFilters(options, db) {
|
|
47377
48836
|
const conditions = [];
|
|
@@ -47499,7 +48958,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47499
48958
|
};
|
|
47500
48959
|
const totalPenalty = Object.values(penalties).reduce((sum, penalty) => sum + penalty, 0);
|
|
47501
48960
|
const score = Math.max(0, 100 - Math.min(100, totalPenalty));
|
|
47502
|
-
const
|
|
48961
|
+
const status3 = score < 40 ? "critical" : score < 70 ? "at_risk" : score < 85 ? "watch" : "healthy";
|
|
47503
48962
|
const recommendations = [];
|
|
47504
48963
|
if (blocked.length > 0)
|
|
47505
48964
|
recommendations.push("Clear blocking dependencies before starting more work.");
|
|
@@ -47523,7 +48982,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47523
48982
|
scope,
|
|
47524
48983
|
scope_id: scopeInfo.id,
|
|
47525
48984
|
score,
|
|
47526
|
-
status:
|
|
48985
|
+
status: status3,
|
|
47527
48986
|
components: {
|
|
47528
48987
|
total_tasks: tasks.length,
|
|
47529
48988
|
blocked_tasks: blocked.length,
|
|
@@ -47770,7 +49229,7 @@ function createRetrospective(input, db) {
|
|
|
47770
49229
|
const d = db || getDatabase();
|
|
47771
49230
|
const report = buildReport(input, d);
|
|
47772
49231
|
const scope = scopeFromInput(input, d);
|
|
47773
|
-
const
|
|
49232
|
+
const timestamp5 = now();
|
|
47774
49233
|
if (input.create_followups) {
|
|
47775
49234
|
for (const followUp of report.follow_up_tasks) {
|
|
47776
49235
|
const task3 = createTask({
|
|
@@ -47786,7 +49245,7 @@ function createRetrospective(input, db) {
|
|
|
47786
49245
|
}
|
|
47787
49246
|
const id = uuid();
|
|
47788
49247
|
d.run(`INSERT INTO local_retrospectives (id, title, scope, project_id, plan_id, agent_id, report_json, created_at, updated_at)
|
|
47789
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report),
|
|
49248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report), timestamp5, timestamp5]);
|
|
47790
49249
|
return getRetrospective(id, d);
|
|
47791
49250
|
}
|
|
47792
49251
|
function getRetrospective(id, db) {
|
|
@@ -48075,7 +49534,7 @@ function createApiKey(input, db) {
|
|
|
48075
49534
|
if (!name)
|
|
48076
49535
|
throw new Error("API key name is required");
|
|
48077
49536
|
const key2 = generatePlaintextKey();
|
|
48078
|
-
const
|
|
49537
|
+
const timestamp5 = now();
|
|
48079
49538
|
const id = uuid();
|
|
48080
49539
|
const prefix = key2.slice(0, 12);
|
|
48081
49540
|
d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
|
|
@@ -48085,7 +49544,7 @@ function createApiKey(input, db) {
|
|
|
48085
49544
|
hashApiKey(key2),
|
|
48086
49545
|
prefix,
|
|
48087
49546
|
JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
|
|
48088
|
-
|
|
49547
|
+
timestamp5,
|
|
48089
49548
|
input.expires_at || null
|
|
48090
49549
|
]);
|
|
48091
49550
|
const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
|
|
@@ -48139,15 +49598,15 @@ function rowToSession(row) {
|
|
|
48139
49598
|
function createSession(input, db) {
|
|
48140
49599
|
const d = db || getDatabase();
|
|
48141
49600
|
const id = uuid();
|
|
48142
|
-
const
|
|
49601
|
+
const timestamp5 = now();
|
|
48143
49602
|
d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
|
|
48144
49603
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
48145
49604
|
id,
|
|
48146
49605
|
input.agent_id || null,
|
|
48147
49606
|
input.project_id || null,
|
|
48148
49607
|
input.working_dir || null,
|
|
48149
|
-
|
|
48150
|
-
|
|
49608
|
+
timestamp5,
|
|
49609
|
+
timestamp5,
|
|
48151
49610
|
JSON.stringify(input.metadata || {})
|
|
48152
49611
|
]);
|
|
48153
49612
|
return getSession(id, d);
|
|
@@ -48320,7 +49779,7 @@ function rowToArtifact2(row) {
|
|
|
48320
49779
|
function addArtifact(input, db, dbPath) {
|
|
48321
49780
|
const d = db || getDatabase();
|
|
48322
49781
|
const id = uuid();
|
|
48323
|
-
const
|
|
49782
|
+
const timestamp5 = now();
|
|
48324
49783
|
const storageMode = input.storage_mode ?? "copy";
|
|
48325
49784
|
const stored = storeArtifactFile({
|
|
48326
49785
|
artifactId: id,
|
|
@@ -48345,8 +49804,8 @@ function addArtifact(input, db, dbPath) {
|
|
|
48345
49804
|
stored.sizeBytes,
|
|
48346
49805
|
input.redaction_status ?? "none",
|
|
48347
49806
|
JSON.stringify(input.metadata ?? {}),
|
|
48348
|
-
|
|
48349
|
-
|
|
49807
|
+
timestamp5,
|
|
49808
|
+
timestamp5
|
|
48350
49809
|
]);
|
|
48351
49810
|
return getArtifact(id, d);
|
|
48352
49811
|
}
|
|
@@ -48379,14 +49838,14 @@ function listArtifacts(filter = {}, db) {
|
|
|
48379
49838
|
}
|
|
48380
49839
|
function updateArtifactRedaction(id, redactionStatus, db) {
|
|
48381
49840
|
const d = db || getDatabase();
|
|
48382
|
-
const
|
|
48383
|
-
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus,
|
|
49841
|
+
const timestamp5 = now();
|
|
49842
|
+
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus, timestamp5, id]);
|
|
48384
49843
|
return getArtifact(id, d);
|
|
48385
49844
|
}
|
|
48386
49845
|
function softDeleteArtifact(id, db) {
|
|
48387
49846
|
const d = db || getDatabase();
|
|
48388
|
-
const
|
|
48389
|
-
const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [
|
|
49847
|
+
const timestamp5 = now();
|
|
49848
|
+
const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [timestamp5, timestamp5, id]);
|
|
48390
49849
|
return result.changes > 0;
|
|
48391
49850
|
}
|
|
48392
49851
|
function purgeArtifact(id, db, dbPath) {
|
|
@@ -48524,7 +49983,7 @@ init_redaction();
|
|
|
48524
49983
|
function createHandoff(input, db) {
|
|
48525
49984
|
const d = db || getDatabase();
|
|
48526
49985
|
const id = uuid();
|
|
48527
|
-
const
|
|
49986
|
+
const timestamp5 = now();
|
|
48528
49987
|
d.run(`INSERT INTO handoffs (id, agent_id, project_id, session_id, summary, completed, in_progress, blockers, next_steps, task_ids, relevant_files, run_ids, created_at)
|
|
48529
49988
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
48530
49989
|
id,
|
|
@@ -48539,7 +49998,7 @@ function createHandoff(input, db) {
|
|
|
48539
49998
|
toJson(input.task_ids),
|
|
48540
49999
|
toJson(input.relevant_files),
|
|
48541
50000
|
toJson(input.run_ids),
|
|
48542
|
-
|
|
50001
|
+
timestamp5
|
|
48543
50002
|
]);
|
|
48544
50003
|
return {
|
|
48545
50004
|
id,
|
|
@@ -48554,7 +50013,7 @@ function createHandoff(input, db) {
|
|
|
48554
50013
|
task_ids: input.task_ids || null,
|
|
48555
50014
|
relevant_files: input.relevant_files || null,
|
|
48556
50015
|
run_ids: input.run_ids || null,
|
|
48557
|
-
created_at:
|
|
50016
|
+
created_at: timestamp5,
|
|
48558
50017
|
acknowledged_by: []
|
|
48559
50018
|
};
|
|
48560
50019
|
}
|
|
@@ -49066,7 +50525,7 @@ function upsertVerificationProvider(input) {
|
|
|
49066
50525
|
const name = normalizeName(input.name);
|
|
49067
50526
|
const config = loadConfig();
|
|
49068
50527
|
const existing = config.verification_providers?.[name];
|
|
49069
|
-
const
|
|
50528
|
+
const timestamp5 = new Date().toISOString();
|
|
49070
50529
|
const provider = {
|
|
49071
50530
|
...existing,
|
|
49072
50531
|
name,
|
|
@@ -49077,8 +50536,8 @@ function upsertVerificationProvider(input) {
|
|
|
49077
50536
|
capabilities: input.capabilities ?? existing?.capabilities,
|
|
49078
50537
|
retry: input.retry ? retryConfig(input.retry) : existing?.retry,
|
|
49079
50538
|
timeout_ms: timeoutMs(input.timeout_ms ?? existing?.timeout_ms),
|
|
49080
|
-
created_at: existing?.created_at ||
|
|
49081
|
-
updated_at:
|
|
50539
|
+
created_at: existing?.created_at || timestamp5,
|
|
50540
|
+
updated_at: timestamp5
|
|
49082
50541
|
};
|
|
49083
50542
|
saveConfig({
|
|
49084
50543
|
...config,
|
|
@@ -49639,7 +51098,7 @@ function upsertPolicyPack(input) {
|
|
|
49639
51098
|
const existing = config.policy_packs?.[input.name];
|
|
49640
51099
|
const root = normalizePath4(input.root || existing?.root || process.cwd());
|
|
49641
51100
|
const base = existing || defaultPolicyPack(input.name, root);
|
|
49642
|
-
const
|
|
51101
|
+
const timestamp5 = new Date().toISOString();
|
|
49643
51102
|
const pack = {
|
|
49644
51103
|
...base,
|
|
49645
51104
|
name: input.name,
|
|
@@ -49657,8 +51116,8 @@ function upsertPolicyPack(input) {
|
|
|
49657
51116
|
require_artifact: input.require_artifact ?? base.require_artifact,
|
|
49658
51117
|
evidence_min_count: input.evidence_min_count ?? base.evidence_min_count,
|
|
49659
51118
|
branch_pattern: input.branch_pattern ?? base.branch_pattern,
|
|
49660
|
-
created_at: existing?.created_at ||
|
|
49661
|
-
updated_at:
|
|
51119
|
+
created_at: existing?.created_at || timestamp5,
|
|
51120
|
+
updated_at: timestamp5
|
|
49662
51121
|
};
|
|
49663
51122
|
saveConfig({
|
|
49664
51123
|
...config,
|
|
@@ -50000,7 +51459,7 @@ function summarizeOutput(stdout, stderr) {
|
|
|
50000
51459
|
function upsertAgentRunAdapter(input) {
|
|
50001
51460
|
const config = loadConfig();
|
|
50002
51461
|
const existing = config.agent_run_adapters?.[input.name];
|
|
50003
|
-
const
|
|
51462
|
+
const timestamp5 = new Date().toISOString();
|
|
50004
51463
|
const adapter = {
|
|
50005
51464
|
...existing,
|
|
50006
51465
|
name: input.name,
|
|
@@ -50008,8 +51467,8 @@ function upsertAgentRunAdapter(input) {
|
|
|
50008
51467
|
sandbox: input.sandbox ?? existing?.sandbox,
|
|
50009
51468
|
cwd: input.cwd ?? existing?.cwd,
|
|
50010
51469
|
env: input.env ?? existing?.env,
|
|
50011
|
-
created_at: existing?.created_at ||
|
|
50012
|
-
updated_at:
|
|
51470
|
+
created_at: existing?.created_at || timestamp5,
|
|
51471
|
+
updated_at: timestamp5
|
|
50013
51472
|
};
|
|
50014
51473
|
saveConfig({
|
|
50015
51474
|
...config,
|
|
@@ -50133,7 +51592,7 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
50133
51592
|
proc.exited
|
|
50134
51593
|
]);
|
|
50135
51594
|
const outputSummary = summarizeOutput(stdout, stderr);
|
|
50136
|
-
const
|
|
51595
|
+
const status3 = exitCode === 0 ? "completed" : "failed";
|
|
50137
51596
|
addTaskRunCommand({
|
|
50138
51597
|
run_id: next.run.id,
|
|
50139
51598
|
command,
|
|
@@ -50142,15 +51601,15 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
50142
51601
|
output_summary: outputSummary ?? undefined,
|
|
50143
51602
|
agent_id: next.run.agent_id ?? undefined
|
|
50144
51603
|
}, d);
|
|
50145
|
-
const completed = { ...running, state:
|
|
51604
|
+
const completed = { ...running, state: status3, completed_at: new Date().toISOString(), last_error: exitCode === 0 ? undefined : outputSummary || `exit ${exitCode}` };
|
|
50146
51605
|
updateDispatcherMetadata(next.run.id, completed, d);
|
|
50147
51606
|
finishTaskRun({
|
|
50148
51607
|
run_id: next.run.id,
|
|
50149
|
-
status:
|
|
50150
|
-
summary: outputSummary || `agent run ${
|
|
51608
|
+
status: status3,
|
|
51609
|
+
summary: outputSummary || `agent run ${status3}`,
|
|
50151
51610
|
agent_id: next.run.agent_id ?? undefined
|
|
50152
51611
|
}, d);
|
|
50153
|
-
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status:
|
|
51612
|
+
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status: status3, exit_code: exitCode, output_summary: outputSummary };
|
|
50154
51613
|
}
|
|
50155
51614
|
function listAgentRuns(filter = {}, db) {
|
|
50156
51615
|
let runs = listAgentRunQueue(db).map(toAgentRun);
|
|
@@ -51119,10 +52578,10 @@ function extractEmbeddedBridge(markdown) {
|
|
|
51119
52578
|
const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
|
|
51120
52579
|
if (!match)
|
|
51121
52580
|
return null;
|
|
51122
|
-
const
|
|
52581
|
+
const json4 = match[1].split(`
|
|
51123
52582
|
`).map((line) => line.replace(/^ /, "")).join(`
|
|
51124
52583
|
`);
|
|
51125
|
-
return JSON.parse(
|
|
52584
|
+
return JSON.parse(json4);
|
|
51126
52585
|
}
|
|
51127
52586
|
function frontmatterValue(markdown, key2) {
|
|
51128
52587
|
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -51143,14 +52602,14 @@ function parseTaskLine(line, currentPlan) {
|
|
|
51143
52602
|
const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
51144
52603
|
if (!match)
|
|
51145
52604
|
return null;
|
|
51146
|
-
const
|
|
52605
|
+
const status3 = match[1].toLowerCase() === "x" ? "completed" : "pending";
|
|
51147
52606
|
const rawTitle = stripInlineMetadata(match[2]).replace(/^\*\*(.*?)\*\*$/, "$1");
|
|
51148
52607
|
const tags = [...rawTitle.matchAll(/(^|\s)#([A-Za-z0-9_-]+)/g)].map((tag) => tag[2]);
|
|
51149
52608
|
const assignee = rawTitle.match(/(^|\s)@([A-Za-z0-9_-]+)/)?.[2];
|
|
51150
52609
|
const title = rawTitle.replace(/(^|\s)#[A-Za-z0-9_-]+/g, "").replace(/(^|\s)@[A-Za-z0-9_-]+/g, "").replace(/\s+/g, " ").trim();
|
|
51151
52610
|
return {
|
|
51152
52611
|
title,
|
|
51153
|
-
status:
|
|
52612
|
+
status: status3,
|
|
51154
52613
|
priority: "medium",
|
|
51155
52614
|
tags,
|
|
51156
52615
|
assigned_to: assignee,
|
|
@@ -52755,7 +54214,7 @@ function normalizeJiraIssue(raw) {
|
|
|
52755
54214
|
if (!summary)
|
|
52756
54215
|
return null;
|
|
52757
54216
|
const statusRec = asRecord3(fields.status);
|
|
52758
|
-
const
|
|
54217
|
+
const status3 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
|
|
52759
54218
|
const labels = labelNames2(fields.labels);
|
|
52760
54219
|
const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord3(fields.description)?.content === "string" ? String(asRecord3(fields.description)?.content).slice(0, 8000) : undefined;
|
|
52761
54220
|
return {
|
|
@@ -52764,13 +54223,13 @@ function normalizeJiraIssue(raw) {
|
|
|
52764
54223
|
external_id: key2,
|
|
52765
54224
|
title: `[${key2}] ${summary}`,
|
|
52766
54225
|
description,
|
|
52767
|
-
status:
|
|
54226
|
+
status: status3,
|
|
52768
54227
|
priority: priorityFromJira(fields.priority),
|
|
52769
54228
|
tags: [...labels.slice(0, 10), "jira-import"],
|
|
52770
54229
|
url: typeof raw.self === "string" ? raw.self : undefined,
|
|
52771
54230
|
raw_metadata: {
|
|
52772
54231
|
jira_key: key2,
|
|
52773
|
-
jira_status:
|
|
54232
|
+
jira_status: status3,
|
|
52774
54233
|
jira_priority: asRecord3(fields.priority)?.name,
|
|
52775
54234
|
jira_id: raw.id
|
|
52776
54235
|
}
|
|
@@ -55515,6 +56974,10 @@ function importBundle(bundle, options = {}, db) {
|
|
|
55515
56974
|
}
|
|
55516
56975
|
for (const dep of bundle.dependencies) {
|
|
55517
56976
|
try {
|
|
56977
|
+
if (!getTask(dep.task_id, d) || !getTask(dep.depends_on, d)) {
|
|
56978
|
+
bump(result.skipped, "dependencies");
|
|
56979
|
+
continue;
|
|
56980
|
+
}
|
|
55518
56981
|
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [dep.task_id, dep.depends_on]);
|
|
55519
56982
|
bump(result.created, "dependencies");
|
|
55520
56983
|
} catch (e) {
|
|
@@ -56320,11 +57783,11 @@ function createTuiDashboardSnapshot(options = {}, db) {
|
|
|
56320
57783
|
const d = db || getDatabase();
|
|
56321
57784
|
const limit = limited(options.limit);
|
|
56322
57785
|
const filter = projectFilter(options.project_id);
|
|
56323
|
-
const counts = TASK_STATUSES2.reduce((acc,
|
|
56324
|
-
acc[
|
|
57786
|
+
const counts = TASK_STATUSES2.reduce((acc, status3) => {
|
|
57787
|
+
acc[status3] = countTasks({ ...filter, status: status3 }, d);
|
|
56325
57788
|
return acc;
|
|
56326
57789
|
}, { total: 0 });
|
|
56327
|
-
counts.total = TASK_STATUSES2.reduce((sum,
|
|
57790
|
+
counts.total = TASK_STATUSES2.reduce((sum, status3) => sum + counts[status3], 0);
|
|
56328
57791
|
const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
|
|
56329
57792
|
id: project.id,
|
|
56330
57793
|
name: project.name,
|
|
@@ -57525,12 +58988,12 @@ function captureGit(root, warnings) {
|
|
|
57525
58988
|
}
|
|
57526
58989
|
const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
|
|
57527
58990
|
const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
|
|
57528
|
-
const
|
|
58991
|
+
const status3 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
|
|
57529
58992
|
if (commit.exitCode !== 0)
|
|
57530
58993
|
warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
|
|
57531
|
-
if (
|
|
57532
|
-
warnings.push(`git status unavailable: ${
|
|
57533
|
-
const lines =
|
|
58994
|
+
if (status3.exitCode !== 0)
|
|
58995
|
+
warnings.push(`git status unavailable: ${status3.stderr || status3.stdout || "unknown error"}`);
|
|
58996
|
+
const lines = status3.stdout ? status3.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
57534
58997
|
return {
|
|
57535
58998
|
present: true,
|
|
57536
58999
|
branch: branch.stdout || null,
|
|
@@ -57572,8 +59035,8 @@ function defaultSnapshotDir() {
|
|
|
57572
59035
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
57573
59036
|
}
|
|
57574
59037
|
function snapshotWithId(snapshot) {
|
|
57575
|
-
const
|
|
57576
|
-
return { id: `env_${
|
|
59038
|
+
const digest3 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
59039
|
+
return { id: `env_${digest3}`, ...snapshot };
|
|
57577
59040
|
}
|
|
57578
59041
|
function captureEnvironmentSnapshot(input = {}) {
|
|
57579
59042
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -57773,7 +59236,7 @@ function createDecisionRecord(input, db) {
|
|
|
57773
59236
|
const ts = now();
|
|
57774
59237
|
const sequenceNum = nextSequenceNum(input.project_id, d);
|
|
57775
59238
|
const shortRef = buildShortRef(input.project_id, sequenceNum, d);
|
|
57776
|
-
const
|
|
59239
|
+
const status3 = input.status ?? "proposed";
|
|
57777
59240
|
d.run(`INSERT INTO decision_records (
|
|
57778
59241
|
id, project_id, task_id, plan_id, agent_id, sequence_num, short_ref,
|
|
57779
59242
|
title, status, context, decision, consequences, alternatives, tags,
|
|
@@ -57787,7 +59250,7 @@ function createDecisionRecord(input, db) {
|
|
|
57787
59250
|
sequenceNum,
|
|
57788
59251
|
shortRef,
|
|
57789
59252
|
input.title,
|
|
57790
|
-
|
|
59253
|
+
status3,
|
|
57791
59254
|
input.context ?? null,
|
|
57792
59255
|
input.decision,
|
|
57793
59256
|
input.consequences ?? null,
|
|
@@ -57865,15 +59328,15 @@ function updateDecisionRecord(id, input, db) {
|
|
|
57865
59328
|
]);
|
|
57866
59329
|
return getDecisionRecord(id, d);
|
|
57867
59330
|
}
|
|
57868
|
-
function setDecisionStatus(id,
|
|
57869
|
-
if (!DECISION_STATUSES.includes(
|
|
57870
|
-
throw new Error(`Invalid decision status: ${
|
|
59331
|
+
function setDecisionStatus(id, status3, db) {
|
|
59332
|
+
if (!DECISION_STATUSES.includes(status3)) {
|
|
59333
|
+
throw new Error(`Invalid decision status: ${status3}`);
|
|
57871
59334
|
}
|
|
57872
59335
|
const d = db || getDatabase();
|
|
57873
59336
|
const existing = getDecisionRecord(id, d);
|
|
57874
59337
|
if (!existing)
|
|
57875
59338
|
throw new Error(`Decision record not found: ${id}`);
|
|
57876
|
-
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [
|
|
59339
|
+
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [status3, now(), id]);
|
|
57877
59340
|
return getDecisionRecord(id, d);
|
|
57878
59341
|
}
|
|
57879
59342
|
function supersedeDecisionRecord(id, input, db) {
|
|
@@ -58154,17 +59617,17 @@ function buildReportExportData(input, db) {
|
|
|
58154
59617
|
throw new Error("Project not found");
|
|
58155
59618
|
projectId = project.id;
|
|
58156
59619
|
title = `Project: ${project.name}`;
|
|
58157
|
-
const
|
|
59620
|
+
const status3 = getStatus({ project_id: project.id }, undefined, undefined, d);
|
|
58158
59621
|
sections.push(section("overview", "Overview", [
|
|
58159
59622
|
`Name: ${project.name}`,
|
|
58160
59623
|
`Path: ${project.path}`,
|
|
58161
59624
|
`Prefix: ${project.task_prefix ?? "\u2014"}`
|
|
58162
59625
|
]), section("status", "Status", [
|
|
58163
|
-
`Pending: ${
|
|
58164
|
-
`In progress: ${
|
|
58165
|
-
`Completed: ${
|
|
58166
|
-
`Total: ${
|
|
58167
|
-
`Stale: ${
|
|
59626
|
+
`Pending: ${status3.pending}`,
|
|
59627
|
+
`In progress: ${status3.in_progress}`,
|
|
59628
|
+
`Completed: ${status3.completed}`,
|
|
59629
|
+
`Total: ${status3.total}`,
|
|
59630
|
+
`Stale: ${status3.stale_count}`
|
|
58168
59631
|
]));
|
|
58169
59632
|
const tasks = listTasks({ project_id: project.id, limit: 50 }, d);
|
|
58170
59633
|
sections.push(section("tasks", "Recent tasks", tasks.length ? tasks.map(taskLine2) : ["No tasks"]));
|
|
@@ -58985,8 +60448,8 @@ function getGitStatus(root, branch, includeGitStatus) {
|
|
|
58985
60448
|
return { has_git: false, current_branch: null, branch_exists: false, dirty_files: [] };
|
|
58986
60449
|
const currentBranch = runGit3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
58987
60450
|
const branchExists = runGit3(root, ["show-ref", "--verify", `refs/heads/${branch}`]) !== null;
|
|
58988
|
-
const
|
|
58989
|
-
const dirtyFiles =
|
|
60451
|
+
const status3 = runGit3(root, ["status", "--short"]) || "";
|
|
60452
|
+
const dirtyFiles = status3.split(`
|
|
58990
60453
|
`).map((line) => line.trim()).filter(Boolean).map((line) => normalizePath6(line.replace(/^.. /, "").replace(/^.* -> /, ""))).filter((path) => Boolean(path));
|
|
58991
60454
|
return { has_git: true, current_branch: currentBranch || null, branch_exists: branchExists, dirty_files: dirtyFiles };
|
|
58992
60455
|
}
|
|
@@ -59384,8 +60847,8 @@ var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
|
|
|
59384
60847
|
var DEMO_DEFAULT_AGENT = "demoagent";
|
|
59385
60848
|
var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
|
|
59386
60849
|
var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
|
|
59387
|
-
function pushStep(steps, action, detail,
|
|
59388
|
-
steps.push({ step: steps.length + 1, action, detail, status:
|
|
60850
|
+
function pushStep(steps, action, detail, status3 = "ok") {
|
|
60851
|
+
steps.push({ step: steps.length + 1, action, detail, status: status3 });
|
|
59389
60852
|
}
|
|
59390
60853
|
function setupEphemeralDemoDb(options = {}) {
|
|
59391
60854
|
const prevPath = process.env["TODOS_DB_PATH"];
|
|
@@ -59512,7 +60975,7 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59512
60975
|
pushStep(steps, "complete_run_record", `Run record status=${finishedRecord.status}`);
|
|
59513
60976
|
completeTask(third.id, agent.id, d, { notes: "Demo: finished via agent run + run record" });
|
|
59514
60977
|
pushStep(steps, "complete_task", `Completed ${third.short_id ?? third.id.slice(0, 8)}`);
|
|
59515
|
-
const
|
|
60978
|
+
const status3 = getStatus({ project_id: project.id }, agent.id, undefined, d);
|
|
59516
60979
|
const tasks = listTasks({ project_id: project.id }, d).map((t) => ({
|
|
59517
60980
|
id: t.id,
|
|
59518
60981
|
short_id: t.short_id,
|
|
@@ -59544,14 +61007,14 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59544
61007
|
objective: finishedRecord.objective
|
|
59545
61008
|
},
|
|
59546
61009
|
status_summary: {
|
|
59547
|
-
pending:
|
|
59548
|
-
in_progress:
|
|
59549
|
-
completed:
|
|
59550
|
-
total:
|
|
61010
|
+
pending: status3.pending,
|
|
61011
|
+
in_progress: status3.in_progress,
|
|
61012
|
+
completed: status3.completed,
|
|
61013
|
+
total: status3.total
|
|
59551
61014
|
},
|
|
59552
61015
|
steps,
|
|
59553
61016
|
completed_at: exportedAt2,
|
|
59554
|
-
message: `Demo complete: ${
|
|
61017
|
+
message: `Demo complete: ${status3.completed}/${status3.total} tasks done, 1 agent run, 1 run record`
|
|
59555
61018
|
};
|
|
59556
61019
|
} finally {
|
|
59557
61020
|
if (dbHandle)
|
|
@@ -60525,8 +61988,8 @@ function rowToOrg(row) {
|
|
|
60525
61988
|
function createOrg(input, db) {
|
|
60526
61989
|
const d = db || getDatabase();
|
|
60527
61990
|
const id = uuid();
|
|
60528
|
-
const
|
|
60529
|
-
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}),
|
|
61991
|
+
const timestamp5 = now();
|
|
61992
|
+
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp5, timestamp5]);
|
|
60530
61993
|
return getOrg(id, d);
|
|
60531
61994
|
}
|
|
60532
61995
|
function getOrg(id, db) {
|
|
@@ -60774,10 +62237,10 @@ function getCriticalPath2(opts, db) {
|
|
|
60774
62237
|
function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
|
|
60775
62238
|
const d = db || getDatabase();
|
|
60776
62239
|
const id = uuid();
|
|
60777
|
-
const
|
|
62240
|
+
const timestamp5 = now();
|
|
60778
62241
|
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
60779
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}),
|
|
60780
|
-
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at:
|
|
62242
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp5]);
|
|
62243
|
+
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp5 };
|
|
60781
62244
|
}
|
|
60782
62245
|
function removeKgEdges(sourceId, targetId, relationType, db) {
|
|
60783
62246
|
const d = db || getDatabase();
|
|
@@ -61677,7 +63140,7 @@ function runSavedSearch(filters = {}, scope = "tasks", db) {
|
|
|
61677
63140
|
function saveSearchView(input, db) {
|
|
61678
63141
|
const d = db || getDatabase();
|
|
61679
63142
|
const name = normalizeName3(input.name);
|
|
61680
|
-
const
|
|
63143
|
+
const timestamp5 = now();
|
|
61681
63144
|
const existing = getSearchView(name, d);
|
|
61682
63145
|
if (existing) {
|
|
61683
63146
|
d.run(`UPDATE saved_search_views
|
|
@@ -61686,7 +63149,7 @@ function saveSearchView(input, db) {
|
|
|
61686
63149
|
input.description ?? existing.description,
|
|
61687
63150
|
normalizeScope(input.scope ?? existing.scope),
|
|
61688
63151
|
JSON.stringify(input.filters ?? existing.filters),
|
|
61689
|
-
|
|
63152
|
+
timestamp5,
|
|
61690
63153
|
existing.id
|
|
61691
63154
|
]);
|
|
61692
63155
|
return getSearchView(existing.id, d);
|
|
@@ -61699,8 +63162,8 @@ function saveSearchView(input, db) {
|
|
|
61699
63162
|
input.description ?? null,
|
|
61700
63163
|
normalizeScope(input.scope),
|
|
61701
63164
|
JSON.stringify(input.filters ?? {}),
|
|
61702
|
-
|
|
61703
|
-
|
|
63165
|
+
timestamp5,
|
|
63166
|
+
timestamp5
|
|
61704
63167
|
]);
|
|
61705
63168
|
return getSearchView(id, d);
|
|
61706
63169
|
}
|
|
@@ -61742,14 +63205,14 @@ function readClaudeTask(dir, filename) {
|
|
|
61742
63205
|
function writeClaudeTask(dir, task3) {
|
|
61743
63206
|
writeJsonFile(join23(dir, `${task3.id}.json`), task3);
|
|
61744
63207
|
}
|
|
61745
|
-
function toClaudeStatus(
|
|
61746
|
-
if (
|
|
61747
|
-
return
|
|
63208
|
+
function toClaudeStatus(status3) {
|
|
63209
|
+
if (status3 === "pending" || status3 === "in_progress" || status3 === "completed") {
|
|
63210
|
+
return status3;
|
|
61748
63211
|
}
|
|
61749
63212
|
return "completed";
|
|
61750
63213
|
}
|
|
61751
|
-
function toSqliteStatus(
|
|
61752
|
-
return
|
|
63214
|
+
function toSqliteStatus(status3) {
|
|
63215
|
+
return status3;
|
|
61753
63216
|
}
|
|
61754
63217
|
function readPrefixCounter(dir) {
|
|
61755
63218
|
const path = join23(dir, ".prefix-counter");
|
|
@@ -62710,7 +64173,7 @@ init_database();
|
|
|
62710
64173
|
function logTrace(input, db) {
|
|
62711
64174
|
const d = db || getDatabase();
|
|
62712
64175
|
const id = uuid();
|
|
62713
|
-
const
|
|
64176
|
+
const timestamp5 = now();
|
|
62714
64177
|
d.run(`INSERT INTO task_traces (id, task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd, created_at)
|
|
62715
64178
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
62716
64179
|
id,
|
|
@@ -62723,7 +64186,7 @@ function logTrace(input, db) {
|
|
|
62723
64186
|
input.duration_ms ?? null,
|
|
62724
64187
|
input.tokens ?? null,
|
|
62725
64188
|
input.cost_usd ?? null,
|
|
62726
|
-
|
|
64189
|
+
timestamp5
|
|
62727
64190
|
]);
|
|
62728
64191
|
return {
|
|
62729
64192
|
id,
|
|
@@ -62736,7 +64199,7 @@ function logTrace(input, db) {
|
|
|
62736
64199
|
duration_ms: input.duration_ms ?? null,
|
|
62737
64200
|
tokens: input.tokens ?? null,
|
|
62738
64201
|
cost_usd: input.cost_usd ?? null,
|
|
62739
|
-
created_at:
|
|
64202
|
+
created_at: timestamp5
|
|
62740
64203
|
};
|
|
62741
64204
|
}
|
|
62742
64205
|
function getTaskTraces(taskId, db) {
|
|
@@ -62760,7 +64223,7 @@ init_database();
|
|
|
62760
64223
|
init_tasks();
|
|
62761
64224
|
function setBudget(agentId, opts, db) {
|
|
62762
64225
|
const d = db || getDatabase();
|
|
62763
|
-
const
|
|
64226
|
+
const timestamp5 = now();
|
|
62764
64227
|
d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
|
|
62765
64228
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
62766
64229
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
@@ -62774,13 +64237,13 @@ function setBudget(agentId, opts, db) {
|
|
|
62774
64237
|
opts.max_cost_usd ?? null,
|
|
62775
64238
|
opts.max_task_minutes ?? null,
|
|
62776
64239
|
opts.period_hours ?? 24,
|
|
62777
|
-
|
|
62778
|
-
|
|
64240
|
+
timestamp5,
|
|
64241
|
+
timestamp5,
|
|
62779
64242
|
opts.max_concurrent ?? null,
|
|
62780
64243
|
opts.max_cost_usd ?? null,
|
|
62781
64244
|
opts.max_task_minutes ?? null,
|
|
62782
64245
|
opts.period_hours ?? null,
|
|
62783
|
-
|
|
64246
|
+
timestamp5
|
|
62784
64247
|
]);
|
|
62785
64248
|
return getBudget(agentId, d);
|
|
62786
64249
|
}
|
|
@@ -63083,11 +64546,11 @@ function resolveWorkflowState(input, projectPath) {
|
|
|
63083
64546
|
return { input, state: byCanonical, matched_by: "canonical_status" };
|
|
63084
64547
|
throw new Error(`Unknown workflow state: ${input}`);
|
|
63085
64548
|
}
|
|
63086
|
-
function stateForCanonicalStatus(
|
|
63087
|
-
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status ===
|
|
64549
|
+
function stateForCanonicalStatus(status3, projectPath) {
|
|
64550
|
+
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status3));
|
|
63088
64551
|
if (configured)
|
|
63089
64552
|
return configured;
|
|
63090
|
-
return listWorkflowStates(projectPath).find((state) => state.canonical_status ===
|
|
64553
|
+
return listWorkflowStates(projectPath).find((state) => state.canonical_status === status3) || resolveWorkflowState(status3, projectPath).state;
|
|
63091
64554
|
}
|
|
63092
64555
|
function getTaskWorkflowState(taskId, db, projectPath) {
|
|
63093
64556
|
const d = db || getDatabase();
|
|
@@ -63251,9 +64714,9 @@ function unpackFixture(input) {
|
|
|
63251
64714
|
const contextPack = input["context_pack"];
|
|
63252
64715
|
return isObject(contextPack) ? contextPack : input;
|
|
63253
64716
|
}
|
|
63254
|
-
function approvalStatus(
|
|
63255
|
-
if (
|
|
63256
|
-
return
|
|
64717
|
+
function approvalStatus(status3) {
|
|
64718
|
+
if (status3 === "approved" || status3 === "rejected" || status3 === "expired")
|
|
64719
|
+
return status3;
|
|
63257
64720
|
return "pending";
|
|
63258
64721
|
}
|
|
63259
64722
|
function approvalsFromFixture(fixture, runs) {
|
|
@@ -63350,17 +64813,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63350
64813
|
if (["completed", "failed", "cancelled"].includes(runStatus))
|
|
63351
64814
|
currentStatus = runStatus;
|
|
63352
64815
|
for (const command of asArray2(run["commands"])) {
|
|
63353
|
-
const
|
|
64816
|
+
const status3 = normalizedStatus(command["status"]);
|
|
63354
64817
|
const commandText = text(command["command"]) || "command";
|
|
63355
64818
|
push({
|
|
63356
64819
|
type: "command",
|
|
63357
64820
|
source: "run.commands",
|
|
63358
64821
|
label: commandText,
|
|
63359
|
-
status:
|
|
64822
|
+
status: status3,
|
|
63360
64823
|
command: commandText,
|
|
63361
64824
|
message: text(command["output_summary"])
|
|
63362
64825
|
});
|
|
63363
|
-
if (
|
|
64826
|
+
if (status3 === "failed")
|
|
63364
64827
|
failures.push({ source: "run.commands", message: text(command["output_summary"]) || "command failed", command: commandText });
|
|
63365
64828
|
}
|
|
63366
64829
|
for (const file of asArray2(run["files"])) {
|
|
@@ -63377,17 +64840,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63377
64840
|
}
|
|
63378
64841
|
}
|
|
63379
64842
|
for (const verification2 of verifications) {
|
|
63380
|
-
const
|
|
64843
|
+
const status3 = normalizedStatus(verification2["status"]);
|
|
63381
64844
|
const commandText = text(verification2["command"]) || "verification";
|
|
63382
64845
|
push({
|
|
63383
64846
|
type: "command",
|
|
63384
64847
|
source: "traceability.verifications",
|
|
63385
64848
|
label: commandText,
|
|
63386
|
-
status:
|
|
64849
|
+
status: status3,
|
|
63387
64850
|
command: commandText,
|
|
63388
64851
|
message: text(verification2["output_summary"])
|
|
63389
64852
|
});
|
|
63390
|
-
if (
|
|
64853
|
+
if (status3 === "failed")
|
|
63391
64854
|
failures.push({ source: "traceability.verifications", message: text(verification2["output_summary"]) || "verification failed", command: commandText });
|
|
63392
64855
|
}
|
|
63393
64856
|
const approvals = approvalsFromFixture(fixture, runs);
|
|
@@ -63923,7 +65386,7 @@ function installLocalExtension(input) {
|
|
|
63923
65386
|
const validation = inspected.validation;
|
|
63924
65387
|
if (!validation.ok)
|
|
63925
65388
|
throw new Error(`extension manifest invalid: ${validation.errors.join("; ")}`);
|
|
63926
|
-
const
|
|
65389
|
+
const timestamp5 = new Date().toISOString();
|
|
63927
65390
|
const config = loadConfig();
|
|
63928
65391
|
const existing = config.extension_registry?.[inspected.manifest.name];
|
|
63929
65392
|
const trusted = Boolean(input.trust);
|
|
@@ -63943,8 +65406,8 @@ function installLocalExtension(input) {
|
|
|
63943
65406
|
...trusted ? [] : ["extension installed as needs_review until trusted explicitly"]
|
|
63944
65407
|
],
|
|
63945
65408
|
diagnostics: testExtensionCompatibility(inspected.source),
|
|
63946
|
-
installed_at: existing?.installed_at ||
|
|
63947
|
-
updated_at:
|
|
65409
|
+
installed_at: existing?.installed_at || timestamp5,
|
|
65410
|
+
updated_at: timestamp5
|
|
63948
65411
|
};
|
|
63949
65412
|
saveConfig({
|
|
63950
65413
|
...config,
|
|
@@ -64306,9 +65769,9 @@ async function sendToTmux(target, message, delayMs, options = false) {
|
|
|
64306
65769
|
ensureTmuxAvailable();
|
|
64307
65770
|
if (!opts.confirmBusy) {
|
|
64308
65771
|
const pane = await inspectTmuxPane(target);
|
|
64309
|
-
const
|
|
64310
|
-
if (
|
|
64311
|
-
throw new Error(`tmux target "${target}" appears busy (${
|
|
65772
|
+
const status3 = tmuxPaneBusyStatus(pane);
|
|
65773
|
+
if (status3.busy) {
|
|
65774
|
+
throw new Error(`tmux target "${target}" appears busy (${status3.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
64312
65775
|
}
|
|
64313
65776
|
}
|
|
64314
65777
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
@@ -64453,10 +65916,10 @@ function normalizeStatus(value) {
|
|
|
64453
65916
|
return "open";
|
|
64454
65917
|
}
|
|
64455
65918
|
function normalizeResolutionStatus(value) {
|
|
64456
|
-
const
|
|
64457
|
-
if (
|
|
65919
|
+
const status3 = normalizeStatus(value || "resolved");
|
|
65920
|
+
if (status3 === "open")
|
|
64458
65921
|
throw new Error("resolve-missing status must be resolved or ignored");
|
|
64459
|
-
return
|
|
65922
|
+
return status3;
|
|
64460
65923
|
}
|
|
64461
65924
|
function redactOptional(value, max = 2000) {
|
|
64462
65925
|
if (!value)
|
|
@@ -64494,7 +65957,7 @@ function compactFinding(finding2) {
|
|
|
64494
65957
|
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
64495
65958
|
};
|
|
64496
65959
|
}
|
|
64497
|
-
function previewFinding(existing, next,
|
|
65960
|
+
function previewFinding(existing, next, timestamp5) {
|
|
64498
65961
|
return {
|
|
64499
65962
|
...existing,
|
|
64500
65963
|
run_id: next.run_id,
|
|
@@ -64505,9 +65968,9 @@ function previewFinding(existing, next, timestamp4) {
|
|
|
64505
65968
|
summary: next.summary,
|
|
64506
65969
|
artifact_path: next.artifact_path,
|
|
64507
65970
|
metadata: next.metadata,
|
|
64508
|
-
last_seen_at:
|
|
64509
|
-
resolved_at: next.status === "open" ? null : existing.resolved_at ||
|
|
64510
|
-
updated_at:
|
|
65971
|
+
last_seen_at: timestamp5,
|
|
65972
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
65973
|
+
updated_at: timestamp5
|
|
64511
65974
|
};
|
|
64512
65975
|
}
|
|
64513
65976
|
function upsertAction(existing, next) {
|
|
@@ -64557,7 +66020,7 @@ function sameFinding(left, right) {
|
|
|
64557
66020
|
function upsertTaskFinding(input, db) {
|
|
64558
66021
|
const d = db || getDatabase();
|
|
64559
66022
|
assertTask(input.task_id, d);
|
|
64560
|
-
const
|
|
66023
|
+
const timestamp5 = input.observed_at || now();
|
|
64561
66024
|
const warnings = [];
|
|
64562
66025
|
const next = nextFinding(input, d);
|
|
64563
66026
|
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
@@ -64568,10 +66031,10 @@ function upsertTaskFinding(input, db) {
|
|
|
64568
66031
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64569
66032
|
local_only: true,
|
|
64570
66033
|
dry_run: true,
|
|
64571
|
-
processed_at:
|
|
66034
|
+
processed_at: timestamp5,
|
|
64572
66035
|
action: action2,
|
|
64573
66036
|
fingerprint: next.fingerprint,
|
|
64574
|
-
finding: existing ? compactFinding(previewFinding(existing, next,
|
|
66037
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp5)) : null,
|
|
64575
66038
|
warnings
|
|
64576
66039
|
};
|
|
64577
66040
|
}
|
|
@@ -64592,17 +66055,17 @@ function upsertTaskFinding(input, db) {
|
|
|
64592
66055
|
next.summary,
|
|
64593
66056
|
next.artifact_path,
|
|
64594
66057
|
JSON.stringify(next.metadata),
|
|
64595
|
-
|
|
64596
|
-
|
|
64597
|
-
next.status === "open" ? null :
|
|
64598
|
-
|
|
64599
|
-
|
|
66058
|
+
timestamp5,
|
|
66059
|
+
timestamp5,
|
|
66060
|
+
next.status === "open" ? null : timestamp5,
|
|
66061
|
+
timestamp5,
|
|
66062
|
+
timestamp5
|
|
64600
66063
|
]);
|
|
64601
66064
|
return {
|
|
64602
66065
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64603
66066
|
local_only: true,
|
|
64604
66067
|
dry_run: false,
|
|
64605
|
-
processed_at:
|
|
66068
|
+
processed_at: timestamp5,
|
|
64606
66069
|
action: "created",
|
|
64607
66070
|
fingerprint: next.fingerprint,
|
|
64608
66071
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64614,7 +66077,7 @@ function upsertTaskFinding(input, db) {
|
|
|
64614
66077
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64615
66078
|
local_only: true,
|
|
64616
66079
|
dry_run: false,
|
|
64617
|
-
processed_at:
|
|
66080
|
+
processed_at: timestamp5,
|
|
64618
66081
|
action: "matched",
|
|
64619
66082
|
fingerprint: next.fingerprint,
|
|
64620
66083
|
finding: compactFinding(existing),
|
|
@@ -64634,16 +66097,16 @@ function upsertTaskFinding(input, db) {
|
|
|
64634
66097
|
next.summary,
|
|
64635
66098
|
next.artifact_path,
|
|
64636
66099
|
JSON.stringify(next.metadata),
|
|
64637
|
-
|
|
64638
|
-
next.status === "open" ? null : existing.resolved_at ||
|
|
64639
|
-
|
|
66100
|
+
timestamp5,
|
|
66101
|
+
next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
66102
|
+
timestamp5,
|
|
64640
66103
|
existing.id
|
|
64641
66104
|
]);
|
|
64642
66105
|
return {
|
|
64643
66106
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64644
66107
|
local_only: true,
|
|
64645
66108
|
dry_run: false,
|
|
64646
|
-
processed_at:
|
|
66109
|
+
processed_at: timestamp5,
|
|
64647
66110
|
action,
|
|
64648
66111
|
fingerprint: next.fingerprint,
|
|
64649
66112
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64680,8 +66143,8 @@ function listCompactTaskFindings(filter = {}, db) {
|
|
|
64680
66143
|
function resolveMissingTaskFindings(input, db) {
|
|
64681
66144
|
const d = db || getDatabase();
|
|
64682
66145
|
assertTask(input.task_id, d);
|
|
64683
|
-
const
|
|
64684
|
-
const
|
|
66146
|
+
const timestamp5 = input.resolved_at || now();
|
|
66147
|
+
const status3 = normalizeResolutionStatus(input.status);
|
|
64685
66148
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
64686
66149
|
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
64687
66150
|
const warnings = [];
|
|
@@ -64700,7 +66163,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64700
66163
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64701
66164
|
local_only: true,
|
|
64702
66165
|
dry_run: true,
|
|
64703
|
-
processed_at:
|
|
66166
|
+
processed_at: timestamp5,
|
|
64704
66167
|
action: candidates.length > 0 ? "preview" : "noop",
|
|
64705
66168
|
task_id: input.task_id,
|
|
64706
66169
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
@@ -64723,10 +66186,10 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64723
66186
|
const tx = d.transaction(() => {
|
|
64724
66187
|
for (const finding2 of candidates) {
|
|
64725
66188
|
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
64726
|
-
|
|
64727
|
-
|
|
66189
|
+
status3,
|
|
66190
|
+
timestamp5,
|
|
64728
66191
|
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
64729
|
-
|
|
66192
|
+
timestamp5,
|
|
64730
66193
|
finding2.id
|
|
64731
66194
|
]);
|
|
64732
66195
|
}
|
|
@@ -64738,8 +66201,8 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64738
66201
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64739
66202
|
local_only: true,
|
|
64740
66203
|
dry_run: false,
|
|
64741
|
-
processed_at:
|
|
64742
|
-
action: updated.length > 0 ?
|
|
66204
|
+
processed_at: timestamp5,
|
|
66205
|
+
action: updated.length > 0 ? status3 : "noop",
|
|
64743
66206
|
task_id: input.task_id,
|
|
64744
66207
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
64745
66208
|
run_id: runId,
|
|
@@ -65632,7 +67095,7 @@ init_tasks();
|
|
|
65632
67095
|
init_task_relations();
|
|
65633
67096
|
init_config2();
|
|
65634
67097
|
var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
|
|
65635
|
-
function
|
|
67098
|
+
function timestamp5() {
|
|
65636
67099
|
return new Date().toISOString();
|
|
65637
67100
|
}
|
|
65638
67101
|
function todayIso() {
|
|
@@ -65716,7 +67179,7 @@ function upsertCapacityProfile(input) {
|
|
|
65716
67179
|
const store = readStore2();
|
|
65717
67180
|
const key2 = profileKey(agentId, projectId);
|
|
65718
67181
|
const existing = store.profiles[key2];
|
|
65719
|
-
const now4 =
|
|
67182
|
+
const now4 = timestamp5();
|
|
65720
67183
|
const profile = {
|
|
65721
67184
|
id: existing?.id ?? key2,
|
|
65722
67185
|
agent_id: agentId,
|
|
@@ -65797,7 +67260,7 @@ function getPlanningForecast(input = {}, db) {
|
|
|
65797
67260
|
return {
|
|
65798
67261
|
schema_version: LOCAL_CAPACITY_SCHEMA_VERSION,
|
|
65799
67262
|
local_only: true,
|
|
65800
|
-
generated_at:
|
|
67263
|
+
generated_at: timestamp5(),
|
|
65801
67264
|
filters: {
|
|
65802
67265
|
project_id: input.project_id ?? null,
|
|
65803
67266
|
plan_id: input.plan_id ?? null,
|
|
@@ -66185,6 +67648,8 @@ export {
|
|
|
66185
67648
|
testTerminalNotificationRule,
|
|
66186
67649
|
testLocalEventHook,
|
|
66187
67650
|
tasksFromTemplate,
|
|
67651
|
+
taskSubtreeTransferRollbackRequestDigest,
|
|
67652
|
+
taskSubtreeTransferRequestDigest,
|
|
66188
67653
|
taskManifestRequestDigest,
|
|
66189
67654
|
taskManifestCompensationRequestDigest,
|
|
66190
67655
|
taskFromTemplate,
|
|
@@ -66207,6 +67672,7 @@ export {
|
|
|
66207
67672
|
startTaskRun,
|
|
66208
67673
|
startTask,
|
|
66209
67674
|
startFocusSession,
|
|
67675
|
+
sqliteTodosTaskSubtreeTransferSchemaSql,
|
|
66210
67676
|
sqliteTodosTaskManifestSchemaSql,
|
|
66211
67677
|
sqliteTodosProjectRegistrationSchemaSql,
|
|
66212
67678
|
softDeleteArtifact,
|
|
@@ -66398,6 +67864,7 @@ export {
|
|
|
66398
67864
|
previewInstalledTemplate,
|
|
66399
67865
|
previewInboxIntake,
|
|
66400
67866
|
previewBuiltinTemplate,
|
|
67867
|
+
postgresTodosTaskSubtreeTransferSchemaSql,
|
|
66401
67868
|
postgresTodosTaskManifestSchemaSql,
|
|
66402
67869
|
postgresTodosSyncSchemaSql,
|
|
66403
67870
|
postgresTodosProjectRegistrationSchemaSql,
|
|
@@ -66406,6 +67873,9 @@ export {
|
|
|
66406
67873
|
planPlanProjectLink,
|
|
66407
67874
|
pauseFocusSession,
|
|
66408
67875
|
patrolTasks,
|
|
67876
|
+
parseTodosTaskSubtreeTransferRollback,
|
|
67877
|
+
parseTodosTaskSubtreeTransferInspect,
|
|
67878
|
+
parseTodosTaskSubtreeTransferApply,
|
|
66409
67879
|
parseTodosTaskManifestCompensation,
|
|
66410
67880
|
parseTodosTaskManifestBindingLookup,
|
|
66411
67881
|
parseTodosTaskManifest,
|
|
@@ -66413,7 +67883,6 @@ export {
|
|
|
66413
67883
|
parseTodosAiOutputSchema,
|
|
66414
67884
|
parseTodosAiJson,
|
|
66415
67885
|
parseTmuxTarget,
|
|
66416
|
-
parseStorageMode,
|
|
66417
67886
|
parseStorageBackend,
|
|
66418
67887
|
parseRecurrenceRule,
|
|
66419
67888
|
parseQuietHours,
|
|
@@ -66564,7 +68033,6 @@ export {
|
|
|
66564
68033
|
issueToTask,
|
|
66565
68034
|
isWorktreePath,
|
|
66566
68035
|
isValidRecurrenceRule,
|
|
66567
|
-
isTodosRemoteStorageEnabled,
|
|
66568
68036
|
isTodosPostgresBackend,
|
|
66569
68037
|
isTodosAiUpdateTaskResult,
|
|
66570
68038
|
isTodosAiRuntimeEvent,
|
|
@@ -66604,6 +68072,7 @@ export {
|
|
|
66604
68072
|
importActivityLog,
|
|
66605
68073
|
hasSecretFindings,
|
|
66606
68074
|
hasActiveApiKeys,
|
|
68075
|
+
handleTodosTaskSubtreeTransferHttpRequest,
|
|
66607
68076
|
handleTodosTaskManifestHttpRequest,
|
|
66608
68077
|
handleTodosProjectRegistrationHttpRequest,
|
|
66609
68078
|
getWorkspaceTrustStatus,
|
|
@@ -66617,7 +68086,6 @@ export {
|
|
|
66617
68086
|
getUnlockImpact,
|
|
66618
68087
|
getTraceStats,
|
|
66619
68088
|
getTopologyDocs,
|
|
66620
|
-
getTodosStorageMode,
|
|
66621
68089
|
getTodosStorageEnvName,
|
|
66622
68090
|
getTodosStorageDatabaseUrl,
|
|
66623
68091
|
getTodosStorageDatabaseEnv,
|
|
@@ -66662,7 +68130,6 @@ export {
|
|
|
66662
68130
|
getTaskBoard,
|
|
66663
68131
|
getTask,
|
|
66664
68132
|
getStoredHandoffAsPacket,
|
|
66665
|
-
getStorageMode,
|
|
66666
68133
|
getStorageDatabaseUrl,
|
|
66667
68134
|
getStorageDatabaseEnv,
|
|
66668
68135
|
getStatus,
|
|
@@ -66921,6 +68388,9 @@ export {
|
|
|
66921
68388
|
detectInboxSourceType,
|
|
66922
68389
|
detectCyclesFromEdges,
|
|
66923
68390
|
describeTerminalNotificationRule,
|
|
68391
|
+
deriveTodosTaskSubtreeTransferRollbackPreconditionDigest,
|
|
68392
|
+
deriveTodosTaskSubtreeTransferIdempotencyKey,
|
|
68393
|
+
deriveTodosTaskSubtreeTransferApplyPreconditionDigest,
|
|
66924
68394
|
deriveTodosTaskManifestIdempotencyKey,
|
|
66925
68395
|
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66926
68396
|
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
@@ -66957,6 +68427,7 @@ export {
|
|
|
66957
68427
|
createVerificationEvidence,
|
|
66958
68428
|
createUserScaffold,
|
|
66959
68429
|
createTuiDashboardSnapshot,
|
|
68430
|
+
createTodosTaskSubtreeTransferHttpClient,
|
|
66960
68431
|
createTodosTaskManifestHttpClient,
|
|
66961
68432
|
createTodosStorageAdapter,
|
|
66962
68433
|
createTodosS3ArtifactStore,
|
|
@@ -66971,6 +68442,7 @@ export {
|
|
|
66971
68442
|
createTaskList,
|
|
66972
68443
|
createTaskBoard,
|
|
66973
68444
|
createTask,
|
|
68445
|
+
createSqliteTodosTaskSubtreeTransferAuthority,
|
|
66974
68446
|
createSqliteTodosTaskManifestAuthority,
|
|
66975
68447
|
createSessionRecoveryHandoff,
|
|
66976
68448
|
createSession,
|
|
@@ -66986,6 +68458,7 @@ export {
|
|
|
66986
68458
|
createReminder,
|
|
66987
68459
|
createReleaseCompatibilityReport,
|
|
66988
68460
|
createProject,
|
|
68461
|
+
createPostgresTodosTaskSubtreeTransferAuthority,
|
|
66989
68462
|
createPostgresTodosTaskManifestAuthority,
|
|
66990
68463
|
createPostgresTodosSyncStore,
|
|
66991
68464
|
createPostgresTodosStorageAdapter,
|
|
@@ -67154,6 +68627,8 @@ export {
|
|
|
67154
68627
|
VersionConflictError,
|
|
67155
68628
|
VERIFICATION_EVIDENCE_SCHEMA,
|
|
67156
68629
|
USER_SCAFFOLD_SCHEMA,
|
|
68630
|
+
TodosTaskSubtreeTransferHttpClient,
|
|
68631
|
+
TodosTaskSubtreeTransferError,
|
|
67157
68632
|
TodosTaskManifestHttpClient,
|
|
67158
68633
|
TodosTaskManifestError,
|
|
67159
68634
|
TodosProjectRegistrationHttpClient,
|
|
@@ -67165,6 +68640,8 @@ export {
|
|
|
67165
68640
|
TaskNotFoundError,
|
|
67166
68641
|
TaskListNotFoundError,
|
|
67167
68642
|
TUI_DASHBOARD_VIEWS,
|
|
68643
|
+
TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
68644
|
+
TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
67168
68645
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
67169
68646
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
67170
68647
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
@@ -67271,6 +68748,7 @@ export {
|
|
|
67271
68748
|
PostgresTodosProjectRegistrationBackend,
|
|
67272
68749
|
PlanProjectLinkError,
|
|
67273
68750
|
PlanNotFoundError,
|
|
68751
|
+
PackageOwnedTodosTaskSubtreeTransferAuthority,
|
|
67274
68752
|
PackageOwnedTodosTaskManifestAuthority,
|
|
67275
68753
|
PackageOwnedTodosProjectRegistrationAuthority,
|
|
67276
68754
|
PROJECT_DEPENDENCY_GRAPH_SCHEMA,
|