@hasna/todos 0.15.35 → 0.15.46
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 +3430 -827
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +66 -58
- 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-graph.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 +1897 -334
- 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/lib/instant-compare.d.ts +35 -0
- package/dist/lib/instant-compare.d.ts.map +1 -0
- package/dist/mcp/index.js +2403 -501
- package/dist/mcp.js +8 -4
- package/dist/pr-groups/postgres.d.ts.map +1 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration.js +290 -81
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +73 -58
- 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 +5161 -8425
- 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 +268 -74
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest.js +15 -6
- 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 +1878 -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 = ?");
|
|
@@ -8658,7 +8651,7 @@ function getTaskGraph(taskId, direction = "both", db) {
|
|
|
8658
8651
|
const deps = getTaskDependencies(t.id, d);
|
|
8659
8652
|
const hasUnfinishedDeps = deps.some((dep) => {
|
|
8660
8653
|
const depTask = getTask(dep.depends_on, d);
|
|
8661
|
-
return depTask && depTask.status
|
|
8654
|
+
return depTask && isBlockingDependencyStatus(depTask.status);
|
|
8662
8655
|
});
|
|
8663
8656
|
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
8664
8657
|
}
|
|
@@ -8918,7 +8911,7 @@ function getBlockingDeps(id, db) {
|
|
|
8918
8911
|
const blocking = [];
|
|
8919
8912
|
for (const dep of deps) {
|
|
8920
8913
|
const task = getTask(dep.depends_on, d);
|
|
8921
|
-
if (task && task.status
|
|
8914
|
+
if (task && isBlockingDependencyStatus(task.status))
|
|
8922
8915
|
blocking.push(task);
|
|
8923
8916
|
}
|
|
8924
8917
|
return blocking;
|
|
@@ -9242,7 +9235,7 @@ function getNextTask(agentId, filters, db) {
|
|
|
9242
9235
|
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
9243
9236
|
params.push(...filters.tags);
|
|
9244
9237
|
}
|
|
9245
|
-
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status
|
|
9238
|
+
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
|
|
9246
9239
|
const where = conditions.join(" AND ");
|
|
9247
9240
|
let recentProjectIds = [];
|
|
9248
9241
|
const assignedAliasParams = [];
|
|
@@ -9286,7 +9279,7 @@ function getActiveWork(filters, db) {
|
|
|
9286
9279
|
}
|
|
9287
9280
|
function getTasksChangedSince(since, filters, db) {
|
|
9288
9281
|
const d = db || getDatabase();
|
|
9289
|
-
const conditions = ["updated_at > ?"];
|
|
9282
|
+
const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
|
|
9290
9283
|
const params = [since];
|
|
9291
9284
|
if (filters?.project_id) {
|
|
9292
9285
|
conditions.push("project_id = ?");
|
|
@@ -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.46",
|
|
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.4",
|
|
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
|
|
@@ -27087,7 +27150,7 @@ function matchesExtraFilters(task2, filter) {
|
|
|
27087
27150
|
}
|
|
27088
27151
|
if (filter.tags?.length) {
|
|
27089
27152
|
const taskTags2 = new Set(task2.tags ?? []);
|
|
27090
|
-
if (!filter.tags.
|
|
27153
|
+
if (!filter.tags.some((tag) => taskTags2.has(tag)))
|
|
27091
27154
|
return false;
|
|
27092
27155
|
}
|
|
27093
27156
|
if (filter.include_subtasks !== true && filter.parent_id === undefined && task2.parent_id)
|
|
@@ -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);
|
|
@@ -27236,6 +27314,59 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
27236
27314
|
init_types();
|
|
27237
27315
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
27238
27316
|
init_creator_identity();
|
|
27317
|
+
|
|
27318
|
+
// src/lib/instant-compare.ts
|
|
27319
|
+
var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
|
|
27320
|
+
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
27321
|
+
function isLeapYear(year) {
|
|
27322
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
27323
|
+
}
|
|
27324
|
+
function sqliteJulianDay(value) {
|
|
27325
|
+
const m = SQLITE_STAMP.exec(value);
|
|
27326
|
+
if (!m)
|
|
27327
|
+
return null;
|
|
27328
|
+
const year = Number(m[1]);
|
|
27329
|
+
const month = Number(m[2]);
|
|
27330
|
+
const day = Number(m[3]);
|
|
27331
|
+
const hour = m[4] === undefined ? 0 : Number(m[4]);
|
|
27332
|
+
const minute = m[5] === undefined ? 0 : Number(m[5]);
|
|
27333
|
+
const second = m[6] === undefined ? 0 : Number(m[6]);
|
|
27334
|
+
const frac = m[7];
|
|
27335
|
+
const sign = m[8];
|
|
27336
|
+
const offsetHour = m[9];
|
|
27337
|
+
const offsetMinute = m[10];
|
|
27338
|
+
if (month < 1 || month > 12)
|
|
27339
|
+
return null;
|
|
27340
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
27341
|
+
return null;
|
|
27342
|
+
const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
|
|
27343
|
+
if (day < 1 || day > maxDay)
|
|
27344
|
+
return null;
|
|
27345
|
+
let offsetMinutes = 0;
|
|
27346
|
+
if (sign !== undefined) {
|
|
27347
|
+
const oh = Number(offsetHour ?? "0");
|
|
27348
|
+
const om = Number(offsetMinute ?? "0");
|
|
27349
|
+
if (oh > 23 || om > 59)
|
|
27350
|
+
return null;
|
|
27351
|
+
offsetMinutes = oh * 60 + om;
|
|
27352
|
+
if (sign === "-")
|
|
27353
|
+
offsetMinutes = -offsetMinutes;
|
|
27354
|
+
}
|
|
27355
|
+
const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
|
|
27356
|
+
const ms = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
27357
|
+
return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
|
|
27358
|
+
}
|
|
27359
|
+
function changedSinceStampNewer(stamp, since) {
|
|
27360
|
+
const stampJd = sqliteJulianDay(stamp);
|
|
27361
|
+
if (stampJd === null)
|
|
27362
|
+
return true;
|
|
27363
|
+
const sinceJd = sqliteJulianDay(since);
|
|
27364
|
+
if (sinceJd === null)
|
|
27365
|
+
return false;
|
|
27366
|
+
return stampJd > sinceJd;
|
|
27367
|
+
}
|
|
27368
|
+
|
|
27369
|
+
// src/storage/postgres-adapter.ts
|
|
27239
27370
|
init_stale_lock_handoff();
|
|
27240
27371
|
|
|
27241
27372
|
// src/storage/postgres-sync.ts
|
|
@@ -27737,7 +27868,26 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27737
27868
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
27738
27869
|
update: (id, input) => updatePlan2(id, input, store),
|
|
27739
27870
|
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
27740
|
-
delete: (id, context) => store.deletePlan(id, context)
|
|
27871
|
+
delete: (id, context) => store.deletePlan(id, context),
|
|
27872
|
+
addComment: (input, context) => addPlanComment2(input, store, context),
|
|
27873
|
+
getComments: async (planId) => {
|
|
27874
|
+
const pages = [];
|
|
27875
|
+
let before;
|
|
27876
|
+
while (true) {
|
|
27877
|
+
const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
|
|
27878
|
+
if (page.length === 0)
|
|
27879
|
+
break;
|
|
27880
|
+
pages.unshift(page);
|
|
27881
|
+
if (page.length < 1000)
|
|
27882
|
+
break;
|
|
27883
|
+
const oldest = page[0];
|
|
27884
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
27885
|
+
}
|
|
27886
|
+
return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27887
|
+
},
|
|
27888
|
+
getCommentsPage: async (planId, options2) => {
|
|
27889
|
+
return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27890
|
+
}
|
|
27741
27891
|
},
|
|
27742
27892
|
planProjectLinks: {
|
|
27743
27893
|
apply: (input, context) => store.applyPlanProjectLink(input, context),
|
|
@@ -27831,11 +27981,16 @@ class PostgresJsonRecordStore {
|
|
|
27831
27981
|
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
27832
27982
|
}
|
|
27833
27983
|
async ensureSchema() {
|
|
27834
|
-
this.schemaReady
|
|
27835
|
-
|
|
27836
|
-
|
|
27837
|
-
|
|
27838
|
-
|
|
27984
|
+
if (!this.schemaReady) {
|
|
27985
|
+
this.schemaReady = (async () => {
|
|
27986
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
27987
|
+
await this.options.client.query(sql);
|
|
27988
|
+
}
|
|
27989
|
+
})().catch((error) => {
|
|
27990
|
+
this.schemaReady = null;
|
|
27991
|
+
throw error;
|
|
27992
|
+
});
|
|
27993
|
+
}
|
|
27839
27994
|
await this.schemaReady;
|
|
27840
27995
|
}
|
|
27841
27996
|
async get(type, id) {
|
|
@@ -27955,6 +28110,27 @@ class PostgresJsonRecordStore {
|
|
|
27955
28110
|
LIMIT $${params.length}`, params);
|
|
27956
28111
|
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
27957
28112
|
}
|
|
28113
|
+
async listPlanComments(planId, options = {}) {
|
|
28114
|
+
await this.ensureSchema();
|
|
28115
|
+
const limit = options.limit ?? 100;
|
|
28116
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
28117
|
+
throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
|
|
28118
|
+
}
|
|
28119
|
+
const params = [this.service, planId];
|
|
28120
|
+
let cursorPredicate = "";
|
|
28121
|
+
if (options.before) {
|
|
28122
|
+
params.push(options.before.created_at, options.before.id);
|
|
28123
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
28124
|
+
}
|
|
28125
|
+
params.push(limit);
|
|
28126
|
+
const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
|
|
28127
|
+
WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
|
|
28128
|
+
AND payload->>'plan_id' = $2
|
|
28129
|
+
${cursorPredicate}
|
|
28130
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
28131
|
+
LIMIT $${params.length}`, params);
|
|
28132
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
28133
|
+
}
|
|
27958
28134
|
async listRecords(type) {
|
|
27959
28135
|
await this.ensureSchema();
|
|
27960
28136
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -29116,6 +29292,15 @@ class PostgresJsonRecordStore {
|
|
|
29116
29292
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
29117
29293
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29118
29294
|
RETURNING 1
|
|
29295
|
+
), deleted_plan_comments AS (
|
|
29296
|
+
UPDATE ${this.tableName} r SET
|
|
29297
|
+
deleted_at = $3::timestamptz,
|
|
29298
|
+
updated_at = $3::timestamptz,
|
|
29299
|
+
source_machine_id = COALESCE($4, r.source_machine_id),
|
|
29300
|
+
version = COALESCE(r.version, 0) + 1
|
|
29301
|
+
WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
|
|
29302
|
+
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29303
|
+
RETURNING 1
|
|
29119
29304
|
), deleted_plan AS (
|
|
29120
29305
|
UPDATE ${this.tableName} r SET
|
|
29121
29306
|
deleted_at = $3::timestamptz,
|
|
@@ -29291,7 +29476,8 @@ async function updateTask2(id, input, store, context) {
|
|
|
29291
29476
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
29292
29477
|
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
29293
29478
|
created_by: existing.created_by,
|
|
29294
|
-
completed_at:
|
|
29479
|
+
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,
|
|
29480
|
+
started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
|
|
29295
29481
|
};
|
|
29296
29482
|
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
29297
29483
|
operation: "update",
|
|
@@ -29336,7 +29522,8 @@ async function failTask2(id, agentId, reason, options, store) {
|
|
|
29336
29522
|
status: "failed",
|
|
29337
29523
|
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
29338
29524
|
reason: reason ?? task2.reason,
|
|
29339
|
-
retry_after: options?.retry_after ?? task2.retry_after
|
|
29525
|
+
retry_after: options?.retry_after ?? task2.retry_after,
|
|
29526
|
+
completed_at: task2.completed_at ?? new Date().toISOString()
|
|
29340
29527
|
}, store);
|
|
29341
29528
|
if (!options?.retry)
|
|
29342
29529
|
return { task: failed };
|
|
@@ -29568,7 +29755,7 @@ async function getActiveWork2(filters, store) {
|
|
|
29568
29755
|
}));
|
|
29569
29756
|
}
|
|
29570
29757
|
async function getChangedSince(since, filters, store) {
|
|
29571
|
-
return (await listTasks2(filters ?? {}, store)).filter((task2) => task2.updated_at
|
|
29758
|
+
return (await listTasks2(filters ?? {}, store)).filter((task2) => changedSinceStampNewer(task2.updated_at ?? "", since));
|
|
29572
29759
|
}
|
|
29573
29760
|
async function createProject2(input, store, context) {
|
|
29574
29761
|
const timestamp3 = new Date().toISOString();
|
|
@@ -29784,16 +29971,22 @@ async function createTaskList2(input, store, context) {
|
|
|
29784
29971
|
async function updateTaskList2(id, input, store) {
|
|
29785
29972
|
const list = await requireRecord("task_lists", id, store);
|
|
29786
29973
|
const patch = definedPatch(input);
|
|
29974
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
|
|
29787
29975
|
if (input.slug !== undefined) {
|
|
29788
29976
|
const slug = slugifyRaw(input.slug);
|
|
29789
29977
|
if (!slug)
|
|
29790
29978
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
29791
|
-
|
|
29979
|
+
patch.slug = slug;
|
|
29980
|
+
}
|
|
29981
|
+
const effectiveSlug = patch.slug ?? list.slug;
|
|
29982
|
+
if (projectId !== list.project_id || effectiveSlug !== list.slug) {
|
|
29983
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
|
|
29792
29984
|
if (duplicate) {
|
|
29793
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
29985
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
29794
29986
|
}
|
|
29795
|
-
patch.slug = slug;
|
|
29796
29987
|
}
|
|
29988
|
+
if (projectId !== list.project_id)
|
|
29989
|
+
patch.project_id = projectId;
|
|
29797
29990
|
return store.upsert("task_lists", {
|
|
29798
29991
|
...list,
|
|
29799
29992
|
...patch,
|
|
@@ -29886,6 +30079,22 @@ async function addComment2(input, store, context) {
|
|
|
29886
30079
|
function redactComment(comment) {
|
|
29887
30080
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
29888
30081
|
}
|
|
30082
|
+
async function addPlanComment2(input, store, context) {
|
|
30083
|
+
const comment = {
|
|
30084
|
+
id: randomUUID3(),
|
|
30085
|
+
plan_id: input.plan_id,
|
|
30086
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
30087
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
30088
|
+
content: redactEvidenceText(input.content),
|
|
30089
|
+
type: input.type ?? "comment",
|
|
30090
|
+
progress_pct: input.progress_pct ?? null,
|
|
30091
|
+
created_at: new Date().toISOString()
|
|
30092
|
+
};
|
|
30093
|
+
return store.upsert("plan_comments", comment, context);
|
|
30094
|
+
}
|
|
30095
|
+
function redactPlanComment(comment) {
|
|
30096
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
30097
|
+
}
|
|
29889
30098
|
async function exportSnapshot(store) {
|
|
29890
30099
|
return {
|
|
29891
30100
|
exportedAt: new Date().toISOString(),
|
|
@@ -34901,14 +35110,23 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
34901
35110
|
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
34902
35111
|
}
|
|
34903
35112
|
async ensureSchema() {
|
|
34904
|
-
this.schemaReady
|
|
34905
|
-
|
|
34906
|
-
|
|
34907
|
-
|
|
34908
|
-
|
|
34909
|
-
|
|
35113
|
+
if (this.schemaReady === null) {
|
|
35114
|
+
const attempt = (async () => {
|
|
35115
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
35116
|
+
await this.client.query(statement);
|
|
35117
|
+
}
|
|
35118
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
35119
|
+
await this.client.query(statement);
|
|
35120
|
+
}
|
|
35121
|
+
})();
|
|
35122
|
+
this.schemaReady = attempt;
|
|
35123
|
+
try {
|
|
35124
|
+
await attempt;
|
|
35125
|
+
} catch (error) {
|
|
35126
|
+
this.schemaReady = null;
|
|
35127
|
+
throw error;
|
|
34910
35128
|
}
|
|
34911
|
-
}
|
|
35129
|
+
}
|
|
34912
35130
|
await this.schemaReady;
|
|
34913
35131
|
}
|
|
34914
35132
|
async transaction(fn) {
|
|
@@ -42566,12 +42784,21 @@ class PostgresTodosTaskManifestBackend {
|
|
|
42566
42784
|
this.tenantId = options.tenantId ?? "default";
|
|
42567
42785
|
}
|
|
42568
42786
|
async ensureSchema() {
|
|
42569
|
-
this.schemaReady
|
|
42570
|
-
|
|
42571
|
-
|
|
42572
|
-
|
|
42573
|
-
|
|
42574
|
-
|
|
42787
|
+
if (this.schemaReady === null) {
|
|
42788
|
+
const attempt = (async () => {
|
|
42789
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
42790
|
+
await this.client.query(sql);
|
|
42791
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
42792
|
+
await this.client.query(sql);
|
|
42793
|
+
})();
|
|
42794
|
+
this.schemaReady = attempt;
|
|
42795
|
+
try {
|
|
42796
|
+
await attempt;
|
|
42797
|
+
} catch (error) {
|
|
42798
|
+
this.schemaReady = null;
|
|
42799
|
+
throw error;
|
|
42800
|
+
}
|
|
42801
|
+
}
|
|
42575
42802
|
await this.schemaReady;
|
|
42576
42803
|
}
|
|
42577
42804
|
async insertSync(tx, objectType2, objectId, payload, now4) {
|
|
@@ -43624,6 +43851,1323 @@ class TodosTaskManifestHttpClient {
|
|
|
43624
43851
|
function createTodosTaskManifestHttpClient(options) {
|
|
43625
43852
|
return new TodosTaskManifestHttpClient(options);
|
|
43626
43853
|
}
|
|
43854
|
+
// src/task-subtree-transfer/types.ts
|
|
43855
|
+
var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1";
|
|
43856
|
+
var TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1;
|
|
43857
|
+
|
|
43858
|
+
class TodosTaskSubtreeTransferError extends Error {
|
|
43859
|
+
code;
|
|
43860
|
+
details;
|
|
43861
|
+
constructor(code, message, details = {}) {
|
|
43862
|
+
super(message);
|
|
43863
|
+
this.code = code;
|
|
43864
|
+
this.details = details;
|
|
43865
|
+
this.name = "TodosTaskSubtreeTransferError";
|
|
43866
|
+
}
|
|
43867
|
+
}
|
|
43868
|
+
|
|
43869
|
+
// src/task-subtree-transfer/canonical.ts
|
|
43870
|
+
function taskSubtreeTransferRequestDigest(input) {
|
|
43871
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43872
|
+
return canonicalDigest(request);
|
|
43873
|
+
}
|
|
43874
|
+
function taskSubtreeTransferRollbackRequestDigest(input) {
|
|
43875
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43876
|
+
return canonicalDigest(request);
|
|
43877
|
+
}
|
|
43878
|
+
function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
|
|
43879
|
+
return `tstk_${canonicalDigest({
|
|
43880
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
43881
|
+
...input
|
|
43882
|
+
}).slice(0, 48)}`;
|
|
43883
|
+
}
|
|
43884
|
+
|
|
43885
|
+
// src/task-subtree-transfer/planner.ts
|
|
43886
|
+
function sortedTaskImage(task3) {
|
|
43887
|
+
return {
|
|
43888
|
+
task_id: task3.task_id,
|
|
43889
|
+
project_id: task3.project_id,
|
|
43890
|
+
parent_id: task3.parent_id,
|
|
43891
|
+
plan_id: task3.plan_id,
|
|
43892
|
+
task_list_id: task3.task_list_id,
|
|
43893
|
+
version: task3.version,
|
|
43894
|
+
updated_at: task3.updated_at
|
|
43895
|
+
};
|
|
43896
|
+
}
|
|
43897
|
+
function sourcePopulationDigest(tasks) {
|
|
43898
|
+
return canonicalDigest(tasks.map((task3) => ({
|
|
43899
|
+
...sortedTaskImage(task3),
|
|
43900
|
+
archived_at: task3.archived_at ?? null
|
|
43901
|
+
})).sort((left, right) => left.task_id.localeCompare(right.task_id)));
|
|
43902
|
+
}
|
|
43903
|
+
function closure(tasks, rootTaskId) {
|
|
43904
|
+
const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
|
|
43905
|
+
if (!byId.has(rootTaskId)) {
|
|
43906
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
|
|
43907
|
+
}
|
|
43908
|
+
const children = new Map;
|
|
43909
|
+
for (const task3 of tasks) {
|
|
43910
|
+
if (!task3.parent_id)
|
|
43911
|
+
continue;
|
|
43912
|
+
const siblings = children.get(task3.parent_id) ?? [];
|
|
43913
|
+
siblings.push(task3.task_id);
|
|
43914
|
+
children.set(task3.parent_id, siblings);
|
|
43915
|
+
}
|
|
43916
|
+
for (const ids2 of children.values())
|
|
43917
|
+
ids2.sort();
|
|
43918
|
+
const visiting = new Set;
|
|
43919
|
+
const visited = new Set;
|
|
43920
|
+
const result = [];
|
|
43921
|
+
const visit = (id) => {
|
|
43922
|
+
if (visiting.has(id)) {
|
|
43923
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
|
|
43924
|
+
}
|
|
43925
|
+
if (visited.has(id))
|
|
43926
|
+
return;
|
|
43927
|
+
const task3 = byId.get(id);
|
|
43928
|
+
if (!task3) {
|
|
43929
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
|
|
43930
|
+
}
|
|
43931
|
+
visiting.add(id);
|
|
43932
|
+
result.push(task3);
|
|
43933
|
+
for (const child of children.get(id) ?? [])
|
|
43934
|
+
visit(child);
|
|
43935
|
+
visiting.delete(id);
|
|
43936
|
+
visited.add(id);
|
|
43937
|
+
};
|
|
43938
|
+
visit(rootTaskId);
|
|
43939
|
+
return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
43940
|
+
}
|
|
43941
|
+
function prepareTransfer(snapshot, input) {
|
|
43942
|
+
if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
|
|
43943
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
|
|
43944
|
+
}
|
|
43945
|
+
const moved = closure(snapshot.source_tasks, input.root_task_id);
|
|
43946
|
+
const movedIds = new Set(moved.map((task3) => task3.task_id));
|
|
43947
|
+
if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
|
|
43948
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
|
|
43949
|
+
}
|
|
43950
|
+
const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
|
|
43951
|
+
const allPlanTasks = new Map;
|
|
43952
|
+
for (const task3 of snapshot.plan_tasks) {
|
|
43953
|
+
if (!task3.plan_id)
|
|
43954
|
+
continue;
|
|
43955
|
+
const rows = allPlanTasks.get(task3.plan_id) ?? [];
|
|
43956
|
+
rows.push(task3);
|
|
43957
|
+
allPlanTasks.set(task3.plan_id, rows);
|
|
43958
|
+
}
|
|
43959
|
+
const contained = [];
|
|
43960
|
+
const shared = [];
|
|
43961
|
+
const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
|
|
43962
|
+
for (const planId of planIds) {
|
|
43963
|
+
const plan = planById.get(planId);
|
|
43964
|
+
if (!plan || plan.project_id !== input.source_project_id) {
|
|
43965
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
|
|
43966
|
+
}
|
|
43967
|
+
const members = allPlanTasks.get(planId) ?? [];
|
|
43968
|
+
if (members.length === 0) {
|
|
43969
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
|
|
43970
|
+
}
|
|
43971
|
+
if (members.every((task3) => movedIds.has(task3.task_id)))
|
|
43972
|
+
contained.push(planId);
|
|
43973
|
+
else
|
|
43974
|
+
shared.push(planId);
|
|
43975
|
+
}
|
|
43976
|
+
const inspection = {
|
|
43977
|
+
source_project_id: input.source_project_id,
|
|
43978
|
+
destination_project_id: input.destination_project_id,
|
|
43979
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
43980
|
+
root_task_id: input.root_task_id,
|
|
43981
|
+
destination_parent_id: input.destination_parent_id,
|
|
43982
|
+
expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
|
|
43983
|
+
source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
|
|
43984
|
+
expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
|
|
43985
|
+
contained_plan_ids: contained,
|
|
43986
|
+
shared_plan_ids: shared,
|
|
43987
|
+
complete: true
|
|
43988
|
+
};
|
|
43989
|
+
return {
|
|
43990
|
+
inspection,
|
|
43991
|
+
prior_tasks: moved.map(sortedTaskImage),
|
|
43992
|
+
prior_plans: contained.map((id) => planById.get(id)),
|
|
43993
|
+
task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
|
|
43994
|
+
};
|
|
43995
|
+
}
|
|
43996
|
+
function validateApplySnapshot(prepared, request, plans) {
|
|
43997
|
+
if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
|
|
43998
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
|
|
43999
|
+
}
|
|
44000
|
+
if (request.source_population_digest !== prepared.inspection.source_population_digest) {
|
|
44001
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
|
|
44002
|
+
}
|
|
44003
|
+
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));
|
|
44004
|
+
if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
|
|
44005
|
+
const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
|
|
44006
|
+
const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
|
|
44007
|
+
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");
|
|
44008
|
+
}
|
|
44009
|
+
const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
|
|
44010
|
+
if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
|
|
44011
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
|
|
44012
|
+
expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
|
|
44013
|
+
received_shared_plan_ids: requestedSources
|
|
44014
|
+
});
|
|
44015
|
+
}
|
|
44016
|
+
const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
|
|
44017
|
+
const destinationIds = new Set;
|
|
44018
|
+
for (const mapping of request.shared_plan_splits) {
|
|
44019
|
+
if (destinationIds.has(mapping.destination_plan_id)) {
|
|
44020
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
|
|
44021
|
+
}
|
|
44022
|
+
destinationIds.add(mapping.destination_plan_id);
|
|
44023
|
+
const destination = planById.get(mapping.destination_plan_id);
|
|
44024
|
+
if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
|
|
44025
|
+
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 });
|
|
44026
|
+
}
|
|
44027
|
+
for (const [taskId, planId] of prepared.task_plan_targets) {
|
|
44028
|
+
if (planId === mapping.source_plan_id) {
|
|
44029
|
+
prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
|
|
44030
|
+
}
|
|
44031
|
+
}
|
|
44032
|
+
}
|
|
44033
|
+
}
|
|
44034
|
+
|
|
44035
|
+
// src/task-subtree-transfer/schema-sql.ts
|
|
44036
|
+
function sqliteTodosTaskSubtreeTransferSchemaSql() {
|
|
44037
|
+
return `
|
|
44038
|
+
CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
44039
|
+
receipt_id TEXT PRIMARY KEY,
|
|
44040
|
+
tenant_id TEXT NOT NULL,
|
|
44041
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
44042
|
+
operation_id TEXT NOT NULL,
|
|
44043
|
+
step_id TEXT NOT NULL,
|
|
44044
|
+
idempotency_key TEXT NOT NULL,
|
|
44045
|
+
request_digest TEXT NOT NULL,
|
|
44046
|
+
precondition_digest TEXT NOT NULL,
|
|
44047
|
+
result_digest TEXT NOT NULL,
|
|
44048
|
+
apply_receipt_id TEXT,
|
|
44049
|
+
request_json TEXT NOT NULL,
|
|
44050
|
+
result_json TEXT NOT NULL,
|
|
44051
|
+
created_at TEXT NOT NULL,
|
|
44052
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
44053
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
44054
|
+
);
|
|
44055
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
44056
|
+
BEFORE UPDATE ON todos_task_subtree_transfer_receipts BEGIN
|
|
44057
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
44058
|
+
END;
|
|
44059
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_delete
|
|
44060
|
+
BEFORE DELETE ON todos_task_subtree_transfer_receipts BEGIN
|
|
44061
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
44062
|
+
END;
|
|
44063
|
+
`;
|
|
44064
|
+
}
|
|
44065
|
+
function ensureSqliteTodosTaskSubtreeTransferSchema(db) {
|
|
44066
|
+
db.exec(sqliteTodosTaskSubtreeTransferSchemaSql());
|
|
44067
|
+
}
|
|
44068
|
+
function postgresTodosTaskSubtreeTransferSchemaSql() {
|
|
44069
|
+
return [
|
|
44070
|
+
`CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
44071
|
+
receipt_id text PRIMARY KEY,
|
|
44072
|
+
tenant_id text NOT NULL,
|
|
44073
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
44074
|
+
operation_id text NOT NULL,
|
|
44075
|
+
step_id text NOT NULL,
|
|
44076
|
+
idempotency_key text NOT NULL,
|
|
44077
|
+
request_digest text NOT NULL,
|
|
44078
|
+
precondition_digest text NOT NULL,
|
|
44079
|
+
result_digest text NOT NULL,
|
|
44080
|
+
apply_receipt_id text,
|
|
44081
|
+
request_json jsonb NOT NULL,
|
|
44082
|
+
result_json jsonb NOT NULL,
|
|
44083
|
+
created_at timestamptz NOT NULL,
|
|
44084
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
44085
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
44086
|
+
)`,
|
|
44087
|
+
`CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
|
|
44088
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
44089
|
+
BEGIN
|
|
44090
|
+
RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
|
|
44091
|
+
END $$`,
|
|
44092
|
+
`DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
44093
|
+
ON todos_task_subtree_transfer_receipts`,
|
|
44094
|
+
`CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
|
|
44095
|
+
BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
|
|
44096
|
+
FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
|
|
44097
|
+
];
|
|
44098
|
+
}
|
|
44099
|
+
|
|
44100
|
+
// src/task-subtree-transfer/postgres.ts
|
|
44101
|
+
function safeIdentifier3(value, fallback) {
|
|
44102
|
+
const candidate = value ?? fallback;
|
|
44103
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
|
|
44104
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
|
|
44105
|
+
}
|
|
44106
|
+
return candidate;
|
|
44107
|
+
}
|
|
44108
|
+
function timestamp4(value) {
|
|
44109
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
44110
|
+
}
|
|
44111
|
+
function payloadOf(row) {
|
|
44112
|
+
return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
|
|
44113
|
+
}
|
|
44114
|
+
function taskRecord(row) {
|
|
44115
|
+
const payload = payloadOf(row);
|
|
44116
|
+
return {
|
|
44117
|
+
task_id: String(payload.id ?? row.object_id),
|
|
44118
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44119
|
+
parent_id: payload.parent_id == null ? null : String(payload.parent_id),
|
|
44120
|
+
plan_id: payload.plan_id == null ? null : String(payload.plan_id),
|
|
44121
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44122
|
+
version: Number(payload.version ?? row.version ?? 1),
|
|
44123
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at),
|
|
44124
|
+
archived_at: payload.archived_at == null ? null : String(payload.archived_at)
|
|
44125
|
+
};
|
|
44126
|
+
}
|
|
44127
|
+
function planRecord(row) {
|
|
44128
|
+
const payload = payloadOf(row);
|
|
44129
|
+
return {
|
|
44130
|
+
plan_id: String(payload.id ?? row.object_id),
|
|
44131
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44132
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44133
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at)
|
|
44134
|
+
};
|
|
44135
|
+
}
|
|
44136
|
+
|
|
44137
|
+
class PostgresTodosTaskSubtreeTransferBackend {
|
|
44138
|
+
client;
|
|
44139
|
+
kind = "postgresql";
|
|
44140
|
+
service;
|
|
44141
|
+
tableName;
|
|
44142
|
+
tenantId;
|
|
44143
|
+
schemaReady = null;
|
|
44144
|
+
constructor(client, options = {}) {
|
|
44145
|
+
this.client = client;
|
|
44146
|
+
this.service = options.service ?? "todos";
|
|
44147
|
+
this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
|
|
44148
|
+
this.tenantId = options.tenantId ?? "default";
|
|
44149
|
+
}
|
|
44150
|
+
async ensureSchema() {
|
|
44151
|
+
if (this.schemaReady === null) {
|
|
44152
|
+
const attempt = (async () => {
|
|
44153
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
44154
|
+
await this.client.query(sql);
|
|
44155
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
44156
|
+
await this.client.query(sql);
|
|
44157
|
+
})();
|
|
44158
|
+
this.schemaReady = attempt;
|
|
44159
|
+
try {
|
|
44160
|
+
await attempt;
|
|
44161
|
+
} catch (error) {
|
|
44162
|
+
this.schemaReady = null;
|
|
44163
|
+
throw error;
|
|
44164
|
+
}
|
|
44165
|
+
}
|
|
44166
|
+
await this.schemaReady;
|
|
44167
|
+
}
|
|
44168
|
+
async snapshot(client, input, forUpdate = false) {
|
|
44169
|
+
const lock = forUpdate ? " FOR UPDATE" : "";
|
|
44170
|
+
const source9 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44171
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
|
|
44172
|
+
AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
|
|
44173
|
+
const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44174
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44175
|
+
const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44176
|
+
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44177
|
+
const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44178
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
44179
|
+
AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
|
|
44180
|
+
const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44181
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
44182
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
|
|
44183
|
+
const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44184
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44185
|
+
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;
|
|
44186
|
+
return {
|
|
44187
|
+
source_tasks: source9.rows.map(taskRecord),
|
|
44188
|
+
plan_tasks: allTasks.rows.map(taskRecord),
|
|
44189
|
+
plans: plans.rows.map(planRecord),
|
|
44190
|
+
destination_project_found: destinationProject.rows.length === 1,
|
|
44191
|
+
destination_task_list_found: destinationList.rows.length === 1,
|
|
44192
|
+
destination_parent_found: destinationParent
|
|
44193
|
+
};
|
|
44194
|
+
}
|
|
44195
|
+
async inspect(input) {
|
|
44196
|
+
await this.ensureSchema();
|
|
44197
|
+
return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
|
|
44198
|
+
}
|
|
44199
|
+
async insertPayload(client, task3, update, now4) {
|
|
44200
|
+
const current = await client.query(`SELECT payload FROM ${this.tableName}
|
|
44201
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44202
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
44203
|
+
const row = current.rows[0];
|
|
44204
|
+
if (!row)
|
|
44205
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
|
|
44206
|
+
const payload = payloadOf(row);
|
|
44207
|
+
if (Number(payload.version ?? 1) !== task3.version) {
|
|
44208
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
|
|
44209
|
+
}
|
|
44210
|
+
const next = {
|
|
44211
|
+
...payload,
|
|
44212
|
+
...update,
|
|
44213
|
+
version: task3.version + 1,
|
|
44214
|
+
updated_at: now4
|
|
44215
|
+
};
|
|
44216
|
+
const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44217
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
|
|
44218
|
+
AND deleted_at IS NULL AND version = $6
|
|
44219
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
|
|
44220
|
+
if (result.rows.length !== 1) {
|
|
44221
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
|
|
44222
|
+
}
|
|
44223
|
+
}
|
|
44224
|
+
async apply(input, options) {
|
|
44225
|
+
await this.ensureSchema();
|
|
44226
|
+
if (!this.client.transaction) {
|
|
44227
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
|
|
44228
|
+
}
|
|
44229
|
+
return this.client.transaction(async (tx) => {
|
|
44230
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44231
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44232
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44233
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44234
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
44235
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44236
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44237
|
+
if (existing.rows[0]) {
|
|
44238
|
+
const row = existing.rows[0];
|
|
44239
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
|
|
44240
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
|
|
44241
|
+
}
|
|
44242
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
|
|
44243
|
+
}
|
|
44244
|
+
const snapshot = await this.snapshot(tx, input, true);
|
|
44245
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44246
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44247
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44248
|
+
const priorImage = {
|
|
44249
|
+
tasks: prepared.prior_tasks,
|
|
44250
|
+
plans: prepared.prior_plans
|
|
44251
|
+
};
|
|
44252
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44253
|
+
await this.insertPayload(tx, task3, {
|
|
44254
|
+
project_id: input.destination_project_id,
|
|
44255
|
+
task_list_id: input.destination_task_list_id,
|
|
44256
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44257
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
|
|
44258
|
+
}, now4);
|
|
44259
|
+
}
|
|
44260
|
+
if (options.faultInjector && await options.faultInjector("after_task_writes"))
|
|
44261
|
+
throw new Error("Injected task-subtree-transfer fault at after_task_writes");
|
|
44262
|
+
for (const plan of prepared.prior_plans) {
|
|
44263
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
44264
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
44265
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
|
|
44266
|
+
const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44267
|
+
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) {
|
|
44268
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
|
|
44269
|
+
}
|
|
44270
|
+
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
|
|
44271
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44272
|
+
RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
|
|
44273
|
+
if (result2.rows.length !== 1) {
|
|
44274
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44275
|
+
}
|
|
44276
|
+
}
|
|
44277
|
+
if (options.faultInjector && await options.faultInjector("after_plan_writes"))
|
|
44278
|
+
throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
|
|
44279
|
+
const postImage = {
|
|
44280
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44281
|
+
...task3,
|
|
44282
|
+
project_id: input.destination_project_id,
|
|
44283
|
+
task_list_id: input.destination_task_list_id,
|
|
44284
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44285
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44286
|
+
version: task3.version + 1,
|
|
44287
|
+
updated_at: now4
|
|
44288
|
+
})),
|
|
44289
|
+
plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
|
|
44290
|
+
};
|
|
44291
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
|
|
44292
|
+
const receipt = {
|
|
44293
|
+
receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
|
|
44294
|
+
authority: "todos",
|
|
44295
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44296
|
+
schema_version: 1,
|
|
44297
|
+
kind: "apply",
|
|
44298
|
+
operation_id: input.operation_id,
|
|
44299
|
+
step_id: input.step_id,
|
|
44300
|
+
idempotency_key: input.idempotency_key,
|
|
44301
|
+
request_digest: requestDigest,
|
|
44302
|
+
precondition_digest: input.precondition_digest,
|
|
44303
|
+
result_digest: resultDigest,
|
|
44304
|
+
apply_receipt_id: null,
|
|
44305
|
+
source_project_id: input.source_project_id,
|
|
44306
|
+
destination_project_id: input.destination_project_id,
|
|
44307
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44308
|
+
root_task_id: input.root_task_id,
|
|
44309
|
+
source_population_digest: input.source_population_digest,
|
|
44310
|
+
prior_image: priorImage,
|
|
44311
|
+
post_image: postImage,
|
|
44312
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44313
|
+
created_at: now4
|
|
44314
|
+
};
|
|
44315
|
+
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 };
|
|
44316
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44317
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44318
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44319
|
+
request_json, result_json, created_at
|
|
44320
|
+
) 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]);
|
|
44321
|
+
if (options.faultInjector && await options.faultInjector("after_receipt_write"))
|
|
44322
|
+
throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
|
|
44323
|
+
return result;
|
|
44324
|
+
});
|
|
44325
|
+
}
|
|
44326
|
+
async readExact(receiptId2) {
|
|
44327
|
+
await this.ensureSchema();
|
|
44328
|
+
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]);
|
|
44329
|
+
if (!row.rows[0])
|
|
44330
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
|
|
44331
|
+
return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
|
|
44332
|
+
}
|
|
44333
|
+
async rollback(input, options) {
|
|
44334
|
+
await this.ensureSchema();
|
|
44335
|
+
if (!this.client.transaction)
|
|
44336
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
|
|
44337
|
+
return this.client.transaction(async (tx) => {
|
|
44338
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44339
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44340
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44341
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44342
|
+
WHERE tenant_id = $1 AND kind = 'rollback'
|
|
44343
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44344
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44345
|
+
if (existing.rows[0]) {
|
|
44346
|
+
const row = existing.rows[0];
|
|
44347
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
|
|
44348
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
|
|
44349
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
|
|
44350
|
+
}
|
|
44351
|
+
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]);
|
|
44352
|
+
if (!applyRow.rows[0])
|
|
44353
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44354
|
+
const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
|
|
44355
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44356
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44357
|
+
}
|
|
44358
|
+
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 });
|
|
44359
|
+
if (input.precondition_digest !== expectedPrecondition)
|
|
44360
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44361
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44362
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44363
|
+
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]);
|
|
44364
|
+
if (!row.rows[0])
|
|
44365
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
|
|
44366
|
+
const payload = payloadOf(row.rows[0]);
|
|
44367
|
+
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)
|
|
44368
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
|
|
44369
|
+
}
|
|
44370
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44371
|
+
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]);
|
|
44372
|
+
const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44373
|
+
if (!payload)
|
|
44374
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
|
|
44375
|
+
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 };
|
|
44376
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44377
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
|
|
44378
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
|
|
44379
|
+
if (result2.rows.length !== 1) {
|
|
44380
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
|
|
44381
|
+
}
|
|
44382
|
+
}
|
|
44383
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
|
|
44384
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
|
|
44385
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44386
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44387
|
+
const result2 = await tx.query(`UPDATE ${this.tableName}
|
|
44388
|
+
SET payload = jsonb_set(
|
|
44389
|
+
jsonb_set(
|
|
44390
|
+
jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
|
|
44391
|
+
'{task_list_id}',
|
|
44392
|
+
COALESCE(to_jsonb($2::text), 'null'::jsonb)
|
|
44393
|
+
),
|
|
44394
|
+
'{updated_at}',
|
|
44395
|
+
to_jsonb($3::text)
|
|
44396
|
+
), updated_at = $3
|
|
44397
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44398
|
+
AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
|
|
44399
|
+
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]);
|
|
44400
|
+
if (result2.rows.length !== 1) {
|
|
44401
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44402
|
+
}
|
|
44403
|
+
}
|
|
44404
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
|
|
44405
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
|
|
44406
|
+
const restored = {
|
|
44407
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
|
|
44408
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44409
|
+
};
|
|
44410
|
+
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 });
|
|
44411
|
+
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 };
|
|
44412
|
+
const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
|
|
44413
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44414
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44415
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44416
|
+
request_json, result_json, created_at
|
|
44417
|
+
) 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]);
|
|
44418
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
|
|
44419
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
|
|
44420
|
+
return result;
|
|
44421
|
+
});
|
|
44422
|
+
}
|
|
44423
|
+
}
|
|
44424
|
+
|
|
44425
|
+
// src/task-subtree-transfer/sqlite.ts
|
|
44426
|
+
var sqliteTails2 = new WeakMap;
|
|
44427
|
+
async function fault3(options, point) {
|
|
44428
|
+
if (options.faultInjector && await options.faultInjector(point) === true) {
|
|
44429
|
+
throw new Error(`Injected task-subtree-transfer fault at ${point}`);
|
|
44430
|
+
}
|
|
44431
|
+
}
|
|
44432
|
+
function parseResult(value, duplicate) {
|
|
44433
|
+
return { ...JSON.parse(value), duplicate };
|
|
44434
|
+
}
|
|
44435
|
+
function rowImage(row) {
|
|
44436
|
+
return {
|
|
44437
|
+
task_id: String(row.id),
|
|
44438
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44439
|
+
parent_id: row.parent_id == null ? null : String(row.parent_id),
|
|
44440
|
+
plan_id: row.plan_id == null ? null : String(row.plan_id),
|
|
44441
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44442
|
+
version: Number(row.version),
|
|
44443
|
+
updated_at: String(row.updated_at),
|
|
44444
|
+
archived_at: row.archived_at == null ? null : String(row.archived_at)
|
|
44445
|
+
};
|
|
44446
|
+
}
|
|
44447
|
+
function planImage(row) {
|
|
44448
|
+
return {
|
|
44449
|
+
plan_id: String(row.id),
|
|
44450
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44451
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44452
|
+
updated_at: String(row.updated_at)
|
|
44453
|
+
};
|
|
44454
|
+
}
|
|
44455
|
+
function imageFromTaskRow(row) {
|
|
44456
|
+
const image = rowImage(row);
|
|
44457
|
+
const { archived_at: _archivedAt, ...withoutArchive } = image;
|
|
44458
|
+
return withoutArchive;
|
|
44459
|
+
}
|
|
44460
|
+
function imageFromPlanRow(row) {
|
|
44461
|
+
return planImage(row);
|
|
44462
|
+
}
|
|
44463
|
+
function receiptId2(input) {
|
|
44464
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, taskSubtreeTransferRequestDigest(input));
|
|
44465
|
+
}
|
|
44466
|
+
function rollbackReceiptId(input) {
|
|
44467
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id);
|
|
44468
|
+
}
|
|
44469
|
+
function taskWhere(db, taskId) {
|
|
44470
|
+
const row = db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44471
|
+
FROM tasks WHERE id = ? LIMIT 1`).get(taskId);
|
|
44472
|
+
if (!row) {
|
|
44473
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${taskId}`, { task_id: taskId });
|
|
44474
|
+
}
|
|
44475
|
+
return row;
|
|
44476
|
+
}
|
|
44477
|
+
|
|
44478
|
+
class SqliteTodosTaskSubtreeTransferBackend {
|
|
44479
|
+
db;
|
|
44480
|
+
tenantId;
|
|
44481
|
+
kind = "sqlite";
|
|
44482
|
+
constructor(db, tenantId = "default") {
|
|
44483
|
+
this.db = db;
|
|
44484
|
+
this.tenantId = tenantId;
|
|
44485
|
+
ensureSqliteTodosTaskSubtreeTransferSchema(db);
|
|
44486
|
+
}
|
|
44487
|
+
async serialized(run) {
|
|
44488
|
+
const previous = sqliteTails2.get(this.db) ?? Promise.resolve();
|
|
44489
|
+
let release;
|
|
44490
|
+
const current = new Promise((resolve10) => {
|
|
44491
|
+
release = resolve10;
|
|
44492
|
+
});
|
|
44493
|
+
const tail = previous.then(() => current);
|
|
44494
|
+
sqliteTails2.set(this.db, tail);
|
|
44495
|
+
await previous;
|
|
44496
|
+
try {
|
|
44497
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
44498
|
+
try {
|
|
44499
|
+
const result = await run();
|
|
44500
|
+
this.db.exec("COMMIT");
|
|
44501
|
+
return result;
|
|
44502
|
+
} catch (error) {
|
|
44503
|
+
this.db.exec("ROLLBACK");
|
|
44504
|
+
throw error;
|
|
44505
|
+
}
|
|
44506
|
+
} finally {
|
|
44507
|
+
release();
|
|
44508
|
+
if (sqliteTails2.get(this.db) === tail)
|
|
44509
|
+
sqliteTails2.delete(this.db);
|
|
44510
|
+
}
|
|
44511
|
+
}
|
|
44512
|
+
snapshot(input) {
|
|
44513
|
+
const sourceTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44514
|
+
FROM tasks WHERE project_id = ? ORDER BY id`).all(input.source_project_id).map(rowImage);
|
|
44515
|
+
const allTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44516
|
+
FROM tasks ORDER BY id`).all().map(rowImage);
|
|
44517
|
+
const plans = this.db.query(`SELECT id, project_id, task_list_id, updated_at
|
|
44518
|
+
FROM plans ORDER BY id`).all().map(planImage);
|
|
44519
|
+
const destinationProjectFound = Boolean(this.db.query("SELECT id FROM projects WHERE id = ? LIMIT 1").get(input.destination_project_id));
|
|
44520
|
+
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);
|
|
44521
|
+
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));
|
|
44522
|
+
return {
|
|
44523
|
+
source_tasks: sourceTasks,
|
|
44524
|
+
plan_tasks: allTasks,
|
|
44525
|
+
plans,
|
|
44526
|
+
destination_project_found: destinationProjectFound,
|
|
44527
|
+
destination_task_list_found: Boolean(destinationTaskList),
|
|
44528
|
+
destination_parent_found: destinationParentFound
|
|
44529
|
+
};
|
|
44530
|
+
}
|
|
44531
|
+
async inspect(input) {
|
|
44532
|
+
return this.serialized(() => prepareTransfer(this.snapshot(input), input).inspection);
|
|
44533
|
+
}
|
|
44534
|
+
async apply(input, options) {
|
|
44535
|
+
return this.serialized(async () => {
|
|
44536
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44537
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44538
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44539
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
44540
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44541
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44542
|
+
if (existing) {
|
|
44543
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44544
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation/step or idempotency key was already accepted for a different request");
|
|
44545
|
+
}
|
|
44546
|
+
return parseResult(String(existing.result_json), true);
|
|
44547
|
+
}
|
|
44548
|
+
const snapshot = this.snapshot(input);
|
|
44549
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44550
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44551
|
+
const priorImage = {
|
|
44552
|
+
tasks: prepared.prior_tasks,
|
|
44553
|
+
plans: prepared.prior_plans
|
|
44554
|
+
};
|
|
44555
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44556
|
+
const movedTaskIds = prepared.prior_tasks.map((task3) => task3.task_id);
|
|
44557
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44558
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44559
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44560
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task changed after inspection", { task_id: task3.task_id });
|
|
44561
|
+
}
|
|
44562
|
+
}
|
|
44563
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44564
|
+
const targetPlan = prepared.task_plan_targets.get(task3.task_id) ?? null;
|
|
44565
|
+
const parentId = task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id;
|
|
44566
|
+
const result2 = this.db.query(`UPDATE tasks
|
|
44567
|
+
SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?, version = version + 1, updated_at = ?
|
|
44568
|
+
WHERE id = ? AND version = ?`).run(input.destination_project_id, input.destination_task_list_id, targetPlan, parentId, now4, task3.task_id, task3.version);
|
|
44569
|
+
if (result2.changes !== 1) {
|
|
44570
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task version changed during the atomic transfer", { task_id: task3.task_id });
|
|
44571
|
+
}
|
|
44572
|
+
}
|
|
44573
|
+
await fault3(options, "after_task_writes");
|
|
44574
|
+
for (const plan of prepared.prior_plans) {
|
|
44575
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44576
|
+
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);
|
|
44577
|
+
if (result2.changes !== 1) {
|
|
44578
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A contained plan changed during the atomic transfer", { plan_id: plan.plan_id });
|
|
44579
|
+
}
|
|
44580
|
+
}
|
|
44581
|
+
await fault3(options, "after_plan_writes");
|
|
44582
|
+
const postImage = {
|
|
44583
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44584
|
+
...task3,
|
|
44585
|
+
project_id: input.destination_project_id,
|
|
44586
|
+
task_list_id: input.destination_task_list_id,
|
|
44587
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44588
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44589
|
+
version: task3.version + 1,
|
|
44590
|
+
updated_at: now4
|
|
44591
|
+
})),
|
|
44592
|
+
plans: prepared.prior_plans.map((plan) => ({
|
|
44593
|
+
...plan,
|
|
44594
|
+
project_id: input.destination_project_id,
|
|
44595
|
+
task_list_id: input.destination_task_list_id,
|
|
44596
|
+
updated_at: now4
|
|
44597
|
+
}))
|
|
44598
|
+
};
|
|
44599
|
+
const resultDigest = canonicalDigest({
|
|
44600
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44601
|
+
request_digest: requestDigest,
|
|
44602
|
+
prior_image: priorImage,
|
|
44603
|
+
post_image: postImage
|
|
44604
|
+
});
|
|
44605
|
+
const receipt = {
|
|
44606
|
+
receipt_id: receiptId2(input),
|
|
44607
|
+
authority: "todos",
|
|
44608
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44609
|
+
schema_version: 1,
|
|
44610
|
+
kind: "apply",
|
|
44611
|
+
operation_id: input.operation_id,
|
|
44612
|
+
step_id: input.step_id,
|
|
44613
|
+
idempotency_key: input.idempotency_key,
|
|
44614
|
+
request_digest: requestDigest,
|
|
44615
|
+
precondition_digest: input.precondition_digest,
|
|
44616
|
+
result_digest: resultDigest,
|
|
44617
|
+
apply_receipt_id: null,
|
|
44618
|
+
source_project_id: input.source_project_id,
|
|
44619
|
+
destination_project_id: input.destination_project_id,
|
|
44620
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44621
|
+
root_task_id: input.root_task_id,
|
|
44622
|
+
source_population_digest: input.source_population_digest,
|
|
44623
|
+
prior_image: priorImage,
|
|
44624
|
+
post_image: postImage,
|
|
44625
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44626
|
+
created_at: now4
|
|
44627
|
+
};
|
|
44628
|
+
const result = {
|
|
44629
|
+
duplicate: false,
|
|
44630
|
+
receipt,
|
|
44631
|
+
moved_task_ids: movedTaskIds,
|
|
44632
|
+
moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id),
|
|
44633
|
+
complete: true
|
|
44634
|
+
};
|
|
44635
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44636
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44637
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44638
|
+
request_json, result_json, created_at
|
|
44639
|
+
) 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);
|
|
44640
|
+
await fault3(options, "after_receipt_write");
|
|
44641
|
+
return result;
|
|
44642
|
+
});
|
|
44643
|
+
}
|
|
44644
|
+
async readExact(receiptIdValue) {
|
|
44645
|
+
const row = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44646
|
+
WHERE tenant_id = ? AND receipt_id = ? LIMIT 1`).get(this.tenantId, receiptIdValue);
|
|
44647
|
+
if (!row) {
|
|
44648
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptIdValue}`);
|
|
44649
|
+
}
|
|
44650
|
+
return parseResult(String(row.result_json), false);
|
|
44651
|
+
}
|
|
44652
|
+
async rollback(input, options) {
|
|
44653
|
+
return this.serialized(async () => {
|
|
44654
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44655
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44656
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44657
|
+
WHERE tenant_id = ? AND kind = 'rollback'
|
|
44658
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44659
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44660
|
+
if (existing) {
|
|
44661
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44662
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation/step or idempotency key was already accepted for a different request");
|
|
44663
|
+
}
|
|
44664
|
+
return parseResult(String(existing.result_json), true);
|
|
44665
|
+
}
|
|
44666
|
+
const applyRow = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44667
|
+
WHERE tenant_id = ? AND kind = 'apply' AND receipt_id = ? LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
44668
|
+
if (!applyRow) {
|
|
44669
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44670
|
+
}
|
|
44671
|
+
const applied = parseResult(String(applyRow.result_json), false);
|
|
44672
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44673
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44674
|
+
}
|
|
44675
|
+
const expectedPrecondition = canonicalDigest({
|
|
44676
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44677
|
+
direction: "rollback",
|
|
44678
|
+
operation_id: input.operation_id,
|
|
44679
|
+
step_id: input.step_id,
|
|
44680
|
+
apply_receipt_id: input.receipt_id,
|
|
44681
|
+
apply_result_digest: applied.receipt.result_digest
|
|
44682
|
+
});
|
|
44683
|
+
if (input.precondition_digest !== expectedPrecondition) {
|
|
44684
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44685
|
+
}
|
|
44686
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44687
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44688
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44689
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44690
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred task changed after apply", { task_id: task3.task_id });
|
|
44691
|
+
}
|
|
44692
|
+
}
|
|
44693
|
+
for (const plan of applied.receipt.post_image.plans) {
|
|
44694
|
+
const row = this.db.query("SELECT id, project_id, task_list_id, updated_at FROM plans WHERE id = ? LIMIT 1").get(plan.plan_id);
|
|
44695
|
+
if (!row || canonicalJson(imageFromPlanRow(row)) !== canonicalJson(plan)) {
|
|
44696
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred plan changed after apply", { plan_id: plan.plan_id });
|
|
44697
|
+
}
|
|
44698
|
+
}
|
|
44699
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44700
|
+
const result2 = this.db.query(`UPDATE tasks SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?,
|
|
44701
|
+
version = version + 1, updated_at = ?
|
|
44702
|
+
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);
|
|
44703
|
+
if (result2.changes !== 1) {
|
|
44704
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback task CAS failed", { task_id: task3.task_id });
|
|
44705
|
+
}
|
|
44706
|
+
}
|
|
44707
|
+
await fault3(options, "after_rollback_task_writes");
|
|
44708
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44709
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44710
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44711
|
+
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);
|
|
44712
|
+
if (result2.changes !== 1) {
|
|
44713
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback plan CAS failed", { plan_id: plan.plan_id });
|
|
44714
|
+
}
|
|
44715
|
+
}
|
|
44716
|
+
await fault3(options, "after_rollback_plan_writes");
|
|
44717
|
+
const restored = {
|
|
44718
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({
|
|
44719
|
+
...task3,
|
|
44720
|
+
version: applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version + 1,
|
|
44721
|
+
updated_at: now4
|
|
44722
|
+
})),
|
|
44723
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44724
|
+
};
|
|
44725
|
+
const resultDigest = canonicalDigest({
|
|
44726
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44727
|
+
direction: "rollback",
|
|
44728
|
+
apply_receipt_id: input.receipt_id,
|
|
44729
|
+
prior_image: applied.receipt.post_image,
|
|
44730
|
+
post_image: restored
|
|
44731
|
+
});
|
|
44732
|
+
const receipt = {
|
|
44733
|
+
...applied.receipt,
|
|
44734
|
+
receipt_id: rollbackReceiptId(input),
|
|
44735
|
+
kind: "rollback",
|
|
44736
|
+
step_id: input.step_id,
|
|
44737
|
+
idempotency_key: input.idempotency_key,
|
|
44738
|
+
request_digest: requestDigest,
|
|
44739
|
+
precondition_digest: input.precondition_digest,
|
|
44740
|
+
result_digest: resultDigest,
|
|
44741
|
+
apply_receipt_id: input.receipt_id,
|
|
44742
|
+
prior_image: applied.receipt.post_image,
|
|
44743
|
+
post_image: restored,
|
|
44744
|
+
created_at: now4
|
|
44745
|
+
};
|
|
44746
|
+
const result = {
|
|
44747
|
+
duplicate: false,
|
|
44748
|
+
receipt,
|
|
44749
|
+
moved_task_ids: applied.moved_task_ids,
|
|
44750
|
+
moved_plan_ids: applied.moved_plan_ids,
|
|
44751
|
+
complete: true
|
|
44752
|
+
};
|
|
44753
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44754
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44755
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44756
|
+
request_json, result_json, created_at
|
|
44757
|
+
) 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);
|
|
44758
|
+
await fault3(options, "after_rollback_receipt_write");
|
|
44759
|
+
return result;
|
|
44760
|
+
});
|
|
44761
|
+
}
|
|
44762
|
+
}
|
|
44763
|
+
|
|
44764
|
+
// src/task-subtree-transfer/authority.ts
|
|
44765
|
+
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;
|
|
44766
|
+
var DIGEST = /^[0-9a-f]{64}$/i;
|
|
44767
|
+
function boundedString(value, field2, max = 200) {
|
|
44768
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max) {
|
|
44769
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a non-empty bounded string`);
|
|
44770
|
+
}
|
|
44771
|
+
return value;
|
|
44772
|
+
}
|
|
44773
|
+
function uuid3(value, field2) {
|
|
44774
|
+
const parsed = boundedString(value, field2, 80);
|
|
44775
|
+
if (!UUID.test(parsed)) {
|
|
44776
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a UUID`);
|
|
44777
|
+
}
|
|
44778
|
+
return parsed;
|
|
44779
|
+
}
|
|
44780
|
+
function digest2(value, field2) {
|
|
44781
|
+
const parsed = boundedString(value, field2, 64);
|
|
44782
|
+
if (!DIGEST.test(parsed)) {
|
|
44783
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a SHA-256 digest`);
|
|
44784
|
+
}
|
|
44785
|
+
return parsed.toLowerCase();
|
|
44786
|
+
}
|
|
44787
|
+
function nullableUuid(value, field2) {
|
|
44788
|
+
if (value === null)
|
|
44789
|
+
return null;
|
|
44790
|
+
return uuid3(value, field2);
|
|
44791
|
+
}
|
|
44792
|
+
function strictObject(input, fields) {
|
|
44793
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
44794
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
|
|
44795
|
+
}
|
|
44796
|
+
const object = input;
|
|
44797
|
+
const allowed = new Set(fields);
|
|
44798
|
+
for (const key2 of Object.keys(object)) {
|
|
44799
|
+
if (!allowed.has(key2)) {
|
|
44800
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
|
|
44801
|
+
}
|
|
44802
|
+
}
|
|
44803
|
+
return object;
|
|
44804
|
+
}
|
|
44805
|
+
var INSPECT_FIELDS = [
|
|
44806
|
+
"source_project_id",
|
|
44807
|
+
"destination_project_id",
|
|
44808
|
+
"destination_task_list_id",
|
|
44809
|
+
"root_task_id",
|
|
44810
|
+
"destination_parent_id"
|
|
44811
|
+
];
|
|
44812
|
+
function parseTodosTaskSubtreeTransferInspect(input) {
|
|
44813
|
+
const value = strictObject(input, INSPECT_FIELDS);
|
|
44814
|
+
const request = {
|
|
44815
|
+
source_project_id: uuid3(value.source_project_id, "source_project_id"),
|
|
44816
|
+
destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
|
|
44817
|
+
destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
|
|
44818
|
+
root_task_id: uuid3(value.root_task_id, "root_task_id"),
|
|
44819
|
+
destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
|
|
44820
|
+
};
|
|
44821
|
+
if (request.source_project_id === request.destination_project_id) {
|
|
44822
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
|
|
44823
|
+
}
|
|
44824
|
+
return request;
|
|
44825
|
+
}
|
|
44826
|
+
var APPLY_FIELDS = [
|
|
44827
|
+
...INSPECT_FIELDS,
|
|
44828
|
+
"version",
|
|
44829
|
+
"operation_id",
|
|
44830
|
+
"step_id",
|
|
44831
|
+
"idempotency_key",
|
|
44832
|
+
"precondition_digest",
|
|
44833
|
+
"expected_root_parent_id",
|
|
44834
|
+
"source_population_digest",
|
|
44835
|
+
"expected_tasks",
|
|
44836
|
+
"shared_plan_splits"
|
|
44837
|
+
];
|
|
44838
|
+
function parseTodosTaskSubtreeTransferApply(input) {
|
|
44839
|
+
const value = strictObject(input, APPLY_FIELDS);
|
|
44840
|
+
if (value.version !== 1) {
|
|
44841
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
|
|
44842
|
+
}
|
|
44843
|
+
if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
|
|
44844
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
|
|
44845
|
+
}
|
|
44846
|
+
const expected_tasks = value.expected_tasks.map((item, index) => {
|
|
44847
|
+
const row = strictObject(item, ["task_id", "version"]);
|
|
44848
|
+
const version = row.version;
|
|
44849
|
+
if (!Number.isSafeInteger(version) || Number(version) < 1) {
|
|
44850
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
|
|
44851
|
+
}
|
|
44852
|
+
return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
|
|
44853
|
+
});
|
|
44854
|
+
const ids2 = new Set;
|
|
44855
|
+
for (const row of expected_tasks) {
|
|
44856
|
+
if (ids2.has(row.task_id)) {
|
|
44857
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
|
|
44858
|
+
}
|
|
44859
|
+
ids2.add(row.task_id);
|
|
44860
|
+
}
|
|
44861
|
+
if (!Array.isArray(value.shared_plan_splits)) {
|
|
44862
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
|
|
44863
|
+
}
|
|
44864
|
+
const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
|
|
44865
|
+
const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
|
|
44866
|
+
return {
|
|
44867
|
+
source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
|
|
44868
|
+
destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
|
|
44869
|
+
};
|
|
44870
|
+
});
|
|
44871
|
+
return {
|
|
44872
|
+
...parseTodosTaskSubtreeTransferInspect({
|
|
44873
|
+
source_project_id: value.source_project_id,
|
|
44874
|
+
destination_project_id: value.destination_project_id,
|
|
44875
|
+
destination_task_list_id: value.destination_task_list_id,
|
|
44876
|
+
root_task_id: value.root_task_id,
|
|
44877
|
+
destination_parent_id: value.destination_parent_id
|
|
44878
|
+
}),
|
|
44879
|
+
version: 1,
|
|
44880
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44881
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44882
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44883
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
|
|
44884
|
+
expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
|
|
44885
|
+
source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
|
|
44886
|
+
expected_tasks,
|
|
44887
|
+
shared_plan_splits
|
|
44888
|
+
};
|
|
44889
|
+
}
|
|
44890
|
+
function parseTodosTaskSubtreeTransferRollback(input) {
|
|
44891
|
+
const value = strictObject(input, [
|
|
44892
|
+
"receipt_id",
|
|
44893
|
+
"operation_id",
|
|
44894
|
+
"step_id",
|
|
44895
|
+
"idempotency_key",
|
|
44896
|
+
"precondition_digest"
|
|
44897
|
+
]);
|
|
44898
|
+
return {
|
|
44899
|
+
receipt_id: uuid3(value.receipt_id, "receipt_id"),
|
|
44900
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44901
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44902
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44903
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest")
|
|
44904
|
+
};
|
|
44905
|
+
}
|
|
44906
|
+
function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
|
|
44907
|
+
const {
|
|
44908
|
+
source_project_id,
|
|
44909
|
+
destination_project_id,
|
|
44910
|
+
destination_task_list_id,
|
|
44911
|
+
root_task_id,
|
|
44912
|
+
expected_root_parent_id,
|
|
44913
|
+
destination_parent_id,
|
|
44914
|
+
source_population_digest,
|
|
44915
|
+
expected_tasks,
|
|
44916
|
+
shared_plan_splits
|
|
44917
|
+
} = input;
|
|
44918
|
+
return canonicalDigest({
|
|
44919
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44920
|
+
direction: "apply",
|
|
44921
|
+
source_project_id,
|
|
44922
|
+
destination_project_id,
|
|
44923
|
+
destination_task_list_id,
|
|
44924
|
+
root_task_id,
|
|
44925
|
+
expected_root_parent_id,
|
|
44926
|
+
destination_parent_id,
|
|
44927
|
+
source_population_digest,
|
|
44928
|
+
expected_tasks,
|
|
44929
|
+
shared_plan_splits
|
|
44930
|
+
});
|
|
44931
|
+
}
|
|
44932
|
+
function deriveTodosTaskSubtreeTransferRollbackPreconditionDigest(input) {
|
|
44933
|
+
return canonicalDigest({
|
|
44934
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44935
|
+
direction: "rollback",
|
|
44936
|
+
apply_receipt_id: input.receipt_id,
|
|
44937
|
+
operation_id: input.operation_id,
|
|
44938
|
+
step_id: input.step_id,
|
|
44939
|
+
apply_result_digest: input.apply_result_digest
|
|
44940
|
+
});
|
|
44941
|
+
}
|
|
44942
|
+
function resolveTenantId2(value) {
|
|
44943
|
+
const tenant = value ?? "default";
|
|
44944
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
|
|
44945
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
44946
|
+
}
|
|
44947
|
+
return tenant;
|
|
44948
|
+
}
|
|
44949
|
+
|
|
44950
|
+
class PackageOwnedTodosTaskSubtreeTransferAuthority {
|
|
44951
|
+
backend;
|
|
44952
|
+
tenantId;
|
|
44953
|
+
backendOptions;
|
|
44954
|
+
constructor(backend, options = {}) {
|
|
44955
|
+
this.backend = backend;
|
|
44956
|
+
this.tenantId = resolveTenantId2(options.tenantId);
|
|
44957
|
+
this.backendOptions = { ...options, tenantId: this.tenantId };
|
|
44958
|
+
}
|
|
44959
|
+
async capability() {
|
|
44960
|
+
return {
|
|
44961
|
+
authority: "todos",
|
|
44962
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44963
|
+
schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
44964
|
+
tenant_id: this.tenantId,
|
|
44965
|
+
backend: this.backend.kind,
|
|
44966
|
+
exact_descendant_closure: true,
|
|
44967
|
+
complete_source_population_digest: true,
|
|
44968
|
+
per_task_version_cas: true,
|
|
44969
|
+
explicit_shared_plan_splits: true,
|
|
44970
|
+
atomic_apply: true,
|
|
44971
|
+
immutable_forward_inverse_receipts: true,
|
|
44972
|
+
prior_image_receipts: true,
|
|
44973
|
+
cas_protected_rollback: true,
|
|
44974
|
+
preserves_descendant_parent_ids: true,
|
|
44975
|
+
preserves_task_and_relation_identities: true
|
|
44976
|
+
};
|
|
44977
|
+
}
|
|
44978
|
+
async inspect(input) {
|
|
44979
|
+
return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
|
|
44980
|
+
}
|
|
44981
|
+
async apply(input) {
|
|
44982
|
+
const request = parseTodosTaskSubtreeTransferApply(input);
|
|
44983
|
+
const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
|
|
44984
|
+
if (request.precondition_digest !== expected) {
|
|
44985
|
+
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 });
|
|
44986
|
+
}
|
|
44987
|
+
const requestDigest = taskSubtreeTransferRequestDigest(request);
|
|
44988
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
44989
|
+
operation_id: request.operation_id,
|
|
44990
|
+
step_id: request.step_id,
|
|
44991
|
+
direction: "apply",
|
|
44992
|
+
target_selector: request.root_task_id,
|
|
44993
|
+
request_digest: requestDigest,
|
|
44994
|
+
precondition_digest: request.precondition_digest
|
|
44995
|
+
});
|
|
44996
|
+
if (request.idempotency_key !== expectedKey) {
|
|
44997
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
|
|
44998
|
+
}
|
|
44999
|
+
return this.backend.apply(request, this.backendOptions);
|
|
45000
|
+
}
|
|
45001
|
+
async readExact(receiptIdValue) {
|
|
45002
|
+
return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
|
|
45003
|
+
}
|
|
45004
|
+
async rollback(input) {
|
|
45005
|
+
const request = parseTodosTaskSubtreeTransferRollback(input);
|
|
45006
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
|
|
45007
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
45008
|
+
operation_id: request.operation_id,
|
|
45009
|
+
step_id: request.step_id,
|
|
45010
|
+
direction: "rollback",
|
|
45011
|
+
target_selector: request.receipt_id,
|
|
45012
|
+
request_digest: requestDigest,
|
|
45013
|
+
precondition_digest: request.precondition_digest
|
|
45014
|
+
});
|
|
45015
|
+
if (request.idempotency_key !== expectedKey) {
|
|
45016
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
|
|
45017
|
+
}
|
|
45018
|
+
return this.backend.rollback(request, this.backendOptions);
|
|
45019
|
+
}
|
|
45020
|
+
}
|
|
45021
|
+
function createSqliteTodosTaskSubtreeTransferAuthority(options) {
|
|
45022
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new SqliteTodosTaskSubtreeTransferBackend(options.database, resolveTenantId2(options.tenantId)), options);
|
|
45023
|
+
}
|
|
45024
|
+
function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
|
|
45025
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
|
|
45026
|
+
}
|
|
45027
|
+
// src/task-subtree-transfer/http.ts
|
|
45028
|
+
var JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
45029
|
+
var REQUEST_BYTES = 2 * 1024 * 1024;
|
|
45030
|
+
var RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
45031
|
+
function json3(body2, status2 = 200) {
|
|
45032
|
+
return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
|
|
45033
|
+
}
|
|
45034
|
+
function status2(error) {
|
|
45035
|
+
switch (error.code) {
|
|
45036
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
|
|
45037
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
|
|
45038
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
|
|
45039
|
+
return 400;
|
|
45040
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
|
|
45041
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
|
|
45042
|
+
return 404;
|
|
45043
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
|
|
45044
|
+
return 503;
|
|
45045
|
+
default:
|
|
45046
|
+
return 409;
|
|
45047
|
+
}
|
|
45048
|
+
}
|
|
45049
|
+
async function boundedText2(message, limit) {
|
|
45050
|
+
if (!message.body)
|
|
45051
|
+
return "";
|
|
45052
|
+
const reader = message.body.getReader();
|
|
45053
|
+
const decoder = new TextDecoder;
|
|
45054
|
+
let bytes = 0;
|
|
45055
|
+
let text = "";
|
|
45056
|
+
try {
|
|
45057
|
+
while (true) {
|
|
45058
|
+
const { done, value } = await reader.read();
|
|
45059
|
+
if (done)
|
|
45060
|
+
break;
|
|
45061
|
+
bytes += value.byteLength;
|
|
45062
|
+
if (bytes > limit) {
|
|
45063
|
+
await reader.cancel();
|
|
45064
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
|
|
45065
|
+
}
|
|
45066
|
+
text += decoder.decode(value, { stream: true });
|
|
45067
|
+
}
|
|
45068
|
+
return text + decoder.decode();
|
|
45069
|
+
} finally {
|
|
45070
|
+
reader.releaseLock();
|
|
45071
|
+
}
|
|
45072
|
+
}
|
|
45073
|
+
async function readBody(request) {
|
|
45074
|
+
const declaredLength = Number(request.headers.get("content-length") ?? 0);
|
|
45075
|
+
if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
|
|
45076
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
|
|
45077
|
+
}
|
|
45078
|
+
const text = await boundedText2(request, REQUEST_BYTES);
|
|
45079
|
+
try {
|
|
45080
|
+
return JSON.parse(text);
|
|
45081
|
+
} catch {
|
|
45082
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
|
|
45083
|
+
}
|
|
45084
|
+
}
|
|
45085
|
+
async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
|
|
45086
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
|
|
45087
|
+
return null;
|
|
45088
|
+
const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
45089
|
+
try {
|
|
45090
|
+
if ((action === "" || action === "capability") && request.method === "GET") {
|
|
45091
|
+
return json3({ capability: await authority.capability() });
|
|
45092
|
+
}
|
|
45093
|
+
if (request.method !== "POST")
|
|
45094
|
+
return json3({ error: "method not allowed" }, 405);
|
|
45095
|
+
if (action === "inspect")
|
|
45096
|
+
return json3({ inspection: await authority.inspect(await readBody(request)) });
|
|
45097
|
+
if (action === "apply")
|
|
45098
|
+
return json3({ result: await authority.apply(await readBody(request)) }, 201);
|
|
45099
|
+
if (action === "rollback")
|
|
45100
|
+
return json3({ result: await authority.rollback(await readBody(request)) }, 201);
|
|
45101
|
+
if (action === "read-exact") {
|
|
45102
|
+
const input = await readBody(request);
|
|
45103
|
+
if (!input || typeof input.receipt_id !== "string") {
|
|
45104
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
|
|
45105
|
+
}
|
|
45106
|
+
return json3({ result: await authority.readExact(input.receipt_id) });
|
|
45107
|
+
}
|
|
45108
|
+
return json3({ error: "unknown task-subtree-transfer route" }, 404);
|
|
45109
|
+
} catch (cause) {
|
|
45110
|
+
if (cause instanceof TodosTaskSubtreeTransferError) {
|
|
45111
|
+
return json3({
|
|
45112
|
+
error: cause.message,
|
|
45113
|
+
code: cause.code,
|
|
45114
|
+
details: cause.details,
|
|
45115
|
+
authoritative: true
|
|
45116
|
+
}, status2(cause));
|
|
45117
|
+
}
|
|
45118
|
+
return json3({ error: "internal task-subtree-transfer error" }, 500);
|
|
45119
|
+
}
|
|
45120
|
+
}
|
|
45121
|
+
|
|
45122
|
+
class TodosTaskSubtreeTransferHttpClient {
|
|
45123
|
+
baseUrl;
|
|
45124
|
+
fetchImpl;
|
|
45125
|
+
headers;
|
|
45126
|
+
constructor(options) {
|
|
45127
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
45128
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
45129
|
+
this.headers = {
|
|
45130
|
+
...options.headers,
|
|
45131
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
45132
|
+
"Content-Type": "application/json"
|
|
45133
|
+
};
|
|
45134
|
+
}
|
|
45135
|
+
async request(action, init = {}) {
|
|
45136
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/task-subtree-transfer${action}`, {
|
|
45137
|
+
...init,
|
|
45138
|
+
headers: { ...this.headers, ...init.headers ?? {} }
|
|
45139
|
+
});
|
|
45140
|
+
const text = await boundedText2(response, RESPONSE_BYTES);
|
|
45141
|
+
let payload;
|
|
45142
|
+
try {
|
|
45143
|
+
payload = JSON.parse(text);
|
|
45144
|
+
} catch {
|
|
45145
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", `Task-subtree-transfer HTTP ${response.status} returned invalid JSON`);
|
|
45146
|
+
}
|
|
45147
|
+
if (!response.ok) {
|
|
45148
|
+
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 : {});
|
|
45149
|
+
}
|
|
45150
|
+
return payload;
|
|
45151
|
+
}
|
|
45152
|
+
async capability() {
|
|
45153
|
+
return (await this.request("/capability")).capability;
|
|
45154
|
+
}
|
|
45155
|
+
async inspect(input) {
|
|
45156
|
+
return (await this.request("/inspect", { method: "POST", body: JSON.stringify(input) })).inspection;
|
|
45157
|
+
}
|
|
45158
|
+
async apply(input) {
|
|
45159
|
+
return (await this.request("/apply", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45160
|
+
}
|
|
45161
|
+
async readExact(receiptId3) {
|
|
45162
|
+
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId3 }) })).result;
|
|
45163
|
+
}
|
|
45164
|
+
async rollback(input) {
|
|
45165
|
+
return (await this.request("/rollback", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45166
|
+
}
|
|
45167
|
+
}
|
|
45168
|
+
function createTodosTaskSubtreeTransferHttpClient(options) {
|
|
45169
|
+
return new TodosTaskSubtreeTransferHttpClient(options);
|
|
45170
|
+
}
|
|
43627
45171
|
// src/ai-tools.ts
|
|
43628
45172
|
import { createHash as createHash16 } from "crypto";
|
|
43629
45173
|
// src/cli/cloud-router.ts
|
|
@@ -43635,26 +45179,26 @@ var retryCapabilityCache = new Map;
|
|
|
43635
45179
|
var taskCreatorCapabilityCache = new Map;
|
|
43636
45180
|
var gitRefCapabilityCache = new Map;
|
|
43637
45181
|
var remoteCommandCapabilityCache = new Map;
|
|
43638
|
-
var
|
|
43639
|
-
|
|
43640
|
-
|
|
43641
|
-
|
|
43642
|
-
|
|
43643
|
-
|
|
43644
|
-
|
|
43645
|
-
|
|
43646
|
-
|
|
43647
|
-
|
|
43648
|
-
|
|
43649
|
-
|
|
45182
|
+
var todosLocalFallbackNoticeEmitted = false;
|
|
45183
|
+
function emitTodosLocalFallbackNotice(env) {
|
|
45184
|
+
if (todosLocalFallbackNoticeEmitted)
|
|
45185
|
+
return;
|
|
45186
|
+
todosLocalFallbackNoticeEmitted = true;
|
|
45187
|
+
const notice = {
|
|
45188
|
+
event: "todos-local-fallback",
|
|
45189
|
+
transport: "sqlite",
|
|
45190
|
+
source: "default",
|
|
45191
|
+
apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
45192
|
+
apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
45193
|
+
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."
|
|
45194
|
+
};
|
|
45195
|
+
console.error(JSON.stringify(notice));
|
|
43650
45196
|
}
|
|
43651
|
-
function
|
|
43652
|
-
assertNoLegacyStorageMode(env);
|
|
45197
|
+
function resolveTodosCliTransport(env = process.env) {
|
|
43653
45198
|
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
43654
45199
|
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
43655
45200
|
if (urlValue && keyValue) {
|
|
43656
45201
|
return {
|
|
43657
|
-
mode: "http",
|
|
43658
45202
|
transport: "http",
|
|
43659
45203
|
selected: true,
|
|
43660
45204
|
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
@@ -43666,15 +45210,15 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
43666
45210
|
if (keyValue) {
|
|
43667
45211
|
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
43668
45212
|
}
|
|
45213
|
+
emitTodosLocalFallbackNotice(env);
|
|
43669
45214
|
return {
|
|
43670
|
-
mode: "sqlite",
|
|
43671
45215
|
transport: "sqlite",
|
|
43672
45216
|
selected: false,
|
|
43673
45217
|
source: "default"
|
|
43674
45218
|
};
|
|
43675
45219
|
}
|
|
43676
45220
|
function requestedTransport(env) {
|
|
43677
|
-
return
|
|
45221
|
+
return resolveTodosCliTransport(env).transport;
|
|
43678
45222
|
}
|
|
43679
45223
|
function normalizeRemoteAuthorityUrl(value) {
|
|
43680
45224
|
const trimmed = value?.trim();
|
|
@@ -43708,13 +45252,13 @@ function normalizeRemoteAuthorityUrl(value) {
|
|
|
43708
45252
|
function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
43709
45253
|
let resolution;
|
|
43710
45254
|
try {
|
|
43711
|
-
resolution =
|
|
45255
|
+
resolution = resolveTodosCliTransport(env);
|
|
43712
45256
|
} catch (error) {
|
|
43713
45257
|
const issue = error instanceof Error ? error.message : String(error);
|
|
43714
45258
|
return {
|
|
43715
45259
|
selected: true,
|
|
43716
45260
|
ok: false,
|
|
43717
|
-
|
|
45261
|
+
transport: "invalid",
|
|
43718
45262
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
43719
45263
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
43720
45264
|
v1_base_url: null,
|
|
@@ -43722,12 +45266,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43722
45266
|
local_fallback: false
|
|
43723
45267
|
};
|
|
43724
45268
|
}
|
|
43725
|
-
const {
|
|
45269
|
+
const { transport, selected } = resolution;
|
|
43726
45270
|
if (!selected) {
|
|
43727
45271
|
return {
|
|
43728
45272
|
selected: false,
|
|
43729
45273
|
ok: true,
|
|
43730
|
-
|
|
45274
|
+
transport,
|
|
43731
45275
|
api_url_configured: false,
|
|
43732
45276
|
api_key_configured: false,
|
|
43733
45277
|
v1_base_url: null,
|
|
@@ -43752,7 +45296,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43752
45296
|
return {
|
|
43753
45297
|
selected: true,
|
|
43754
45298
|
ok: issues.length === 0,
|
|
43755
|
-
|
|
45299
|
+
transport,
|
|
43756
45300
|
api_url_configured: apiUrl !== null,
|
|
43757
45301
|
api_key_configured: apiKeyConfigured,
|
|
43758
45302
|
v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
|
|
@@ -43761,24 +45305,24 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43761
45305
|
};
|
|
43762
45306
|
}
|
|
43763
45307
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
43764
|
-
const
|
|
43765
|
-
if (!
|
|
43766
|
-
throw new Error(
|
|
45308
|
+
const status3 = getTodosRemoteAuthorityConfigStatus(env);
|
|
45309
|
+
if (!status3.ok)
|
|
45310
|
+
throw new Error(status3.issues[0]);
|
|
43767
45311
|
return {
|
|
43768
45312
|
...env,
|
|
43769
|
-
HASNA_TODOS_API_URL:
|
|
45313
|
+
HASNA_TODOS_API_URL: status3.v1_base_url.replace(/\/v1$/, ""),
|
|
43770
45314
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
43771
45315
|
};
|
|
43772
45316
|
}
|
|
43773
45317
|
function classifyRemoteRequestError(baseUrl, route, error) {
|
|
43774
|
-
const
|
|
43775
|
-
if (
|
|
45318
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45319
|
+
if (status3 === 401) {
|
|
43776
45320
|
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
45321
|
}
|
|
43778
|
-
if (
|
|
45322
|
+
if (status3 === 403) {
|
|
43779
45323
|
throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
|
|
43780
45324
|
}
|
|
43781
|
-
if (
|
|
45325
|
+
if (status3 === 409) {
|
|
43782
45326
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43783
45327
|
if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
|
|
43784
45328
|
const remoteError = body2;
|
|
@@ -43788,17 +45332,18 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
43788
45332
|
}
|
|
43789
45333
|
throw error;
|
|
43790
45334
|
}
|
|
43791
|
-
if (typeof
|
|
45335
|
+
if (typeof status3 === "number" && status3 >= 300 && status3 < 400) {
|
|
43792
45336
|
throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
|
|
43793
45337
|
}
|
|
43794
|
-
if (typeof
|
|
43795
|
-
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${
|
|
45338
|
+
if (typeof status3 === "number" && status3 >= 500) {
|
|
45339
|
+
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${status3} for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43796
45340
|
}
|
|
43797
45341
|
const message = error instanceof Error ? error.message : String(error);
|
|
43798
45342
|
if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
|
|
43799
|
-
|
|
45343
|
+
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";
|
|
45344
|
+
throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
|
|
43800
45345
|
}
|
|
43801
|
-
if (
|
|
45346
|
+
if (status3 === undefined) {
|
|
43802
45347
|
throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43803
45348
|
}
|
|
43804
45349
|
throw error;
|
|
@@ -43866,8 +45411,8 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
43866
45411
|
try {
|
|
43867
45412
|
return await request();
|
|
43868
45413
|
} catch (error) {
|
|
43869
|
-
const
|
|
43870
|
-
if (
|
|
45414
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45415
|
+
if (status3 === 404 || status3 === 405) {
|
|
43871
45416
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43872
45417
|
const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
|
|
43873
45418
|
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
@@ -43884,11 +45429,10 @@ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUES
|
|
|
43884
45429
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
43885
45430
|
timeoutMs: requestTimeoutMs
|
|
43886
45431
|
});
|
|
43887
|
-
if (resolved.transport === "cloud-http")
|
|
43888
|
-
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
43889
45432
|
const transportName = resolved.transport;
|
|
43890
|
-
if (transportName === "http")
|
|
45433
|
+
if (transportName === "cloud-http" || transportName === "http") {
|
|
43891
45434
|
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
45435
|
+
}
|
|
43892
45436
|
return null;
|
|
43893
45437
|
}
|
|
43894
45438
|
function unwrapTask(raw) {
|
|
@@ -44039,9 +45583,9 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
44039
45583
|
const { status: _status, limit, offset, ...baseFilter } = filter;
|
|
44040
45584
|
const start = offset ?? 0;
|
|
44041
45585
|
const windowEnd = typeof limit === "number" ? start + limit : undefined;
|
|
44042
|
-
const pages = await Promise.all(statuses.map((
|
|
45586
|
+
const pages = await Promise.all(statuses.map((status3) => requestCloudTaskPage(client, {
|
|
44043
45587
|
...baseFilter,
|
|
44044
|
-
status:
|
|
45588
|
+
status: status3,
|
|
44045
45589
|
...windowEnd === undefined ? {} : { limit: windowEnd }
|
|
44046
45590
|
})));
|
|
44047
45591
|
const seen = new Set;
|
|
@@ -44319,11 +45863,11 @@ init_audit();
|
|
|
44319
45863
|
init_database();
|
|
44320
45864
|
function upsertCheckpoint(task_id, step, updates, db) {
|
|
44321
45865
|
const d = db || getDatabase();
|
|
44322
|
-
const
|
|
45866
|
+
const timestamp5 = now();
|
|
44323
45867
|
const existing = d.query("SELECT id FROM task_checkpoints WHERE task_id = ? AND step = ?").get(task_id, step);
|
|
44324
45868
|
const id = existing?.id ?? uuid();
|
|
44325
45869
|
const agentId = updates.agent_id ?? null;
|
|
44326
|
-
const
|
|
45870
|
+
const status3 = updates.status ?? "pending";
|
|
44327
45871
|
const data = updates.data ? JSON.stringify(updates.data) : JSON.stringify({});
|
|
44328
45872
|
const error = updates.error ?? null;
|
|
44329
45873
|
const attempt = updates.attempt ?? 1;
|
|
@@ -44337,22 +45881,22 @@ function upsertCheckpoint(task_id, step, updates, db) {
|
|
|
44337
45881
|
task_id,
|
|
44338
45882
|
agentId,
|
|
44339
45883
|
step,
|
|
44340
|
-
|
|
45884
|
+
status3,
|
|
44341
45885
|
data,
|
|
44342
45886
|
error,
|
|
44343
45887
|
attempt,
|
|
44344
45888
|
maxAttempts,
|
|
44345
45889
|
startedAt,
|
|
44346
45890
|
completedAt2,
|
|
44347
|
-
|
|
44348
|
-
|
|
45891
|
+
timestamp5,
|
|
45892
|
+
status3,
|
|
44349
45893
|
data,
|
|
44350
45894
|
error,
|
|
44351
45895
|
attempt,
|
|
44352
45896
|
maxAttempts,
|
|
44353
45897
|
startedAt,
|
|
44354
45898
|
completedAt2,
|
|
44355
|
-
|
|
45899
|
+
timestamp5
|
|
44356
45900
|
]);
|
|
44357
45901
|
return rowToCheckpoint(d.query("SELECT * FROM task_checkpoints WHERE id = ?").get(id));
|
|
44358
45902
|
}
|
|
@@ -44415,10 +45959,10 @@ function stepForGate(gate) {
|
|
|
44415
45959
|
throw new Error("Approval gate name is required");
|
|
44416
45960
|
return `approval:${trimmed}`;
|
|
44417
45961
|
}
|
|
44418
|
-
function approvalStatusToCheckpointStatus(
|
|
44419
|
-
if (
|
|
45962
|
+
function approvalStatusToCheckpointStatus(status3) {
|
|
45963
|
+
if (status3 === "approved")
|
|
44420
45964
|
return "completed";
|
|
44421
|
-
if (
|
|
45965
|
+
if (status3 === "pending")
|
|
44422
45966
|
return "pending";
|
|
44423
45967
|
return "failed";
|
|
44424
45968
|
}
|
|
@@ -44440,12 +45984,12 @@ function isExpired(expiresAt, at = new Date) {
|
|
|
44440
45984
|
return Boolean(expiresAt && new Date(expiresAt).getTime() <= at.getTime());
|
|
44441
45985
|
}
|
|
44442
45986
|
function gateFromCheckpoint(checkpoint) {
|
|
44443
|
-
const
|
|
45987
|
+
const status3 = checkpointStatusToApprovalStatus(checkpoint);
|
|
44444
45988
|
return {
|
|
44445
45989
|
id: checkpoint.id,
|
|
44446
45990
|
task_id: checkpoint.task_id,
|
|
44447
45991
|
gate: String(checkpoint.data["approval_gate_name"] || checkpoint.step.replace(/^approval:/, "")),
|
|
44448
|
-
status:
|
|
45992
|
+
status: status3,
|
|
44449
45993
|
reviewer: typeof checkpoint.data["reviewer"] === "string" ? checkpoint.data["reviewer"] : null,
|
|
44450
45994
|
requester: typeof checkpoint.data["requester"] === "string" ? checkpoint.data["requester"] : null,
|
|
44451
45995
|
reason: typeof checkpoint.data["reason"] === "string" ? checkpoint.data["reason"] : null,
|
|
@@ -44490,13 +46034,13 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
|
|
|
44490
46034
|
});
|
|
44491
46035
|
}
|
|
44492
46036
|
}
|
|
44493
|
-
function writeGate(input,
|
|
46037
|
+
function writeGate(input, status3, decision, db) {
|
|
44494
46038
|
const d = db || getDatabase();
|
|
44495
46039
|
ensureTask(input.task_id, d);
|
|
44496
46040
|
const step = stepForGate(input.gate);
|
|
44497
46041
|
const existing = getCheckpoint(input.task_id, step, d);
|
|
44498
46042
|
const existingData = existing?.data || {};
|
|
44499
|
-
const
|
|
46043
|
+
const timestamp5 = decision?.decided_at || now();
|
|
44500
46044
|
const existingRunId = typeof existingData["run_id"] === "string" ? existingData["run_id"] : undefined;
|
|
44501
46045
|
const runId = input.run_id ? resolveTaskRunId(input.run_id, d) : existingRunId;
|
|
44502
46046
|
const data = {
|
|
@@ -44504,7 +46048,7 @@ function writeGate(input, status2, decision, db) {
|
|
|
44504
46048
|
...input.metadata || {},
|
|
44505
46049
|
approval_gate: true,
|
|
44506
46050
|
approval_gate_name: input.gate.trim(),
|
|
44507
|
-
approval_status:
|
|
46051
|
+
approval_status: status3,
|
|
44508
46052
|
requester: input.requester ?? existingData["requester"] ?? null,
|
|
44509
46053
|
reviewer: input.reviewer ?? decision?.decided_by ?? existingData["reviewer"] ?? null,
|
|
44510
46054
|
reason: decision?.reason ?? input.reason ?? existingData["reason"] ?? null,
|
|
@@ -44517,11 +46061,11 @@ function writeGate(input, status2, decision, db) {
|
|
|
44517
46061
|
};
|
|
44518
46062
|
const checkpoint = upsertCheckpoint(input.task_id, step, {
|
|
44519
46063
|
agent_id: decision?.decided_by || input.requester || input.reviewer,
|
|
44520
|
-
status: approvalStatusToCheckpointStatus(
|
|
46064
|
+
status: approvalStatusToCheckpointStatus(status3),
|
|
44521
46065
|
data,
|
|
44522
|
-
error:
|
|
44523
|
-
started_at: existing?.started_at ||
|
|
44524
|
-
completed_at:
|
|
46066
|
+
error: status3 === "rejected" || status3 === "expired" ? String(data.reason || status3) : null,
|
|
46067
|
+
started_at: existing?.started_at || timestamp5,
|
|
46068
|
+
completed_at: status3 === "pending" ? null : timestamp5
|
|
44525
46069
|
}, d);
|
|
44526
46070
|
return gateFromCheckpoint(checkpoint);
|
|
44527
46071
|
}
|
|
@@ -45369,7 +46913,7 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45369
46913
|
const planId = boundedOptionalString(record, "plan_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45370
46914
|
const taskListId = boundedOptionalString(record, "task_list_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45371
46915
|
const assignedTo = boundedOptionalString(record, "assigned_to", TODOS_AI_READ_TOOL_LIMITS.max_filter_string_bytes);
|
|
45372
|
-
const
|
|
46916
|
+
const status3 = boundedEnum(record, "status", TASK_STATUSES);
|
|
45373
46917
|
const priority = boundedEnum(record, "priority", TASK_PRIORITIES);
|
|
45374
46918
|
const tags = boundedTags(record);
|
|
45375
46919
|
if (projectId !== undefined)
|
|
@@ -45380,8 +46924,8 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45380
46924
|
filter.task_list_id = taskListId;
|
|
45381
46925
|
if (assignedTo !== undefined)
|
|
45382
46926
|
filter.assigned_to = assignedTo;
|
|
45383
|
-
if (
|
|
45384
|
-
filter.status =
|
|
46927
|
+
if (status3 !== undefined)
|
|
46928
|
+
filter.status = status3;
|
|
45385
46929
|
if (priority !== undefined)
|
|
45386
46930
|
filter.priority = priority;
|
|
45387
46931
|
if (tags !== undefined)
|
|
@@ -45723,29 +47267,29 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
45723
47267
|
};
|
|
45724
47268
|
}
|
|
45725
47269
|
function getNativeStorageSyncPlan(env = process.env, options = {}) {
|
|
45726
|
-
const
|
|
47270
|
+
const status3 = getNativeStorageStatus(env);
|
|
45727
47271
|
const steps = [
|
|
45728
47272
|
"Read local SQLite snapshot",
|
|
45729
|
-
|
|
45730
|
-
|
|
47273
|
+
status3.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
|
|
47274
|
+
status3.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
|
|
45731
47275
|
"Report planned changes without opening network connections"
|
|
45732
47276
|
];
|
|
45733
47277
|
return {
|
|
45734
|
-
ok:
|
|
47278
|
+
ok: status3.ok,
|
|
45735
47279
|
service: "todos",
|
|
45736
47280
|
dry_run: true,
|
|
45737
47281
|
no_network: true,
|
|
45738
|
-
status:
|
|
47282
|
+
status: status3,
|
|
45739
47283
|
postgres: {
|
|
45740
|
-
required:
|
|
45741
|
-
configured:
|
|
47284
|
+
required: status3.remote_enabled,
|
|
47285
|
+
configured: status3.database.configured,
|
|
45742
47286
|
schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
|
|
45743
47287
|
},
|
|
45744
47288
|
object_storage: {
|
|
45745
|
-
required:
|
|
45746
|
-
configured:
|
|
45747
|
-
bucket:
|
|
45748
|
-
prefix:
|
|
47289
|
+
required: status3.remote_enabled,
|
|
47290
|
+
configured: status3.object_storage.configured,
|
|
47291
|
+
bucket: status3.object_storage.bucket,
|
|
47292
|
+
prefix: status3.object_storage.prefix
|
|
45749
47293
|
},
|
|
45750
47294
|
steps
|
|
45751
47295
|
};
|
|
@@ -45851,8 +47395,8 @@ function resolveTaskList(task3, project, db) {
|
|
|
45851
47395
|
}
|
|
45852
47396
|
return null;
|
|
45853
47397
|
}
|
|
45854
|
-
function isTerminal2(
|
|
45855
|
-
return
|
|
47398
|
+
function isTerminal2(status3) {
|
|
47399
|
+
return status3 === "completed" || status3 === "cancelled" || status3 === "failed";
|
|
45856
47400
|
}
|
|
45857
47401
|
function routeConcurrencyKey(task3, project, taskList, projectPath) {
|
|
45858
47402
|
if (project?.id)
|
|
@@ -46033,7 +47577,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
46033
47577
|
latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
|
|
46034
47578
|
workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
|
|
46035
47579
|
});
|
|
46036
|
-
const
|
|
47580
|
+
const timestamp5 = now();
|
|
46037
47581
|
const {
|
|
46038
47582
|
current_workflow_invocation_id,
|
|
46039
47583
|
current_run_id,
|
|
@@ -46049,7 +47593,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
46049
47593
|
workflow_invocation: {
|
|
46050
47594
|
schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
|
|
46051
47595
|
...next,
|
|
46052
|
-
updated_at:
|
|
47596
|
+
updated_at: timestamp5,
|
|
46053
47597
|
updated_by: input.actor ?? null
|
|
46054
47598
|
}
|
|
46055
47599
|
};
|
|
@@ -46083,8 +47627,8 @@ function normalizePath3(input) {
|
|
|
46083
47627
|
return resolve10(input);
|
|
46084
47628
|
}
|
|
46085
47629
|
function sourceStoreId(sourceDbPath) {
|
|
46086
|
-
const
|
|
46087
|
-
return `sqlite:${
|
|
47630
|
+
const digest3 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
47631
|
+
return `sqlite:${digest3}`;
|
|
46088
47632
|
}
|
|
46089
47633
|
function inferSourceRepoPath(sourceDbPath) {
|
|
46090
47634
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46855,7 +48399,7 @@ function rowToSnapshot(row) {
|
|
|
46855
48399
|
function saveSnapshot(input, db) {
|
|
46856
48400
|
const d = db || getDatabase();
|
|
46857
48401
|
const id = uuid();
|
|
46858
|
-
const
|
|
48402
|
+
const timestamp5 = now();
|
|
46859
48403
|
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
48404
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
46861
48405
|
id,
|
|
@@ -46869,7 +48413,7 @@ function saveSnapshot(input, db) {
|
|
|
46869
48413
|
JSON.stringify(input.blockers || []),
|
|
46870
48414
|
input.next_steps || null,
|
|
46871
48415
|
JSON.stringify(input.metadata || {}),
|
|
46872
|
-
|
|
48416
|
+
timestamp5
|
|
46873
48417
|
]);
|
|
46874
48418
|
return {
|
|
46875
48419
|
id,
|
|
@@ -46883,7 +48427,7 @@ function saveSnapshot(input, db) {
|
|
|
46883
48427
|
blockers: input.blockers || [],
|
|
46884
48428
|
next_steps: input.next_steps || null,
|
|
46885
48429
|
metadata: input.metadata || {},
|
|
46886
|
-
created_at:
|
|
48430
|
+
created_at: timestamp5
|
|
46887
48431
|
};
|
|
46888
48432
|
}
|
|
46889
48433
|
function getLatestSnapshot(agentId, taskId, db) {
|
|
@@ -46999,7 +48543,7 @@ function createKnowledgeRecord(input, db) {
|
|
|
46999
48543
|
if (!title)
|
|
47000
48544
|
throw new Error("Knowledge record title is required");
|
|
47001
48545
|
const d = db || getDatabase();
|
|
47002
|
-
const
|
|
48546
|
+
const timestamp5 = now();
|
|
47003
48547
|
const id = uuid();
|
|
47004
48548
|
d.run(`INSERT INTO project_knowledge_records (
|
|
47005
48549
|
id, record_type, title, content, decision, rationale, alternatives,
|
|
@@ -47019,8 +48563,8 @@ function createKnowledgeRecord(input, db) {
|
|
|
47019
48563
|
input.snapshot_id || null,
|
|
47020
48564
|
JSON.stringify(normalizeTags(input.tags)),
|
|
47021
48565
|
JSON.stringify(input.metadata || {}),
|
|
47022
|
-
|
|
47023
|
-
|
|
48566
|
+
timestamp5,
|
|
48567
|
+
timestamp5
|
|
47024
48568
|
]);
|
|
47025
48569
|
return getKnowledgeRecord(id, d);
|
|
47026
48570
|
}
|
|
@@ -47211,9 +48755,9 @@ function normalizeText4(value) {
|
|
|
47211
48755
|
const trimmed = value.trim();
|
|
47212
48756
|
return trimmed.length > 0 ? trimmed : null;
|
|
47213
48757
|
}
|
|
47214
|
-
function assertStatus(
|
|
47215
|
-
if (!VALID_STATUSES.has(
|
|
47216
|
-
throw new Error(`Invalid risk status: ${
|
|
48758
|
+
function assertStatus(status3) {
|
|
48759
|
+
if (!VALID_STATUSES.has(status3))
|
|
48760
|
+
throw new Error(`Invalid risk status: ${status3}`);
|
|
47217
48761
|
}
|
|
47218
48762
|
function assertSeverity(severity) {
|
|
47219
48763
|
if (!VALID_SEVERITIES2.has(severity))
|
|
@@ -47258,16 +48802,16 @@ function createRisk(input, db) {
|
|
|
47258
48802
|
const title = input.title.trim();
|
|
47259
48803
|
if (!title)
|
|
47260
48804
|
throw new Error("Risk title is required");
|
|
47261
|
-
const
|
|
48805
|
+
const status3 = input.status || "open";
|
|
47262
48806
|
const severity = input.severity || "medium";
|
|
47263
48807
|
const probability = input.probability || "medium";
|
|
47264
|
-
assertStatus(
|
|
48808
|
+
assertStatus(status3);
|
|
47265
48809
|
assertSeverity(severity);
|
|
47266
48810
|
assertProbability(probability);
|
|
47267
48811
|
const d = db || getDatabase();
|
|
47268
|
-
const
|
|
48812
|
+
const timestamp5 = now();
|
|
47269
48813
|
const id = uuid();
|
|
47270
|
-
const closedAt =
|
|
48814
|
+
const closedAt = status3 === "resolved" || status3 === "accepted" ? timestamp5 : null;
|
|
47271
48815
|
d.run(`INSERT INTO project_risks (
|
|
47272
48816
|
id, title, description, status, severity, probability, owner, mitigation, due_at,
|
|
47273
48817
|
project_id, plan_id, task_id, tags, metadata, created_at, updated_at, closed_at
|
|
@@ -47275,7 +48819,7 @@ function createRisk(input, db) {
|
|
|
47275
48819
|
id,
|
|
47276
48820
|
title,
|
|
47277
48821
|
normalizeText4(input.description),
|
|
47278
|
-
|
|
48822
|
+
status3,
|
|
47279
48823
|
severity,
|
|
47280
48824
|
probability,
|
|
47281
48825
|
normalizeText4(input.owner),
|
|
@@ -47286,8 +48830,8 @@ function createRisk(input, db) {
|
|
|
47286
48830
|
resolveKnownId2("tasks", input.task_id, d),
|
|
47287
48831
|
JSON.stringify(normalizeTags2(input.tags)),
|
|
47288
48832
|
JSON.stringify(input.metadata || {}),
|
|
47289
|
-
|
|
47290
|
-
|
|
48833
|
+
timestamp5,
|
|
48834
|
+
timestamp5,
|
|
47291
48835
|
closedAt
|
|
47292
48836
|
]);
|
|
47293
48837
|
return getRisk(id, d);
|
|
@@ -47370,8 +48914,8 @@ function updateRisk(id, input, db) {
|
|
|
47370
48914
|
d.run(`UPDATE project_risks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
47371
48915
|
return getRisk(resolved, d);
|
|
47372
48916
|
}
|
|
47373
|
-
function closeRisk(id,
|
|
47374
|
-
return updateRisk(id, { status:
|
|
48917
|
+
function closeRisk(id, status3 = "resolved", db) {
|
|
48918
|
+
return updateRisk(id, { status: status3 }, db);
|
|
47375
48919
|
}
|
|
47376
48920
|
function buildRiskFilters(options, db) {
|
|
47377
48921
|
const conditions = [];
|
|
@@ -47469,7 +49013,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47469
49013
|
const blockers = d.query(`SELECT dep.id, dep.short_id, dep.title, dep.status
|
|
47470
49014
|
FROM task_dependencies td
|
|
47471
49015
|
JOIN tasks dep ON dep.id = td.depends_on
|
|
47472
|
-
WHERE td.task_id = ? AND dep.status
|
|
49016
|
+
WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).all(task3.id);
|
|
47473
49017
|
return { id: task3.id, short_id: task3.short_id, title: redactEvidenceText(task3.title), blockers };
|
|
47474
49018
|
}).filter((entry2) => entry2.blockers.length > 0);
|
|
47475
49019
|
const overdue = tasks.filter((task3) => activeTaskIds.has(task3.id) && Boolean(task3.due_at && task3.due_at < generatedAt)).map((task3) => ({ id: task3.id, short_id: task3.short_id, title: redactEvidenceText(task3.title), due_at: task3.due_at }));
|
|
@@ -47499,7 +49043,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47499
49043
|
};
|
|
47500
49044
|
const totalPenalty = Object.values(penalties).reduce((sum, penalty) => sum + penalty, 0);
|
|
47501
49045
|
const score = Math.max(0, 100 - Math.min(100, totalPenalty));
|
|
47502
|
-
const
|
|
49046
|
+
const status3 = score < 40 ? "critical" : score < 70 ? "at_risk" : score < 85 ? "watch" : "healthy";
|
|
47503
49047
|
const recommendations = [];
|
|
47504
49048
|
if (blocked.length > 0)
|
|
47505
49049
|
recommendations.push("Clear blocking dependencies before starting more work.");
|
|
@@ -47523,7 +49067,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47523
49067
|
scope,
|
|
47524
49068
|
scope_id: scopeInfo.id,
|
|
47525
49069
|
score,
|
|
47526
|
-
status:
|
|
49070
|
+
status: status3,
|
|
47527
49071
|
components: {
|
|
47528
49072
|
total_tasks: tasks.length,
|
|
47529
49073
|
blocked_tasks: blocked.length,
|
|
@@ -47770,7 +49314,7 @@ function createRetrospective(input, db) {
|
|
|
47770
49314
|
const d = db || getDatabase();
|
|
47771
49315
|
const report = buildReport(input, d);
|
|
47772
49316
|
const scope = scopeFromInput(input, d);
|
|
47773
|
-
const
|
|
49317
|
+
const timestamp5 = now();
|
|
47774
49318
|
if (input.create_followups) {
|
|
47775
49319
|
for (const followUp of report.follow_up_tasks) {
|
|
47776
49320
|
const task3 = createTask({
|
|
@@ -47786,7 +49330,7 @@ function createRetrospective(input, db) {
|
|
|
47786
49330
|
}
|
|
47787
49331
|
const id = uuid();
|
|
47788
49332
|
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),
|
|
49333
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report), timestamp5, timestamp5]);
|
|
47790
49334
|
return getRetrospective(id, d);
|
|
47791
49335
|
}
|
|
47792
49336
|
function getRetrospective(id, db) {
|
|
@@ -48075,7 +49619,7 @@ function createApiKey(input, db) {
|
|
|
48075
49619
|
if (!name)
|
|
48076
49620
|
throw new Error("API key name is required");
|
|
48077
49621
|
const key2 = generatePlaintextKey();
|
|
48078
|
-
const
|
|
49622
|
+
const timestamp5 = now();
|
|
48079
49623
|
const id = uuid();
|
|
48080
49624
|
const prefix = key2.slice(0, 12);
|
|
48081
49625
|
d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
|
|
@@ -48085,7 +49629,7 @@ function createApiKey(input, db) {
|
|
|
48085
49629
|
hashApiKey(key2),
|
|
48086
49630
|
prefix,
|
|
48087
49631
|
JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
|
|
48088
|
-
|
|
49632
|
+
timestamp5,
|
|
48089
49633
|
input.expires_at || null
|
|
48090
49634
|
]);
|
|
48091
49635
|
const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
|
|
@@ -48139,15 +49683,15 @@ function rowToSession(row) {
|
|
|
48139
49683
|
function createSession(input, db) {
|
|
48140
49684
|
const d = db || getDatabase();
|
|
48141
49685
|
const id = uuid();
|
|
48142
|
-
const
|
|
49686
|
+
const timestamp5 = now();
|
|
48143
49687
|
d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
|
|
48144
49688
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
48145
49689
|
id,
|
|
48146
49690
|
input.agent_id || null,
|
|
48147
49691
|
input.project_id || null,
|
|
48148
49692
|
input.working_dir || null,
|
|
48149
|
-
|
|
48150
|
-
|
|
49693
|
+
timestamp5,
|
|
49694
|
+
timestamp5,
|
|
48151
49695
|
JSON.stringify(input.metadata || {})
|
|
48152
49696
|
]);
|
|
48153
49697
|
return getSession(id, d);
|
|
@@ -48320,7 +49864,7 @@ function rowToArtifact2(row) {
|
|
|
48320
49864
|
function addArtifact(input, db, dbPath) {
|
|
48321
49865
|
const d = db || getDatabase();
|
|
48322
49866
|
const id = uuid();
|
|
48323
|
-
const
|
|
49867
|
+
const timestamp5 = now();
|
|
48324
49868
|
const storageMode = input.storage_mode ?? "copy";
|
|
48325
49869
|
const stored = storeArtifactFile({
|
|
48326
49870
|
artifactId: id,
|
|
@@ -48345,8 +49889,8 @@ function addArtifact(input, db, dbPath) {
|
|
|
48345
49889
|
stored.sizeBytes,
|
|
48346
49890
|
input.redaction_status ?? "none",
|
|
48347
49891
|
JSON.stringify(input.metadata ?? {}),
|
|
48348
|
-
|
|
48349
|
-
|
|
49892
|
+
timestamp5,
|
|
49893
|
+
timestamp5
|
|
48350
49894
|
]);
|
|
48351
49895
|
return getArtifact(id, d);
|
|
48352
49896
|
}
|
|
@@ -48379,14 +49923,14 @@ function listArtifacts(filter = {}, db) {
|
|
|
48379
49923
|
}
|
|
48380
49924
|
function updateArtifactRedaction(id, redactionStatus, db) {
|
|
48381
49925
|
const d = db || getDatabase();
|
|
48382
|
-
const
|
|
48383
|
-
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus,
|
|
49926
|
+
const timestamp5 = now();
|
|
49927
|
+
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus, timestamp5, id]);
|
|
48384
49928
|
return getArtifact(id, d);
|
|
48385
49929
|
}
|
|
48386
49930
|
function softDeleteArtifact(id, db) {
|
|
48387
49931
|
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", [
|
|
49932
|
+
const timestamp5 = now();
|
|
49933
|
+
const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [timestamp5, timestamp5, id]);
|
|
48390
49934
|
return result.changes > 0;
|
|
48391
49935
|
}
|
|
48392
49936
|
function purgeArtifact(id, db, dbPath) {
|
|
@@ -48524,7 +50068,7 @@ init_redaction();
|
|
|
48524
50068
|
function createHandoff(input, db) {
|
|
48525
50069
|
const d = db || getDatabase();
|
|
48526
50070
|
const id = uuid();
|
|
48527
|
-
const
|
|
50071
|
+
const timestamp5 = now();
|
|
48528
50072
|
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
50073
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
48530
50074
|
id,
|
|
@@ -48539,7 +50083,7 @@ function createHandoff(input, db) {
|
|
|
48539
50083
|
toJson(input.task_ids),
|
|
48540
50084
|
toJson(input.relevant_files),
|
|
48541
50085
|
toJson(input.run_ids),
|
|
48542
|
-
|
|
50086
|
+
timestamp5
|
|
48543
50087
|
]);
|
|
48544
50088
|
return {
|
|
48545
50089
|
id,
|
|
@@ -48554,7 +50098,7 @@ function createHandoff(input, db) {
|
|
|
48554
50098
|
task_ids: input.task_ids || null,
|
|
48555
50099
|
relevant_files: input.relevant_files || null,
|
|
48556
50100
|
run_ids: input.run_ids || null,
|
|
48557
|
-
created_at:
|
|
50101
|
+
created_at: timestamp5,
|
|
48558
50102
|
acknowledged_by: []
|
|
48559
50103
|
};
|
|
48560
50104
|
}
|
|
@@ -49066,7 +50610,7 @@ function upsertVerificationProvider(input) {
|
|
|
49066
50610
|
const name = normalizeName(input.name);
|
|
49067
50611
|
const config = loadConfig();
|
|
49068
50612
|
const existing = config.verification_providers?.[name];
|
|
49069
|
-
const
|
|
50613
|
+
const timestamp5 = new Date().toISOString();
|
|
49070
50614
|
const provider = {
|
|
49071
50615
|
...existing,
|
|
49072
50616
|
name,
|
|
@@ -49077,8 +50621,8 @@ function upsertVerificationProvider(input) {
|
|
|
49077
50621
|
capabilities: input.capabilities ?? existing?.capabilities,
|
|
49078
50622
|
retry: input.retry ? retryConfig(input.retry) : existing?.retry,
|
|
49079
50623
|
timeout_ms: timeoutMs(input.timeout_ms ?? existing?.timeout_ms),
|
|
49080
|
-
created_at: existing?.created_at ||
|
|
49081
|
-
updated_at:
|
|
50624
|
+
created_at: existing?.created_at || timestamp5,
|
|
50625
|
+
updated_at: timestamp5
|
|
49082
50626
|
};
|
|
49083
50627
|
saveConfig({
|
|
49084
50628
|
...config,
|
|
@@ -49639,7 +51183,7 @@ function upsertPolicyPack(input) {
|
|
|
49639
51183
|
const existing = config.policy_packs?.[input.name];
|
|
49640
51184
|
const root = normalizePath4(input.root || existing?.root || process.cwd());
|
|
49641
51185
|
const base = existing || defaultPolicyPack(input.name, root);
|
|
49642
|
-
const
|
|
51186
|
+
const timestamp5 = new Date().toISOString();
|
|
49643
51187
|
const pack = {
|
|
49644
51188
|
...base,
|
|
49645
51189
|
name: input.name,
|
|
@@ -49657,8 +51201,8 @@ function upsertPolicyPack(input) {
|
|
|
49657
51201
|
require_artifact: input.require_artifact ?? base.require_artifact,
|
|
49658
51202
|
evidence_min_count: input.evidence_min_count ?? base.evidence_min_count,
|
|
49659
51203
|
branch_pattern: input.branch_pattern ?? base.branch_pattern,
|
|
49660
|
-
created_at: existing?.created_at ||
|
|
49661
|
-
updated_at:
|
|
51204
|
+
created_at: existing?.created_at || timestamp5,
|
|
51205
|
+
updated_at: timestamp5
|
|
49662
51206
|
};
|
|
49663
51207
|
saveConfig({
|
|
49664
51208
|
...config,
|
|
@@ -50000,7 +51544,7 @@ function summarizeOutput(stdout, stderr) {
|
|
|
50000
51544
|
function upsertAgentRunAdapter(input) {
|
|
50001
51545
|
const config = loadConfig();
|
|
50002
51546
|
const existing = config.agent_run_adapters?.[input.name];
|
|
50003
|
-
const
|
|
51547
|
+
const timestamp5 = new Date().toISOString();
|
|
50004
51548
|
const adapter = {
|
|
50005
51549
|
...existing,
|
|
50006
51550
|
name: input.name,
|
|
@@ -50008,8 +51552,8 @@ function upsertAgentRunAdapter(input) {
|
|
|
50008
51552
|
sandbox: input.sandbox ?? existing?.sandbox,
|
|
50009
51553
|
cwd: input.cwd ?? existing?.cwd,
|
|
50010
51554
|
env: input.env ?? existing?.env,
|
|
50011
|
-
created_at: existing?.created_at ||
|
|
50012
|
-
updated_at:
|
|
51555
|
+
created_at: existing?.created_at || timestamp5,
|
|
51556
|
+
updated_at: timestamp5
|
|
50013
51557
|
};
|
|
50014
51558
|
saveConfig({
|
|
50015
51559
|
...config,
|
|
@@ -50133,7 +51677,7 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
50133
51677
|
proc.exited
|
|
50134
51678
|
]);
|
|
50135
51679
|
const outputSummary = summarizeOutput(stdout, stderr);
|
|
50136
|
-
const
|
|
51680
|
+
const status3 = exitCode === 0 ? "completed" : "failed";
|
|
50137
51681
|
addTaskRunCommand({
|
|
50138
51682
|
run_id: next.run.id,
|
|
50139
51683
|
command,
|
|
@@ -50142,15 +51686,15 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
50142
51686
|
output_summary: outputSummary ?? undefined,
|
|
50143
51687
|
agent_id: next.run.agent_id ?? undefined
|
|
50144
51688
|
}, d);
|
|
50145
|
-
const completed = { ...running, state:
|
|
51689
|
+
const completed = { ...running, state: status3, completed_at: new Date().toISOString(), last_error: exitCode === 0 ? undefined : outputSummary || `exit ${exitCode}` };
|
|
50146
51690
|
updateDispatcherMetadata(next.run.id, completed, d);
|
|
50147
51691
|
finishTaskRun({
|
|
50148
51692
|
run_id: next.run.id,
|
|
50149
|
-
status:
|
|
50150
|
-
summary: outputSummary || `agent run ${
|
|
51693
|
+
status: status3,
|
|
51694
|
+
summary: outputSummary || `agent run ${status3}`,
|
|
50151
51695
|
agent_id: next.run.agent_id ?? undefined
|
|
50152
51696
|
}, d);
|
|
50153
|
-
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status:
|
|
51697
|
+
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
51698
|
}
|
|
50155
51699
|
function listAgentRuns(filter = {}, db) {
|
|
50156
51700
|
let runs = listAgentRunQueue(db).map(toAgentRun);
|
|
@@ -51119,10 +52663,10 @@ function extractEmbeddedBridge(markdown) {
|
|
|
51119
52663
|
const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
|
|
51120
52664
|
if (!match)
|
|
51121
52665
|
return null;
|
|
51122
|
-
const
|
|
52666
|
+
const json4 = match[1].split(`
|
|
51123
52667
|
`).map((line) => line.replace(/^ /, "")).join(`
|
|
51124
52668
|
`);
|
|
51125
|
-
return JSON.parse(
|
|
52669
|
+
return JSON.parse(json4);
|
|
51126
52670
|
}
|
|
51127
52671
|
function frontmatterValue(markdown, key2) {
|
|
51128
52672
|
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -51143,14 +52687,14 @@ function parseTaskLine(line, currentPlan) {
|
|
|
51143
52687
|
const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
51144
52688
|
if (!match)
|
|
51145
52689
|
return null;
|
|
51146
|
-
const
|
|
52690
|
+
const status3 = match[1].toLowerCase() === "x" ? "completed" : "pending";
|
|
51147
52691
|
const rawTitle = stripInlineMetadata(match[2]).replace(/^\*\*(.*?)\*\*$/, "$1");
|
|
51148
52692
|
const tags = [...rawTitle.matchAll(/(^|\s)#([A-Za-z0-9_-]+)/g)].map((tag) => tag[2]);
|
|
51149
52693
|
const assignee = rawTitle.match(/(^|\s)@([A-Za-z0-9_-]+)/)?.[2];
|
|
51150
52694
|
const title = rawTitle.replace(/(^|\s)#[A-Za-z0-9_-]+/g, "").replace(/(^|\s)@[A-Za-z0-9_-]+/g, "").replace(/\s+/g, " ").trim();
|
|
51151
52695
|
return {
|
|
51152
52696
|
title,
|
|
51153
|
-
status:
|
|
52697
|
+
status: status3,
|
|
51154
52698
|
priority: "medium",
|
|
51155
52699
|
tags,
|
|
51156
52700
|
assigned_to: assignee,
|
|
@@ -52755,7 +54299,7 @@ function normalizeJiraIssue(raw) {
|
|
|
52755
54299
|
if (!summary)
|
|
52756
54300
|
return null;
|
|
52757
54301
|
const statusRec = asRecord3(fields.status);
|
|
52758
|
-
const
|
|
54302
|
+
const status3 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
|
|
52759
54303
|
const labels = labelNames2(fields.labels);
|
|
52760
54304
|
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
54305
|
return {
|
|
@@ -52764,13 +54308,13 @@ function normalizeJiraIssue(raw) {
|
|
|
52764
54308
|
external_id: key2,
|
|
52765
54309
|
title: `[${key2}] ${summary}`,
|
|
52766
54310
|
description,
|
|
52767
|
-
status:
|
|
54311
|
+
status: status3,
|
|
52768
54312
|
priority: priorityFromJira(fields.priority),
|
|
52769
54313
|
tags: [...labels.slice(0, 10), "jira-import"],
|
|
52770
54314
|
url: typeof raw.self === "string" ? raw.self : undefined,
|
|
52771
54315
|
raw_metadata: {
|
|
52772
54316
|
jira_key: key2,
|
|
52773
|
-
jira_status:
|
|
54317
|
+
jira_status: status3,
|
|
52774
54318
|
jira_priority: asRecord3(fields.priority)?.name,
|
|
52775
54319
|
jira_id: raw.id
|
|
52776
54320
|
}
|
|
@@ -55515,6 +57059,10 @@ function importBundle(bundle, options = {}, db) {
|
|
|
55515
57059
|
}
|
|
55516
57060
|
for (const dep of bundle.dependencies) {
|
|
55517
57061
|
try {
|
|
57062
|
+
if (!getTask(dep.task_id, d) || !getTask(dep.depends_on, d)) {
|
|
57063
|
+
bump(result.skipped, "dependencies");
|
|
57064
|
+
continue;
|
|
57065
|
+
}
|
|
55518
57066
|
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [dep.task_id, dep.depends_on]);
|
|
55519
57067
|
bump(result.created, "dependencies");
|
|
55520
57068
|
} catch (e) {
|
|
@@ -56320,11 +57868,11 @@ function createTuiDashboardSnapshot(options = {}, db) {
|
|
|
56320
57868
|
const d = db || getDatabase();
|
|
56321
57869
|
const limit = limited(options.limit);
|
|
56322
57870
|
const filter = projectFilter(options.project_id);
|
|
56323
|
-
const counts = TASK_STATUSES2.reduce((acc,
|
|
56324
|
-
acc[
|
|
57871
|
+
const counts = TASK_STATUSES2.reduce((acc, status3) => {
|
|
57872
|
+
acc[status3] = countTasks({ ...filter, status: status3 }, d);
|
|
56325
57873
|
return acc;
|
|
56326
57874
|
}, { total: 0 });
|
|
56327
|
-
counts.total = TASK_STATUSES2.reduce((sum,
|
|
57875
|
+
counts.total = TASK_STATUSES2.reduce((sum, status3) => sum + counts[status3], 0);
|
|
56328
57876
|
const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
|
|
56329
57877
|
id: project.id,
|
|
56330
57878
|
name: project.name,
|
|
@@ -57525,12 +59073,12 @@ function captureGit(root, warnings) {
|
|
|
57525
59073
|
}
|
|
57526
59074
|
const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
|
|
57527
59075
|
const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
|
|
57528
|
-
const
|
|
59076
|
+
const status3 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
|
|
57529
59077
|
if (commit.exitCode !== 0)
|
|
57530
59078
|
warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
|
|
57531
|
-
if (
|
|
57532
|
-
warnings.push(`git status unavailable: ${
|
|
57533
|
-
const lines =
|
|
59079
|
+
if (status3.exitCode !== 0)
|
|
59080
|
+
warnings.push(`git status unavailable: ${status3.stderr || status3.stdout || "unknown error"}`);
|
|
59081
|
+
const lines = status3.stdout ? status3.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
57534
59082
|
return {
|
|
57535
59083
|
present: true,
|
|
57536
59084
|
branch: branch.stdout || null,
|
|
@@ -57572,8 +59120,8 @@ function defaultSnapshotDir() {
|
|
|
57572
59120
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
57573
59121
|
}
|
|
57574
59122
|
function snapshotWithId(snapshot) {
|
|
57575
|
-
const
|
|
57576
|
-
return { id: `env_${
|
|
59123
|
+
const digest3 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
59124
|
+
return { id: `env_${digest3}`, ...snapshot };
|
|
57577
59125
|
}
|
|
57578
59126
|
function captureEnvironmentSnapshot(input = {}) {
|
|
57579
59127
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -57773,7 +59321,7 @@ function createDecisionRecord(input, db) {
|
|
|
57773
59321
|
const ts = now();
|
|
57774
59322
|
const sequenceNum = nextSequenceNum(input.project_id, d);
|
|
57775
59323
|
const shortRef = buildShortRef(input.project_id, sequenceNum, d);
|
|
57776
|
-
const
|
|
59324
|
+
const status3 = input.status ?? "proposed";
|
|
57777
59325
|
d.run(`INSERT INTO decision_records (
|
|
57778
59326
|
id, project_id, task_id, plan_id, agent_id, sequence_num, short_ref,
|
|
57779
59327
|
title, status, context, decision, consequences, alternatives, tags,
|
|
@@ -57787,7 +59335,7 @@ function createDecisionRecord(input, db) {
|
|
|
57787
59335
|
sequenceNum,
|
|
57788
59336
|
shortRef,
|
|
57789
59337
|
input.title,
|
|
57790
|
-
|
|
59338
|
+
status3,
|
|
57791
59339
|
input.context ?? null,
|
|
57792
59340
|
input.decision,
|
|
57793
59341
|
input.consequences ?? null,
|
|
@@ -57865,15 +59413,15 @@ function updateDecisionRecord(id, input, db) {
|
|
|
57865
59413
|
]);
|
|
57866
59414
|
return getDecisionRecord(id, d);
|
|
57867
59415
|
}
|
|
57868
|
-
function setDecisionStatus(id,
|
|
57869
|
-
if (!DECISION_STATUSES.includes(
|
|
57870
|
-
throw new Error(`Invalid decision status: ${
|
|
59416
|
+
function setDecisionStatus(id, status3, db) {
|
|
59417
|
+
if (!DECISION_STATUSES.includes(status3)) {
|
|
59418
|
+
throw new Error(`Invalid decision status: ${status3}`);
|
|
57871
59419
|
}
|
|
57872
59420
|
const d = db || getDatabase();
|
|
57873
59421
|
const existing = getDecisionRecord(id, d);
|
|
57874
59422
|
if (!existing)
|
|
57875
59423
|
throw new Error(`Decision record not found: ${id}`);
|
|
57876
|
-
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [
|
|
59424
|
+
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [status3, now(), id]);
|
|
57877
59425
|
return getDecisionRecord(id, d);
|
|
57878
59426
|
}
|
|
57879
59427
|
function supersedeDecisionRecord(id, input, db) {
|
|
@@ -58154,17 +59702,17 @@ function buildReportExportData(input, db) {
|
|
|
58154
59702
|
throw new Error("Project not found");
|
|
58155
59703
|
projectId = project.id;
|
|
58156
59704
|
title = `Project: ${project.name}`;
|
|
58157
|
-
const
|
|
59705
|
+
const status3 = getStatus({ project_id: project.id }, undefined, undefined, d);
|
|
58158
59706
|
sections.push(section("overview", "Overview", [
|
|
58159
59707
|
`Name: ${project.name}`,
|
|
58160
59708
|
`Path: ${project.path}`,
|
|
58161
59709
|
`Prefix: ${project.task_prefix ?? "\u2014"}`
|
|
58162
59710
|
]), section("status", "Status", [
|
|
58163
|
-
`Pending: ${
|
|
58164
|
-
`In progress: ${
|
|
58165
|
-
`Completed: ${
|
|
58166
|
-
`Total: ${
|
|
58167
|
-
`Stale: ${
|
|
59711
|
+
`Pending: ${status3.pending}`,
|
|
59712
|
+
`In progress: ${status3.in_progress}`,
|
|
59713
|
+
`Completed: ${status3.completed}`,
|
|
59714
|
+
`Total: ${status3.total}`,
|
|
59715
|
+
`Stale: ${status3.stale_count}`
|
|
58168
59716
|
]));
|
|
58169
59717
|
const tasks = listTasks({ project_id: project.id, limit: 50 }, d);
|
|
58170
59718
|
sections.push(section("tasks", "Recent tasks", tasks.length ? tasks.map(taskLine2) : ["No tasks"]));
|
|
@@ -58985,8 +60533,8 @@ function getGitStatus(root, branch, includeGitStatus) {
|
|
|
58985
60533
|
return { has_git: false, current_branch: null, branch_exists: false, dirty_files: [] };
|
|
58986
60534
|
const currentBranch = runGit3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
58987
60535
|
const branchExists = runGit3(root, ["show-ref", "--verify", `refs/heads/${branch}`]) !== null;
|
|
58988
|
-
const
|
|
58989
|
-
const dirtyFiles =
|
|
60536
|
+
const status3 = runGit3(root, ["status", "--short"]) || "";
|
|
60537
|
+
const dirtyFiles = status3.split(`
|
|
58990
60538
|
`).map((line) => line.trim()).filter(Boolean).map((line) => normalizePath6(line.replace(/^.. /, "").replace(/^.* -> /, ""))).filter((path) => Boolean(path));
|
|
58991
60539
|
return { has_git: true, current_branch: currentBranch || null, branch_exists: branchExists, dirty_files: dirtyFiles };
|
|
58992
60540
|
}
|
|
@@ -59384,8 +60932,8 @@ var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
|
|
|
59384
60932
|
var DEMO_DEFAULT_AGENT = "demoagent";
|
|
59385
60933
|
var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
|
|
59386
60934
|
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:
|
|
60935
|
+
function pushStep(steps, action, detail, status3 = "ok") {
|
|
60936
|
+
steps.push({ step: steps.length + 1, action, detail, status: status3 });
|
|
59389
60937
|
}
|
|
59390
60938
|
function setupEphemeralDemoDb(options = {}) {
|
|
59391
60939
|
const prevPath = process.env["TODOS_DB_PATH"];
|
|
@@ -59512,7 +61060,7 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59512
61060
|
pushStep(steps, "complete_run_record", `Run record status=${finishedRecord.status}`);
|
|
59513
61061
|
completeTask(third.id, agent.id, d, { notes: "Demo: finished via agent run + run record" });
|
|
59514
61062
|
pushStep(steps, "complete_task", `Completed ${third.short_id ?? third.id.slice(0, 8)}`);
|
|
59515
|
-
const
|
|
61063
|
+
const status3 = getStatus({ project_id: project.id }, agent.id, undefined, d);
|
|
59516
61064
|
const tasks = listTasks({ project_id: project.id }, d).map((t) => ({
|
|
59517
61065
|
id: t.id,
|
|
59518
61066
|
short_id: t.short_id,
|
|
@@ -59544,14 +61092,14 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59544
61092
|
objective: finishedRecord.objective
|
|
59545
61093
|
},
|
|
59546
61094
|
status_summary: {
|
|
59547
|
-
pending:
|
|
59548
|
-
in_progress:
|
|
59549
|
-
completed:
|
|
59550
|
-
total:
|
|
61095
|
+
pending: status3.pending,
|
|
61096
|
+
in_progress: status3.in_progress,
|
|
61097
|
+
completed: status3.completed,
|
|
61098
|
+
total: status3.total
|
|
59551
61099
|
},
|
|
59552
61100
|
steps,
|
|
59553
61101
|
completed_at: exportedAt2,
|
|
59554
|
-
message: `Demo complete: ${
|
|
61102
|
+
message: `Demo complete: ${status3.completed}/${status3.total} tasks done, 1 agent run, 1 run record`
|
|
59555
61103
|
};
|
|
59556
61104
|
} finally {
|
|
59557
61105
|
if (dbHandle)
|
|
@@ -60525,8 +62073,8 @@ function rowToOrg(row) {
|
|
|
60525
62073
|
function createOrg(input, db) {
|
|
60526
62074
|
const d = db || getDatabase();
|
|
60527
62075
|
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 || {}),
|
|
62076
|
+
const timestamp5 = now();
|
|
62077
|
+
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
62078
|
return getOrg(id, d);
|
|
60531
62079
|
}
|
|
60532
62080
|
function getOrg(id, db) {
|
|
@@ -60774,10 +62322,10 @@ function getCriticalPath2(opts, db) {
|
|
|
60774
62322
|
function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
|
|
60775
62323
|
const d = db || getDatabase();
|
|
60776
62324
|
const id = uuid();
|
|
60777
|
-
const
|
|
62325
|
+
const timestamp5 = now();
|
|
60778
62326
|
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:
|
|
62327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp5]);
|
|
62328
|
+
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp5 };
|
|
60781
62329
|
}
|
|
60782
62330
|
function removeKgEdges(sourceId, targetId, relationType, db) {
|
|
60783
62331
|
const d = db || getDatabase();
|
|
@@ -61677,7 +63225,7 @@ function runSavedSearch(filters = {}, scope = "tasks", db) {
|
|
|
61677
63225
|
function saveSearchView(input, db) {
|
|
61678
63226
|
const d = db || getDatabase();
|
|
61679
63227
|
const name = normalizeName3(input.name);
|
|
61680
|
-
const
|
|
63228
|
+
const timestamp5 = now();
|
|
61681
63229
|
const existing = getSearchView(name, d);
|
|
61682
63230
|
if (existing) {
|
|
61683
63231
|
d.run(`UPDATE saved_search_views
|
|
@@ -61686,7 +63234,7 @@ function saveSearchView(input, db) {
|
|
|
61686
63234
|
input.description ?? existing.description,
|
|
61687
63235
|
normalizeScope(input.scope ?? existing.scope),
|
|
61688
63236
|
JSON.stringify(input.filters ?? existing.filters),
|
|
61689
|
-
|
|
63237
|
+
timestamp5,
|
|
61690
63238
|
existing.id
|
|
61691
63239
|
]);
|
|
61692
63240
|
return getSearchView(existing.id, d);
|
|
@@ -61699,8 +63247,8 @@ function saveSearchView(input, db) {
|
|
|
61699
63247
|
input.description ?? null,
|
|
61700
63248
|
normalizeScope(input.scope),
|
|
61701
63249
|
JSON.stringify(input.filters ?? {}),
|
|
61702
|
-
|
|
61703
|
-
|
|
63250
|
+
timestamp5,
|
|
63251
|
+
timestamp5
|
|
61704
63252
|
]);
|
|
61705
63253
|
return getSearchView(id, d);
|
|
61706
63254
|
}
|
|
@@ -61742,14 +63290,14 @@ function readClaudeTask(dir, filename) {
|
|
|
61742
63290
|
function writeClaudeTask(dir, task3) {
|
|
61743
63291
|
writeJsonFile(join23(dir, `${task3.id}.json`), task3);
|
|
61744
63292
|
}
|
|
61745
|
-
function toClaudeStatus(
|
|
61746
|
-
if (
|
|
61747
|
-
return
|
|
63293
|
+
function toClaudeStatus(status3) {
|
|
63294
|
+
if (status3 === "pending" || status3 === "in_progress" || status3 === "completed") {
|
|
63295
|
+
return status3;
|
|
61748
63296
|
}
|
|
61749
63297
|
return "completed";
|
|
61750
63298
|
}
|
|
61751
|
-
function toSqliteStatus(
|
|
61752
|
-
return
|
|
63299
|
+
function toSqliteStatus(status3) {
|
|
63300
|
+
return status3;
|
|
61753
63301
|
}
|
|
61754
63302
|
function readPrefixCounter(dir) {
|
|
61755
63303
|
const path = join23(dir, ".prefix-counter");
|
|
@@ -62710,7 +64258,7 @@ init_database();
|
|
|
62710
64258
|
function logTrace(input, db) {
|
|
62711
64259
|
const d = db || getDatabase();
|
|
62712
64260
|
const id = uuid();
|
|
62713
|
-
const
|
|
64261
|
+
const timestamp5 = now();
|
|
62714
64262
|
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
64263
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
62716
64264
|
id,
|
|
@@ -62723,7 +64271,7 @@ function logTrace(input, db) {
|
|
|
62723
64271
|
input.duration_ms ?? null,
|
|
62724
64272
|
input.tokens ?? null,
|
|
62725
64273
|
input.cost_usd ?? null,
|
|
62726
|
-
|
|
64274
|
+
timestamp5
|
|
62727
64275
|
]);
|
|
62728
64276
|
return {
|
|
62729
64277
|
id,
|
|
@@ -62736,7 +64284,7 @@ function logTrace(input, db) {
|
|
|
62736
64284
|
duration_ms: input.duration_ms ?? null,
|
|
62737
64285
|
tokens: input.tokens ?? null,
|
|
62738
64286
|
cost_usd: input.cost_usd ?? null,
|
|
62739
|
-
created_at:
|
|
64287
|
+
created_at: timestamp5
|
|
62740
64288
|
};
|
|
62741
64289
|
}
|
|
62742
64290
|
function getTaskTraces(taskId, db) {
|
|
@@ -62760,7 +64308,7 @@ init_database();
|
|
|
62760
64308
|
init_tasks();
|
|
62761
64309
|
function setBudget(agentId, opts, db) {
|
|
62762
64310
|
const d = db || getDatabase();
|
|
62763
|
-
const
|
|
64311
|
+
const timestamp5 = now();
|
|
62764
64312
|
d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
|
|
62765
64313
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
62766
64314
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
@@ -62774,13 +64322,13 @@ function setBudget(agentId, opts, db) {
|
|
|
62774
64322
|
opts.max_cost_usd ?? null,
|
|
62775
64323
|
opts.max_task_minutes ?? null,
|
|
62776
64324
|
opts.period_hours ?? 24,
|
|
62777
|
-
|
|
62778
|
-
|
|
64325
|
+
timestamp5,
|
|
64326
|
+
timestamp5,
|
|
62779
64327
|
opts.max_concurrent ?? null,
|
|
62780
64328
|
opts.max_cost_usd ?? null,
|
|
62781
64329
|
opts.max_task_minutes ?? null,
|
|
62782
64330
|
opts.period_hours ?? null,
|
|
62783
|
-
|
|
64331
|
+
timestamp5
|
|
62784
64332
|
]);
|
|
62785
64333
|
return getBudget(agentId, d);
|
|
62786
64334
|
}
|
|
@@ -63083,11 +64631,11 @@ function resolveWorkflowState(input, projectPath) {
|
|
|
63083
64631
|
return { input, state: byCanonical, matched_by: "canonical_status" };
|
|
63084
64632
|
throw new Error(`Unknown workflow state: ${input}`);
|
|
63085
64633
|
}
|
|
63086
|
-
function stateForCanonicalStatus(
|
|
63087
|
-
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status ===
|
|
64634
|
+
function stateForCanonicalStatus(status3, projectPath) {
|
|
64635
|
+
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status3));
|
|
63088
64636
|
if (configured)
|
|
63089
64637
|
return configured;
|
|
63090
|
-
return listWorkflowStates(projectPath).find((state) => state.canonical_status ===
|
|
64638
|
+
return listWorkflowStates(projectPath).find((state) => state.canonical_status === status3) || resolveWorkflowState(status3, projectPath).state;
|
|
63091
64639
|
}
|
|
63092
64640
|
function getTaskWorkflowState(taskId, db, projectPath) {
|
|
63093
64641
|
const d = db || getDatabase();
|
|
@@ -63251,9 +64799,9 @@ function unpackFixture(input) {
|
|
|
63251
64799
|
const contextPack = input["context_pack"];
|
|
63252
64800
|
return isObject(contextPack) ? contextPack : input;
|
|
63253
64801
|
}
|
|
63254
|
-
function approvalStatus(
|
|
63255
|
-
if (
|
|
63256
|
-
return
|
|
64802
|
+
function approvalStatus(status3) {
|
|
64803
|
+
if (status3 === "approved" || status3 === "rejected" || status3 === "expired")
|
|
64804
|
+
return status3;
|
|
63257
64805
|
return "pending";
|
|
63258
64806
|
}
|
|
63259
64807
|
function approvalsFromFixture(fixture, runs) {
|
|
@@ -63350,17 +64898,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63350
64898
|
if (["completed", "failed", "cancelled"].includes(runStatus))
|
|
63351
64899
|
currentStatus = runStatus;
|
|
63352
64900
|
for (const command of asArray2(run["commands"])) {
|
|
63353
|
-
const
|
|
64901
|
+
const status3 = normalizedStatus(command["status"]);
|
|
63354
64902
|
const commandText = text(command["command"]) || "command";
|
|
63355
64903
|
push({
|
|
63356
64904
|
type: "command",
|
|
63357
64905
|
source: "run.commands",
|
|
63358
64906
|
label: commandText,
|
|
63359
|
-
status:
|
|
64907
|
+
status: status3,
|
|
63360
64908
|
command: commandText,
|
|
63361
64909
|
message: text(command["output_summary"])
|
|
63362
64910
|
});
|
|
63363
|
-
if (
|
|
64911
|
+
if (status3 === "failed")
|
|
63364
64912
|
failures.push({ source: "run.commands", message: text(command["output_summary"]) || "command failed", command: commandText });
|
|
63365
64913
|
}
|
|
63366
64914
|
for (const file of asArray2(run["files"])) {
|
|
@@ -63377,17 +64925,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63377
64925
|
}
|
|
63378
64926
|
}
|
|
63379
64927
|
for (const verification2 of verifications) {
|
|
63380
|
-
const
|
|
64928
|
+
const status3 = normalizedStatus(verification2["status"]);
|
|
63381
64929
|
const commandText = text(verification2["command"]) || "verification";
|
|
63382
64930
|
push({
|
|
63383
64931
|
type: "command",
|
|
63384
64932
|
source: "traceability.verifications",
|
|
63385
64933
|
label: commandText,
|
|
63386
|
-
status:
|
|
64934
|
+
status: status3,
|
|
63387
64935
|
command: commandText,
|
|
63388
64936
|
message: text(verification2["output_summary"])
|
|
63389
64937
|
});
|
|
63390
|
-
if (
|
|
64938
|
+
if (status3 === "failed")
|
|
63391
64939
|
failures.push({ source: "traceability.verifications", message: text(verification2["output_summary"]) || "verification failed", command: commandText });
|
|
63392
64940
|
}
|
|
63393
64941
|
const approvals = approvalsFromFixture(fixture, runs);
|
|
@@ -63923,7 +65471,7 @@ function installLocalExtension(input) {
|
|
|
63923
65471
|
const validation = inspected.validation;
|
|
63924
65472
|
if (!validation.ok)
|
|
63925
65473
|
throw new Error(`extension manifest invalid: ${validation.errors.join("; ")}`);
|
|
63926
|
-
const
|
|
65474
|
+
const timestamp5 = new Date().toISOString();
|
|
63927
65475
|
const config = loadConfig();
|
|
63928
65476
|
const existing = config.extension_registry?.[inspected.manifest.name];
|
|
63929
65477
|
const trusted = Boolean(input.trust);
|
|
@@ -63943,8 +65491,8 @@ function installLocalExtension(input) {
|
|
|
63943
65491
|
...trusted ? [] : ["extension installed as needs_review until trusted explicitly"]
|
|
63944
65492
|
],
|
|
63945
65493
|
diagnostics: testExtensionCompatibility(inspected.source),
|
|
63946
|
-
installed_at: existing?.installed_at ||
|
|
63947
|
-
updated_at:
|
|
65494
|
+
installed_at: existing?.installed_at || timestamp5,
|
|
65495
|
+
updated_at: timestamp5
|
|
63948
65496
|
};
|
|
63949
65497
|
saveConfig({
|
|
63950
65498
|
...config,
|
|
@@ -64306,9 +65854,9 @@ async function sendToTmux(target, message, delayMs, options = false) {
|
|
|
64306
65854
|
ensureTmuxAvailable();
|
|
64307
65855
|
if (!opts.confirmBusy) {
|
|
64308
65856
|
const pane = await inspectTmuxPane(target);
|
|
64309
|
-
const
|
|
64310
|
-
if (
|
|
64311
|
-
throw new Error(`tmux target "${target}" appears busy (${
|
|
65857
|
+
const status3 = tmuxPaneBusyStatus(pane);
|
|
65858
|
+
if (status3.busy) {
|
|
65859
|
+
throw new Error(`tmux target "${target}" appears busy (${status3.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
64312
65860
|
}
|
|
64313
65861
|
}
|
|
64314
65862
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
@@ -64453,10 +66001,10 @@ function normalizeStatus(value) {
|
|
|
64453
66001
|
return "open";
|
|
64454
66002
|
}
|
|
64455
66003
|
function normalizeResolutionStatus(value) {
|
|
64456
|
-
const
|
|
64457
|
-
if (
|
|
66004
|
+
const status3 = normalizeStatus(value || "resolved");
|
|
66005
|
+
if (status3 === "open")
|
|
64458
66006
|
throw new Error("resolve-missing status must be resolved or ignored");
|
|
64459
|
-
return
|
|
66007
|
+
return status3;
|
|
64460
66008
|
}
|
|
64461
66009
|
function redactOptional(value, max = 2000) {
|
|
64462
66010
|
if (!value)
|
|
@@ -64494,7 +66042,7 @@ function compactFinding(finding2) {
|
|
|
64494
66042
|
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
64495
66043
|
};
|
|
64496
66044
|
}
|
|
64497
|
-
function previewFinding(existing, next,
|
|
66045
|
+
function previewFinding(existing, next, timestamp5) {
|
|
64498
66046
|
return {
|
|
64499
66047
|
...existing,
|
|
64500
66048
|
run_id: next.run_id,
|
|
@@ -64505,9 +66053,9 @@ function previewFinding(existing, next, timestamp4) {
|
|
|
64505
66053
|
summary: next.summary,
|
|
64506
66054
|
artifact_path: next.artifact_path,
|
|
64507
66055
|
metadata: next.metadata,
|
|
64508
|
-
last_seen_at:
|
|
64509
|
-
resolved_at: next.status === "open" ? null : existing.resolved_at ||
|
|
64510
|
-
updated_at:
|
|
66056
|
+
last_seen_at: timestamp5,
|
|
66057
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
66058
|
+
updated_at: timestamp5
|
|
64511
66059
|
};
|
|
64512
66060
|
}
|
|
64513
66061
|
function upsertAction(existing, next) {
|
|
@@ -64557,7 +66105,7 @@ function sameFinding(left, right) {
|
|
|
64557
66105
|
function upsertTaskFinding(input, db) {
|
|
64558
66106
|
const d = db || getDatabase();
|
|
64559
66107
|
assertTask(input.task_id, d);
|
|
64560
|
-
const
|
|
66108
|
+
const timestamp5 = input.observed_at || now();
|
|
64561
66109
|
const warnings = [];
|
|
64562
66110
|
const next = nextFinding(input, d);
|
|
64563
66111
|
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
@@ -64568,10 +66116,10 @@ function upsertTaskFinding(input, db) {
|
|
|
64568
66116
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64569
66117
|
local_only: true,
|
|
64570
66118
|
dry_run: true,
|
|
64571
|
-
processed_at:
|
|
66119
|
+
processed_at: timestamp5,
|
|
64572
66120
|
action: action2,
|
|
64573
66121
|
fingerprint: next.fingerprint,
|
|
64574
|
-
finding: existing ? compactFinding(previewFinding(existing, next,
|
|
66122
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp5)) : null,
|
|
64575
66123
|
warnings
|
|
64576
66124
|
};
|
|
64577
66125
|
}
|
|
@@ -64592,17 +66140,17 @@ function upsertTaskFinding(input, db) {
|
|
|
64592
66140
|
next.summary,
|
|
64593
66141
|
next.artifact_path,
|
|
64594
66142
|
JSON.stringify(next.metadata),
|
|
64595
|
-
|
|
64596
|
-
|
|
64597
|
-
next.status === "open" ? null :
|
|
64598
|
-
|
|
64599
|
-
|
|
66143
|
+
timestamp5,
|
|
66144
|
+
timestamp5,
|
|
66145
|
+
next.status === "open" ? null : timestamp5,
|
|
66146
|
+
timestamp5,
|
|
66147
|
+
timestamp5
|
|
64600
66148
|
]);
|
|
64601
66149
|
return {
|
|
64602
66150
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64603
66151
|
local_only: true,
|
|
64604
66152
|
dry_run: false,
|
|
64605
|
-
processed_at:
|
|
66153
|
+
processed_at: timestamp5,
|
|
64606
66154
|
action: "created",
|
|
64607
66155
|
fingerprint: next.fingerprint,
|
|
64608
66156
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64614,7 +66162,7 @@ function upsertTaskFinding(input, db) {
|
|
|
64614
66162
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64615
66163
|
local_only: true,
|
|
64616
66164
|
dry_run: false,
|
|
64617
|
-
processed_at:
|
|
66165
|
+
processed_at: timestamp5,
|
|
64618
66166
|
action: "matched",
|
|
64619
66167
|
fingerprint: next.fingerprint,
|
|
64620
66168
|
finding: compactFinding(existing),
|
|
@@ -64634,16 +66182,16 @@ function upsertTaskFinding(input, db) {
|
|
|
64634
66182
|
next.summary,
|
|
64635
66183
|
next.artifact_path,
|
|
64636
66184
|
JSON.stringify(next.metadata),
|
|
64637
|
-
|
|
64638
|
-
next.status === "open" ? null : existing.resolved_at ||
|
|
64639
|
-
|
|
66185
|
+
timestamp5,
|
|
66186
|
+
next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
66187
|
+
timestamp5,
|
|
64640
66188
|
existing.id
|
|
64641
66189
|
]);
|
|
64642
66190
|
return {
|
|
64643
66191
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64644
66192
|
local_only: true,
|
|
64645
66193
|
dry_run: false,
|
|
64646
|
-
processed_at:
|
|
66194
|
+
processed_at: timestamp5,
|
|
64647
66195
|
action,
|
|
64648
66196
|
fingerprint: next.fingerprint,
|
|
64649
66197
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64680,8 +66228,8 @@ function listCompactTaskFindings(filter = {}, db) {
|
|
|
64680
66228
|
function resolveMissingTaskFindings(input, db) {
|
|
64681
66229
|
const d = db || getDatabase();
|
|
64682
66230
|
assertTask(input.task_id, d);
|
|
64683
|
-
const
|
|
64684
|
-
const
|
|
66231
|
+
const timestamp5 = input.resolved_at || now();
|
|
66232
|
+
const status3 = normalizeResolutionStatus(input.status);
|
|
64685
66233
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
64686
66234
|
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
64687
66235
|
const warnings = [];
|
|
@@ -64700,7 +66248,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64700
66248
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64701
66249
|
local_only: true,
|
|
64702
66250
|
dry_run: true,
|
|
64703
|
-
processed_at:
|
|
66251
|
+
processed_at: timestamp5,
|
|
64704
66252
|
action: candidates.length > 0 ? "preview" : "noop",
|
|
64705
66253
|
task_id: input.task_id,
|
|
64706
66254
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
@@ -64723,10 +66271,10 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64723
66271
|
const tx = d.transaction(() => {
|
|
64724
66272
|
for (const finding2 of candidates) {
|
|
64725
66273
|
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
64726
|
-
|
|
64727
|
-
|
|
66274
|
+
status3,
|
|
66275
|
+
timestamp5,
|
|
64728
66276
|
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
64729
|
-
|
|
66277
|
+
timestamp5,
|
|
64730
66278
|
finding2.id
|
|
64731
66279
|
]);
|
|
64732
66280
|
}
|
|
@@ -64738,8 +66286,8 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64738
66286
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64739
66287
|
local_only: true,
|
|
64740
66288
|
dry_run: false,
|
|
64741
|
-
processed_at:
|
|
64742
|
-
action: updated.length > 0 ?
|
|
66289
|
+
processed_at: timestamp5,
|
|
66290
|
+
action: updated.length > 0 ? status3 : "noop",
|
|
64743
66291
|
task_id: input.task_id,
|
|
64744
66292
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
64745
66293
|
run_id: runId,
|
|
@@ -65632,7 +67180,7 @@ init_tasks();
|
|
|
65632
67180
|
init_task_relations();
|
|
65633
67181
|
init_config2();
|
|
65634
67182
|
var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
|
|
65635
|
-
function
|
|
67183
|
+
function timestamp5() {
|
|
65636
67184
|
return new Date().toISOString();
|
|
65637
67185
|
}
|
|
65638
67186
|
function todayIso() {
|
|
@@ -65716,7 +67264,7 @@ function upsertCapacityProfile(input) {
|
|
|
65716
67264
|
const store = readStore2();
|
|
65717
67265
|
const key2 = profileKey(agentId, projectId);
|
|
65718
67266
|
const existing = store.profiles[key2];
|
|
65719
|
-
const now4 =
|
|
67267
|
+
const now4 = timestamp5();
|
|
65720
67268
|
const profile = {
|
|
65721
67269
|
id: existing?.id ?? key2,
|
|
65722
67270
|
agent_id: agentId,
|
|
@@ -65797,7 +67345,7 @@ function getPlanningForecast(input = {}, db) {
|
|
|
65797
67345
|
return {
|
|
65798
67346
|
schema_version: LOCAL_CAPACITY_SCHEMA_VERSION,
|
|
65799
67347
|
local_only: true,
|
|
65800
|
-
generated_at:
|
|
67348
|
+
generated_at: timestamp5(),
|
|
65801
67349
|
filters: {
|
|
65802
67350
|
project_id: input.project_id ?? null,
|
|
65803
67351
|
plan_id: input.plan_id ?? null,
|
|
@@ -66185,6 +67733,8 @@ export {
|
|
|
66185
67733
|
testTerminalNotificationRule,
|
|
66186
67734
|
testLocalEventHook,
|
|
66187
67735
|
tasksFromTemplate,
|
|
67736
|
+
taskSubtreeTransferRollbackRequestDigest,
|
|
67737
|
+
taskSubtreeTransferRequestDigest,
|
|
66188
67738
|
taskManifestRequestDigest,
|
|
66189
67739
|
taskManifestCompensationRequestDigest,
|
|
66190
67740
|
taskFromTemplate,
|
|
@@ -66207,6 +67757,7 @@ export {
|
|
|
66207
67757
|
startTaskRun,
|
|
66208
67758
|
startTask,
|
|
66209
67759
|
startFocusSession,
|
|
67760
|
+
sqliteTodosTaskSubtreeTransferSchemaSql,
|
|
66210
67761
|
sqliteTodosTaskManifestSchemaSql,
|
|
66211
67762
|
sqliteTodosProjectRegistrationSchemaSql,
|
|
66212
67763
|
softDeleteArtifact,
|
|
@@ -66398,6 +67949,7 @@ export {
|
|
|
66398
67949
|
previewInstalledTemplate,
|
|
66399
67950
|
previewInboxIntake,
|
|
66400
67951
|
previewBuiltinTemplate,
|
|
67952
|
+
postgresTodosTaskSubtreeTransferSchemaSql,
|
|
66401
67953
|
postgresTodosTaskManifestSchemaSql,
|
|
66402
67954
|
postgresTodosSyncSchemaSql,
|
|
66403
67955
|
postgresTodosProjectRegistrationSchemaSql,
|
|
@@ -66406,6 +67958,9 @@ export {
|
|
|
66406
67958
|
planPlanProjectLink,
|
|
66407
67959
|
pauseFocusSession,
|
|
66408
67960
|
patrolTasks,
|
|
67961
|
+
parseTodosTaskSubtreeTransferRollback,
|
|
67962
|
+
parseTodosTaskSubtreeTransferInspect,
|
|
67963
|
+
parseTodosTaskSubtreeTransferApply,
|
|
66409
67964
|
parseTodosTaskManifestCompensation,
|
|
66410
67965
|
parseTodosTaskManifestBindingLookup,
|
|
66411
67966
|
parseTodosTaskManifest,
|
|
@@ -66413,7 +67968,6 @@ export {
|
|
|
66413
67968
|
parseTodosAiOutputSchema,
|
|
66414
67969
|
parseTodosAiJson,
|
|
66415
67970
|
parseTmuxTarget,
|
|
66416
|
-
parseStorageMode,
|
|
66417
67971
|
parseStorageBackend,
|
|
66418
67972
|
parseRecurrenceRule,
|
|
66419
67973
|
parseQuietHours,
|
|
@@ -66564,7 +68118,6 @@ export {
|
|
|
66564
68118
|
issueToTask,
|
|
66565
68119
|
isWorktreePath,
|
|
66566
68120
|
isValidRecurrenceRule,
|
|
66567
|
-
isTodosRemoteStorageEnabled,
|
|
66568
68121
|
isTodosPostgresBackend,
|
|
66569
68122
|
isTodosAiUpdateTaskResult,
|
|
66570
68123
|
isTodosAiRuntimeEvent,
|
|
@@ -66604,6 +68157,7 @@ export {
|
|
|
66604
68157
|
importActivityLog,
|
|
66605
68158
|
hasSecretFindings,
|
|
66606
68159
|
hasActiveApiKeys,
|
|
68160
|
+
handleTodosTaskSubtreeTransferHttpRequest,
|
|
66607
68161
|
handleTodosTaskManifestHttpRequest,
|
|
66608
68162
|
handleTodosProjectRegistrationHttpRequest,
|
|
66609
68163
|
getWorkspaceTrustStatus,
|
|
@@ -66617,7 +68171,6 @@ export {
|
|
|
66617
68171
|
getUnlockImpact,
|
|
66618
68172
|
getTraceStats,
|
|
66619
68173
|
getTopologyDocs,
|
|
66620
|
-
getTodosStorageMode,
|
|
66621
68174
|
getTodosStorageEnvName,
|
|
66622
68175
|
getTodosStorageDatabaseUrl,
|
|
66623
68176
|
getTodosStorageDatabaseEnv,
|
|
@@ -66662,7 +68215,6 @@ export {
|
|
|
66662
68215
|
getTaskBoard,
|
|
66663
68216
|
getTask,
|
|
66664
68217
|
getStoredHandoffAsPacket,
|
|
66665
|
-
getStorageMode,
|
|
66666
68218
|
getStorageDatabaseUrl,
|
|
66667
68219
|
getStorageDatabaseEnv,
|
|
66668
68220
|
getStatus,
|
|
@@ -66921,6 +68473,9 @@ export {
|
|
|
66921
68473
|
detectInboxSourceType,
|
|
66922
68474
|
detectCyclesFromEdges,
|
|
66923
68475
|
describeTerminalNotificationRule,
|
|
68476
|
+
deriveTodosTaskSubtreeTransferRollbackPreconditionDigest,
|
|
68477
|
+
deriveTodosTaskSubtreeTransferIdempotencyKey,
|
|
68478
|
+
deriveTodosTaskSubtreeTransferApplyPreconditionDigest,
|
|
66924
68479
|
deriveTodosTaskManifestIdempotencyKey,
|
|
66925
68480
|
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66926
68481
|
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
@@ -66957,6 +68512,7 @@ export {
|
|
|
66957
68512
|
createVerificationEvidence,
|
|
66958
68513
|
createUserScaffold,
|
|
66959
68514
|
createTuiDashboardSnapshot,
|
|
68515
|
+
createTodosTaskSubtreeTransferHttpClient,
|
|
66960
68516
|
createTodosTaskManifestHttpClient,
|
|
66961
68517
|
createTodosStorageAdapter,
|
|
66962
68518
|
createTodosS3ArtifactStore,
|
|
@@ -66971,6 +68527,7 @@ export {
|
|
|
66971
68527
|
createTaskList,
|
|
66972
68528
|
createTaskBoard,
|
|
66973
68529
|
createTask,
|
|
68530
|
+
createSqliteTodosTaskSubtreeTransferAuthority,
|
|
66974
68531
|
createSqliteTodosTaskManifestAuthority,
|
|
66975
68532
|
createSessionRecoveryHandoff,
|
|
66976
68533
|
createSession,
|
|
@@ -66986,6 +68543,7 @@ export {
|
|
|
66986
68543
|
createReminder,
|
|
66987
68544
|
createReleaseCompatibilityReport,
|
|
66988
68545
|
createProject,
|
|
68546
|
+
createPostgresTodosTaskSubtreeTransferAuthority,
|
|
66989
68547
|
createPostgresTodosTaskManifestAuthority,
|
|
66990
68548
|
createPostgresTodosSyncStore,
|
|
66991
68549
|
createPostgresTodosStorageAdapter,
|
|
@@ -67154,6 +68712,8 @@ export {
|
|
|
67154
68712
|
VersionConflictError,
|
|
67155
68713
|
VERIFICATION_EVIDENCE_SCHEMA,
|
|
67156
68714
|
USER_SCAFFOLD_SCHEMA,
|
|
68715
|
+
TodosTaskSubtreeTransferHttpClient,
|
|
68716
|
+
TodosTaskSubtreeTransferError,
|
|
67157
68717
|
TodosTaskManifestHttpClient,
|
|
67158
68718
|
TodosTaskManifestError,
|
|
67159
68719
|
TodosProjectRegistrationHttpClient,
|
|
@@ -67165,6 +68725,8 @@ export {
|
|
|
67165
68725
|
TaskNotFoundError,
|
|
67166
68726
|
TaskListNotFoundError,
|
|
67167
68727
|
TUI_DASHBOARD_VIEWS,
|
|
68728
|
+
TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
68729
|
+
TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
67168
68730
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
67169
68731
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
67170
68732
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
@@ -67271,6 +68833,7 @@ export {
|
|
|
67271
68833
|
PostgresTodosProjectRegistrationBackend,
|
|
67272
68834
|
PlanProjectLinkError,
|
|
67273
68835
|
PlanNotFoundError,
|
|
68836
|
+
PackageOwnedTodosTaskSubtreeTransferAuthority,
|
|
67274
68837
|
PackageOwnedTodosTaskManifestAuthority,
|
|
67275
68838
|
PackageOwnedTodosProjectRegistrationAuthority,
|
|
67276
68839
|
PROJECT_DEPENDENCY_GRAPH_SCHEMA,
|