@hasna/todos 0.15.34 → 0.15.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +53 -19
- 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 +3551 -836
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +147 -57
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plan-comments.d.ts +14 -0
- package/dist/db/plan-comments.d.ts.map +1 -0
- package/dist/db/projects.d.ts +23 -0
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-relations.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1994 -346
- package/dist/json-contracts.d.ts.map +1 -1
- 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/onboarding-fixtures.d.ts.map +1 -1
- package/dist/mcp/index.js +2491 -507
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp.js +11 -4
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +362 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +154 -57
- 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 +144 -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 +5247 -8429
- package/dist/server/openapi.d.ts +2104 -1314
- 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 +327 -75
- package/dist/task-subtree-transfer/authority.d.ts +27 -0
- package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
- package/dist/task-subtree-transfer/backend.d.ts +9 -0
- package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
- package/dist/task-subtree-transfer/canonical.d.ts +14 -0
- package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
- package/dist/task-subtree-transfer/http.d.ts +16 -0
- package/dist/task-subtree-transfer/http.d.ts.map +1 -0
- package/dist/task-subtree-transfer/index.d.ts +6 -0
- package/dist/task-subtree-transfer/index.d.ts.map +1 -0
- package/dist/task-subtree-transfer/planner.d.ts +24 -0
- package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
- package/dist/task-subtree-transfer/postgres.d.ts +19 -0
- package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
- package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
- package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
- package/dist/task-subtree-transfer/types.d.ts +148 -0
- package/dist/task-subtree-transfer/types.d.ts.map +1 -0
- package/dist/task-subtree-transfer.d.ts +2 -0
- package/dist/task-subtree-transfer.d.ts.map +1 -0
- package/dist/task-subtree-transfer.js +1869 -0
- package/dist/testing.d.ts +41 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +44 -1
- package/dist/types/index.d.ts +29 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -1884,6 +1884,10 @@ var init_migrations = __esm(() => {
|
|
|
1884
1884
|
);
|
|
1885
1885
|
INSERT OR IGNORE INTO _migrations (id) VALUES (70);
|
|
1886
1886
|
COMMIT;
|
|
1887
|
+
`,
|
|
1888
|
+
`BEGIN;
|
|
1889
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (71);
|
|
1890
|
+
COMMIT;
|
|
1887
1891
|
`
|
|
1888
1892
|
];
|
|
1889
1893
|
});
|
|
@@ -2557,6 +2561,8 @@ function ensureSchema(db) {
|
|
|
2557
2561
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2558
2562
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2559
2563
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
2564
|
+
ensureColumn("projects", "parent_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
2565
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_parent_id ON projects(parent_id)");
|
|
2560
2566
|
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
2561
2567
|
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
2562
2568
|
ensureColumn("tasks", "short_id", "TEXT");
|
|
@@ -2670,6 +2676,20 @@ function ensureSchema(db) {
|
|
|
2670
2676
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
2671
2677
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
2672
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)");
|
|
2673
2693
|
ensureColumn("projects", "machine_id", "TEXT");
|
|
2674
2694
|
ensureColumn("projects", "synced_at", "TEXT");
|
|
2675
2695
|
ensureColumn("tasks", "machine_id", "TEXT");
|
|
@@ -3651,6 +3671,7 @@ var init_machines = __esm(() => {
|
|
|
3651
3671
|
"task_lists",
|
|
3652
3672
|
"plans",
|
|
3653
3673
|
"task_comments",
|
|
3674
|
+
"plan_comments",
|
|
3654
3675
|
"sessions",
|
|
3655
3676
|
"task_history",
|
|
3656
3677
|
"webhooks",
|
|
@@ -4154,20 +4175,16 @@ var init_sync_utils = __esm(() => {
|
|
|
4154
4175
|
// src/storage/config.ts
|
|
4155
4176
|
var exports_config = {};
|
|
4156
4177
|
__export(exports_config, {
|
|
4157
|
-
parseStorageMode: () => parseStorageMode,
|
|
4158
4178
|
parseStorageBackend: () => parseStorageBackend,
|
|
4159
4179
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
4160
4180
|
loadStorageConfig: () => loadStorageConfig,
|
|
4161
4181
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
4162
|
-
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
4163
4182
|
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
4164
4183
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
4165
|
-
getTodosStorageMode: () => getTodosStorageMode,
|
|
4166
4184
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
4167
4185
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
4168
4186
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
4169
4187
|
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
4170
|
-
getStorageMode: () => getStorageMode,
|
|
4171
4188
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
4172
4189
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
4173
4190
|
getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
|
|
@@ -4177,7 +4194,6 @@ __export(exports_config, {
|
|
|
4177
4194
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4178
4195
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4179
4196
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4180
|
-
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4181
4197
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4182
4198
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4183
4199
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4232,9 +4248,6 @@ function loadStorageConfig(env = process.env) {
|
|
|
4232
4248
|
function isTodosPostgresBackend(config) {
|
|
4233
4249
|
return config.mode === "postgres";
|
|
4234
4250
|
}
|
|
4235
|
-
function isTodosRemoteStorageEnabled(config) {
|
|
4236
|
-
return isTodosPostgresBackend(config);
|
|
4237
|
-
}
|
|
4238
4251
|
function assertTodosRemoteStorageConfig(config) {
|
|
4239
4252
|
if (!isTodosPostgresBackend(config))
|
|
4240
4253
|
return;
|
|
@@ -4250,32 +4263,15 @@ function parseStorageBackend(value) {
|
|
|
4250
4263
|
return "sqlite";
|
|
4251
4264
|
if (normalized === "postgres" || normalized === "postgresql")
|
|
4252
4265
|
return "postgres";
|
|
4253
|
-
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4254
|
-
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.`);
|
|
4255
|
-
}
|
|
4256
4266
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4257
4267
|
}
|
|
4258
|
-
function parseStorageMode(value) {
|
|
4259
|
-
return parseStorageBackend(value);
|
|
4260
|
-
}
|
|
4261
4268
|
function getTodosStorageBackend(env = process.env) {
|
|
4262
|
-
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4263
|
-
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4264
|
-
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.`);
|
|
4265
|
-
}
|
|
4266
|
-
}
|
|
4267
4269
|
if (!getTodosStorageDatabaseUrl(env))
|
|
4268
4270
|
return "sqlite";
|
|
4269
4271
|
if (isTodosShadowEnabled(env))
|
|
4270
4272
|
return "sqlite";
|
|
4271
4273
|
return "postgres";
|
|
4272
4274
|
}
|
|
4273
|
-
function getTodosStorageMode(env = process.env) {
|
|
4274
|
-
return getTodosStorageBackend(env);
|
|
4275
|
-
}
|
|
4276
|
-
function getStorageMode(env = process.env) {
|
|
4277
|
-
return getTodosStorageBackend(env);
|
|
4278
|
-
}
|
|
4279
4275
|
function isTodosShadowEnabled(env = process.env) {
|
|
4280
4276
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
4281
4277
|
}
|
|
@@ -4342,7 +4338,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
4342
4338
|
}
|
|
4343
4339
|
return parsed;
|
|
4344
4340
|
}
|
|
4345
|
-
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";
|
|
4346
4342
|
var init_config = __esm(() => {
|
|
4347
4343
|
TODOS_STORAGE_TABLES = [
|
|
4348
4344
|
"todos_sync_records",
|
|
@@ -4381,12 +4377,6 @@ var init_config = __esm(() => {
|
|
|
4381
4377
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
4382
4378
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
4383
4379
|
};
|
|
4384
|
-
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
4385
|
-
"HASNA_TODOS_STORAGE_MODE",
|
|
4386
|
-
"HASNA_TODOS_MODE",
|
|
4387
|
-
"TODOS_STORAGE_MODE",
|
|
4388
|
-
"TODOS_MODE"
|
|
4389
|
-
];
|
|
4390
4380
|
});
|
|
4391
4381
|
|
|
4392
4382
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -5045,17 +5035,24 @@ function createProject(input, db) {
|
|
|
5045
5035
|
const id = uuid();
|
|
5046
5036
|
const timestamp2 = now();
|
|
5047
5037
|
const derivedSlug = slugify(input.name);
|
|
5048
|
-
const taskListId = input.task_list_id === undefined ?
|
|
5038
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugify(input.task_list_id);
|
|
5049
5039
|
if (!derivedSlug || !taskListId)
|
|
5050
5040
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
5051
5041
|
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
5052
5042
|
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
5053
5043
|
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
5054
5044
|
}
|
|
5045
|
+
const parentId = input.parent_id ?? null;
|
|
5046
|
+
if (parentId !== null) {
|
|
5047
|
+
const parent = getProject(parentId, d);
|
|
5048
|
+
if (!parent)
|
|
5049
|
+
throw new ProjectNotFoundError(parentId);
|
|
5050
|
+
assertNotProjectAncestor(id, parentId, d);
|
|
5051
|
+
}
|
|
5055
5052
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
5056
5053
|
const machineId = currentStorageMachineId(d);
|
|
5057
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
5058
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
5054
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at, machine_id)
|
|
5055
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, parentId, timestamp2, timestamp2, machineId]);
|
|
5059
5056
|
return getProject(id, d);
|
|
5060
5057
|
})();
|
|
5061
5058
|
}
|
|
@@ -5080,6 +5077,60 @@ function listProjects(db) {
|
|
|
5080
5077
|
const d = db || getDatabase();
|
|
5081
5078
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
5082
5079
|
}
|
|
5080
|
+
function orderProjectsParentFirst(projects) {
|
|
5081
|
+
const projectId = (project) => {
|
|
5082
|
+
const id = project.id;
|
|
5083
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
5084
|
+
};
|
|
5085
|
+
const parentId = (project) => {
|
|
5086
|
+
const parent = project.parent_id;
|
|
5087
|
+
return parent == null ? null : String(parent);
|
|
5088
|
+
};
|
|
5089
|
+
const byId = new Set;
|
|
5090
|
+
for (const project of projects) {
|
|
5091
|
+
const id = projectId(project);
|
|
5092
|
+
if (id !== null)
|
|
5093
|
+
byId.add(id);
|
|
5094
|
+
}
|
|
5095
|
+
const ordered = [];
|
|
5096
|
+
const emitted = new Set;
|
|
5097
|
+
let remaining = [...projects];
|
|
5098
|
+
let progress = true;
|
|
5099
|
+
while (progress && remaining.length > 0) {
|
|
5100
|
+
progress = false;
|
|
5101
|
+
const deferred = [];
|
|
5102
|
+
for (const project of remaining) {
|
|
5103
|
+
const parent = parentId(project);
|
|
5104
|
+
if (parent === null || emitted.has(parent) || !byId.has(parent)) {
|
|
5105
|
+
ordered.push(project);
|
|
5106
|
+
const id = projectId(project);
|
|
5107
|
+
if (id !== null)
|
|
5108
|
+
emitted.add(id);
|
|
5109
|
+
progress = true;
|
|
5110
|
+
} else {
|
|
5111
|
+
deferred.push(project);
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
remaining = deferred;
|
|
5115
|
+
}
|
|
5116
|
+
ordered.push(...remaining);
|
|
5117
|
+
return ordered;
|
|
5118
|
+
}
|
|
5119
|
+
function assertNotProjectAncestor(projectId, ancestorId, db) {
|
|
5120
|
+
const d = db || getDatabase();
|
|
5121
|
+
let cursor = ancestorId;
|
|
5122
|
+
const seen = new Set;
|
|
5123
|
+
while (cursor !== null) {
|
|
5124
|
+
if (cursor === projectId) {
|
|
5125
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
5126
|
+
}
|
|
5127
|
+
if (seen.has(cursor))
|
|
5128
|
+
break;
|
|
5129
|
+
seen.add(cursor);
|
|
5130
|
+
const row = d.query("SELECT parent_id FROM projects WHERE id = ?").get(cursor);
|
|
5131
|
+
cursor = row?.parent_id ?? null;
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5083
5134
|
function updateProject(id, input, db) {
|
|
5084
5135
|
const d = db || getDatabase();
|
|
5085
5136
|
const project = getProject(id, d);
|
|
@@ -5102,6 +5153,16 @@ function updateProject(id, input, db) {
|
|
|
5102
5153
|
sets.push("path = ?");
|
|
5103
5154
|
params.push(input.path);
|
|
5104
5155
|
}
|
|
5156
|
+
if (input.parent_id !== undefined) {
|
|
5157
|
+
if (input.parent_id !== null) {
|
|
5158
|
+
const parent = getProject(input.parent_id, d);
|
|
5159
|
+
if (!parent)
|
|
5160
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
5161
|
+
assertNotProjectAncestor(id, input.parent_id, d);
|
|
5162
|
+
}
|
|
5163
|
+
sets.push("parent_id = ?");
|
|
5164
|
+
params.push(input.parent_id);
|
|
5165
|
+
}
|
|
5105
5166
|
params.push(id);
|
|
5106
5167
|
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5107
5168
|
return getProject(id, d);
|
|
@@ -6780,22 +6841,31 @@ function updateTaskList(id, input, db) {
|
|
|
6780
6841
|
throw new TaskListNotFoundError(id);
|
|
6781
6842
|
const sets = ["updated_at = ?"];
|
|
6782
6843
|
const params = [now()];
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
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);
|
|
6788
6852
|
if (duplicate) {
|
|
6789
|
-
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`);
|
|
6790
6854
|
}
|
|
6791
|
-
if (
|
|
6855
|
+
if (scopeChanged || effectiveSlug !== existing.slug) {
|
|
6792
6856
|
releaseCanonicalSlugClaims("task_list", id, d);
|
|
6793
|
-
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(
|
|
6794
|
-
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`);
|
|
6795
6859
|
}
|
|
6796
6860
|
}
|
|
6861
|
+
}
|
|
6862
|
+
if (input.slug !== undefined) {
|
|
6797
6863
|
sets.push("slug = ?");
|
|
6798
|
-
params.push(
|
|
6864
|
+
params.push(effectiveSlug);
|
|
6865
|
+
}
|
|
6866
|
+
if (projectId !== existing.project_id) {
|
|
6867
|
+
sets.push("project_id = ?");
|
|
6868
|
+
params.push(projectId);
|
|
6799
6869
|
}
|
|
6800
6870
|
if (input.name !== undefined) {
|
|
6801
6871
|
sets.push("name = ?");
|
|
@@ -9244,8 +9314,8 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
9244
9314
|
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
9245
9315
|
const timestamp2 = now();
|
|
9246
9316
|
const failTx = d.transaction(() => {
|
|
9247
|
-
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
9248
|
-
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]);
|
|
9249
9319
|
if (res.changes === 0) {
|
|
9250
9320
|
const current = getTask(id, d);
|
|
9251
9321
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -9257,6 +9327,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
9257
9327
|
status: "failed",
|
|
9258
9328
|
locked_by: null,
|
|
9259
9329
|
locked_at: null,
|
|
9330
|
+
completed_at: task.completed_at ?? timestamp2,
|
|
9260
9331
|
reason: safeReason,
|
|
9261
9332
|
metadata: safeMeta,
|
|
9262
9333
|
version: task.version + 1,
|
|
@@ -9905,13 +9976,20 @@ function updateTaskStored(id, input, db) {
|
|
|
9905
9976
|
if (isTerminalStatus(input.status)) {
|
|
9906
9977
|
sets.push("locked_by = NULL");
|
|
9907
9978
|
sets.push("locked_at = NULL");
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
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
|
+
}
|
|
9912
9986
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
9913
9987
|
sets.push("completed_at = NULL");
|
|
9914
9988
|
}
|
|
9989
|
+
if (input.status === "in_progress") {
|
|
9990
|
+
sets.push("started_at = COALESCE(started_at, ?)");
|
|
9991
|
+
params.push(timestamp2);
|
|
9992
|
+
}
|
|
9915
9993
|
}
|
|
9916
9994
|
if (input.priority !== undefined) {
|
|
9917
9995
|
sets.push("priority = ?");
|
|
@@ -10049,7 +10127,6 @@ function updateTaskStored(id, input, db) {
|
|
|
10049
10127
|
if (input.approved_by !== undefined)
|
|
10050
10128
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10051
10129
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
10052
|
-
const completedNow = input.status === "completed";
|
|
10053
10130
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
10054
10131
|
const updatedTask = {
|
|
10055
10132
|
...task,
|
|
@@ -10060,7 +10137,8 @@ function updateTaskStored(id, input, db) {
|
|
|
10060
10137
|
updated_at: timestamp2,
|
|
10061
10138
|
locked_by: terminalNow ? null : task.locked_by,
|
|
10062
10139
|
locked_at: terminalNow ? null : task.locked_at,
|
|
10063
|
-
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,
|
|
10064
10142
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
10065
10143
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
10066
10144
|
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
@@ -10121,6 +10199,9 @@ function deleteTask(id, db) {
|
|
|
10121
10199
|
version: row.version
|
|
10122
10200
|
}, d);
|
|
10123
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
|
+
}
|
|
10124
10205
|
return result.changes > 0;
|
|
10125
10206
|
}
|
|
10126
10207
|
var init_task_crud = __esm(() => {
|
|
@@ -12805,10 +12886,10 @@ function cancelDispatch(id, db) {
|
|
|
12805
12886
|
_db2.run("UPDATE dispatches SET status = 'cancelled' WHERE id = ?", [id]);
|
|
12806
12887
|
return getDispatch(id, _db2);
|
|
12807
12888
|
}
|
|
12808
|
-
function updateDispatchStatus(id,
|
|
12889
|
+
function updateDispatchStatus(id, status3, opts = {}, db) {
|
|
12809
12890
|
const _db2 = db ?? getDatabase();
|
|
12810
12891
|
const error = opts.error ? sanitizePreWriteText(opts.error, "dispatch.error") : null;
|
|
12811
|
-
_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]);
|
|
12812
12893
|
}
|
|
12813
12894
|
function createDispatchLog(log, db) {
|
|
12814
12895
|
const _db2 = db ?? getDatabase();
|
|
@@ -12841,7 +12922,7 @@ var init_dispatches = __esm(() => {
|
|
|
12841
12922
|
// package.json
|
|
12842
12923
|
var package_default = {
|
|
12843
12924
|
name: "@hasna/todos",
|
|
12844
|
-
version: "0.15.
|
|
12925
|
+
version: "0.15.41",
|
|
12845
12926
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12846
12927
|
type: "module",
|
|
12847
12928
|
main: "dist/index.js",
|
|
@@ -12887,6 +12968,10 @@ var package_default = {
|
|
|
12887
12968
|
"./task-manifest": {
|
|
12888
12969
|
types: "./dist/task-manifest.d.ts",
|
|
12889
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"
|
|
12890
12975
|
}
|
|
12891
12976
|
},
|
|
12892
12977
|
workspaces: [
|
|
@@ -12900,8 +12985,8 @@ var package_default = {
|
|
|
12900
12985
|
"README.md"
|
|
12901
12986
|
],
|
|
12902
12987
|
scripts: {
|
|
12903
|
-
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",
|
|
12904
|
-
"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/*'",
|
|
12905
12990
|
migrate: "bun run src/server/index.ts migrate",
|
|
12906
12991
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
12907
12992
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -12916,6 +13001,9 @@ var package_default = {
|
|
|
12916
13001
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
12917
13002
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
12918
13003
|
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
13004
|
+
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
13005
|
+
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
13006
|
+
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
12919
13007
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
12920
13008
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
12921
13009
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
@@ -12952,7 +13040,7 @@ var package_default = {
|
|
|
12952
13040
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
12953
13041
|
license: "Apache-2.0",
|
|
12954
13042
|
dependencies: {
|
|
12955
|
-
"@hasna/contracts": "0.
|
|
13043
|
+
"@hasna/contracts": "0.13.3",
|
|
12956
13044
|
"@hasna/events": "^0.1.11",
|
|
12957
13045
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
12958
13046
|
chalk: "^5.4.1",
|
|
@@ -13604,6 +13692,7 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
13604
13692
|
task_list_id: nullableIdField,
|
|
13605
13693
|
task_prefix: field(["string", "null"], "Optional task prefix.", true),
|
|
13606
13694
|
task_counter: field("integer", "Monotonic project task counter."),
|
|
13695
|
+
parent_id: field(["string", "null"], "Optional parent project id; null means top-level.", true),
|
|
13607
13696
|
created_at: isoDateField,
|
|
13608
13697
|
updated_at: isoDateField
|
|
13609
13698
|
},
|
|
@@ -16889,6 +16978,7 @@ function createAgentProjectDemoBundle() {
|
|
|
16889
16978
|
task_list_id: ids.list,
|
|
16890
16979
|
task_prefix: "DEMO",
|
|
16891
16980
|
task_counter: 4,
|
|
16981
|
+
parent_id: null,
|
|
16892
16982
|
created_at: createdAt,
|
|
16893
16983
|
updated_at: completedAt
|
|
16894
16984
|
});
|
|
@@ -25136,6 +25226,13 @@ var TODOS_PACKAGE_EXPORTS = [
|
|
|
25136
25226
|
types: "./dist/task-manifest.d.ts",
|
|
25137
25227
|
description: "Transactional package-owned authority for deterministic plan and task graph manifests.",
|
|
25138
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"
|
|
25139
25236
|
}
|
|
25140
25237
|
];
|
|
25141
25238
|
function source8(version) {
|
|
@@ -25328,6 +25425,40 @@ init_tasks();
|
|
|
25328
25425
|
init_projects();
|
|
25329
25426
|
init_plans();
|
|
25330
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
|
+
|
|
25331
25462
|
// src/db/agents.ts
|
|
25332
25463
|
init_types();
|
|
25333
25464
|
init_database();
|
|
@@ -25985,6 +26116,20 @@ init_task_lists();
|
|
|
25985
26116
|
init_templates();
|
|
25986
26117
|
init_audit();
|
|
25987
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
|
|
25988
26133
|
init_database();
|
|
25989
26134
|
|
|
25990
26135
|
// src/db/integrity.ts
|
|
@@ -26529,6 +26674,7 @@ var PROJECT_COLUMNS = [
|
|
|
26529
26674
|
"task_list_id",
|
|
26530
26675
|
"task_prefix",
|
|
26531
26676
|
"task_counter",
|
|
26677
|
+
"parent_id",
|
|
26532
26678
|
"created_at",
|
|
26533
26679
|
"updated_at",
|
|
26534
26680
|
"machine_id",
|
|
@@ -26748,7 +26894,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
26748
26894
|
}
|
|
26749
26895
|
}
|
|
26750
26896
|
};
|
|
26751
|
-
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
26897
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, orderProjectsParentFirst(snapshot.projects), "updated_at");
|
|
26752
26898
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
26753
26899
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
26754
26900
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
@@ -27080,7 +27226,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
27080
27226
|
list: (projectId) => listPlans(projectId, database()),
|
|
27081
27227
|
update: (id, input) => updatePlan(id, input, database()),
|
|
27082
27228
|
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
27083
|
-
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
|
+
}
|
|
27084
27245
|
},
|
|
27085
27246
|
planProjectLinks: {
|
|
27086
27247
|
apply: (input) => applyPlanProjectLinkSqlite(input, database()),
|
|
@@ -27121,10 +27282,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
27121
27282
|
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
27122
27283
|
}
|
|
27123
27284
|
let comments = listComments(taskId, database());
|
|
27124
|
-
comments = comments.sort(
|
|
27285
|
+
comments = comments.sort(compareCommentKeyset);
|
|
27125
27286
|
if (options2?.before) {
|
|
27126
27287
|
const before = options2.before;
|
|
27127
|
-
comments = comments.filter((comment) => comment
|
|
27288
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
27128
27289
|
}
|
|
27129
27290
|
if (options2?.limit !== undefined)
|
|
27130
27291
|
comments = comments.slice(-options2.limit);
|
|
@@ -27654,7 +27815,26 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27654
27815
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
27655
27816
|
update: (id, input) => updatePlan2(id, input, store),
|
|
27656
27817
|
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
27657
|
-
delete: (id, context) => store.deletePlan(id, context)
|
|
27818
|
+
delete: (id, context) => store.deletePlan(id, context),
|
|
27819
|
+
addComment: (input, context) => addPlanComment2(input, store, context),
|
|
27820
|
+
getComments: async (planId) => {
|
|
27821
|
+
const pages = [];
|
|
27822
|
+
let before;
|
|
27823
|
+
while (true) {
|
|
27824
|
+
const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
|
|
27825
|
+
if (page.length === 0)
|
|
27826
|
+
break;
|
|
27827
|
+
pages.unshift(page);
|
|
27828
|
+
if (page.length < 1000)
|
|
27829
|
+
break;
|
|
27830
|
+
const oldest = page[0];
|
|
27831
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
27832
|
+
}
|
|
27833
|
+
return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27834
|
+
},
|
|
27835
|
+
getCommentsPage: async (planId, options2) => {
|
|
27836
|
+
return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
27837
|
+
}
|
|
27658
27838
|
},
|
|
27659
27839
|
planProjectLinks: {
|
|
27660
27840
|
apply: (input, context) => store.applyPlanProjectLink(input, context),
|
|
@@ -27872,6 +28052,27 @@ class PostgresJsonRecordStore {
|
|
|
27872
28052
|
LIMIT $${params.length}`, params);
|
|
27873
28053
|
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
27874
28054
|
}
|
|
28055
|
+
async listPlanComments(planId, options = {}) {
|
|
28056
|
+
await this.ensureSchema();
|
|
28057
|
+
const limit = options.limit ?? 100;
|
|
28058
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
28059
|
+
throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
|
|
28060
|
+
}
|
|
28061
|
+
const params = [this.service, planId];
|
|
28062
|
+
let cursorPredicate = "";
|
|
28063
|
+
if (options.before) {
|
|
28064
|
+
params.push(options.before.created_at, options.before.id);
|
|
28065
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
28066
|
+
}
|
|
28067
|
+
params.push(limit);
|
|
28068
|
+
const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
|
|
28069
|
+
WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
|
|
28070
|
+
AND payload->>'plan_id' = $2
|
|
28071
|
+
${cursorPredicate}
|
|
28072
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
28073
|
+
LIMIT $${params.length}`, params);
|
|
28074
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
28075
|
+
}
|
|
27875
28076
|
async listRecords(type) {
|
|
27876
28077
|
await this.ensureSchema();
|
|
27877
28078
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -28203,8 +28404,12 @@ class PostgresJsonRecordStore {
|
|
|
28203
28404
|
OR ($11::text <> $2
|
|
28204
28405
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
28205
28406
|
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
28206
|
-
(SELECT
|
|
28207
|
-
|
|
28407
|
+
(COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
28408
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', '')) AS membership_changed,
|
|
28409
|
+
(NOT (COALESCE((SELECT payload->>'plan_id' FROM locked_task), '')
|
|
28410
|
+
IS DISTINCT FROM COALESCE($3::jsonb->>'plan_id', ''))
|
|
28411
|
+
OR $8::text IS NULL
|
|
28412
|
+
OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
28208
28413
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
28209
28414
|
), guarded AS (
|
|
28210
28415
|
SELECT
|
|
@@ -28227,7 +28432,6 @@ class PostgresJsonRecordStore {
|
|
|
28227
28432
|
AND guarded.version_matches
|
|
28228
28433
|
AND guarded.parent_found
|
|
28229
28434
|
AND guarded.parent_acyclic
|
|
28230
|
-
AND guarded.all_plans_found
|
|
28231
28435
|
AND guarded.target_plan_found
|
|
28232
28436
|
AND NOT guarded.project_conflict
|
|
28233
28437
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
@@ -28243,7 +28447,7 @@ class PostgresJsonRecordStore {
|
|
|
28243
28447
|
RETURNING payload
|
|
28244
28448
|
)
|
|
28245
28449
|
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
28246
|
-
guarded.
|
|
28450
|
+
guarded.membership_changed, guarded.target_plan_found, guarded.project_conflict,
|
|
28247
28451
|
(SELECT payload FROM stored) AS payload,
|
|
28248
28452
|
(SELECT payload FROM locked_task) AS current_payload
|
|
28249
28453
|
FROM guarded`, [
|
|
@@ -28275,7 +28479,7 @@ class PostgresJsonRecordStore {
|
|
|
28275
28479
|
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
28276
28480
|
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
28277
28481
|
}
|
|
28278
|
-
if (!row?.
|
|
28482
|
+
if (!row?.target_plan_found) {
|
|
28279
28483
|
throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
|
|
28280
28484
|
}
|
|
28281
28485
|
if (row.project_conflict) {
|
|
@@ -29030,6 +29234,15 @@ class PostgresJsonRecordStore {
|
|
|
29030
29234
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
29031
29235
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29032
29236
|
RETURNING 1
|
|
29237
|
+
), deleted_plan_comments AS (
|
|
29238
|
+
UPDATE ${this.tableName} r SET
|
|
29239
|
+
deleted_at = $3::timestamptz,
|
|
29240
|
+
updated_at = $3::timestamptz,
|
|
29241
|
+
source_machine_id = COALESCE($4, r.source_machine_id),
|
|
29242
|
+
version = COALESCE(r.version, 0) + 1
|
|
29243
|
+
WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
|
|
29244
|
+
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
29245
|
+
RETURNING 1
|
|
29033
29246
|
), deleted_plan AS (
|
|
29034
29247
|
UPDATE ${this.tableName} r SET
|
|
29035
29248
|
deleted_at = $3::timestamptz,
|
|
@@ -29205,7 +29418,8 @@ async function updateTask2(id, input, store, context) {
|
|
|
29205
29418
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
29206
29419
|
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
29207
29420
|
created_by: existing.created_by,
|
|
29208
|
-
completed_at:
|
|
29421
|
+
completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
|
|
29422
|
+
started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
|
|
29209
29423
|
};
|
|
29210
29424
|
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
29211
29425
|
operation: "update",
|
|
@@ -29250,7 +29464,8 @@ async function failTask2(id, agentId, reason, options, store) {
|
|
|
29250
29464
|
status: "failed",
|
|
29251
29465
|
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
29252
29466
|
reason: reason ?? task2.reason,
|
|
29253
|
-
retry_after: options?.retry_after ?? task2.retry_after
|
|
29467
|
+
retry_after: options?.retry_after ?? task2.retry_after,
|
|
29468
|
+
completed_at: task2.completed_at ?? new Date().toISOString()
|
|
29254
29469
|
}, store);
|
|
29255
29470
|
if (!options?.retry)
|
|
29256
29471
|
return { task: failed };
|
|
@@ -29487,17 +29702,26 @@ async function getChangedSince(since, filters, store) {
|
|
|
29487
29702
|
async function createProject2(input, store, context) {
|
|
29488
29703
|
const timestamp3 = new Date().toISOString();
|
|
29489
29704
|
const derivedSlug = slugifyRaw(input.name);
|
|
29490
|
-
const taskListId = input.task_list_id === undefined ?
|
|
29705
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : slugifyRaw(input.task_list_id);
|
|
29491
29706
|
if (!derivedSlug || !taskListId)
|
|
29492
29707
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
29708
|
+
const parentId = input.parent_id ?? null;
|
|
29709
|
+
const id = randomUUID3();
|
|
29710
|
+
if (parentId !== null) {
|
|
29711
|
+
const parent = await store.get("projects", parentId);
|
|
29712
|
+
if (!parent)
|
|
29713
|
+
throw new ProjectNotFoundError(parentId);
|
|
29714
|
+
await assertNotProjectAncestorPostgres(id, parentId, store);
|
|
29715
|
+
}
|
|
29493
29716
|
const project = {
|
|
29494
|
-
id
|
|
29717
|
+
id,
|
|
29495
29718
|
name: input.name,
|
|
29496
29719
|
path: input.path,
|
|
29497
29720
|
description: input.description ?? null,
|
|
29498
29721
|
task_list_id: taskListId,
|
|
29499
29722
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
29500
29723
|
task_counter: 0,
|
|
29724
|
+
parent_id: parentId,
|
|
29501
29725
|
created_at: timestamp3,
|
|
29502
29726
|
updated_at: timestamp3,
|
|
29503
29727
|
machine_id: store.machineId(context),
|
|
@@ -29505,12 +29729,38 @@ async function createProject2(input, store, context) {
|
|
|
29505
29729
|
};
|
|
29506
29730
|
return store.upsert("projects", project, context);
|
|
29507
29731
|
}
|
|
29732
|
+
async function assertNotProjectAncestorPostgres(projectId, candidateParentId, store) {
|
|
29733
|
+
const all = await store.list("projects");
|
|
29734
|
+
const byId = new Map(all.map((project) => [project.id, project.parent_id ?? null]));
|
|
29735
|
+
let cursor = candidateParentId;
|
|
29736
|
+
const seen = new Set;
|
|
29737
|
+
while (cursor !== null) {
|
|
29738
|
+
if (cursor === projectId) {
|
|
29739
|
+
throw new ResourceConflictError("PROJECT_PARENT_CYCLE", `Project "${projectId}" cannot be placed under its own descendant`);
|
|
29740
|
+
}
|
|
29741
|
+
if (seen.has(cursor))
|
|
29742
|
+
break;
|
|
29743
|
+
seen.add(cursor);
|
|
29744
|
+
cursor = byId.get(cursor) ?? null;
|
|
29745
|
+
}
|
|
29746
|
+
}
|
|
29508
29747
|
async function updateProject2(id, input, store) {
|
|
29509
29748
|
if ("task_list_id" in input) {
|
|
29510
29749
|
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
29511
29750
|
}
|
|
29512
29751
|
const project = await requireRecord("projects", id, store);
|
|
29513
|
-
|
|
29752
|
+
if (input.parent_id !== undefined && input.parent_id !== null) {
|
|
29753
|
+
const parent = await store.get("projects", input.parent_id);
|
|
29754
|
+
if (!parent)
|
|
29755
|
+
throw new ProjectNotFoundError(input.parent_id);
|
|
29756
|
+
await assertNotProjectAncestorPostgres(id, input.parent_id, store);
|
|
29757
|
+
}
|
|
29758
|
+
const updated = {
|
|
29759
|
+
...project,
|
|
29760
|
+
...definedPatch(input),
|
|
29761
|
+
...input.parent_id !== undefined ? { parent_id: input.parent_id } : {},
|
|
29762
|
+
updated_at: new Date().toISOString()
|
|
29763
|
+
};
|
|
29514
29764
|
return store.upsert("projects", updated);
|
|
29515
29765
|
}
|
|
29516
29766
|
async function createPlan2(input, store, context) {
|
|
@@ -29663,16 +29913,22 @@ async function createTaskList2(input, store, context) {
|
|
|
29663
29913
|
async function updateTaskList2(id, input, store) {
|
|
29664
29914
|
const list = await requireRecord("task_lists", id, store);
|
|
29665
29915
|
const patch = definedPatch(input);
|
|
29916
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
|
|
29666
29917
|
if (input.slug !== undefined) {
|
|
29667
29918
|
const slug = slugifyRaw(input.slug);
|
|
29668
29919
|
if (!slug)
|
|
29669
29920
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
29670
|
-
|
|
29921
|
+
patch.slug = slug;
|
|
29922
|
+
}
|
|
29923
|
+
const effectiveSlug = patch.slug ?? list.slug;
|
|
29924
|
+
if (projectId !== list.project_id || effectiveSlug !== list.slug) {
|
|
29925
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
|
|
29671
29926
|
if (duplicate) {
|
|
29672
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
29927
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
29673
29928
|
}
|
|
29674
|
-
patch.slug = slug;
|
|
29675
29929
|
}
|
|
29930
|
+
if (projectId !== list.project_id)
|
|
29931
|
+
patch.project_id = projectId;
|
|
29676
29932
|
return store.upsert("task_lists", {
|
|
29677
29933
|
...list,
|
|
29678
29934
|
...patch,
|
|
@@ -29765,6 +30021,22 @@ async function addComment2(input, store, context) {
|
|
|
29765
30021
|
function redactComment(comment) {
|
|
29766
30022
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
29767
30023
|
}
|
|
30024
|
+
async function addPlanComment2(input, store, context) {
|
|
30025
|
+
const comment = {
|
|
30026
|
+
id: randomUUID3(),
|
|
30027
|
+
plan_id: input.plan_id,
|
|
30028
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
30029
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
30030
|
+
content: redactEvidenceText(input.content),
|
|
30031
|
+
type: input.type ?? "comment",
|
|
30032
|
+
progress_pct: input.progress_pct ?? null,
|
|
30033
|
+
created_at: new Date().toISOString()
|
|
30034
|
+
};
|
|
30035
|
+
return store.upsert("plan_comments", comment, context);
|
|
30036
|
+
}
|
|
30037
|
+
function redactPlanComment(comment) {
|
|
30038
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
30039
|
+
}
|
|
29768
30040
|
async function exportSnapshot(store) {
|
|
29769
30041
|
return {
|
|
29770
30042
|
exportedAt: new Date().toISOString(),
|
|
@@ -35340,7 +35612,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35340
35612
|
}
|
|
35341
35613
|
async createProject(input) {
|
|
35342
35614
|
const derivedSlug = normalizeSlug(input.name);
|
|
35343
|
-
const taskListId = input.task_list_id === undefined ?
|
|
35615
|
+
const taskListId = input.task_list_id === undefined ? derivedSlug : normalizeSlug(input.task_list_id);
|
|
35344
35616
|
if (!derivedSlug || !taskListId) {
|
|
35345
35617
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
35346
35618
|
}
|
|
@@ -35352,6 +35624,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35352
35624
|
task_list_id: taskListId,
|
|
35353
35625
|
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
35354
35626
|
task_counter: 0,
|
|
35627
|
+
parent_id: input.parent_id ?? null,
|
|
35355
35628
|
created_at: now(),
|
|
35356
35629
|
updated_at: now(),
|
|
35357
35630
|
machine_id: currentStorageMachineId(this.db)
|
|
@@ -35362,14 +35635,15 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
35362
35635
|
try {
|
|
35363
35636
|
const result = this.db.run(`INSERT INTO projects (
|
|
35364
35637
|
id, name, path, description, task_list_id, task_prefix,
|
|
35365
|
-
task_counter, created_at, updated_at, machine_id
|
|
35366
|
-
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
35638
|
+
task_counter, parent_id, created_at, updated_at, machine_id
|
|
35639
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, [
|
|
35367
35640
|
project.id,
|
|
35368
35641
|
project.name,
|
|
35369
35642
|
project.path,
|
|
35370
35643
|
project.description,
|
|
35371
35644
|
project.task_list_id,
|
|
35372
35645
|
project.task_prefix,
|
|
35646
|
+
project.parent_id,
|
|
35373
35647
|
project.created_at,
|
|
35374
35648
|
project.updated_at,
|
|
35375
35649
|
project.machine_id ?? null
|
|
@@ -35765,7 +36039,10 @@ function taskListSlug(projectSlug) {
|
|
|
35765
36039
|
if (!slug || slug !== projectSlug) {
|
|
35766
36040
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
35767
36041
|
}
|
|
35768
|
-
return
|
|
36042
|
+
return slug;
|
|
36043
|
+
}
|
|
36044
|
+
function legacyTaskListSlug(projectSlug) {
|
|
36045
|
+
return `todos-${normalizeSlug(projectSlug)}`;
|
|
35769
36046
|
}
|
|
35770
36047
|
function deterministicTaskPrefix(projectSlug) {
|
|
35771
36048
|
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
@@ -36284,6 +36561,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36284
36561
|
created_by_operation: false
|
|
36285
36562
|
};
|
|
36286
36563
|
}
|
|
36564
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === legacyTaskListSlug(request.project_slug)) {
|
|
36565
|
+
return {
|
|
36566
|
+
record: boundExistingProjectRecord(conflict2),
|
|
36567
|
+
created_by_operation: false
|
|
36568
|
+
};
|
|
36569
|
+
}
|
|
36287
36570
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
36288
36571
|
}
|
|
36289
36572
|
await this.fault("before_object_write", request);
|
|
@@ -36320,6 +36603,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
36320
36603
|
}
|
|
36321
36604
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
36322
36605
|
}
|
|
36606
|
+
if (request.bind_existing === true) {
|
|
36607
|
+
const legacy = await transaction.findTaskListConflict(todosProjectId, legacyTaskListSlug(request.project_slug));
|
|
36608
|
+
if (legacy && legacy.project_id === todosProjectId) {
|
|
36609
|
+
return {
|
|
36610
|
+
record: boundExistingTaskListRecord(legacy),
|
|
36611
|
+
created_by_operation: false
|
|
36612
|
+
};
|
|
36613
|
+
}
|
|
36614
|
+
}
|
|
36323
36615
|
await this.fault("before_object_write", request);
|
|
36324
36616
|
const taskList = await transaction.createTaskList({
|
|
36325
36617
|
name: request.project_name,
|
|
@@ -43483,6 +43775,1314 @@ class TodosTaskManifestHttpClient {
|
|
|
43483
43775
|
function createTodosTaskManifestHttpClient(options) {
|
|
43484
43776
|
return new TodosTaskManifestHttpClient(options);
|
|
43485
43777
|
}
|
|
43778
|
+
// src/task-subtree-transfer/types.ts
|
|
43779
|
+
var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1";
|
|
43780
|
+
var TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1;
|
|
43781
|
+
|
|
43782
|
+
class TodosTaskSubtreeTransferError extends Error {
|
|
43783
|
+
code;
|
|
43784
|
+
details;
|
|
43785
|
+
constructor(code, message, details = {}) {
|
|
43786
|
+
super(message);
|
|
43787
|
+
this.code = code;
|
|
43788
|
+
this.details = details;
|
|
43789
|
+
this.name = "TodosTaskSubtreeTransferError";
|
|
43790
|
+
}
|
|
43791
|
+
}
|
|
43792
|
+
|
|
43793
|
+
// src/task-subtree-transfer/canonical.ts
|
|
43794
|
+
function taskSubtreeTransferRequestDigest(input) {
|
|
43795
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43796
|
+
return canonicalDigest(request);
|
|
43797
|
+
}
|
|
43798
|
+
function taskSubtreeTransferRollbackRequestDigest(input) {
|
|
43799
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
43800
|
+
return canonicalDigest(request);
|
|
43801
|
+
}
|
|
43802
|
+
function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
|
|
43803
|
+
return `tstk_${canonicalDigest({
|
|
43804
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
43805
|
+
...input
|
|
43806
|
+
}).slice(0, 48)}`;
|
|
43807
|
+
}
|
|
43808
|
+
|
|
43809
|
+
// src/task-subtree-transfer/planner.ts
|
|
43810
|
+
function sortedTaskImage(task3) {
|
|
43811
|
+
return {
|
|
43812
|
+
task_id: task3.task_id,
|
|
43813
|
+
project_id: task3.project_id,
|
|
43814
|
+
parent_id: task3.parent_id,
|
|
43815
|
+
plan_id: task3.plan_id,
|
|
43816
|
+
task_list_id: task3.task_list_id,
|
|
43817
|
+
version: task3.version,
|
|
43818
|
+
updated_at: task3.updated_at
|
|
43819
|
+
};
|
|
43820
|
+
}
|
|
43821
|
+
function sourcePopulationDigest(tasks) {
|
|
43822
|
+
return canonicalDigest(tasks.map((task3) => ({
|
|
43823
|
+
...sortedTaskImage(task3),
|
|
43824
|
+
archived_at: task3.archived_at ?? null
|
|
43825
|
+
})).sort((left, right) => left.task_id.localeCompare(right.task_id)));
|
|
43826
|
+
}
|
|
43827
|
+
function closure(tasks, rootTaskId) {
|
|
43828
|
+
const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
|
|
43829
|
+
if (!byId.has(rootTaskId)) {
|
|
43830
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
|
|
43831
|
+
}
|
|
43832
|
+
const children = new Map;
|
|
43833
|
+
for (const task3 of tasks) {
|
|
43834
|
+
if (!task3.parent_id)
|
|
43835
|
+
continue;
|
|
43836
|
+
const siblings = children.get(task3.parent_id) ?? [];
|
|
43837
|
+
siblings.push(task3.task_id);
|
|
43838
|
+
children.set(task3.parent_id, siblings);
|
|
43839
|
+
}
|
|
43840
|
+
for (const ids2 of children.values())
|
|
43841
|
+
ids2.sort();
|
|
43842
|
+
const visiting = new Set;
|
|
43843
|
+
const visited = new Set;
|
|
43844
|
+
const result = [];
|
|
43845
|
+
const visit = (id) => {
|
|
43846
|
+
if (visiting.has(id)) {
|
|
43847
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
|
|
43848
|
+
}
|
|
43849
|
+
if (visited.has(id))
|
|
43850
|
+
return;
|
|
43851
|
+
const task3 = byId.get(id);
|
|
43852
|
+
if (!task3) {
|
|
43853
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
|
|
43854
|
+
}
|
|
43855
|
+
visiting.add(id);
|
|
43856
|
+
result.push(task3);
|
|
43857
|
+
for (const child of children.get(id) ?? [])
|
|
43858
|
+
visit(child);
|
|
43859
|
+
visiting.delete(id);
|
|
43860
|
+
visited.add(id);
|
|
43861
|
+
};
|
|
43862
|
+
visit(rootTaskId);
|
|
43863
|
+
return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
43864
|
+
}
|
|
43865
|
+
function prepareTransfer(snapshot, input) {
|
|
43866
|
+
if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
|
|
43867
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
|
|
43868
|
+
}
|
|
43869
|
+
const moved = closure(snapshot.source_tasks, input.root_task_id);
|
|
43870
|
+
const movedIds = new Set(moved.map((task3) => task3.task_id));
|
|
43871
|
+
if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
|
|
43872
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
|
|
43873
|
+
}
|
|
43874
|
+
const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
|
|
43875
|
+
const allPlanTasks = new Map;
|
|
43876
|
+
for (const task3 of snapshot.plan_tasks) {
|
|
43877
|
+
if (!task3.plan_id)
|
|
43878
|
+
continue;
|
|
43879
|
+
const rows = allPlanTasks.get(task3.plan_id) ?? [];
|
|
43880
|
+
rows.push(task3);
|
|
43881
|
+
allPlanTasks.set(task3.plan_id, rows);
|
|
43882
|
+
}
|
|
43883
|
+
const contained = [];
|
|
43884
|
+
const shared = [];
|
|
43885
|
+
const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
|
|
43886
|
+
for (const planId of planIds) {
|
|
43887
|
+
const plan = planById.get(planId);
|
|
43888
|
+
if (!plan || plan.project_id !== input.source_project_id) {
|
|
43889
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
|
|
43890
|
+
}
|
|
43891
|
+
const members = allPlanTasks.get(planId) ?? [];
|
|
43892
|
+
if (members.length === 0) {
|
|
43893
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
|
|
43894
|
+
}
|
|
43895
|
+
if (members.every((task3) => movedIds.has(task3.task_id)))
|
|
43896
|
+
contained.push(planId);
|
|
43897
|
+
else
|
|
43898
|
+
shared.push(planId);
|
|
43899
|
+
}
|
|
43900
|
+
const inspection = {
|
|
43901
|
+
source_project_id: input.source_project_id,
|
|
43902
|
+
destination_project_id: input.destination_project_id,
|
|
43903
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
43904
|
+
root_task_id: input.root_task_id,
|
|
43905
|
+
destination_parent_id: input.destination_parent_id,
|
|
43906
|
+
expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
|
|
43907
|
+
source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
|
|
43908
|
+
expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
|
|
43909
|
+
contained_plan_ids: contained,
|
|
43910
|
+
shared_plan_ids: shared,
|
|
43911
|
+
complete: true
|
|
43912
|
+
};
|
|
43913
|
+
return {
|
|
43914
|
+
inspection,
|
|
43915
|
+
prior_tasks: moved.map(sortedTaskImage),
|
|
43916
|
+
prior_plans: contained.map((id) => planById.get(id)),
|
|
43917
|
+
task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
|
|
43918
|
+
};
|
|
43919
|
+
}
|
|
43920
|
+
function validateApplySnapshot(prepared, request, plans) {
|
|
43921
|
+
if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
|
|
43922
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
|
|
43923
|
+
}
|
|
43924
|
+
if (request.source_population_digest !== prepared.inspection.source_population_digest) {
|
|
43925
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
|
|
43926
|
+
}
|
|
43927
|
+
const expected = request.expected_tasks.map((task3) => ({ task_id: task3.task_id, version: task3.version })).sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
43928
|
+
if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
|
|
43929
|
+
const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
|
|
43930
|
+
const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
|
|
43931
|
+
throw new TodosTaskSubtreeTransferError(sameIds ? "TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT" : "TODOS_TASK_SUBTREE_TRANSFER_CLOSURE_DRIFT", sameIds ? "At least one exact task version changed since inspection" : "Exact descendant closure changed since inspection");
|
|
43932
|
+
}
|
|
43933
|
+
const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
|
|
43934
|
+
if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
|
|
43935
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
|
|
43936
|
+
expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
|
|
43937
|
+
received_shared_plan_ids: requestedSources
|
|
43938
|
+
});
|
|
43939
|
+
}
|
|
43940
|
+
const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
|
|
43941
|
+
const destinationIds = new Set;
|
|
43942
|
+
for (const mapping of request.shared_plan_splits) {
|
|
43943
|
+
if (destinationIds.has(mapping.destination_plan_id)) {
|
|
43944
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
|
|
43945
|
+
}
|
|
43946
|
+
destinationIds.add(mapping.destination_plan_id);
|
|
43947
|
+
const destination = planById.get(mapping.destination_plan_id);
|
|
43948
|
+
if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
|
|
43949
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Shared-plan destination does not match the exact destination project and task list", { destination_plan_id: mapping.destination_plan_id });
|
|
43950
|
+
}
|
|
43951
|
+
for (const [taskId, planId] of prepared.task_plan_targets) {
|
|
43952
|
+
if (planId === mapping.source_plan_id) {
|
|
43953
|
+
prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
|
|
43954
|
+
}
|
|
43955
|
+
}
|
|
43956
|
+
}
|
|
43957
|
+
}
|
|
43958
|
+
|
|
43959
|
+
// src/task-subtree-transfer/schema-sql.ts
|
|
43960
|
+
function sqliteTodosTaskSubtreeTransferSchemaSql() {
|
|
43961
|
+
return `
|
|
43962
|
+
CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
43963
|
+
receipt_id TEXT PRIMARY KEY,
|
|
43964
|
+
tenant_id TEXT NOT NULL,
|
|
43965
|
+
kind TEXT NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
43966
|
+
operation_id TEXT NOT NULL,
|
|
43967
|
+
step_id TEXT NOT NULL,
|
|
43968
|
+
idempotency_key TEXT NOT NULL,
|
|
43969
|
+
request_digest TEXT NOT NULL,
|
|
43970
|
+
precondition_digest TEXT NOT NULL,
|
|
43971
|
+
result_digest TEXT NOT NULL,
|
|
43972
|
+
apply_receipt_id TEXT,
|
|
43973
|
+
request_json TEXT NOT NULL,
|
|
43974
|
+
result_json TEXT NOT NULL,
|
|
43975
|
+
created_at TEXT NOT NULL,
|
|
43976
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
43977
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
43978
|
+
);
|
|
43979
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
43980
|
+
BEFORE UPDATE ON todos_task_subtree_transfer_receipts BEGIN
|
|
43981
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
43982
|
+
END;
|
|
43983
|
+
CREATE TRIGGER IF NOT EXISTS todos_task_subtree_transfer_receipts_immutable_delete
|
|
43984
|
+
BEFORE DELETE ON todos_task_subtree_transfer_receipts BEGIN
|
|
43985
|
+
SELECT RAISE(ABORT, 'todos task subtree transfer receipts are immutable');
|
|
43986
|
+
END;
|
|
43987
|
+
`;
|
|
43988
|
+
}
|
|
43989
|
+
function ensureSqliteTodosTaskSubtreeTransferSchema(db) {
|
|
43990
|
+
db.exec(sqliteTodosTaskSubtreeTransferSchemaSql());
|
|
43991
|
+
}
|
|
43992
|
+
function postgresTodosTaskSubtreeTransferSchemaSql() {
|
|
43993
|
+
return [
|
|
43994
|
+
`CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
43995
|
+
receipt_id text PRIMARY KEY,
|
|
43996
|
+
tenant_id text NOT NULL,
|
|
43997
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
43998
|
+
operation_id text NOT NULL,
|
|
43999
|
+
step_id text NOT NULL,
|
|
44000
|
+
idempotency_key text NOT NULL,
|
|
44001
|
+
request_digest text NOT NULL,
|
|
44002
|
+
precondition_digest text NOT NULL,
|
|
44003
|
+
result_digest text NOT NULL,
|
|
44004
|
+
apply_receipt_id text,
|
|
44005
|
+
request_json jsonb NOT NULL,
|
|
44006
|
+
result_json jsonb NOT NULL,
|
|
44007
|
+
created_at timestamptz NOT NULL,
|
|
44008
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
44009
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
44010
|
+
)`,
|
|
44011
|
+
`CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
|
|
44012
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
44013
|
+
BEGIN
|
|
44014
|
+
RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
|
|
44015
|
+
END $$`,
|
|
44016
|
+
`DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
44017
|
+
ON todos_task_subtree_transfer_receipts`,
|
|
44018
|
+
`CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
|
|
44019
|
+
BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
|
|
44020
|
+
FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
|
|
44021
|
+
];
|
|
44022
|
+
}
|
|
44023
|
+
|
|
44024
|
+
// src/task-subtree-transfer/postgres.ts
|
|
44025
|
+
function safeIdentifier3(value, fallback) {
|
|
44026
|
+
const candidate = value ?? fallback;
|
|
44027
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
|
|
44028
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
|
|
44029
|
+
}
|
|
44030
|
+
return candidate;
|
|
44031
|
+
}
|
|
44032
|
+
function timestamp4(value) {
|
|
44033
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
44034
|
+
}
|
|
44035
|
+
function payloadOf(row) {
|
|
44036
|
+
return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
|
|
44037
|
+
}
|
|
44038
|
+
function taskRecord(row) {
|
|
44039
|
+
const payload = payloadOf(row);
|
|
44040
|
+
return {
|
|
44041
|
+
task_id: String(payload.id ?? row.object_id),
|
|
44042
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44043
|
+
parent_id: payload.parent_id == null ? null : String(payload.parent_id),
|
|
44044
|
+
plan_id: payload.plan_id == null ? null : String(payload.plan_id),
|
|
44045
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44046
|
+
version: Number(payload.version ?? row.version ?? 1),
|
|
44047
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at),
|
|
44048
|
+
archived_at: payload.archived_at == null ? null : String(payload.archived_at)
|
|
44049
|
+
};
|
|
44050
|
+
}
|
|
44051
|
+
function planRecord(row) {
|
|
44052
|
+
const payload = payloadOf(row);
|
|
44053
|
+
return {
|
|
44054
|
+
plan_id: String(payload.id ?? row.object_id),
|
|
44055
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
44056
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
44057
|
+
updated_at: timestamp4(payload.updated_at ?? row.updated_at)
|
|
44058
|
+
};
|
|
44059
|
+
}
|
|
44060
|
+
|
|
44061
|
+
class PostgresTodosTaskSubtreeTransferBackend {
|
|
44062
|
+
client;
|
|
44063
|
+
kind = "postgresql";
|
|
44064
|
+
service;
|
|
44065
|
+
tableName;
|
|
44066
|
+
tenantId;
|
|
44067
|
+
schemaReady = null;
|
|
44068
|
+
constructor(client, options = {}) {
|
|
44069
|
+
this.client = client;
|
|
44070
|
+
this.service = options.service ?? "todos";
|
|
44071
|
+
this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
|
|
44072
|
+
this.tenantId = options.tenantId ?? "default";
|
|
44073
|
+
}
|
|
44074
|
+
async ensureSchema() {
|
|
44075
|
+
this.schemaReady ??= (async () => {
|
|
44076
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
44077
|
+
await this.client.query(sql);
|
|
44078
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
44079
|
+
await this.client.query(sql);
|
|
44080
|
+
})();
|
|
44081
|
+
await this.schemaReady;
|
|
44082
|
+
}
|
|
44083
|
+
async snapshot(client, input, forUpdate = false) {
|
|
44084
|
+
const lock = forUpdate ? " FOR UPDATE" : "";
|
|
44085
|
+
const source9 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44086
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
|
|
44087
|
+
AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
|
|
44088
|
+
const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44089
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44090
|
+
const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
44091
|
+
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
44092
|
+
const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44093
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
44094
|
+
AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
|
|
44095
|
+
const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44096
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
44097
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
|
|
44098
|
+
const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
|
|
44099
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44100
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_parent_id, input.destination_project_id])).rows.length === 1;
|
|
44101
|
+
return {
|
|
44102
|
+
source_tasks: source9.rows.map(taskRecord),
|
|
44103
|
+
plan_tasks: allTasks.rows.map(taskRecord),
|
|
44104
|
+
plans: plans.rows.map(planRecord),
|
|
44105
|
+
destination_project_found: destinationProject.rows.length === 1,
|
|
44106
|
+
destination_task_list_found: destinationList.rows.length === 1,
|
|
44107
|
+
destination_parent_found: destinationParent
|
|
44108
|
+
};
|
|
44109
|
+
}
|
|
44110
|
+
async inspect(input) {
|
|
44111
|
+
await this.ensureSchema();
|
|
44112
|
+
return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
|
|
44113
|
+
}
|
|
44114
|
+
async insertPayload(client, task3, update, now4) {
|
|
44115
|
+
const current = await client.query(`SELECT payload FROM ${this.tableName}
|
|
44116
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
44117
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
44118
|
+
const row = current.rows[0];
|
|
44119
|
+
if (!row)
|
|
44120
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
|
|
44121
|
+
const payload = payloadOf(row);
|
|
44122
|
+
if (Number(payload.version ?? 1) !== task3.version) {
|
|
44123
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
|
|
44124
|
+
}
|
|
44125
|
+
const next = {
|
|
44126
|
+
...payload,
|
|
44127
|
+
...update,
|
|
44128
|
+
version: task3.version + 1,
|
|
44129
|
+
updated_at: now4
|
|
44130
|
+
};
|
|
44131
|
+
const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44132
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
|
|
44133
|
+
AND deleted_at IS NULL AND version = $6
|
|
44134
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
|
|
44135
|
+
if (result.rows.length !== 1) {
|
|
44136
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
|
|
44137
|
+
}
|
|
44138
|
+
}
|
|
44139
|
+
async apply(input, options) {
|
|
44140
|
+
await this.ensureSchema();
|
|
44141
|
+
if (!this.client.transaction) {
|
|
44142
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
|
|
44143
|
+
}
|
|
44144
|
+
return this.client.transaction(async (tx) => {
|
|
44145
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44146
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44147
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44148
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44149
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
44150
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44151
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44152
|
+
if (existing.rows[0]) {
|
|
44153
|
+
const row = existing.rows[0];
|
|
44154
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
|
|
44155
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
|
|
44156
|
+
}
|
|
44157
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
|
|
44158
|
+
}
|
|
44159
|
+
const snapshot = await this.snapshot(tx, input, true);
|
|
44160
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44161
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44162
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44163
|
+
const priorImage = {
|
|
44164
|
+
tasks: prepared.prior_tasks,
|
|
44165
|
+
plans: prepared.prior_plans
|
|
44166
|
+
};
|
|
44167
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44168
|
+
await this.insertPayload(tx, task3, {
|
|
44169
|
+
project_id: input.destination_project_id,
|
|
44170
|
+
task_list_id: input.destination_task_list_id,
|
|
44171
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44172
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
|
|
44173
|
+
}, now4);
|
|
44174
|
+
}
|
|
44175
|
+
if (options.faultInjector && await options.faultInjector("after_task_writes"))
|
|
44176
|
+
throw new Error("Injected task-subtree-transfer fault at after_task_writes");
|
|
44177
|
+
for (const plan of prepared.prior_plans) {
|
|
44178
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
44179
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
44180
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
|
|
44181
|
+
const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44182
|
+
if (!current || String(current.project_id ?? "") !== String(plan.project_id ?? "") || (current.task_list_id ?? null) !== plan.task_list_id || timestamp4(current.updated_at ?? row.rows[0]?.updated_at) !== plan.updated_at) {
|
|
44183
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
|
|
44184
|
+
}
|
|
44185
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = jsonb_set(jsonb_set(jsonb_set(payload, '{project_id}', to_jsonb($1::text)), '{task_list_id}', to_jsonb($2::text)), '{updated_at}', to_jsonb($3::text)), updated_at = $3
|
|
44186
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44187
|
+
RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
|
|
44188
|
+
if (result2.rows.length !== 1) {
|
|
44189
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44190
|
+
}
|
|
44191
|
+
}
|
|
44192
|
+
if (options.faultInjector && await options.faultInjector("after_plan_writes"))
|
|
44193
|
+
throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
|
|
44194
|
+
const postImage = {
|
|
44195
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44196
|
+
...task3,
|
|
44197
|
+
project_id: input.destination_project_id,
|
|
44198
|
+
task_list_id: input.destination_task_list_id,
|
|
44199
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44200
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44201
|
+
version: task3.version + 1,
|
|
44202
|
+
updated_at: now4
|
|
44203
|
+
})),
|
|
44204
|
+
plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
|
|
44205
|
+
};
|
|
44206
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
|
|
44207
|
+
const receipt = {
|
|
44208
|
+
receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
|
|
44209
|
+
authority: "todos",
|
|
44210
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44211
|
+
schema_version: 1,
|
|
44212
|
+
kind: "apply",
|
|
44213
|
+
operation_id: input.operation_id,
|
|
44214
|
+
step_id: input.step_id,
|
|
44215
|
+
idempotency_key: input.idempotency_key,
|
|
44216
|
+
request_digest: requestDigest,
|
|
44217
|
+
precondition_digest: input.precondition_digest,
|
|
44218
|
+
result_digest: resultDigest,
|
|
44219
|
+
apply_receipt_id: null,
|
|
44220
|
+
source_project_id: input.source_project_id,
|
|
44221
|
+
destination_project_id: input.destination_project_id,
|
|
44222
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44223
|
+
root_task_id: input.root_task_id,
|
|
44224
|
+
source_population_digest: input.source_population_digest,
|
|
44225
|
+
prior_image: priorImage,
|
|
44226
|
+
post_image: postImage,
|
|
44227
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44228
|
+
created_at: now4
|
|
44229
|
+
};
|
|
44230
|
+
const result = { duplicate: false, receipt, moved_task_ids: prepared.prior_tasks.map((task3) => task3.task_id), moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id), complete: true };
|
|
44231
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44232
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44233
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44234
|
+
request_json, result_json, created_at
|
|
44235
|
+
) VALUES ($1, $2, 'apply', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10::jsonb, $11)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4]);
|
|
44236
|
+
if (options.faultInjector && await options.faultInjector("after_receipt_write"))
|
|
44237
|
+
throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
|
|
44238
|
+
return result;
|
|
44239
|
+
});
|
|
44240
|
+
}
|
|
44241
|
+
async readExact(receiptId2) {
|
|
44242
|
+
await this.ensureSchema();
|
|
44243
|
+
const row = await this.client.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND receipt_id = $2 LIMIT 1`, [this.tenantId, receiptId2]);
|
|
44244
|
+
if (!row.rows[0])
|
|
44245
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
|
|
44246
|
+
return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
|
|
44247
|
+
}
|
|
44248
|
+
async rollback(input, options) {
|
|
44249
|
+
await this.ensureSchema();
|
|
44250
|
+
if (!this.client.transaction)
|
|
44251
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
|
|
44252
|
+
return this.client.transaction(async (tx) => {
|
|
44253
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
44254
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44255
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44256
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44257
|
+
WHERE tenant_id = $1 AND kind = 'rollback'
|
|
44258
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
44259
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
44260
|
+
if (existing.rows[0]) {
|
|
44261
|
+
const row = existing.rows[0];
|
|
44262
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
|
|
44263
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
|
|
44264
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
|
|
44265
|
+
}
|
|
44266
|
+
const applyRow = await tx.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND kind = 'apply' AND receipt_id = $2 LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
44267
|
+
if (!applyRow.rows[0])
|
|
44268
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44269
|
+
const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
|
|
44270
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44271
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44272
|
+
}
|
|
44273
|
+
const expectedPrecondition = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", operation_id: input.operation_id, step_id: input.step_id, apply_receipt_id: input.receipt_id, apply_result_digest: applied.receipt.result_digest });
|
|
44274
|
+
if (input.precondition_digest !== expectedPrecondition)
|
|
44275
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44276
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44277
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44278
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
44279
|
+
if (!row.rows[0])
|
|
44280
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
|
|
44281
|
+
const payload = payloadOf(row.rows[0]);
|
|
44282
|
+
if (Number(payload.version ?? 1) !== task3.version || String(payload.project_id ?? "") !== String(task3.project_id ?? "") || (payload.parent_id ?? null) !== task3.parent_id || (payload.plan_id ?? null) !== task3.plan_id || (payload.task_list_id ?? null) !== task3.task_list_id)
|
|
44283
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
|
|
44284
|
+
}
|
|
44285
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44286
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL`, [this.service, task3.task_id]);
|
|
44287
|
+
const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
44288
|
+
if (!payload)
|
|
44289
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
|
|
44290
|
+
const next = { ...payload, project_id: task3.project_id, task_list_id: task3.task_list_id, plan_id: task3.plan_id, parent_id: task3.parent_id, version: task3.version + 2, updated_at: now4 };
|
|
44291
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
44292
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
|
|
44293
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
|
|
44294
|
+
if (result2.rows.length !== 1) {
|
|
44295
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
|
|
44296
|
+
}
|
|
44297
|
+
}
|
|
44298
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
|
|
44299
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
|
|
44300
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44301
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44302
|
+
const result2 = await tx.query(`UPDATE ${this.tableName}
|
|
44303
|
+
SET payload = jsonb_set(
|
|
44304
|
+
jsonb_set(
|
|
44305
|
+
jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
|
|
44306
|
+
'{task_list_id}',
|
|
44307
|
+
COALESCE(to_jsonb($2::text), 'null'::jsonb)
|
|
44308
|
+
),
|
|
44309
|
+
'{updated_at}',
|
|
44310
|
+
to_jsonb($3::text)
|
|
44311
|
+
), updated_at = $3
|
|
44312
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
44313
|
+
AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
|
|
44314
|
+
RETURNING object_id`, [plan.project_id, plan.task_list_id, now4, this.service, plan.plan_id, post.project_id, post.task_list_id, post.updated_at]);
|
|
44315
|
+
if (result2.rows.length !== 1) {
|
|
44316
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
|
|
44317
|
+
}
|
|
44318
|
+
}
|
|
44319
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
|
|
44320
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
|
|
44321
|
+
const restored = {
|
|
44322
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
|
|
44323
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44324
|
+
};
|
|
44325
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored });
|
|
44326
|
+
const receipt = { ...applied.receipt, receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id), kind: "rollback", operation_id: input.operation_id, step_id: input.step_id, idempotency_key: input.idempotency_key, request_digest: requestDigest, precondition_digest: input.precondition_digest, result_digest: resultDigest, apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored, created_at: now4 };
|
|
44327
|
+
const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
|
|
44328
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44329
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44330
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44331
|
+
request_json, result_json, created_at
|
|
44332
|
+
) VALUES ($1, $2, 'rollback', $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4]);
|
|
44333
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
|
|
44334
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
|
|
44335
|
+
return result;
|
|
44336
|
+
});
|
|
44337
|
+
}
|
|
44338
|
+
}
|
|
44339
|
+
|
|
44340
|
+
// src/task-subtree-transfer/sqlite.ts
|
|
44341
|
+
var sqliteTails2 = new WeakMap;
|
|
44342
|
+
async function fault3(options, point) {
|
|
44343
|
+
if (options.faultInjector && await options.faultInjector(point) === true) {
|
|
44344
|
+
throw new Error(`Injected task-subtree-transfer fault at ${point}`);
|
|
44345
|
+
}
|
|
44346
|
+
}
|
|
44347
|
+
function parseResult(value, duplicate) {
|
|
44348
|
+
return { ...JSON.parse(value), duplicate };
|
|
44349
|
+
}
|
|
44350
|
+
function rowImage(row) {
|
|
44351
|
+
return {
|
|
44352
|
+
task_id: String(row.id),
|
|
44353
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44354
|
+
parent_id: row.parent_id == null ? null : String(row.parent_id),
|
|
44355
|
+
plan_id: row.plan_id == null ? null : String(row.plan_id),
|
|
44356
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44357
|
+
version: Number(row.version),
|
|
44358
|
+
updated_at: String(row.updated_at),
|
|
44359
|
+
archived_at: row.archived_at == null ? null : String(row.archived_at)
|
|
44360
|
+
};
|
|
44361
|
+
}
|
|
44362
|
+
function planImage(row) {
|
|
44363
|
+
return {
|
|
44364
|
+
plan_id: String(row.id),
|
|
44365
|
+
project_id: row.project_id == null ? null : String(row.project_id),
|
|
44366
|
+
task_list_id: row.task_list_id == null ? null : String(row.task_list_id),
|
|
44367
|
+
updated_at: String(row.updated_at)
|
|
44368
|
+
};
|
|
44369
|
+
}
|
|
44370
|
+
function imageFromTaskRow(row) {
|
|
44371
|
+
const image = rowImage(row);
|
|
44372
|
+
const { archived_at: _archivedAt, ...withoutArchive } = image;
|
|
44373
|
+
return withoutArchive;
|
|
44374
|
+
}
|
|
44375
|
+
function imageFromPlanRow(row) {
|
|
44376
|
+
return planImage(row);
|
|
44377
|
+
}
|
|
44378
|
+
function receiptId2(input) {
|
|
44379
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, taskSubtreeTransferRequestDigest(input));
|
|
44380
|
+
}
|
|
44381
|
+
function rollbackReceiptId(input) {
|
|
44382
|
+
return deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id);
|
|
44383
|
+
}
|
|
44384
|
+
function taskWhere(db, taskId) {
|
|
44385
|
+
const row = db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44386
|
+
FROM tasks WHERE id = ? LIMIT 1`).get(taskId);
|
|
44387
|
+
if (!row) {
|
|
44388
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${taskId}`, { task_id: taskId });
|
|
44389
|
+
}
|
|
44390
|
+
return row;
|
|
44391
|
+
}
|
|
44392
|
+
|
|
44393
|
+
class SqliteTodosTaskSubtreeTransferBackend {
|
|
44394
|
+
db;
|
|
44395
|
+
tenantId;
|
|
44396
|
+
kind = "sqlite";
|
|
44397
|
+
constructor(db, tenantId = "default") {
|
|
44398
|
+
this.db = db;
|
|
44399
|
+
this.tenantId = tenantId;
|
|
44400
|
+
ensureSqliteTodosTaskSubtreeTransferSchema(db);
|
|
44401
|
+
}
|
|
44402
|
+
async serialized(run) {
|
|
44403
|
+
const previous = sqliteTails2.get(this.db) ?? Promise.resolve();
|
|
44404
|
+
let release;
|
|
44405
|
+
const current = new Promise((resolve10) => {
|
|
44406
|
+
release = resolve10;
|
|
44407
|
+
});
|
|
44408
|
+
const tail = previous.then(() => current);
|
|
44409
|
+
sqliteTails2.set(this.db, tail);
|
|
44410
|
+
await previous;
|
|
44411
|
+
try {
|
|
44412
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
44413
|
+
try {
|
|
44414
|
+
const result = await run();
|
|
44415
|
+
this.db.exec("COMMIT");
|
|
44416
|
+
return result;
|
|
44417
|
+
} catch (error) {
|
|
44418
|
+
this.db.exec("ROLLBACK");
|
|
44419
|
+
throw error;
|
|
44420
|
+
}
|
|
44421
|
+
} finally {
|
|
44422
|
+
release();
|
|
44423
|
+
if (sqliteTails2.get(this.db) === tail)
|
|
44424
|
+
sqliteTails2.delete(this.db);
|
|
44425
|
+
}
|
|
44426
|
+
}
|
|
44427
|
+
snapshot(input) {
|
|
44428
|
+
const sourceTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44429
|
+
FROM tasks WHERE project_id = ? ORDER BY id`).all(input.source_project_id).map(rowImage);
|
|
44430
|
+
const allTasks = this.db.query(`SELECT id, project_id, parent_id, plan_id, task_list_id, version, updated_at, archived_at
|
|
44431
|
+
FROM tasks ORDER BY id`).all().map(rowImage);
|
|
44432
|
+
const plans = this.db.query(`SELECT id, project_id, task_list_id, updated_at
|
|
44433
|
+
FROM plans ORDER BY id`).all().map(planImage);
|
|
44434
|
+
const destinationProjectFound = Boolean(this.db.query("SELECT id FROM projects WHERE id = ? LIMIT 1").get(input.destination_project_id));
|
|
44435
|
+
const destinationTaskList = this.db.query("SELECT id FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_task_list_id, input.destination_project_id);
|
|
44436
|
+
const destinationParentFound = input.destination_parent_id === null || Boolean(this.db.query("SELECT id FROM tasks WHERE id = ? AND project_id = ? LIMIT 1").get(input.destination_parent_id, input.destination_project_id));
|
|
44437
|
+
return {
|
|
44438
|
+
source_tasks: sourceTasks,
|
|
44439
|
+
plan_tasks: allTasks,
|
|
44440
|
+
plans,
|
|
44441
|
+
destination_project_found: destinationProjectFound,
|
|
44442
|
+
destination_task_list_found: Boolean(destinationTaskList),
|
|
44443
|
+
destination_parent_found: destinationParentFound
|
|
44444
|
+
};
|
|
44445
|
+
}
|
|
44446
|
+
async inspect(input) {
|
|
44447
|
+
return this.serialized(() => prepareTransfer(this.snapshot(input), input).inspection);
|
|
44448
|
+
}
|
|
44449
|
+
async apply(input, options) {
|
|
44450
|
+
return this.serialized(async () => {
|
|
44451
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
44452
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44453
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44454
|
+
WHERE tenant_id = ? AND kind = 'apply'
|
|
44455
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44456
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44457
|
+
if (existing) {
|
|
44458
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44459
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation/step or idempotency key was already accepted for a different request");
|
|
44460
|
+
}
|
|
44461
|
+
return parseResult(String(existing.result_json), true);
|
|
44462
|
+
}
|
|
44463
|
+
const snapshot = this.snapshot(input);
|
|
44464
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
44465
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
44466
|
+
const priorImage = {
|
|
44467
|
+
tasks: prepared.prior_tasks,
|
|
44468
|
+
plans: prepared.prior_plans
|
|
44469
|
+
};
|
|
44470
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44471
|
+
const movedTaskIds = prepared.prior_tasks.map((task3) => task3.task_id);
|
|
44472
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44473
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44474
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44475
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task changed after inspection", { task_id: task3.task_id });
|
|
44476
|
+
}
|
|
44477
|
+
}
|
|
44478
|
+
for (const task3 of prepared.prior_tasks) {
|
|
44479
|
+
const targetPlan = prepared.task_plan_targets.get(task3.task_id) ?? null;
|
|
44480
|
+
const parentId = task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id;
|
|
44481
|
+
const result2 = this.db.query(`UPDATE tasks
|
|
44482
|
+
SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?, version = version + 1, updated_at = ?
|
|
44483
|
+
WHERE id = ? AND version = ?`).run(input.destination_project_id, input.destination_task_list_id, targetPlan, parentId, now4, task3.task_id, task3.version);
|
|
44484
|
+
if (result2.changes !== 1) {
|
|
44485
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A task version changed during the atomic transfer", { task_id: task3.task_id });
|
|
44486
|
+
}
|
|
44487
|
+
}
|
|
44488
|
+
await fault3(options, "after_task_writes");
|
|
44489
|
+
for (const plan of prepared.prior_plans) {
|
|
44490
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44491
|
+
WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(input.destination_project_id, input.destination_task_list_id, now4, plan.plan_id, plan.project_id, plan.task_list_id, plan.updated_at);
|
|
44492
|
+
if (result2.changes !== 1) {
|
|
44493
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "A contained plan changed during the atomic transfer", { plan_id: plan.plan_id });
|
|
44494
|
+
}
|
|
44495
|
+
}
|
|
44496
|
+
await fault3(options, "after_plan_writes");
|
|
44497
|
+
const postImage = {
|
|
44498
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
44499
|
+
...task3,
|
|
44500
|
+
project_id: input.destination_project_id,
|
|
44501
|
+
task_list_id: input.destination_task_list_id,
|
|
44502
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
44503
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
44504
|
+
version: task3.version + 1,
|
|
44505
|
+
updated_at: now4
|
|
44506
|
+
})),
|
|
44507
|
+
plans: prepared.prior_plans.map((plan) => ({
|
|
44508
|
+
...plan,
|
|
44509
|
+
project_id: input.destination_project_id,
|
|
44510
|
+
task_list_id: input.destination_task_list_id,
|
|
44511
|
+
updated_at: now4
|
|
44512
|
+
}))
|
|
44513
|
+
};
|
|
44514
|
+
const resultDigest = canonicalDigest({
|
|
44515
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44516
|
+
request_digest: requestDigest,
|
|
44517
|
+
prior_image: priorImage,
|
|
44518
|
+
post_image: postImage
|
|
44519
|
+
});
|
|
44520
|
+
const receipt = {
|
|
44521
|
+
receipt_id: receiptId2(input),
|
|
44522
|
+
authority: "todos",
|
|
44523
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44524
|
+
schema_version: 1,
|
|
44525
|
+
kind: "apply",
|
|
44526
|
+
operation_id: input.operation_id,
|
|
44527
|
+
step_id: input.step_id,
|
|
44528
|
+
idempotency_key: input.idempotency_key,
|
|
44529
|
+
request_digest: requestDigest,
|
|
44530
|
+
precondition_digest: input.precondition_digest,
|
|
44531
|
+
result_digest: resultDigest,
|
|
44532
|
+
apply_receipt_id: null,
|
|
44533
|
+
source_project_id: input.source_project_id,
|
|
44534
|
+
destination_project_id: input.destination_project_id,
|
|
44535
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
44536
|
+
root_task_id: input.root_task_id,
|
|
44537
|
+
source_population_digest: input.source_population_digest,
|
|
44538
|
+
prior_image: priorImage,
|
|
44539
|
+
post_image: postImage,
|
|
44540
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
44541
|
+
created_at: now4
|
|
44542
|
+
};
|
|
44543
|
+
const result = {
|
|
44544
|
+
duplicate: false,
|
|
44545
|
+
receipt,
|
|
44546
|
+
moved_task_ids: movedTaskIds,
|
|
44547
|
+
moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id),
|
|
44548
|
+
complete: true
|
|
44549
|
+
};
|
|
44550
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44551
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44552
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44553
|
+
request_json, result_json, created_at
|
|
44554
|
+
) VALUES (?, ?, 'apply', ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4);
|
|
44555
|
+
await fault3(options, "after_receipt_write");
|
|
44556
|
+
return result;
|
|
44557
|
+
});
|
|
44558
|
+
}
|
|
44559
|
+
async readExact(receiptIdValue) {
|
|
44560
|
+
const row = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44561
|
+
WHERE tenant_id = ? AND receipt_id = ? LIMIT 1`).get(this.tenantId, receiptIdValue);
|
|
44562
|
+
if (!row) {
|
|
44563
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptIdValue}`);
|
|
44564
|
+
}
|
|
44565
|
+
return parseResult(String(row.result_json), false);
|
|
44566
|
+
}
|
|
44567
|
+
async rollback(input, options) {
|
|
44568
|
+
return this.serialized(async () => {
|
|
44569
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
44570
|
+
const existing = this.db.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
44571
|
+
FROM todos_task_subtree_transfer_receipts
|
|
44572
|
+
WHERE tenant_id = ? AND kind = 'rollback'
|
|
44573
|
+
AND (idempotency_key = ? OR (operation_id = ? AND step_id = ?))
|
|
44574
|
+
ORDER BY created_at ASC LIMIT 1`).get(this.tenantId, input.idempotency_key, input.operation_id, input.step_id);
|
|
44575
|
+
if (existing) {
|
|
44576
|
+
if (existing.request_digest !== requestDigest || existing.precondition_digest !== input.precondition_digest || existing.idempotency_key !== input.idempotency_key) {
|
|
44577
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation/step or idempotency key was already accepted for a different request");
|
|
44578
|
+
}
|
|
44579
|
+
return parseResult(String(existing.result_json), true);
|
|
44580
|
+
}
|
|
44581
|
+
const applyRow = this.db.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts
|
|
44582
|
+
WHERE tenant_id = ? AND kind = 'apply' AND receipt_id = ? LIMIT 1`).get(this.tenantId, input.receipt_id);
|
|
44583
|
+
if (!applyRow) {
|
|
44584
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
44585
|
+
}
|
|
44586
|
+
const applied = parseResult(String(applyRow.result_json), false);
|
|
44587
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
44588
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
44589
|
+
}
|
|
44590
|
+
const expectedPrecondition = canonicalDigest({
|
|
44591
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44592
|
+
direction: "rollback",
|
|
44593
|
+
operation_id: input.operation_id,
|
|
44594
|
+
step_id: input.step_id,
|
|
44595
|
+
apply_receipt_id: input.receipt_id,
|
|
44596
|
+
apply_result_digest: applied.receipt.result_digest
|
|
44597
|
+
});
|
|
44598
|
+
if (input.precondition_digest !== expectedPrecondition) {
|
|
44599
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
44600
|
+
}
|
|
44601
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
44602
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
44603
|
+
const current = imageFromTaskRow(taskWhere(this.db, task3.task_id));
|
|
44604
|
+
if (canonicalJson(current) !== canonicalJson(task3)) {
|
|
44605
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred task changed after apply", { task_id: task3.task_id });
|
|
44606
|
+
}
|
|
44607
|
+
}
|
|
44608
|
+
for (const plan of applied.receipt.post_image.plans) {
|
|
44609
|
+
const row = this.db.query("SELECT id, project_id, task_list_id, updated_at FROM plans WHERE id = ? LIMIT 1").get(plan.plan_id);
|
|
44610
|
+
if (!row || canonicalJson(imageFromPlanRow(row)) !== canonicalJson(plan)) {
|
|
44611
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback refused because a transferred plan changed after apply", { plan_id: plan.plan_id });
|
|
44612
|
+
}
|
|
44613
|
+
}
|
|
44614
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
44615
|
+
const result2 = this.db.query(`UPDATE tasks SET project_id = ?, task_list_id = ?, plan_id = ?, parent_id = ?,
|
|
44616
|
+
version = version + 1, updated_at = ?
|
|
44617
|
+
WHERE id = ? AND version = ?`).run(task3.project_id, task3.task_list_id, task3.plan_id, task3.parent_id, now4, task3.task_id, applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version);
|
|
44618
|
+
if (result2.changes !== 1) {
|
|
44619
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback task CAS failed", { task_id: task3.task_id });
|
|
44620
|
+
}
|
|
44621
|
+
}
|
|
44622
|
+
await fault3(options, "after_rollback_task_writes");
|
|
44623
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
44624
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
44625
|
+
const result2 = this.db.query(`UPDATE plans SET project_id = ?, task_list_id = ?, updated_at = ?
|
|
44626
|
+
WHERE id = ? AND project_id IS ? AND task_list_id IS ? AND updated_at = ?`).run(plan.project_id, plan.task_list_id, now4, plan.plan_id, post.project_id, post.task_list_id, post.updated_at);
|
|
44627
|
+
if (result2.changes !== 1) {
|
|
44628
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback plan CAS failed", { plan_id: plan.plan_id });
|
|
44629
|
+
}
|
|
44630
|
+
}
|
|
44631
|
+
await fault3(options, "after_rollback_plan_writes");
|
|
44632
|
+
const restored = {
|
|
44633
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({
|
|
44634
|
+
...task3,
|
|
44635
|
+
version: applied.receipt.post_image.tasks.find((candidate) => candidate.task_id === task3.task_id).version + 1,
|
|
44636
|
+
updated_at: now4
|
|
44637
|
+
})),
|
|
44638
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
44639
|
+
};
|
|
44640
|
+
const resultDigest = canonicalDigest({
|
|
44641
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44642
|
+
direction: "rollback",
|
|
44643
|
+
apply_receipt_id: input.receipt_id,
|
|
44644
|
+
prior_image: applied.receipt.post_image,
|
|
44645
|
+
post_image: restored
|
|
44646
|
+
});
|
|
44647
|
+
const receipt = {
|
|
44648
|
+
...applied.receipt,
|
|
44649
|
+
receipt_id: rollbackReceiptId(input),
|
|
44650
|
+
kind: "rollback",
|
|
44651
|
+
step_id: input.step_id,
|
|
44652
|
+
idempotency_key: input.idempotency_key,
|
|
44653
|
+
request_digest: requestDigest,
|
|
44654
|
+
precondition_digest: input.precondition_digest,
|
|
44655
|
+
result_digest: resultDigest,
|
|
44656
|
+
apply_receipt_id: input.receipt_id,
|
|
44657
|
+
prior_image: applied.receipt.post_image,
|
|
44658
|
+
post_image: restored,
|
|
44659
|
+
created_at: now4
|
|
44660
|
+
};
|
|
44661
|
+
const result = {
|
|
44662
|
+
duplicate: false,
|
|
44663
|
+
receipt,
|
|
44664
|
+
moved_task_ids: applied.moved_task_ids,
|
|
44665
|
+
moved_plan_ids: applied.moved_plan_ids,
|
|
44666
|
+
complete: true
|
|
44667
|
+
};
|
|
44668
|
+
this.db.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
44669
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
44670
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
44671
|
+
request_json, result_json, created_at
|
|
44672
|
+
) VALUES (?, ?, 'rollback', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4);
|
|
44673
|
+
await fault3(options, "after_rollback_receipt_write");
|
|
44674
|
+
return result;
|
|
44675
|
+
});
|
|
44676
|
+
}
|
|
44677
|
+
}
|
|
44678
|
+
|
|
44679
|
+
// src/task-subtree-transfer/authority.ts
|
|
44680
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
44681
|
+
var DIGEST = /^[0-9a-f]{64}$/i;
|
|
44682
|
+
function boundedString(value, field2, max = 200) {
|
|
44683
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max) {
|
|
44684
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a non-empty bounded string`);
|
|
44685
|
+
}
|
|
44686
|
+
return value;
|
|
44687
|
+
}
|
|
44688
|
+
function uuid3(value, field2) {
|
|
44689
|
+
const parsed = boundedString(value, field2, 80);
|
|
44690
|
+
if (!UUID.test(parsed)) {
|
|
44691
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a UUID`);
|
|
44692
|
+
}
|
|
44693
|
+
return parsed;
|
|
44694
|
+
}
|
|
44695
|
+
function digest2(value, field2) {
|
|
44696
|
+
const parsed = boundedString(value, field2, 64);
|
|
44697
|
+
if (!DIGEST.test(parsed)) {
|
|
44698
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field2} must be a SHA-256 digest`);
|
|
44699
|
+
}
|
|
44700
|
+
return parsed.toLowerCase();
|
|
44701
|
+
}
|
|
44702
|
+
function nullableUuid(value, field2) {
|
|
44703
|
+
if (value === null)
|
|
44704
|
+
return null;
|
|
44705
|
+
return uuid3(value, field2);
|
|
44706
|
+
}
|
|
44707
|
+
function strictObject(input, fields) {
|
|
44708
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
44709
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
|
|
44710
|
+
}
|
|
44711
|
+
const object = input;
|
|
44712
|
+
const allowed = new Set(fields);
|
|
44713
|
+
for (const key2 of Object.keys(object)) {
|
|
44714
|
+
if (!allowed.has(key2)) {
|
|
44715
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
|
|
44716
|
+
}
|
|
44717
|
+
}
|
|
44718
|
+
return object;
|
|
44719
|
+
}
|
|
44720
|
+
var INSPECT_FIELDS = [
|
|
44721
|
+
"source_project_id",
|
|
44722
|
+
"destination_project_id",
|
|
44723
|
+
"destination_task_list_id",
|
|
44724
|
+
"root_task_id",
|
|
44725
|
+
"destination_parent_id"
|
|
44726
|
+
];
|
|
44727
|
+
function parseTodosTaskSubtreeTransferInspect(input) {
|
|
44728
|
+
const value = strictObject(input, INSPECT_FIELDS);
|
|
44729
|
+
const request = {
|
|
44730
|
+
source_project_id: uuid3(value.source_project_id, "source_project_id"),
|
|
44731
|
+
destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
|
|
44732
|
+
destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
|
|
44733
|
+
root_task_id: uuid3(value.root_task_id, "root_task_id"),
|
|
44734
|
+
destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
|
|
44735
|
+
};
|
|
44736
|
+
if (request.source_project_id === request.destination_project_id) {
|
|
44737
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
|
|
44738
|
+
}
|
|
44739
|
+
return request;
|
|
44740
|
+
}
|
|
44741
|
+
var APPLY_FIELDS = [
|
|
44742
|
+
...INSPECT_FIELDS,
|
|
44743
|
+
"version",
|
|
44744
|
+
"operation_id",
|
|
44745
|
+
"step_id",
|
|
44746
|
+
"idempotency_key",
|
|
44747
|
+
"precondition_digest",
|
|
44748
|
+
"expected_root_parent_id",
|
|
44749
|
+
"source_population_digest",
|
|
44750
|
+
"expected_tasks",
|
|
44751
|
+
"shared_plan_splits"
|
|
44752
|
+
];
|
|
44753
|
+
function parseTodosTaskSubtreeTransferApply(input) {
|
|
44754
|
+
const value = strictObject(input, APPLY_FIELDS);
|
|
44755
|
+
if (value.version !== 1) {
|
|
44756
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
|
|
44757
|
+
}
|
|
44758
|
+
if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
|
|
44759
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
|
|
44760
|
+
}
|
|
44761
|
+
const expected_tasks = value.expected_tasks.map((item, index) => {
|
|
44762
|
+
const row = strictObject(item, ["task_id", "version"]);
|
|
44763
|
+
const version = row.version;
|
|
44764
|
+
if (!Number.isSafeInteger(version) || Number(version) < 1) {
|
|
44765
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
|
|
44766
|
+
}
|
|
44767
|
+
return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
|
|
44768
|
+
});
|
|
44769
|
+
const ids2 = new Set;
|
|
44770
|
+
for (const row of expected_tasks) {
|
|
44771
|
+
if (ids2.has(row.task_id)) {
|
|
44772
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
|
|
44773
|
+
}
|
|
44774
|
+
ids2.add(row.task_id);
|
|
44775
|
+
}
|
|
44776
|
+
if (!Array.isArray(value.shared_plan_splits)) {
|
|
44777
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
|
|
44778
|
+
}
|
|
44779
|
+
const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
|
|
44780
|
+
const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
|
|
44781
|
+
return {
|
|
44782
|
+
source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
|
|
44783
|
+
destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
|
|
44784
|
+
};
|
|
44785
|
+
});
|
|
44786
|
+
return {
|
|
44787
|
+
...parseTodosTaskSubtreeTransferInspect({
|
|
44788
|
+
source_project_id: value.source_project_id,
|
|
44789
|
+
destination_project_id: value.destination_project_id,
|
|
44790
|
+
destination_task_list_id: value.destination_task_list_id,
|
|
44791
|
+
root_task_id: value.root_task_id,
|
|
44792
|
+
destination_parent_id: value.destination_parent_id
|
|
44793
|
+
}),
|
|
44794
|
+
version: 1,
|
|
44795
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44796
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44797
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44798
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
|
|
44799
|
+
expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
|
|
44800
|
+
source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
|
|
44801
|
+
expected_tasks,
|
|
44802
|
+
shared_plan_splits
|
|
44803
|
+
};
|
|
44804
|
+
}
|
|
44805
|
+
function parseTodosTaskSubtreeTransferRollback(input) {
|
|
44806
|
+
const value = strictObject(input, [
|
|
44807
|
+
"receipt_id",
|
|
44808
|
+
"operation_id",
|
|
44809
|
+
"step_id",
|
|
44810
|
+
"idempotency_key",
|
|
44811
|
+
"precondition_digest"
|
|
44812
|
+
]);
|
|
44813
|
+
return {
|
|
44814
|
+
receipt_id: uuid3(value.receipt_id, "receipt_id"),
|
|
44815
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
44816
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
44817
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
44818
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest")
|
|
44819
|
+
};
|
|
44820
|
+
}
|
|
44821
|
+
function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
|
|
44822
|
+
const {
|
|
44823
|
+
source_project_id,
|
|
44824
|
+
destination_project_id,
|
|
44825
|
+
destination_task_list_id,
|
|
44826
|
+
root_task_id,
|
|
44827
|
+
expected_root_parent_id,
|
|
44828
|
+
destination_parent_id,
|
|
44829
|
+
source_population_digest,
|
|
44830
|
+
expected_tasks,
|
|
44831
|
+
shared_plan_splits
|
|
44832
|
+
} = input;
|
|
44833
|
+
return canonicalDigest({
|
|
44834
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44835
|
+
direction: "apply",
|
|
44836
|
+
source_project_id,
|
|
44837
|
+
destination_project_id,
|
|
44838
|
+
destination_task_list_id,
|
|
44839
|
+
root_task_id,
|
|
44840
|
+
expected_root_parent_id,
|
|
44841
|
+
destination_parent_id,
|
|
44842
|
+
source_population_digest,
|
|
44843
|
+
expected_tasks,
|
|
44844
|
+
shared_plan_splits
|
|
44845
|
+
});
|
|
44846
|
+
}
|
|
44847
|
+
function deriveTodosTaskSubtreeTransferRollbackPreconditionDigest(input) {
|
|
44848
|
+
return canonicalDigest({
|
|
44849
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44850
|
+
direction: "rollback",
|
|
44851
|
+
apply_receipt_id: input.receipt_id,
|
|
44852
|
+
operation_id: input.operation_id,
|
|
44853
|
+
step_id: input.step_id,
|
|
44854
|
+
apply_result_digest: input.apply_result_digest
|
|
44855
|
+
});
|
|
44856
|
+
}
|
|
44857
|
+
function resolveTenantId2(value) {
|
|
44858
|
+
const tenant = value ?? "default";
|
|
44859
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
|
|
44860
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
44861
|
+
}
|
|
44862
|
+
return tenant;
|
|
44863
|
+
}
|
|
44864
|
+
|
|
44865
|
+
class PackageOwnedTodosTaskSubtreeTransferAuthority {
|
|
44866
|
+
backend;
|
|
44867
|
+
tenantId;
|
|
44868
|
+
backendOptions;
|
|
44869
|
+
constructor(backend, options = {}) {
|
|
44870
|
+
this.backend = backend;
|
|
44871
|
+
this.tenantId = resolveTenantId2(options.tenantId);
|
|
44872
|
+
this.backendOptions = { ...options, tenantId: this.tenantId };
|
|
44873
|
+
}
|
|
44874
|
+
async capability() {
|
|
44875
|
+
return {
|
|
44876
|
+
authority: "todos",
|
|
44877
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
44878
|
+
schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
44879
|
+
tenant_id: this.tenantId,
|
|
44880
|
+
backend: this.backend.kind,
|
|
44881
|
+
exact_descendant_closure: true,
|
|
44882
|
+
complete_source_population_digest: true,
|
|
44883
|
+
per_task_version_cas: true,
|
|
44884
|
+
explicit_shared_plan_splits: true,
|
|
44885
|
+
atomic_apply: true,
|
|
44886
|
+
immutable_forward_inverse_receipts: true,
|
|
44887
|
+
prior_image_receipts: true,
|
|
44888
|
+
cas_protected_rollback: true,
|
|
44889
|
+
preserves_descendant_parent_ids: true,
|
|
44890
|
+
preserves_task_and_relation_identities: true
|
|
44891
|
+
};
|
|
44892
|
+
}
|
|
44893
|
+
async inspect(input) {
|
|
44894
|
+
return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
|
|
44895
|
+
}
|
|
44896
|
+
async apply(input) {
|
|
44897
|
+
const request = parseTodosTaskSubtreeTransferApply(input);
|
|
44898
|
+
const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
|
|
44899
|
+
if (request.precondition_digest !== expected) {
|
|
44900
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "precondition_digest does not match the exact transfer target and source snapshot", { expected_precondition_digest: expected });
|
|
44901
|
+
}
|
|
44902
|
+
const requestDigest = taskSubtreeTransferRequestDigest(request);
|
|
44903
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
44904
|
+
operation_id: request.operation_id,
|
|
44905
|
+
step_id: request.step_id,
|
|
44906
|
+
direction: "apply",
|
|
44907
|
+
target_selector: request.root_task_id,
|
|
44908
|
+
request_digest: requestDigest,
|
|
44909
|
+
precondition_digest: request.precondition_digest
|
|
44910
|
+
});
|
|
44911
|
+
if (request.idempotency_key !== expectedKey) {
|
|
44912
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
|
|
44913
|
+
}
|
|
44914
|
+
return this.backend.apply(request, this.backendOptions);
|
|
44915
|
+
}
|
|
44916
|
+
async readExact(receiptIdValue) {
|
|
44917
|
+
return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
|
|
44918
|
+
}
|
|
44919
|
+
async rollback(input) {
|
|
44920
|
+
const request = parseTodosTaskSubtreeTransferRollback(input);
|
|
44921
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
|
|
44922
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
44923
|
+
operation_id: request.operation_id,
|
|
44924
|
+
step_id: request.step_id,
|
|
44925
|
+
direction: "rollback",
|
|
44926
|
+
target_selector: request.receipt_id,
|
|
44927
|
+
request_digest: requestDigest,
|
|
44928
|
+
precondition_digest: request.precondition_digest
|
|
44929
|
+
});
|
|
44930
|
+
if (request.idempotency_key !== expectedKey) {
|
|
44931
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
|
|
44932
|
+
}
|
|
44933
|
+
return this.backend.rollback(request, this.backendOptions);
|
|
44934
|
+
}
|
|
44935
|
+
}
|
|
44936
|
+
function createSqliteTodosTaskSubtreeTransferAuthority(options) {
|
|
44937
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new SqliteTodosTaskSubtreeTransferBackend(options.database, resolveTenantId2(options.tenantId)), options);
|
|
44938
|
+
}
|
|
44939
|
+
function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
|
|
44940
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
|
|
44941
|
+
}
|
|
44942
|
+
// src/task-subtree-transfer/http.ts
|
|
44943
|
+
var JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
44944
|
+
var REQUEST_BYTES = 2 * 1024 * 1024;
|
|
44945
|
+
var RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
44946
|
+
function json3(body2, status2 = 200) {
|
|
44947
|
+
return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
|
|
44948
|
+
}
|
|
44949
|
+
function status2(error) {
|
|
44950
|
+
switch (error.code) {
|
|
44951
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
|
|
44952
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
|
|
44953
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
|
|
44954
|
+
return 400;
|
|
44955
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
|
|
44956
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
|
|
44957
|
+
return 404;
|
|
44958
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
|
|
44959
|
+
return 503;
|
|
44960
|
+
default:
|
|
44961
|
+
return 409;
|
|
44962
|
+
}
|
|
44963
|
+
}
|
|
44964
|
+
async function boundedText2(message, limit) {
|
|
44965
|
+
if (!message.body)
|
|
44966
|
+
return "";
|
|
44967
|
+
const reader = message.body.getReader();
|
|
44968
|
+
const decoder = new TextDecoder;
|
|
44969
|
+
let bytes = 0;
|
|
44970
|
+
let text = "";
|
|
44971
|
+
try {
|
|
44972
|
+
while (true) {
|
|
44973
|
+
const { done, value } = await reader.read();
|
|
44974
|
+
if (done)
|
|
44975
|
+
break;
|
|
44976
|
+
bytes += value.byteLength;
|
|
44977
|
+
if (bytes > limit) {
|
|
44978
|
+
await reader.cancel();
|
|
44979
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
|
|
44980
|
+
}
|
|
44981
|
+
text += decoder.decode(value, { stream: true });
|
|
44982
|
+
}
|
|
44983
|
+
return text + decoder.decode();
|
|
44984
|
+
} finally {
|
|
44985
|
+
reader.releaseLock();
|
|
44986
|
+
}
|
|
44987
|
+
}
|
|
44988
|
+
async function readBody(request) {
|
|
44989
|
+
const declaredLength = Number(request.headers.get("content-length") ?? 0);
|
|
44990
|
+
if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
|
|
44991
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
|
|
44992
|
+
}
|
|
44993
|
+
const text = await boundedText2(request, REQUEST_BYTES);
|
|
44994
|
+
try {
|
|
44995
|
+
return JSON.parse(text);
|
|
44996
|
+
} catch {
|
|
44997
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
|
|
44998
|
+
}
|
|
44999
|
+
}
|
|
45000
|
+
async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
|
|
45001
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
|
|
45002
|
+
return null;
|
|
45003
|
+
const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
45004
|
+
try {
|
|
45005
|
+
if ((action === "" || action === "capability") && request.method === "GET") {
|
|
45006
|
+
return json3({ capability: await authority.capability() });
|
|
45007
|
+
}
|
|
45008
|
+
if (request.method !== "POST")
|
|
45009
|
+
return json3({ error: "method not allowed" }, 405);
|
|
45010
|
+
if (action === "inspect")
|
|
45011
|
+
return json3({ inspection: await authority.inspect(await readBody(request)) });
|
|
45012
|
+
if (action === "apply")
|
|
45013
|
+
return json3({ result: await authority.apply(await readBody(request)) }, 201);
|
|
45014
|
+
if (action === "rollback")
|
|
45015
|
+
return json3({ result: await authority.rollback(await readBody(request)) }, 201);
|
|
45016
|
+
if (action === "read-exact") {
|
|
45017
|
+
const input = await readBody(request);
|
|
45018
|
+
if (!input || typeof input.receipt_id !== "string") {
|
|
45019
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
|
|
45020
|
+
}
|
|
45021
|
+
return json3({ result: await authority.readExact(input.receipt_id) });
|
|
45022
|
+
}
|
|
45023
|
+
return json3({ error: "unknown task-subtree-transfer route" }, 404);
|
|
45024
|
+
} catch (cause) {
|
|
45025
|
+
if (cause instanceof TodosTaskSubtreeTransferError) {
|
|
45026
|
+
return json3({
|
|
45027
|
+
error: cause.message,
|
|
45028
|
+
code: cause.code,
|
|
45029
|
+
details: cause.details,
|
|
45030
|
+
authoritative: true
|
|
45031
|
+
}, status2(cause));
|
|
45032
|
+
}
|
|
45033
|
+
return json3({ error: "internal task-subtree-transfer error" }, 500);
|
|
45034
|
+
}
|
|
45035
|
+
}
|
|
45036
|
+
|
|
45037
|
+
class TodosTaskSubtreeTransferHttpClient {
|
|
45038
|
+
baseUrl;
|
|
45039
|
+
fetchImpl;
|
|
45040
|
+
headers;
|
|
45041
|
+
constructor(options) {
|
|
45042
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
45043
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
45044
|
+
this.headers = {
|
|
45045
|
+
...options.headers,
|
|
45046
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
45047
|
+
"Content-Type": "application/json"
|
|
45048
|
+
};
|
|
45049
|
+
}
|
|
45050
|
+
async request(action, init = {}) {
|
|
45051
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/task-subtree-transfer${action}`, {
|
|
45052
|
+
...init,
|
|
45053
|
+
headers: { ...this.headers, ...init.headers ?? {} }
|
|
45054
|
+
});
|
|
45055
|
+
const text = await boundedText2(response, RESPONSE_BYTES);
|
|
45056
|
+
let payload;
|
|
45057
|
+
try {
|
|
45058
|
+
payload = JSON.parse(text);
|
|
45059
|
+
} catch {
|
|
45060
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", `Task-subtree-transfer HTTP ${response.status} returned invalid JSON`);
|
|
45061
|
+
}
|
|
45062
|
+
if (!response.ok) {
|
|
45063
|
+
throw new TodosTaskSubtreeTransferError(typeof payload.code === "string" ? payload.code : "TODOS_TASK_SUBTREE_TRANSFER_HTTP_ERROR", typeof payload.error === "string" ? payload.error : `Task-subtree-transfer HTTP ${response.status}`, payload.details && typeof payload.details === "object" ? payload.details : {});
|
|
45064
|
+
}
|
|
45065
|
+
return payload;
|
|
45066
|
+
}
|
|
45067
|
+
async capability() {
|
|
45068
|
+
return (await this.request("/capability")).capability;
|
|
45069
|
+
}
|
|
45070
|
+
async inspect(input) {
|
|
45071
|
+
return (await this.request("/inspect", { method: "POST", body: JSON.stringify(input) })).inspection;
|
|
45072
|
+
}
|
|
45073
|
+
async apply(input) {
|
|
45074
|
+
return (await this.request("/apply", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45075
|
+
}
|
|
45076
|
+
async readExact(receiptId3) {
|
|
45077
|
+
return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId3 }) })).result;
|
|
45078
|
+
}
|
|
45079
|
+
async rollback(input) {
|
|
45080
|
+
return (await this.request("/rollback", { method: "POST", body: JSON.stringify(input) })).result;
|
|
45081
|
+
}
|
|
45082
|
+
}
|
|
45083
|
+
function createTodosTaskSubtreeTransferHttpClient(options) {
|
|
45084
|
+
return new TodosTaskSubtreeTransferHttpClient(options);
|
|
45085
|
+
}
|
|
43486
45086
|
// src/ai-tools.ts
|
|
43487
45087
|
import { createHash as createHash16 } from "crypto";
|
|
43488
45088
|
// src/cli/cloud-router.ts
|
|
@@ -43494,26 +45094,26 @@ var retryCapabilityCache = new Map;
|
|
|
43494
45094
|
var taskCreatorCapabilityCache = new Map;
|
|
43495
45095
|
var gitRefCapabilityCache = new Map;
|
|
43496
45096
|
var remoteCommandCapabilityCache = new Map;
|
|
43497
|
-
var
|
|
43498
|
-
|
|
43499
|
-
|
|
43500
|
-
|
|
43501
|
-
|
|
43502
|
-
|
|
43503
|
-
|
|
43504
|
-
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
45097
|
+
var todosLocalFallbackNoticeEmitted = false;
|
|
45098
|
+
function emitTodosLocalFallbackNotice(env) {
|
|
45099
|
+
if (todosLocalFallbackNoticeEmitted)
|
|
45100
|
+
return;
|
|
45101
|
+
todosLocalFallbackNoticeEmitted = true;
|
|
45102
|
+
const notice = {
|
|
45103
|
+
event: "todos-local-fallback",
|
|
45104
|
+
transport: "sqlite",
|
|
45105
|
+
source: "default",
|
|
45106
|
+
apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
45107
|
+
apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
45108
|
+
notice: "No hosted API config (HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY) is present; " + "using local SQLite. Hosted todos are NOT visible in this output."
|
|
45109
|
+
};
|
|
45110
|
+
console.error(JSON.stringify(notice));
|
|
43509
45111
|
}
|
|
43510
|
-
function
|
|
43511
|
-
assertNoLegacyStorageMode(env);
|
|
45112
|
+
function resolveTodosCliTransport(env = process.env) {
|
|
43512
45113
|
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
43513
45114
|
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
43514
45115
|
if (urlValue && keyValue) {
|
|
43515
45116
|
return {
|
|
43516
|
-
mode: "http",
|
|
43517
45117
|
transport: "http",
|
|
43518
45118
|
selected: true,
|
|
43519
45119
|
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
@@ -43525,15 +45125,15 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
43525
45125
|
if (keyValue) {
|
|
43526
45126
|
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
43527
45127
|
}
|
|
45128
|
+
emitTodosLocalFallbackNotice(env);
|
|
43528
45129
|
return {
|
|
43529
|
-
mode: "sqlite",
|
|
43530
45130
|
transport: "sqlite",
|
|
43531
45131
|
selected: false,
|
|
43532
45132
|
source: "default"
|
|
43533
45133
|
};
|
|
43534
45134
|
}
|
|
43535
45135
|
function requestedTransport(env) {
|
|
43536
|
-
return
|
|
45136
|
+
return resolveTodosCliTransport(env).transport;
|
|
43537
45137
|
}
|
|
43538
45138
|
function normalizeRemoteAuthorityUrl(value) {
|
|
43539
45139
|
const trimmed = value?.trim();
|
|
@@ -43567,13 +45167,13 @@ function normalizeRemoteAuthorityUrl(value) {
|
|
|
43567
45167
|
function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
43568
45168
|
let resolution;
|
|
43569
45169
|
try {
|
|
43570
|
-
resolution =
|
|
45170
|
+
resolution = resolveTodosCliTransport(env);
|
|
43571
45171
|
} catch (error) {
|
|
43572
45172
|
const issue = error instanceof Error ? error.message : String(error);
|
|
43573
45173
|
return {
|
|
43574
45174
|
selected: true,
|
|
43575
45175
|
ok: false,
|
|
43576
|
-
|
|
45176
|
+
transport: "invalid",
|
|
43577
45177
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
43578
45178
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
43579
45179
|
v1_base_url: null,
|
|
@@ -43581,12 +45181,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43581
45181
|
local_fallback: false
|
|
43582
45182
|
};
|
|
43583
45183
|
}
|
|
43584
|
-
const {
|
|
45184
|
+
const { transport, selected } = resolution;
|
|
43585
45185
|
if (!selected) {
|
|
43586
45186
|
return {
|
|
43587
45187
|
selected: false,
|
|
43588
45188
|
ok: true,
|
|
43589
|
-
|
|
45189
|
+
transport,
|
|
43590
45190
|
api_url_configured: false,
|
|
43591
45191
|
api_key_configured: false,
|
|
43592
45192
|
v1_base_url: null,
|
|
@@ -43611,7 +45211,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43611
45211
|
return {
|
|
43612
45212
|
selected: true,
|
|
43613
45213
|
ok: issues.length === 0,
|
|
43614
|
-
|
|
45214
|
+
transport,
|
|
43615
45215
|
api_url_configured: apiUrl !== null,
|
|
43616
45216
|
api_key_configured: apiKeyConfigured,
|
|
43617
45217
|
v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
|
|
@@ -43620,24 +45220,24 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
43620
45220
|
};
|
|
43621
45221
|
}
|
|
43622
45222
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
43623
|
-
const
|
|
43624
|
-
if (!
|
|
43625
|
-
throw new Error(
|
|
45223
|
+
const status3 = getTodosRemoteAuthorityConfigStatus(env);
|
|
45224
|
+
if (!status3.ok)
|
|
45225
|
+
throw new Error(status3.issues[0]);
|
|
43626
45226
|
return {
|
|
43627
45227
|
...env,
|
|
43628
|
-
HASNA_TODOS_API_URL:
|
|
45228
|
+
HASNA_TODOS_API_URL: status3.v1_base_url.replace(/\/v1$/, ""),
|
|
43629
45229
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
43630
45230
|
};
|
|
43631
45231
|
}
|
|
43632
45232
|
function classifyRemoteRequestError(baseUrl, route, error) {
|
|
43633
|
-
const
|
|
43634
|
-
if (
|
|
45233
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45234
|
+
if (status3 === 401) {
|
|
43635
45235
|
throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43636
45236
|
}
|
|
43637
|
-
if (
|
|
45237
|
+
if (status3 === 403) {
|
|
43638
45238
|
throw new Error(`REMOTE_API_FORBIDDEN: configured Todos authority ${baseUrl} denied ${route}; local SQLite fallback is disabled`, { cause: error });
|
|
43639
45239
|
}
|
|
43640
|
-
if (
|
|
45240
|
+
if (status3 === 409) {
|
|
43641
45241
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43642
45242
|
if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
|
|
43643
45243
|
const remoteError = body2;
|
|
@@ -43647,22 +45247,48 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
43647
45247
|
}
|
|
43648
45248
|
throw error;
|
|
43649
45249
|
}
|
|
43650
|
-
if (typeof
|
|
45250
|
+
if (typeof status3 === "number" && status3 >= 300 && status3 < 400) {
|
|
43651
45251
|
throw new Error(`REMOTE_API_REDIRECT_REJECTED: configured Todos authority ${baseUrl} redirected ${route}; ` + "authenticated redirects are disabled to prevent credential leakage", { cause: error });
|
|
43652
45252
|
}
|
|
43653
|
-
if (typeof
|
|
43654
|
-
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${
|
|
45253
|
+
if (typeof status3 === "number" && status3 >= 500) {
|
|
45254
|
+
throw new Error(`REMOTE_API_UNAVAILABLE: configured Todos authority ${baseUrl} returned HTTP ${status3} for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43655
45255
|
}
|
|
43656
45256
|
const message = error instanceof Error ? error.message : String(error);
|
|
43657
45257
|
if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
|
|
43658
|
-
|
|
45258
|
+
const unboundedNote = route === "/tasks" ? "; this is likely an UNBOUNDED task read timing out (bounded task reads from this authority return in ~1 s) \u2014 retry with a bounded/count-specific shape such as todos list --limit; local SQLite fallback is disabled" : "; local SQLite fallback is disabled";
|
|
45259
|
+
throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
|
|
43659
45260
|
}
|
|
43660
|
-
if (
|
|
45261
|
+
if (status3 === undefined) {
|
|
43661
45262
|
throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
43662
45263
|
}
|
|
43663
45264
|
throw error;
|
|
43664
45265
|
}
|
|
43665
|
-
|
|
45266
|
+
var REMOTE_REQUEST_TIMEOUT_MS = 1e4;
|
|
45267
|
+
function withBoundedRemoteRequest(options, requestTimeoutMs, run) {
|
|
45268
|
+
const controller = new AbortController;
|
|
45269
|
+
const onBaseAbort = () => controller.abort();
|
|
45270
|
+
if (options?.signal) {
|
|
45271
|
+
if (options.signal.aborted)
|
|
45272
|
+
controller.abort();
|
|
45273
|
+
else
|
|
45274
|
+
options.signal.addEventListener("abort", onBaseAbort, { once: true });
|
|
45275
|
+
}
|
|
45276
|
+
let timer;
|
|
45277
|
+
const deadline = new Promise((_, reject) => {
|
|
45278
|
+
timer = setTimeout(() => {
|
|
45279
|
+
controller.abort();
|
|
45280
|
+
reject(new DOMException(`Todos remote request exceeded the ${requestTimeoutMs}ms bounded request timeout`, "AbortError"));
|
|
45281
|
+
}, requestTimeoutMs);
|
|
45282
|
+
});
|
|
45283
|
+
const attempt = run({ ...options, signal: controller.signal });
|
|
45284
|
+
attempt.catch(() => {});
|
|
45285
|
+
return Promise.race([attempt, deadline]).finally(() => {
|
|
45286
|
+
if (timer)
|
|
45287
|
+
clearTimeout(timer);
|
|
45288
|
+
options?.signal?.removeEventListener("abort", onBaseAbort);
|
|
45289
|
+
});
|
|
45290
|
+
}
|
|
45291
|
+
function protectRemoteClient(client, requestTimeoutMs) {
|
|
43666
45292
|
const baseUrl = remoteAuthorityBase(client);
|
|
43667
45293
|
const protect = async (route, request) => {
|
|
43668
45294
|
try {
|
|
@@ -43671,25 +45297,26 @@ function protectRemoteClient(client) {
|
|
|
43671
45297
|
return classifyRemoteRequestError(baseUrl, route, error);
|
|
43672
45298
|
}
|
|
43673
45299
|
};
|
|
45300
|
+
const bounded = (options, run) => withBoundedRemoteRequest(options, requestTimeoutMs, run);
|
|
43674
45301
|
const transport = client.transport;
|
|
43675
45302
|
const protectedTransport = {
|
|
43676
45303
|
baseUrl: transport.baseUrl,
|
|
43677
|
-
request: (method, path, body2, options) => protect(path, () => transport.request(method, path, body2,
|
|
43678
|
-
get: (path, options) => protect(path, () => transport.get(path,
|
|
43679
|
-
post: (path, body2, options) => protect(path, () => transport.post(path, body2,
|
|
43680
|
-
put: (path, body2, options) => protect(path, () => transport.put(path, body2,
|
|
43681
|
-
patch: (path, body2, options) => protect(path, () => transport.patch(path, body2,
|
|
43682
|
-
del: (path, body2, options) => protect(path, () => transport.del(path, body2,
|
|
45304
|
+
request: (method, path, body2, options) => protect(path, () => bounded(options, (opts) => transport.request(method, path, body2, opts))),
|
|
45305
|
+
get: (path, options) => protect(path, () => bounded(options, (opts) => transport.get(path, opts))),
|
|
45306
|
+
post: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.post(path, body2, opts))),
|
|
45307
|
+
put: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.put(path, body2, opts))),
|
|
45308
|
+
patch: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.patch(path, body2, opts))),
|
|
45309
|
+
del: (path, body2, options) => protect(path, () => bounded(options, (opts) => transport.del(path, body2, opts)))
|
|
43683
45310
|
};
|
|
43684
45311
|
return {
|
|
43685
45312
|
name: client.name,
|
|
43686
45313
|
baseUrl: client.baseUrl,
|
|
43687
45314
|
transport: protectedTransport,
|
|
43688
|
-
list: (resource, options) => protect(`/${resource}`, () => client.list(resource,
|
|
43689
|
-
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.get(resource, id,
|
|
43690
|
-
create: (resource, body2, options) => protect(`/${resource}`, () => client.create(resource, body2,
|
|
43691
|
-
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.update(resource, id, patch,
|
|
43692
|
-
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => client.delete(resource, id,
|
|
45315
|
+
list: (resource, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.list(resource, opts))),
|
|
45316
|
+
get: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.get(resource, id, opts))),
|
|
45317
|
+
create: (resource, body2, options) => protect(`/${resource}`, () => bounded(options, (opts) => client.create(resource, body2, opts))),
|
|
45318
|
+
update: (resource, id, patch, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.update(resource, id, patch, opts))),
|
|
45319
|
+
delete: (resource, id, options) => protect(`/${resource}/${encodeURIComponent(id)}`, () => bounded(options, (opts) => client.delete(resource, id, opts)))
|
|
43693
45320
|
};
|
|
43694
45321
|
}
|
|
43695
45322
|
function remoteAuthorityBase(client) {
|
|
@@ -43699,8 +45326,8 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
43699
45326
|
try {
|
|
43700
45327
|
return await request();
|
|
43701
45328
|
} catch (error) {
|
|
43702
|
-
const
|
|
43703
|
-
if (
|
|
45329
|
+
const status3 = error && typeof error === "object" ? error.status : undefined;
|
|
45330
|
+
if (status3 === 404 || status3 === 405) {
|
|
43704
45331
|
const body2 = error && typeof error === "object" ? error.body : undefined;
|
|
43705
45332
|
const code = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.code : undefined;
|
|
43706
45333
|
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
@@ -43710,17 +45337,17 @@ async function requiredRemoteRoute(client, route, request, recognized404Codes =
|
|
|
43710
45337
|
throw error;
|
|
43711
45338
|
}
|
|
43712
45339
|
}
|
|
43713
|
-
function getTodosCloudClient(env = process.env) {
|
|
45340
|
+
function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUEST_TIMEOUT_MS) {
|
|
43714
45341
|
if (requestedTransport(env) !== "http")
|
|
43715
45342
|
return null;
|
|
43716
45343
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
43717
|
-
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
45344
|
+
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
45345
|
+
timeoutMs: requestTimeoutMs
|
|
43718
45346
|
});
|
|
43719
|
-
if (resolved.transport === "cloud-http")
|
|
43720
|
-
return protectRemoteClient(resolved.client);
|
|
43721
45347
|
const transportName = resolved.transport;
|
|
43722
|
-
if (transportName === "http")
|
|
43723
|
-
return protectRemoteClient(resolved.client);
|
|
45348
|
+
if (transportName === "cloud-http" || transportName === "http") {
|
|
45349
|
+
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
45350
|
+
}
|
|
43724
45351
|
return null;
|
|
43725
45352
|
}
|
|
43726
45353
|
function unwrapTask(raw) {
|
|
@@ -43871,9 +45498,9 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
43871
45498
|
const { status: _status, limit, offset, ...baseFilter } = filter;
|
|
43872
45499
|
const start = offset ?? 0;
|
|
43873
45500
|
const windowEnd = typeof limit === "number" ? start + limit : undefined;
|
|
43874
|
-
const pages = await Promise.all(statuses.map((
|
|
45501
|
+
const pages = await Promise.all(statuses.map((status3) => requestCloudTaskPage(client, {
|
|
43875
45502
|
...baseFilter,
|
|
43876
|
-
status:
|
|
45503
|
+
status: status3,
|
|
43877
45504
|
...windowEnd === undefined ? {} : { limit: windowEnd }
|
|
43878
45505
|
})));
|
|
43879
45506
|
const seen = new Set;
|
|
@@ -44151,11 +45778,11 @@ init_audit();
|
|
|
44151
45778
|
init_database();
|
|
44152
45779
|
function upsertCheckpoint(task_id, step, updates, db) {
|
|
44153
45780
|
const d = db || getDatabase();
|
|
44154
|
-
const
|
|
45781
|
+
const timestamp5 = now();
|
|
44155
45782
|
const existing = d.query("SELECT id FROM task_checkpoints WHERE task_id = ? AND step = ?").get(task_id, step);
|
|
44156
45783
|
const id = existing?.id ?? uuid();
|
|
44157
45784
|
const agentId = updates.agent_id ?? null;
|
|
44158
|
-
const
|
|
45785
|
+
const status3 = updates.status ?? "pending";
|
|
44159
45786
|
const data = updates.data ? JSON.stringify(updates.data) : JSON.stringify({});
|
|
44160
45787
|
const error = updates.error ?? null;
|
|
44161
45788
|
const attempt = updates.attempt ?? 1;
|
|
@@ -44169,22 +45796,22 @@ function upsertCheckpoint(task_id, step, updates, db) {
|
|
|
44169
45796
|
task_id,
|
|
44170
45797
|
agentId,
|
|
44171
45798
|
step,
|
|
44172
|
-
|
|
45799
|
+
status3,
|
|
44173
45800
|
data,
|
|
44174
45801
|
error,
|
|
44175
45802
|
attempt,
|
|
44176
45803
|
maxAttempts,
|
|
44177
45804
|
startedAt,
|
|
44178
45805
|
completedAt2,
|
|
44179
|
-
|
|
44180
|
-
|
|
45806
|
+
timestamp5,
|
|
45807
|
+
status3,
|
|
44181
45808
|
data,
|
|
44182
45809
|
error,
|
|
44183
45810
|
attempt,
|
|
44184
45811
|
maxAttempts,
|
|
44185
45812
|
startedAt,
|
|
44186
45813
|
completedAt2,
|
|
44187
|
-
|
|
45814
|
+
timestamp5
|
|
44188
45815
|
]);
|
|
44189
45816
|
return rowToCheckpoint(d.query("SELECT * FROM task_checkpoints WHERE id = ?").get(id));
|
|
44190
45817
|
}
|
|
@@ -44247,10 +45874,10 @@ function stepForGate(gate) {
|
|
|
44247
45874
|
throw new Error("Approval gate name is required");
|
|
44248
45875
|
return `approval:${trimmed}`;
|
|
44249
45876
|
}
|
|
44250
|
-
function approvalStatusToCheckpointStatus(
|
|
44251
|
-
if (
|
|
45877
|
+
function approvalStatusToCheckpointStatus(status3) {
|
|
45878
|
+
if (status3 === "approved")
|
|
44252
45879
|
return "completed";
|
|
44253
|
-
if (
|
|
45880
|
+
if (status3 === "pending")
|
|
44254
45881
|
return "pending";
|
|
44255
45882
|
return "failed";
|
|
44256
45883
|
}
|
|
@@ -44272,12 +45899,12 @@ function isExpired(expiresAt, at = new Date) {
|
|
|
44272
45899
|
return Boolean(expiresAt && new Date(expiresAt).getTime() <= at.getTime());
|
|
44273
45900
|
}
|
|
44274
45901
|
function gateFromCheckpoint(checkpoint) {
|
|
44275
|
-
const
|
|
45902
|
+
const status3 = checkpointStatusToApprovalStatus(checkpoint);
|
|
44276
45903
|
return {
|
|
44277
45904
|
id: checkpoint.id,
|
|
44278
45905
|
task_id: checkpoint.task_id,
|
|
44279
45906
|
gate: String(checkpoint.data["approval_gate_name"] || checkpoint.step.replace(/^approval:/, "")),
|
|
44280
|
-
status:
|
|
45907
|
+
status: status3,
|
|
44281
45908
|
reviewer: typeof checkpoint.data["reviewer"] === "string" ? checkpoint.data["reviewer"] : null,
|
|
44282
45909
|
requester: typeof checkpoint.data["requester"] === "string" ? checkpoint.data["requester"] : null,
|
|
44283
45910
|
reason: typeof checkpoint.data["reason"] === "string" ? checkpoint.data["reason"] : null,
|
|
@@ -44322,13 +45949,13 @@ function logApprovalEvent(taskId, action, gate, agentId, db) {
|
|
|
44322
45949
|
});
|
|
44323
45950
|
}
|
|
44324
45951
|
}
|
|
44325
|
-
function writeGate(input,
|
|
45952
|
+
function writeGate(input, status3, decision, db) {
|
|
44326
45953
|
const d = db || getDatabase();
|
|
44327
45954
|
ensureTask(input.task_id, d);
|
|
44328
45955
|
const step = stepForGate(input.gate);
|
|
44329
45956
|
const existing = getCheckpoint(input.task_id, step, d);
|
|
44330
45957
|
const existingData = existing?.data || {};
|
|
44331
|
-
const
|
|
45958
|
+
const timestamp5 = decision?.decided_at || now();
|
|
44332
45959
|
const existingRunId = typeof existingData["run_id"] === "string" ? existingData["run_id"] : undefined;
|
|
44333
45960
|
const runId = input.run_id ? resolveTaskRunId(input.run_id, d) : existingRunId;
|
|
44334
45961
|
const data = {
|
|
@@ -44336,7 +45963,7 @@ function writeGate(input, status2, decision, db) {
|
|
|
44336
45963
|
...input.metadata || {},
|
|
44337
45964
|
approval_gate: true,
|
|
44338
45965
|
approval_gate_name: input.gate.trim(),
|
|
44339
|
-
approval_status:
|
|
45966
|
+
approval_status: status3,
|
|
44340
45967
|
requester: input.requester ?? existingData["requester"] ?? null,
|
|
44341
45968
|
reviewer: input.reviewer ?? decision?.decided_by ?? existingData["reviewer"] ?? null,
|
|
44342
45969
|
reason: decision?.reason ?? input.reason ?? existingData["reason"] ?? null,
|
|
@@ -44349,11 +45976,11 @@ function writeGate(input, status2, decision, db) {
|
|
|
44349
45976
|
};
|
|
44350
45977
|
const checkpoint = upsertCheckpoint(input.task_id, step, {
|
|
44351
45978
|
agent_id: decision?.decided_by || input.requester || input.reviewer,
|
|
44352
|
-
status: approvalStatusToCheckpointStatus(
|
|
45979
|
+
status: approvalStatusToCheckpointStatus(status3),
|
|
44353
45980
|
data,
|
|
44354
|
-
error:
|
|
44355
|
-
started_at: existing?.started_at ||
|
|
44356
|
-
completed_at:
|
|
45981
|
+
error: status3 === "rejected" || status3 === "expired" ? String(data.reason || status3) : null,
|
|
45982
|
+
started_at: existing?.started_at || timestamp5,
|
|
45983
|
+
completed_at: status3 === "pending" ? null : timestamp5
|
|
44357
45984
|
}, d);
|
|
44358
45985
|
return gateFromCheckpoint(checkpoint);
|
|
44359
45986
|
}
|
|
@@ -45201,7 +46828,7 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45201
46828
|
const planId = boundedOptionalString(record, "plan_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45202
46829
|
const taskListId = boundedOptionalString(record, "task_list_id", TODOS_AI_READ_TOOL_LIMITS.max_identifier_bytes);
|
|
45203
46830
|
const assignedTo = boundedOptionalString(record, "assigned_to", TODOS_AI_READ_TOOL_LIMITS.max_filter_string_bytes);
|
|
45204
|
-
const
|
|
46831
|
+
const status3 = boundedEnum(record, "status", TASK_STATUSES);
|
|
45205
46832
|
const priority = boundedEnum(record, "priority", TASK_PRIORITIES);
|
|
45206
46833
|
const tags = boundedTags(record);
|
|
45207
46834
|
if (projectId !== undefined)
|
|
@@ -45212,8 +46839,8 @@ async function executeListTasks(adapter, state, request, input) {
|
|
|
45212
46839
|
filter.task_list_id = taskListId;
|
|
45213
46840
|
if (assignedTo !== undefined)
|
|
45214
46841
|
filter.assigned_to = assignedTo;
|
|
45215
|
-
if (
|
|
45216
|
-
filter.status =
|
|
46842
|
+
if (status3 !== undefined)
|
|
46843
|
+
filter.status = status3;
|
|
45217
46844
|
if (priority !== undefined)
|
|
45218
46845
|
filter.priority = priority;
|
|
45219
46846
|
if (tags !== undefined)
|
|
@@ -45555,29 +47182,29 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
45555
47182
|
};
|
|
45556
47183
|
}
|
|
45557
47184
|
function getNativeStorageSyncPlan(env = process.env, options = {}) {
|
|
45558
|
-
const
|
|
47185
|
+
const status3 = getNativeStorageStatus(env);
|
|
45559
47186
|
const steps = [
|
|
45560
47187
|
"Read local SQLite snapshot",
|
|
45561
|
-
|
|
45562
|
-
|
|
47188
|
+
status3.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
|
|
47189
|
+
status3.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
|
|
45563
47190
|
"Report planned changes without opening network connections"
|
|
45564
47191
|
];
|
|
45565
47192
|
return {
|
|
45566
|
-
ok:
|
|
47193
|
+
ok: status3.ok,
|
|
45567
47194
|
service: "todos",
|
|
45568
47195
|
dry_run: true,
|
|
45569
47196
|
no_network: true,
|
|
45570
|
-
status:
|
|
47197
|
+
status: status3,
|
|
45571
47198
|
postgres: {
|
|
45572
|
-
required:
|
|
45573
|
-
configured:
|
|
47199
|
+
required: status3.remote_enabled,
|
|
47200
|
+
configured: status3.database.configured,
|
|
45574
47201
|
schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
|
|
45575
47202
|
},
|
|
45576
47203
|
object_storage: {
|
|
45577
|
-
required:
|
|
45578
|
-
configured:
|
|
45579
|
-
bucket:
|
|
45580
|
-
prefix:
|
|
47204
|
+
required: status3.remote_enabled,
|
|
47205
|
+
configured: status3.object_storage.configured,
|
|
47206
|
+
bucket: status3.object_storage.bucket,
|
|
47207
|
+
prefix: status3.object_storage.prefix
|
|
45581
47208
|
},
|
|
45582
47209
|
steps
|
|
45583
47210
|
};
|
|
@@ -45683,8 +47310,8 @@ function resolveTaskList(task3, project, db) {
|
|
|
45683
47310
|
}
|
|
45684
47311
|
return null;
|
|
45685
47312
|
}
|
|
45686
|
-
function isTerminal2(
|
|
45687
|
-
return
|
|
47313
|
+
function isTerminal2(status3) {
|
|
47314
|
+
return status3 === "completed" || status3 === "cancelled" || status3 === "failed";
|
|
45688
47315
|
}
|
|
45689
47316
|
function routeConcurrencyKey(task3, project, taskList, projectPath) {
|
|
45690
47317
|
if (project?.id)
|
|
@@ -45865,7 +47492,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
45865
47492
|
latest_evaluation_path: pointerPatch(previous.latest_evaluation_path, input, "latest_evaluation_path"),
|
|
45866
47493
|
workflow_state: pointerPatch(previous.workflow_state, input, "workflow_state")
|
|
45867
47494
|
});
|
|
45868
|
-
const
|
|
47495
|
+
const timestamp5 = now();
|
|
45869
47496
|
const {
|
|
45870
47497
|
current_workflow_invocation_id,
|
|
45871
47498
|
current_run_id,
|
|
@@ -45881,7 +47508,7 @@ function setTaskWorkflowPointers(taskId, input, db) {
|
|
|
45881
47508
|
workflow_invocation: {
|
|
45882
47509
|
schema_version: TASK_WORKFLOW_POINTER_SCHEMA_VERSION,
|
|
45883
47510
|
...next,
|
|
45884
|
-
updated_at:
|
|
47511
|
+
updated_at: timestamp5,
|
|
45885
47512
|
updated_by: input.actor ?? null
|
|
45886
47513
|
}
|
|
45887
47514
|
};
|
|
@@ -45915,8 +47542,8 @@ function normalizePath3(input) {
|
|
|
45915
47542
|
return resolve10(input);
|
|
45916
47543
|
}
|
|
45917
47544
|
function sourceStoreId(sourceDbPath) {
|
|
45918
|
-
const
|
|
45919
|
-
return `sqlite:${
|
|
47545
|
+
const digest3 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
47546
|
+
return `sqlite:${digest3}`;
|
|
45920
47547
|
}
|
|
45921
47548
|
function inferSourceRepoPath(sourceDbPath) {
|
|
45922
47549
|
const normalized = normalizePath3(sourceDbPath);
|
|
@@ -46687,7 +48314,7 @@ function rowToSnapshot(row) {
|
|
|
46687
48314
|
function saveSnapshot(input, db) {
|
|
46688
48315
|
const d = db || getDatabase();
|
|
46689
48316
|
const id = uuid();
|
|
46690
|
-
const
|
|
48317
|
+
const timestamp5 = now();
|
|
46691
48318
|
d.run(`INSERT INTO context_snapshots (id, agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps, metadata, created_at)
|
|
46692
48319
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
46693
48320
|
id,
|
|
@@ -46701,7 +48328,7 @@ function saveSnapshot(input, db) {
|
|
|
46701
48328
|
JSON.stringify(input.blockers || []),
|
|
46702
48329
|
input.next_steps || null,
|
|
46703
48330
|
JSON.stringify(input.metadata || {}),
|
|
46704
|
-
|
|
48331
|
+
timestamp5
|
|
46705
48332
|
]);
|
|
46706
48333
|
return {
|
|
46707
48334
|
id,
|
|
@@ -46715,7 +48342,7 @@ function saveSnapshot(input, db) {
|
|
|
46715
48342
|
blockers: input.blockers || [],
|
|
46716
48343
|
next_steps: input.next_steps || null,
|
|
46717
48344
|
metadata: input.metadata || {},
|
|
46718
|
-
created_at:
|
|
48345
|
+
created_at: timestamp5
|
|
46719
48346
|
};
|
|
46720
48347
|
}
|
|
46721
48348
|
function getLatestSnapshot(agentId, taskId, db) {
|
|
@@ -46831,7 +48458,7 @@ function createKnowledgeRecord(input, db) {
|
|
|
46831
48458
|
if (!title)
|
|
46832
48459
|
throw new Error("Knowledge record title is required");
|
|
46833
48460
|
const d = db || getDatabase();
|
|
46834
|
-
const
|
|
48461
|
+
const timestamp5 = now();
|
|
46835
48462
|
const id = uuid();
|
|
46836
48463
|
d.run(`INSERT INTO project_knowledge_records (
|
|
46837
48464
|
id, record_type, title, content, decision, rationale, alternatives,
|
|
@@ -46851,8 +48478,8 @@ function createKnowledgeRecord(input, db) {
|
|
|
46851
48478
|
input.snapshot_id || null,
|
|
46852
48479
|
JSON.stringify(normalizeTags(input.tags)),
|
|
46853
48480
|
JSON.stringify(input.metadata || {}),
|
|
46854
|
-
|
|
46855
|
-
|
|
48481
|
+
timestamp5,
|
|
48482
|
+
timestamp5
|
|
46856
48483
|
]);
|
|
46857
48484
|
return getKnowledgeRecord(id, d);
|
|
46858
48485
|
}
|
|
@@ -47043,9 +48670,9 @@ function normalizeText4(value) {
|
|
|
47043
48670
|
const trimmed = value.trim();
|
|
47044
48671
|
return trimmed.length > 0 ? trimmed : null;
|
|
47045
48672
|
}
|
|
47046
|
-
function assertStatus(
|
|
47047
|
-
if (!VALID_STATUSES.has(
|
|
47048
|
-
throw new Error(`Invalid risk status: ${
|
|
48673
|
+
function assertStatus(status3) {
|
|
48674
|
+
if (!VALID_STATUSES.has(status3))
|
|
48675
|
+
throw new Error(`Invalid risk status: ${status3}`);
|
|
47049
48676
|
}
|
|
47050
48677
|
function assertSeverity(severity) {
|
|
47051
48678
|
if (!VALID_SEVERITIES2.has(severity))
|
|
@@ -47090,16 +48717,16 @@ function createRisk(input, db) {
|
|
|
47090
48717
|
const title = input.title.trim();
|
|
47091
48718
|
if (!title)
|
|
47092
48719
|
throw new Error("Risk title is required");
|
|
47093
|
-
const
|
|
48720
|
+
const status3 = input.status || "open";
|
|
47094
48721
|
const severity = input.severity || "medium";
|
|
47095
48722
|
const probability = input.probability || "medium";
|
|
47096
|
-
assertStatus(
|
|
48723
|
+
assertStatus(status3);
|
|
47097
48724
|
assertSeverity(severity);
|
|
47098
48725
|
assertProbability(probability);
|
|
47099
48726
|
const d = db || getDatabase();
|
|
47100
|
-
const
|
|
48727
|
+
const timestamp5 = now();
|
|
47101
48728
|
const id = uuid();
|
|
47102
|
-
const closedAt =
|
|
48729
|
+
const closedAt = status3 === "resolved" || status3 === "accepted" ? timestamp5 : null;
|
|
47103
48730
|
d.run(`INSERT INTO project_risks (
|
|
47104
48731
|
id, title, description, status, severity, probability, owner, mitigation, due_at,
|
|
47105
48732
|
project_id, plan_id, task_id, tags, metadata, created_at, updated_at, closed_at
|
|
@@ -47107,7 +48734,7 @@ function createRisk(input, db) {
|
|
|
47107
48734
|
id,
|
|
47108
48735
|
title,
|
|
47109
48736
|
normalizeText4(input.description),
|
|
47110
|
-
|
|
48737
|
+
status3,
|
|
47111
48738
|
severity,
|
|
47112
48739
|
probability,
|
|
47113
48740
|
normalizeText4(input.owner),
|
|
@@ -47118,8 +48745,8 @@ function createRisk(input, db) {
|
|
|
47118
48745
|
resolveKnownId2("tasks", input.task_id, d),
|
|
47119
48746
|
JSON.stringify(normalizeTags2(input.tags)),
|
|
47120
48747
|
JSON.stringify(input.metadata || {}),
|
|
47121
|
-
|
|
47122
|
-
|
|
48748
|
+
timestamp5,
|
|
48749
|
+
timestamp5,
|
|
47123
48750
|
closedAt
|
|
47124
48751
|
]);
|
|
47125
48752
|
return getRisk(id, d);
|
|
@@ -47202,8 +48829,8 @@ function updateRisk(id, input, db) {
|
|
|
47202
48829
|
d.run(`UPDATE project_risks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
47203
48830
|
return getRisk(resolved, d);
|
|
47204
48831
|
}
|
|
47205
|
-
function closeRisk(id,
|
|
47206
|
-
return updateRisk(id, { status:
|
|
48832
|
+
function closeRisk(id, status3 = "resolved", db) {
|
|
48833
|
+
return updateRisk(id, { status: status3 }, db);
|
|
47207
48834
|
}
|
|
47208
48835
|
function buildRiskFilters(options, db) {
|
|
47209
48836
|
const conditions = [];
|
|
@@ -47331,7 +48958,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47331
48958
|
};
|
|
47332
48959
|
const totalPenalty = Object.values(penalties).reduce((sum, penalty) => sum + penalty, 0);
|
|
47333
48960
|
const score = Math.max(0, 100 - Math.min(100, totalPenalty));
|
|
47334
|
-
const
|
|
48961
|
+
const status3 = score < 40 ? "critical" : score < 70 ? "at_risk" : score < 85 ? "watch" : "healthy";
|
|
47335
48962
|
const recommendations = [];
|
|
47336
48963
|
if (blocked.length > 0)
|
|
47337
48964
|
recommendations.push("Clear blocking dependencies before starting more work.");
|
|
@@ -47355,7 +48982,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
47355
48982
|
scope,
|
|
47356
48983
|
scope_id: scopeInfo.id,
|
|
47357
48984
|
score,
|
|
47358
|
-
status:
|
|
48985
|
+
status: status3,
|
|
47359
48986
|
components: {
|
|
47360
48987
|
total_tasks: tasks.length,
|
|
47361
48988
|
blocked_tasks: blocked.length,
|
|
@@ -47602,7 +49229,7 @@ function createRetrospective(input, db) {
|
|
|
47602
49229
|
const d = db || getDatabase();
|
|
47603
49230
|
const report = buildReport(input, d);
|
|
47604
49231
|
const scope = scopeFromInput(input, d);
|
|
47605
|
-
const
|
|
49232
|
+
const timestamp5 = now();
|
|
47606
49233
|
if (input.create_followups) {
|
|
47607
49234
|
for (const followUp of report.follow_up_tasks) {
|
|
47608
49235
|
const task3 = createTask({
|
|
@@ -47618,7 +49245,7 @@ function createRetrospective(input, db) {
|
|
|
47618
49245
|
}
|
|
47619
49246
|
const id = uuid();
|
|
47620
49247
|
d.run(`INSERT INTO local_retrospectives (id, title, scope, project_id, plan_id, agent_id, report_json, created_at, updated_at)
|
|
47621
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report),
|
|
49248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, report.title, report.scope, scope.projectId, scope.planId, input.agent_id || null, JSON.stringify(report), timestamp5, timestamp5]);
|
|
47622
49249
|
return getRetrospective(id, d);
|
|
47623
49250
|
}
|
|
47624
49251
|
function getRetrospective(id, db) {
|
|
@@ -47793,7 +49420,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
47793
49420
|
function bootstrapProject(options = {}, db) {
|
|
47794
49421
|
const d = db || getDatabase();
|
|
47795
49422
|
const discovery = discoverProjectWorkspace(options.path);
|
|
47796
|
-
const taskListSlug2 = options.taskListSlug ||
|
|
49423
|
+
const taskListSlug2 = options.taskListSlug || slugify(options.name || discovery.projectName);
|
|
47797
49424
|
if (options.dryRun) {
|
|
47798
49425
|
return {
|
|
47799
49426
|
dryRun: true,
|
|
@@ -47907,7 +49534,7 @@ function createApiKey(input, db) {
|
|
|
47907
49534
|
if (!name)
|
|
47908
49535
|
throw new Error("API key name is required");
|
|
47909
49536
|
const key2 = generatePlaintextKey();
|
|
47910
|
-
const
|
|
49537
|
+
const timestamp5 = now();
|
|
47911
49538
|
const id = uuid();
|
|
47912
49539
|
const prefix = key2.slice(0, 12);
|
|
47913
49540
|
d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
|
|
@@ -47917,7 +49544,7 @@ function createApiKey(input, db) {
|
|
|
47917
49544
|
hashApiKey(key2),
|
|
47918
49545
|
prefix,
|
|
47919
49546
|
JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
|
|
47920
|
-
|
|
49547
|
+
timestamp5,
|
|
47921
49548
|
input.expires_at || null
|
|
47922
49549
|
]);
|
|
47923
49550
|
const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
|
|
@@ -47971,15 +49598,15 @@ function rowToSession(row) {
|
|
|
47971
49598
|
function createSession(input, db) {
|
|
47972
49599
|
const d = db || getDatabase();
|
|
47973
49600
|
const id = uuid();
|
|
47974
|
-
const
|
|
49601
|
+
const timestamp5 = now();
|
|
47975
49602
|
d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
|
|
47976
49603
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
47977
49604
|
id,
|
|
47978
49605
|
input.agent_id || null,
|
|
47979
49606
|
input.project_id || null,
|
|
47980
49607
|
input.working_dir || null,
|
|
47981
|
-
|
|
47982
|
-
|
|
49608
|
+
timestamp5,
|
|
49609
|
+
timestamp5,
|
|
47983
49610
|
JSON.stringify(input.metadata || {})
|
|
47984
49611
|
]);
|
|
47985
49612
|
return getSession(id, d);
|
|
@@ -48152,7 +49779,7 @@ function rowToArtifact2(row) {
|
|
|
48152
49779
|
function addArtifact(input, db, dbPath) {
|
|
48153
49780
|
const d = db || getDatabase();
|
|
48154
49781
|
const id = uuid();
|
|
48155
|
-
const
|
|
49782
|
+
const timestamp5 = now();
|
|
48156
49783
|
const storageMode = input.storage_mode ?? "copy";
|
|
48157
49784
|
const stored = storeArtifactFile({
|
|
48158
49785
|
artifactId: id,
|
|
@@ -48177,8 +49804,8 @@ function addArtifact(input, db, dbPath) {
|
|
|
48177
49804
|
stored.sizeBytes,
|
|
48178
49805
|
input.redaction_status ?? "none",
|
|
48179
49806
|
JSON.stringify(input.metadata ?? {}),
|
|
48180
|
-
|
|
48181
|
-
|
|
49807
|
+
timestamp5,
|
|
49808
|
+
timestamp5
|
|
48182
49809
|
]);
|
|
48183
49810
|
return getArtifact(id, d);
|
|
48184
49811
|
}
|
|
@@ -48211,14 +49838,14 @@ function listArtifacts(filter = {}, db) {
|
|
|
48211
49838
|
}
|
|
48212
49839
|
function updateArtifactRedaction(id, redactionStatus, db) {
|
|
48213
49840
|
const d = db || getDatabase();
|
|
48214
|
-
const
|
|
48215
|
-
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus,
|
|
49841
|
+
const timestamp5 = now();
|
|
49842
|
+
d.run("UPDATE artifacts SET redaction_status = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [redactionStatus, timestamp5, id]);
|
|
48216
49843
|
return getArtifact(id, d);
|
|
48217
49844
|
}
|
|
48218
49845
|
function softDeleteArtifact(id, db) {
|
|
48219
49846
|
const d = db || getDatabase();
|
|
48220
|
-
const
|
|
48221
|
-
const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [
|
|
49847
|
+
const timestamp5 = now();
|
|
49848
|
+
const result = d.run("UPDATE artifacts SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL", [timestamp5, timestamp5, id]);
|
|
48222
49849
|
return result.changes > 0;
|
|
48223
49850
|
}
|
|
48224
49851
|
function purgeArtifact(id, db, dbPath) {
|
|
@@ -48356,7 +49983,7 @@ init_redaction();
|
|
|
48356
49983
|
function createHandoff(input, db) {
|
|
48357
49984
|
const d = db || getDatabase();
|
|
48358
49985
|
const id = uuid();
|
|
48359
|
-
const
|
|
49986
|
+
const timestamp5 = now();
|
|
48360
49987
|
d.run(`INSERT INTO handoffs (id, agent_id, project_id, session_id, summary, completed, in_progress, blockers, next_steps, task_ids, relevant_files, run_ids, created_at)
|
|
48361
49988
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
48362
49989
|
id,
|
|
@@ -48371,7 +49998,7 @@ function createHandoff(input, db) {
|
|
|
48371
49998
|
toJson(input.task_ids),
|
|
48372
49999
|
toJson(input.relevant_files),
|
|
48373
50000
|
toJson(input.run_ids),
|
|
48374
|
-
|
|
50001
|
+
timestamp5
|
|
48375
50002
|
]);
|
|
48376
50003
|
return {
|
|
48377
50004
|
id,
|
|
@@ -48386,7 +50013,7 @@ function createHandoff(input, db) {
|
|
|
48386
50013
|
task_ids: input.task_ids || null,
|
|
48387
50014
|
relevant_files: input.relevant_files || null,
|
|
48388
50015
|
run_ids: input.run_ids || null,
|
|
48389
|
-
created_at:
|
|
50016
|
+
created_at: timestamp5,
|
|
48390
50017
|
acknowledged_by: []
|
|
48391
50018
|
};
|
|
48392
50019
|
}
|
|
@@ -48898,7 +50525,7 @@ function upsertVerificationProvider(input) {
|
|
|
48898
50525
|
const name = normalizeName(input.name);
|
|
48899
50526
|
const config = loadConfig();
|
|
48900
50527
|
const existing = config.verification_providers?.[name];
|
|
48901
|
-
const
|
|
50528
|
+
const timestamp5 = new Date().toISOString();
|
|
48902
50529
|
const provider = {
|
|
48903
50530
|
...existing,
|
|
48904
50531
|
name,
|
|
@@ -48909,8 +50536,8 @@ function upsertVerificationProvider(input) {
|
|
|
48909
50536
|
capabilities: input.capabilities ?? existing?.capabilities,
|
|
48910
50537
|
retry: input.retry ? retryConfig(input.retry) : existing?.retry,
|
|
48911
50538
|
timeout_ms: timeoutMs(input.timeout_ms ?? existing?.timeout_ms),
|
|
48912
|
-
created_at: existing?.created_at ||
|
|
48913
|
-
updated_at:
|
|
50539
|
+
created_at: existing?.created_at || timestamp5,
|
|
50540
|
+
updated_at: timestamp5
|
|
48914
50541
|
};
|
|
48915
50542
|
saveConfig({
|
|
48916
50543
|
...config,
|
|
@@ -49471,7 +51098,7 @@ function upsertPolicyPack(input) {
|
|
|
49471
51098
|
const existing = config.policy_packs?.[input.name];
|
|
49472
51099
|
const root = normalizePath4(input.root || existing?.root || process.cwd());
|
|
49473
51100
|
const base = existing || defaultPolicyPack(input.name, root);
|
|
49474
|
-
const
|
|
51101
|
+
const timestamp5 = new Date().toISOString();
|
|
49475
51102
|
const pack = {
|
|
49476
51103
|
...base,
|
|
49477
51104
|
name: input.name,
|
|
@@ -49489,8 +51116,8 @@ function upsertPolicyPack(input) {
|
|
|
49489
51116
|
require_artifact: input.require_artifact ?? base.require_artifact,
|
|
49490
51117
|
evidence_min_count: input.evidence_min_count ?? base.evidence_min_count,
|
|
49491
51118
|
branch_pattern: input.branch_pattern ?? base.branch_pattern,
|
|
49492
|
-
created_at: existing?.created_at ||
|
|
49493
|
-
updated_at:
|
|
51119
|
+
created_at: existing?.created_at || timestamp5,
|
|
51120
|
+
updated_at: timestamp5
|
|
49494
51121
|
};
|
|
49495
51122
|
saveConfig({
|
|
49496
51123
|
...config,
|
|
@@ -49832,7 +51459,7 @@ function summarizeOutput(stdout, stderr) {
|
|
|
49832
51459
|
function upsertAgentRunAdapter(input) {
|
|
49833
51460
|
const config = loadConfig();
|
|
49834
51461
|
const existing = config.agent_run_adapters?.[input.name];
|
|
49835
|
-
const
|
|
51462
|
+
const timestamp5 = new Date().toISOString();
|
|
49836
51463
|
const adapter = {
|
|
49837
51464
|
...existing,
|
|
49838
51465
|
name: input.name,
|
|
@@ -49840,8 +51467,8 @@ function upsertAgentRunAdapter(input) {
|
|
|
49840
51467
|
sandbox: input.sandbox ?? existing?.sandbox,
|
|
49841
51468
|
cwd: input.cwd ?? existing?.cwd,
|
|
49842
51469
|
env: input.env ?? existing?.env,
|
|
49843
|
-
created_at: existing?.created_at ||
|
|
49844
|
-
updated_at:
|
|
51470
|
+
created_at: existing?.created_at || timestamp5,
|
|
51471
|
+
updated_at: timestamp5
|
|
49845
51472
|
};
|
|
49846
51473
|
saveConfig({
|
|
49847
51474
|
...config,
|
|
@@ -49965,7 +51592,7 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
49965
51592
|
proc.exited
|
|
49966
51593
|
]);
|
|
49967
51594
|
const outputSummary = summarizeOutput(stdout, stderr);
|
|
49968
|
-
const
|
|
51595
|
+
const status3 = exitCode === 0 ? "completed" : "failed";
|
|
49969
51596
|
addTaskRunCommand({
|
|
49970
51597
|
run_id: next.run.id,
|
|
49971
51598
|
command,
|
|
@@ -49974,15 +51601,15 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
49974
51601
|
output_summary: outputSummary ?? undefined,
|
|
49975
51602
|
agent_id: next.run.agent_id ?? undefined
|
|
49976
51603
|
}, d);
|
|
49977
|
-
const completed = { ...running, state:
|
|
51604
|
+
const completed = { ...running, state: status3, completed_at: new Date().toISOString(), last_error: exitCode === 0 ? undefined : outputSummary || `exit ${exitCode}` };
|
|
49978
51605
|
updateDispatcherMetadata(next.run.id, completed, d);
|
|
49979
51606
|
finishTaskRun({
|
|
49980
51607
|
run_id: next.run.id,
|
|
49981
|
-
status:
|
|
49982
|
-
summary: outputSummary || `agent run ${
|
|
51608
|
+
status: status3,
|
|
51609
|
+
summary: outputSummary || `agent run ${status3}`,
|
|
49983
51610
|
agent_id: next.run.agent_id ?? undefined
|
|
49984
51611
|
}, d);
|
|
49985
|
-
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status:
|
|
51612
|
+
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status: status3, exit_code: exitCode, output_summary: outputSummary };
|
|
49986
51613
|
}
|
|
49987
51614
|
function listAgentRuns(filter = {}, db) {
|
|
49988
51615
|
let runs = listAgentRunQueue(db).map(toAgentRun);
|
|
@@ -50951,10 +52578,10 @@ function extractEmbeddedBridge(markdown) {
|
|
|
50951
52578
|
const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
|
|
50952
52579
|
if (!match)
|
|
50953
52580
|
return null;
|
|
50954
|
-
const
|
|
52581
|
+
const json4 = match[1].split(`
|
|
50955
52582
|
`).map((line) => line.replace(/^ /, "")).join(`
|
|
50956
52583
|
`);
|
|
50957
|
-
return JSON.parse(
|
|
52584
|
+
return JSON.parse(json4);
|
|
50958
52585
|
}
|
|
50959
52586
|
function frontmatterValue(markdown, key2) {
|
|
50960
52587
|
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -50975,14 +52602,14 @@ function parseTaskLine(line, currentPlan) {
|
|
|
50975
52602
|
const match = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
50976
52603
|
if (!match)
|
|
50977
52604
|
return null;
|
|
50978
|
-
const
|
|
52605
|
+
const status3 = match[1].toLowerCase() === "x" ? "completed" : "pending";
|
|
50979
52606
|
const rawTitle = stripInlineMetadata(match[2]).replace(/^\*\*(.*?)\*\*$/, "$1");
|
|
50980
52607
|
const tags = [...rawTitle.matchAll(/(^|\s)#([A-Za-z0-9_-]+)/g)].map((tag) => tag[2]);
|
|
50981
52608
|
const assignee = rawTitle.match(/(^|\s)@([A-Za-z0-9_-]+)/)?.[2];
|
|
50982
52609
|
const title = rawTitle.replace(/(^|\s)#[A-Za-z0-9_-]+/g, "").replace(/(^|\s)@[A-Za-z0-9_-]+/g, "").replace(/\s+/g, " ").trim();
|
|
50983
52610
|
return {
|
|
50984
52611
|
title,
|
|
50985
|
-
status:
|
|
52612
|
+
status: status3,
|
|
50986
52613
|
priority: "medium",
|
|
50987
52614
|
tags,
|
|
50988
52615
|
assigned_to: assignee,
|
|
@@ -52587,7 +54214,7 @@ function normalizeJiraIssue(raw) {
|
|
|
52587
54214
|
if (!summary)
|
|
52588
54215
|
return null;
|
|
52589
54216
|
const statusRec = asRecord3(fields.status);
|
|
52590
|
-
const
|
|
54217
|
+
const status3 = typeof statusRec?.name === "string" ? statusRec.name : undefined;
|
|
52591
54218
|
const labels = labelNames2(fields.labels);
|
|
52592
54219
|
const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord3(fields.description)?.content === "string" ? String(asRecord3(fields.description)?.content).slice(0, 8000) : undefined;
|
|
52593
54220
|
return {
|
|
@@ -52596,13 +54223,13 @@ function normalizeJiraIssue(raw) {
|
|
|
52596
54223
|
external_id: key2,
|
|
52597
54224
|
title: `[${key2}] ${summary}`,
|
|
52598
54225
|
description,
|
|
52599
|
-
status:
|
|
54226
|
+
status: status3,
|
|
52600
54227
|
priority: priorityFromJira(fields.priority),
|
|
52601
54228
|
tags: [...labels.slice(0, 10), "jira-import"],
|
|
52602
54229
|
url: typeof raw.self === "string" ? raw.self : undefined,
|
|
52603
54230
|
raw_metadata: {
|
|
52604
54231
|
jira_key: key2,
|
|
52605
|
-
jira_status:
|
|
54232
|
+
jira_status: status3,
|
|
52606
54233
|
jira_priority: asRecord3(fields.priority)?.name,
|
|
52607
54234
|
jira_id: raw.id
|
|
52608
54235
|
}
|
|
@@ -55211,21 +56838,22 @@ function upsertProject(raw, d) {
|
|
|
55211
56838
|
const existing = getProject(id, d);
|
|
55212
56839
|
const ts = now();
|
|
55213
56840
|
if (!existing) {
|
|
55214
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
|
|
55215
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
56841
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, parent_id, created_at, updated_at)
|
|
56842
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
55216
56843
|
id,
|
|
55217
56844
|
raw.name,
|
|
55218
56845
|
raw.path,
|
|
55219
56846
|
raw.description ?? null,
|
|
55220
|
-
raw.task_list_id ??
|
|
56847
|
+
raw.task_list_id ?? String(raw.name).toLowerCase().replace(/\s+/g, "-"),
|
|
55221
56848
|
raw.task_prefix ?? "TSK",
|
|
55222
56849
|
raw.task_counter ?? 0,
|
|
56850
|
+
raw.parent_id ?? null,
|
|
55223
56851
|
raw.created_at ?? ts,
|
|
55224
56852
|
raw.updated_at ?? ts
|
|
55225
56853
|
].map(sqlValue));
|
|
55226
56854
|
return "created";
|
|
55227
56855
|
}
|
|
55228
|
-
d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, updated_at = ?
|
|
56856
|
+
d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, parent_id = ?, updated_at = ?
|
|
55229
56857
|
WHERE id = ?`, [
|
|
55230
56858
|
raw.name ?? existing.name,
|
|
55231
56859
|
raw.path ?? existing.path,
|
|
@@ -55233,6 +56861,7 @@ function upsertProject(raw, d) {
|
|
|
55233
56861
|
raw.task_list_id ?? existing.task_list_id,
|
|
55234
56862
|
raw.task_prefix ?? existing.task_prefix,
|
|
55235
56863
|
raw.task_counter ?? existing.task_counter,
|
|
56864
|
+
raw.parent_id ?? existing.parent_id,
|
|
55236
56865
|
raw.updated_at ?? ts,
|
|
55237
56866
|
id
|
|
55238
56867
|
].map(sqlValue));
|
|
@@ -55313,7 +56942,7 @@ function importBundle(bundle, options = {}, db) {
|
|
|
55313
56942
|
};
|
|
55314
56943
|
if (options.dry_run)
|
|
55315
56944
|
return result;
|
|
55316
|
-
for (const raw of bundle.projects) {
|
|
56945
|
+
for (const raw of orderProjectsParentFirst(bundle.projects)) {
|
|
55317
56946
|
try {
|
|
55318
56947
|
const id = raw.id;
|
|
55319
56948
|
const local = getProject(id, d);
|
|
@@ -55345,6 +56974,10 @@ function importBundle(bundle, options = {}, db) {
|
|
|
55345
56974
|
}
|
|
55346
56975
|
for (const dep of bundle.dependencies) {
|
|
55347
56976
|
try {
|
|
56977
|
+
if (!getTask(dep.task_id, d) || !getTask(dep.depends_on, d)) {
|
|
56978
|
+
bump(result.skipped, "dependencies");
|
|
56979
|
+
continue;
|
|
56980
|
+
}
|
|
55348
56981
|
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [dep.task_id, dep.depends_on]);
|
|
55349
56982
|
bump(result.created, "dependencies");
|
|
55350
56983
|
} catch (e) {
|
|
@@ -56150,11 +57783,11 @@ function createTuiDashboardSnapshot(options = {}, db) {
|
|
|
56150
57783
|
const d = db || getDatabase();
|
|
56151
57784
|
const limit = limited(options.limit);
|
|
56152
57785
|
const filter = projectFilter(options.project_id);
|
|
56153
|
-
const counts = TASK_STATUSES2.reduce((acc,
|
|
56154
|
-
acc[
|
|
57786
|
+
const counts = TASK_STATUSES2.reduce((acc, status3) => {
|
|
57787
|
+
acc[status3] = countTasks({ ...filter, status: status3 }, d);
|
|
56155
57788
|
return acc;
|
|
56156
57789
|
}, { total: 0 });
|
|
56157
|
-
counts.total = TASK_STATUSES2.reduce((sum,
|
|
57790
|
+
counts.total = TASK_STATUSES2.reduce((sum, status3) => sum + counts[status3], 0);
|
|
56158
57791
|
const projectRows = listProjects(d).filter((project) => !options.project_id || project.id === options.project_id).slice(0, limit).map((project) => ({
|
|
56159
57792
|
id: project.id,
|
|
56160
57793
|
name: project.name,
|
|
@@ -57355,12 +58988,12 @@ function captureGit(root, warnings) {
|
|
|
57355
58988
|
}
|
|
57356
58989
|
const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
|
|
57357
58990
|
const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
|
|
57358
|
-
const
|
|
58991
|
+
const status3 = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
|
|
57359
58992
|
if (commit.exitCode !== 0)
|
|
57360
58993
|
warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
|
|
57361
|
-
if (
|
|
57362
|
-
warnings.push(`git status unavailable: ${
|
|
57363
|
-
const lines =
|
|
58994
|
+
if (status3.exitCode !== 0)
|
|
58995
|
+
warnings.push(`git status unavailable: ${status3.stderr || status3.stdout || "unknown error"}`);
|
|
58996
|
+
const lines = status3.stdout ? status3.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
57364
58997
|
return {
|
|
57365
58998
|
present: true,
|
|
57366
58999
|
branch: branch.stdout || null,
|
|
@@ -57402,8 +59035,8 @@ function defaultSnapshotDir() {
|
|
|
57402
59035
|
return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
|
|
57403
59036
|
}
|
|
57404
59037
|
function snapshotWithId(snapshot) {
|
|
57405
|
-
const
|
|
57406
|
-
return { id: `env_${
|
|
59038
|
+
const digest3 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
|
|
59039
|
+
return { id: `env_${digest3}`, ...snapshot };
|
|
57407
59040
|
}
|
|
57408
59041
|
function captureEnvironmentSnapshot(input = {}) {
|
|
57409
59042
|
const root = resolve17(input.root || process.cwd());
|
|
@@ -57603,7 +59236,7 @@ function createDecisionRecord(input, db) {
|
|
|
57603
59236
|
const ts = now();
|
|
57604
59237
|
const sequenceNum = nextSequenceNum(input.project_id, d);
|
|
57605
59238
|
const shortRef = buildShortRef(input.project_id, sequenceNum, d);
|
|
57606
|
-
const
|
|
59239
|
+
const status3 = input.status ?? "proposed";
|
|
57607
59240
|
d.run(`INSERT INTO decision_records (
|
|
57608
59241
|
id, project_id, task_id, plan_id, agent_id, sequence_num, short_ref,
|
|
57609
59242
|
title, status, context, decision, consequences, alternatives, tags,
|
|
@@ -57617,7 +59250,7 @@ function createDecisionRecord(input, db) {
|
|
|
57617
59250
|
sequenceNum,
|
|
57618
59251
|
shortRef,
|
|
57619
59252
|
input.title,
|
|
57620
|
-
|
|
59253
|
+
status3,
|
|
57621
59254
|
input.context ?? null,
|
|
57622
59255
|
input.decision,
|
|
57623
59256
|
input.consequences ?? null,
|
|
@@ -57695,15 +59328,15 @@ function updateDecisionRecord(id, input, db) {
|
|
|
57695
59328
|
]);
|
|
57696
59329
|
return getDecisionRecord(id, d);
|
|
57697
59330
|
}
|
|
57698
|
-
function setDecisionStatus(id,
|
|
57699
|
-
if (!DECISION_STATUSES.includes(
|
|
57700
|
-
throw new Error(`Invalid decision status: ${
|
|
59331
|
+
function setDecisionStatus(id, status3, db) {
|
|
59332
|
+
if (!DECISION_STATUSES.includes(status3)) {
|
|
59333
|
+
throw new Error(`Invalid decision status: ${status3}`);
|
|
57701
59334
|
}
|
|
57702
59335
|
const d = db || getDatabase();
|
|
57703
59336
|
const existing = getDecisionRecord(id, d);
|
|
57704
59337
|
if (!existing)
|
|
57705
59338
|
throw new Error(`Decision record not found: ${id}`);
|
|
57706
|
-
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [
|
|
59339
|
+
d.run(`UPDATE decision_records SET status = ?, updated_at = ? WHERE id = ?`, [status3, now(), id]);
|
|
57707
59340
|
return getDecisionRecord(id, d);
|
|
57708
59341
|
}
|
|
57709
59342
|
function supersedeDecisionRecord(id, input, db) {
|
|
@@ -57984,17 +59617,17 @@ function buildReportExportData(input, db) {
|
|
|
57984
59617
|
throw new Error("Project not found");
|
|
57985
59618
|
projectId = project.id;
|
|
57986
59619
|
title = `Project: ${project.name}`;
|
|
57987
|
-
const
|
|
59620
|
+
const status3 = getStatus({ project_id: project.id }, undefined, undefined, d);
|
|
57988
59621
|
sections.push(section("overview", "Overview", [
|
|
57989
59622
|
`Name: ${project.name}`,
|
|
57990
59623
|
`Path: ${project.path}`,
|
|
57991
59624
|
`Prefix: ${project.task_prefix ?? "\u2014"}`
|
|
57992
59625
|
]), section("status", "Status", [
|
|
57993
|
-
`Pending: ${
|
|
57994
|
-
`In progress: ${
|
|
57995
|
-
`Completed: ${
|
|
57996
|
-
`Total: ${
|
|
57997
|
-
`Stale: ${
|
|
59626
|
+
`Pending: ${status3.pending}`,
|
|
59627
|
+
`In progress: ${status3.in_progress}`,
|
|
59628
|
+
`Completed: ${status3.completed}`,
|
|
59629
|
+
`Total: ${status3.total}`,
|
|
59630
|
+
`Stale: ${status3.stale_count}`
|
|
57998
59631
|
]));
|
|
57999
59632
|
const tasks = listTasks({ project_id: project.id, limit: 50 }, d);
|
|
58000
59633
|
sections.push(section("tasks", "Recent tasks", tasks.length ? tasks.map(taskLine2) : ["No tasks"]));
|
|
@@ -58815,8 +60448,8 @@ function getGitStatus(root, branch, includeGitStatus) {
|
|
|
58815
60448
|
return { has_git: false, current_branch: null, branch_exists: false, dirty_files: [] };
|
|
58816
60449
|
const currentBranch = runGit3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
58817
60450
|
const branchExists = runGit3(root, ["show-ref", "--verify", `refs/heads/${branch}`]) !== null;
|
|
58818
|
-
const
|
|
58819
|
-
const dirtyFiles =
|
|
60451
|
+
const status3 = runGit3(root, ["status", "--short"]) || "";
|
|
60452
|
+
const dirtyFiles = status3.split(`
|
|
58820
60453
|
`).map((line) => line.trim()).filter(Boolean).map((line) => normalizePath6(line.replace(/^.. /, "").replace(/^.* -> /, ""))).filter((path) => Boolean(path));
|
|
58821
60454
|
return { has_git: true, current_branch: currentBranch || null, branch_exists: branchExists, dirty_files: dirtyFiles };
|
|
58822
60455
|
}
|
|
@@ -59214,8 +60847,8 @@ var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
|
|
|
59214
60847
|
var DEMO_DEFAULT_AGENT = "demoagent";
|
|
59215
60848
|
var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
|
|
59216
60849
|
var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
|
|
59217
|
-
function pushStep(steps, action, detail,
|
|
59218
|
-
steps.push({ step: steps.length + 1, action, detail, status:
|
|
60850
|
+
function pushStep(steps, action, detail, status3 = "ok") {
|
|
60851
|
+
steps.push({ step: steps.length + 1, action, detail, status: status3 });
|
|
59219
60852
|
}
|
|
59220
60853
|
function setupEphemeralDemoDb(options = {}) {
|
|
59221
60854
|
const prevPath = process.env["TODOS_DB_PATH"];
|
|
@@ -59342,7 +60975,7 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59342
60975
|
pushStep(steps, "complete_run_record", `Run record status=${finishedRecord.status}`);
|
|
59343
60976
|
completeTask(third.id, agent.id, d, { notes: "Demo: finished via agent run + run record" });
|
|
59344
60977
|
pushStep(steps, "complete_task", `Completed ${third.short_id ?? third.id.slice(0, 8)}`);
|
|
59345
|
-
const
|
|
60978
|
+
const status3 = getStatus({ project_id: project.id }, agent.id, undefined, d);
|
|
59346
60979
|
const tasks = listTasks({ project_id: project.id }, d).map((t) => ({
|
|
59347
60980
|
id: t.id,
|
|
59348
60981
|
short_id: t.short_id,
|
|
@@ -59374,14 +61007,14 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
59374
61007
|
objective: finishedRecord.objective
|
|
59375
61008
|
},
|
|
59376
61009
|
status_summary: {
|
|
59377
|
-
pending:
|
|
59378
|
-
in_progress:
|
|
59379
|
-
completed:
|
|
59380
|
-
total:
|
|
61010
|
+
pending: status3.pending,
|
|
61011
|
+
in_progress: status3.in_progress,
|
|
61012
|
+
completed: status3.completed,
|
|
61013
|
+
total: status3.total
|
|
59381
61014
|
},
|
|
59382
61015
|
steps,
|
|
59383
61016
|
completed_at: exportedAt2,
|
|
59384
|
-
message: `Demo complete: ${
|
|
61017
|
+
message: `Demo complete: ${status3.completed}/${status3.total} tasks done, 1 agent run, 1 run record`
|
|
59385
61018
|
};
|
|
59386
61019
|
} finally {
|
|
59387
61020
|
if (dbHandle)
|
|
@@ -60355,8 +61988,8 @@ function rowToOrg(row) {
|
|
|
60355
61988
|
function createOrg(input, db) {
|
|
60356
61989
|
const d = db || getDatabase();
|
|
60357
61990
|
const id = uuid();
|
|
60358
|
-
const
|
|
60359
|
-
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}),
|
|
61991
|
+
const timestamp5 = now();
|
|
61992
|
+
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp5, timestamp5]);
|
|
60360
61993
|
return getOrg(id, d);
|
|
60361
61994
|
}
|
|
60362
61995
|
function getOrg(id, db) {
|
|
@@ -60604,10 +62237,10 @@ function getCriticalPath2(opts, db) {
|
|
|
60604
62237
|
function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
|
|
60605
62238
|
const d = db || getDatabase();
|
|
60606
62239
|
const id = uuid();
|
|
60607
|
-
const
|
|
62240
|
+
const timestamp5 = now();
|
|
60608
62241
|
d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
|
|
60609
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}),
|
|
60610
|
-
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at:
|
|
62242
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp5]);
|
|
62243
|
+
return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp5 };
|
|
60611
62244
|
}
|
|
60612
62245
|
function removeKgEdges(sourceId, targetId, relationType, db) {
|
|
60613
62246
|
const d = db || getDatabase();
|
|
@@ -61507,7 +63140,7 @@ function runSavedSearch(filters = {}, scope = "tasks", db) {
|
|
|
61507
63140
|
function saveSearchView(input, db) {
|
|
61508
63141
|
const d = db || getDatabase();
|
|
61509
63142
|
const name = normalizeName3(input.name);
|
|
61510
|
-
const
|
|
63143
|
+
const timestamp5 = now();
|
|
61511
63144
|
const existing = getSearchView(name, d);
|
|
61512
63145
|
if (existing) {
|
|
61513
63146
|
d.run(`UPDATE saved_search_views
|
|
@@ -61516,7 +63149,7 @@ function saveSearchView(input, db) {
|
|
|
61516
63149
|
input.description ?? existing.description,
|
|
61517
63150
|
normalizeScope(input.scope ?? existing.scope),
|
|
61518
63151
|
JSON.stringify(input.filters ?? existing.filters),
|
|
61519
|
-
|
|
63152
|
+
timestamp5,
|
|
61520
63153
|
existing.id
|
|
61521
63154
|
]);
|
|
61522
63155
|
return getSearchView(existing.id, d);
|
|
@@ -61529,8 +63162,8 @@ function saveSearchView(input, db) {
|
|
|
61529
63162
|
input.description ?? null,
|
|
61530
63163
|
normalizeScope(input.scope),
|
|
61531
63164
|
JSON.stringify(input.filters ?? {}),
|
|
61532
|
-
|
|
61533
|
-
|
|
63165
|
+
timestamp5,
|
|
63166
|
+
timestamp5
|
|
61534
63167
|
]);
|
|
61535
63168
|
return getSearchView(id, d);
|
|
61536
63169
|
}
|
|
@@ -61572,14 +63205,14 @@ function readClaudeTask(dir, filename) {
|
|
|
61572
63205
|
function writeClaudeTask(dir, task3) {
|
|
61573
63206
|
writeJsonFile(join23(dir, `${task3.id}.json`), task3);
|
|
61574
63207
|
}
|
|
61575
|
-
function toClaudeStatus(
|
|
61576
|
-
if (
|
|
61577
|
-
return
|
|
63208
|
+
function toClaudeStatus(status3) {
|
|
63209
|
+
if (status3 === "pending" || status3 === "in_progress" || status3 === "completed") {
|
|
63210
|
+
return status3;
|
|
61578
63211
|
}
|
|
61579
63212
|
return "completed";
|
|
61580
63213
|
}
|
|
61581
|
-
function toSqliteStatus(
|
|
61582
|
-
return
|
|
63214
|
+
function toSqliteStatus(status3) {
|
|
63215
|
+
return status3;
|
|
61583
63216
|
}
|
|
61584
63217
|
function readPrefixCounter(dir) {
|
|
61585
63218
|
const path = join23(dir, ".prefix-counter");
|
|
@@ -62540,7 +64173,7 @@ init_database();
|
|
|
62540
64173
|
function logTrace(input, db) {
|
|
62541
64174
|
const d = db || getDatabase();
|
|
62542
64175
|
const id = uuid();
|
|
62543
|
-
const
|
|
64176
|
+
const timestamp5 = now();
|
|
62544
64177
|
d.run(`INSERT INTO task_traces (id, task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd, created_at)
|
|
62545
64178
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
62546
64179
|
id,
|
|
@@ -62553,7 +64186,7 @@ function logTrace(input, db) {
|
|
|
62553
64186
|
input.duration_ms ?? null,
|
|
62554
64187
|
input.tokens ?? null,
|
|
62555
64188
|
input.cost_usd ?? null,
|
|
62556
|
-
|
|
64189
|
+
timestamp5
|
|
62557
64190
|
]);
|
|
62558
64191
|
return {
|
|
62559
64192
|
id,
|
|
@@ -62566,7 +64199,7 @@ function logTrace(input, db) {
|
|
|
62566
64199
|
duration_ms: input.duration_ms ?? null,
|
|
62567
64200
|
tokens: input.tokens ?? null,
|
|
62568
64201
|
cost_usd: input.cost_usd ?? null,
|
|
62569
|
-
created_at:
|
|
64202
|
+
created_at: timestamp5
|
|
62570
64203
|
};
|
|
62571
64204
|
}
|
|
62572
64205
|
function getTaskTraces(taskId, db) {
|
|
@@ -62590,7 +64223,7 @@ init_database();
|
|
|
62590
64223
|
init_tasks();
|
|
62591
64224
|
function setBudget(agentId, opts, db) {
|
|
62592
64225
|
const d = db || getDatabase();
|
|
62593
|
-
const
|
|
64226
|
+
const timestamp5 = now();
|
|
62594
64227
|
d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
|
|
62595
64228
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
62596
64229
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
@@ -62604,13 +64237,13 @@ function setBudget(agentId, opts, db) {
|
|
|
62604
64237
|
opts.max_cost_usd ?? null,
|
|
62605
64238
|
opts.max_task_minutes ?? null,
|
|
62606
64239
|
opts.period_hours ?? 24,
|
|
62607
|
-
|
|
62608
|
-
|
|
64240
|
+
timestamp5,
|
|
64241
|
+
timestamp5,
|
|
62609
64242
|
opts.max_concurrent ?? null,
|
|
62610
64243
|
opts.max_cost_usd ?? null,
|
|
62611
64244
|
opts.max_task_minutes ?? null,
|
|
62612
64245
|
opts.period_hours ?? null,
|
|
62613
|
-
|
|
64246
|
+
timestamp5
|
|
62614
64247
|
]);
|
|
62615
64248
|
return getBudget(agentId, d);
|
|
62616
64249
|
}
|
|
@@ -62913,11 +64546,11 @@ function resolveWorkflowState(input, projectPath) {
|
|
|
62913
64546
|
return { input, state: byCanonical, matched_by: "canonical_status" };
|
|
62914
64547
|
throw new Error(`Unknown workflow state: ${input}`);
|
|
62915
64548
|
}
|
|
62916
|
-
function stateForCanonicalStatus(
|
|
62917
|
-
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status ===
|
|
64549
|
+
function stateForCanonicalStatus(status3, projectPath) {
|
|
64550
|
+
const configured = (getWorkflowConfig(projectPath)?.states || []).map(normalizeState).find((state) => Boolean(state && state.canonical_status === status3));
|
|
62918
64551
|
if (configured)
|
|
62919
64552
|
return configured;
|
|
62920
|
-
return listWorkflowStates(projectPath).find((state) => state.canonical_status ===
|
|
64553
|
+
return listWorkflowStates(projectPath).find((state) => state.canonical_status === status3) || resolveWorkflowState(status3, projectPath).state;
|
|
62921
64554
|
}
|
|
62922
64555
|
function getTaskWorkflowState(taskId, db, projectPath) {
|
|
62923
64556
|
const d = db || getDatabase();
|
|
@@ -63081,9 +64714,9 @@ function unpackFixture(input) {
|
|
|
63081
64714
|
const contextPack = input["context_pack"];
|
|
63082
64715
|
return isObject(contextPack) ? contextPack : input;
|
|
63083
64716
|
}
|
|
63084
|
-
function approvalStatus(
|
|
63085
|
-
if (
|
|
63086
|
-
return
|
|
64717
|
+
function approvalStatus(status3) {
|
|
64718
|
+
if (status3 === "approved" || status3 === "rejected" || status3 === "expired")
|
|
64719
|
+
return status3;
|
|
63087
64720
|
return "pending";
|
|
63088
64721
|
}
|
|
63089
64722
|
function approvalsFromFixture(fixture, runs) {
|
|
@@ -63180,17 +64813,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63180
64813
|
if (["completed", "failed", "cancelled"].includes(runStatus))
|
|
63181
64814
|
currentStatus = runStatus;
|
|
63182
64815
|
for (const command of asArray2(run["commands"])) {
|
|
63183
|
-
const
|
|
64816
|
+
const status3 = normalizedStatus(command["status"]);
|
|
63184
64817
|
const commandText = text(command["command"]) || "command";
|
|
63185
64818
|
push({
|
|
63186
64819
|
type: "command",
|
|
63187
64820
|
source: "run.commands",
|
|
63188
64821
|
label: commandText,
|
|
63189
|
-
status:
|
|
64822
|
+
status: status3,
|
|
63190
64823
|
command: commandText,
|
|
63191
64824
|
message: text(command["output_summary"])
|
|
63192
64825
|
});
|
|
63193
|
-
if (
|
|
64826
|
+
if (status3 === "failed")
|
|
63194
64827
|
failures.push({ source: "run.commands", message: text(command["output_summary"]) || "command failed", command: commandText });
|
|
63195
64828
|
}
|
|
63196
64829
|
for (const file of asArray2(run["files"])) {
|
|
@@ -63207,17 +64840,17 @@ function simulateAgentReplay(input, options = {}) {
|
|
|
63207
64840
|
}
|
|
63208
64841
|
}
|
|
63209
64842
|
for (const verification2 of verifications) {
|
|
63210
|
-
const
|
|
64843
|
+
const status3 = normalizedStatus(verification2["status"]);
|
|
63211
64844
|
const commandText = text(verification2["command"]) || "verification";
|
|
63212
64845
|
push({
|
|
63213
64846
|
type: "command",
|
|
63214
64847
|
source: "traceability.verifications",
|
|
63215
64848
|
label: commandText,
|
|
63216
|
-
status:
|
|
64849
|
+
status: status3,
|
|
63217
64850
|
command: commandText,
|
|
63218
64851
|
message: text(verification2["output_summary"])
|
|
63219
64852
|
});
|
|
63220
|
-
if (
|
|
64853
|
+
if (status3 === "failed")
|
|
63221
64854
|
failures.push({ source: "traceability.verifications", message: text(verification2["output_summary"]) || "verification failed", command: commandText });
|
|
63222
64855
|
}
|
|
63223
64856
|
const approvals = approvalsFromFixture(fixture, runs);
|
|
@@ -63753,7 +65386,7 @@ function installLocalExtension(input) {
|
|
|
63753
65386
|
const validation = inspected.validation;
|
|
63754
65387
|
if (!validation.ok)
|
|
63755
65388
|
throw new Error(`extension manifest invalid: ${validation.errors.join("; ")}`);
|
|
63756
|
-
const
|
|
65389
|
+
const timestamp5 = new Date().toISOString();
|
|
63757
65390
|
const config = loadConfig();
|
|
63758
65391
|
const existing = config.extension_registry?.[inspected.manifest.name];
|
|
63759
65392
|
const trusted = Boolean(input.trust);
|
|
@@ -63773,8 +65406,8 @@ function installLocalExtension(input) {
|
|
|
63773
65406
|
...trusted ? [] : ["extension installed as needs_review until trusted explicitly"]
|
|
63774
65407
|
],
|
|
63775
65408
|
diagnostics: testExtensionCompatibility(inspected.source),
|
|
63776
|
-
installed_at: existing?.installed_at ||
|
|
63777
|
-
updated_at:
|
|
65409
|
+
installed_at: existing?.installed_at || timestamp5,
|
|
65410
|
+
updated_at: timestamp5
|
|
63778
65411
|
};
|
|
63779
65412
|
saveConfig({
|
|
63780
65413
|
...config,
|
|
@@ -64136,9 +65769,9 @@ async function sendToTmux(target, message, delayMs, options = false) {
|
|
|
64136
65769
|
ensureTmuxAvailable();
|
|
64137
65770
|
if (!opts.confirmBusy) {
|
|
64138
65771
|
const pane = await inspectTmuxPane(target);
|
|
64139
|
-
const
|
|
64140
|
-
if (
|
|
64141
|
-
throw new Error(`tmux target "${target}" appears busy (${
|
|
65772
|
+
const status3 = tmuxPaneBusyStatus(pane);
|
|
65773
|
+
if (status3.busy) {
|
|
65774
|
+
throw new Error(`tmux target "${target}" appears busy (${status3.reason}). Re-run with --confirm-busy to send anyway.`);
|
|
64142
65775
|
}
|
|
64143
65776
|
}
|
|
64144
65777
|
const sendProc = Bun.spawn(["tmux", "send-keys", "-t", target, message, ""], {
|
|
@@ -64283,10 +65916,10 @@ function normalizeStatus(value) {
|
|
|
64283
65916
|
return "open";
|
|
64284
65917
|
}
|
|
64285
65918
|
function normalizeResolutionStatus(value) {
|
|
64286
|
-
const
|
|
64287
|
-
if (
|
|
65919
|
+
const status3 = normalizeStatus(value || "resolved");
|
|
65920
|
+
if (status3 === "open")
|
|
64288
65921
|
throw new Error("resolve-missing status must be resolved or ignored");
|
|
64289
|
-
return
|
|
65922
|
+
return status3;
|
|
64290
65923
|
}
|
|
64291
65924
|
function redactOptional(value, max = 2000) {
|
|
64292
65925
|
if (!value)
|
|
@@ -64324,7 +65957,7 @@ function compactFinding(finding2) {
|
|
|
64324
65957
|
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
64325
65958
|
};
|
|
64326
65959
|
}
|
|
64327
|
-
function previewFinding(existing, next,
|
|
65960
|
+
function previewFinding(existing, next, timestamp5) {
|
|
64328
65961
|
return {
|
|
64329
65962
|
...existing,
|
|
64330
65963
|
run_id: next.run_id,
|
|
@@ -64335,9 +65968,9 @@ function previewFinding(existing, next, timestamp4) {
|
|
|
64335
65968
|
summary: next.summary,
|
|
64336
65969
|
artifact_path: next.artifact_path,
|
|
64337
65970
|
metadata: next.metadata,
|
|
64338
|
-
last_seen_at:
|
|
64339
|
-
resolved_at: next.status === "open" ? null : existing.resolved_at ||
|
|
64340
|
-
updated_at:
|
|
65971
|
+
last_seen_at: timestamp5,
|
|
65972
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
65973
|
+
updated_at: timestamp5
|
|
64341
65974
|
};
|
|
64342
65975
|
}
|
|
64343
65976
|
function upsertAction(existing, next) {
|
|
@@ -64387,7 +66020,7 @@ function sameFinding(left, right) {
|
|
|
64387
66020
|
function upsertTaskFinding(input, db) {
|
|
64388
66021
|
const d = db || getDatabase();
|
|
64389
66022
|
assertTask(input.task_id, d);
|
|
64390
|
-
const
|
|
66023
|
+
const timestamp5 = input.observed_at || now();
|
|
64391
66024
|
const warnings = [];
|
|
64392
66025
|
const next = nextFinding(input, d);
|
|
64393
66026
|
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
@@ -64398,10 +66031,10 @@ function upsertTaskFinding(input, db) {
|
|
|
64398
66031
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64399
66032
|
local_only: true,
|
|
64400
66033
|
dry_run: true,
|
|
64401
|
-
processed_at:
|
|
66034
|
+
processed_at: timestamp5,
|
|
64402
66035
|
action: action2,
|
|
64403
66036
|
fingerprint: next.fingerprint,
|
|
64404
|
-
finding: existing ? compactFinding(previewFinding(existing, next,
|
|
66037
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp5)) : null,
|
|
64405
66038
|
warnings
|
|
64406
66039
|
};
|
|
64407
66040
|
}
|
|
@@ -64422,17 +66055,17 @@ function upsertTaskFinding(input, db) {
|
|
|
64422
66055
|
next.summary,
|
|
64423
66056
|
next.artifact_path,
|
|
64424
66057
|
JSON.stringify(next.metadata),
|
|
64425
|
-
|
|
64426
|
-
|
|
64427
|
-
next.status === "open" ? null :
|
|
64428
|
-
|
|
64429
|
-
|
|
66058
|
+
timestamp5,
|
|
66059
|
+
timestamp5,
|
|
66060
|
+
next.status === "open" ? null : timestamp5,
|
|
66061
|
+
timestamp5,
|
|
66062
|
+
timestamp5
|
|
64430
66063
|
]);
|
|
64431
66064
|
return {
|
|
64432
66065
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64433
66066
|
local_only: true,
|
|
64434
66067
|
dry_run: false,
|
|
64435
|
-
processed_at:
|
|
66068
|
+
processed_at: timestamp5,
|
|
64436
66069
|
action: "created",
|
|
64437
66070
|
fingerprint: next.fingerprint,
|
|
64438
66071
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64444,7 +66077,7 @@ function upsertTaskFinding(input, db) {
|
|
|
64444
66077
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64445
66078
|
local_only: true,
|
|
64446
66079
|
dry_run: false,
|
|
64447
|
-
processed_at:
|
|
66080
|
+
processed_at: timestamp5,
|
|
64448
66081
|
action: "matched",
|
|
64449
66082
|
fingerprint: next.fingerprint,
|
|
64450
66083
|
finding: compactFinding(existing),
|
|
@@ -64464,16 +66097,16 @@ function upsertTaskFinding(input, db) {
|
|
|
64464
66097
|
next.summary,
|
|
64465
66098
|
next.artifact_path,
|
|
64466
66099
|
JSON.stringify(next.metadata),
|
|
64467
|
-
|
|
64468
|
-
next.status === "open" ? null : existing.resolved_at ||
|
|
64469
|
-
|
|
66100
|
+
timestamp5,
|
|
66101
|
+
next.status === "open" ? null : existing.resolved_at || timestamp5,
|
|
66102
|
+
timestamp5,
|
|
64470
66103
|
existing.id
|
|
64471
66104
|
]);
|
|
64472
66105
|
return {
|
|
64473
66106
|
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
64474
66107
|
local_only: true,
|
|
64475
66108
|
dry_run: false,
|
|
64476
|
-
processed_at:
|
|
66109
|
+
processed_at: timestamp5,
|
|
64477
66110
|
action,
|
|
64478
66111
|
fingerprint: next.fingerprint,
|
|
64479
66112
|
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
@@ -64510,8 +66143,8 @@ function listCompactTaskFindings(filter = {}, db) {
|
|
|
64510
66143
|
function resolveMissingTaskFindings(input, db) {
|
|
64511
66144
|
const d = db || getDatabase();
|
|
64512
66145
|
assertTask(input.task_id, d);
|
|
64513
|
-
const
|
|
64514
|
-
const
|
|
66146
|
+
const timestamp5 = input.resolved_at || now();
|
|
66147
|
+
const status3 = normalizeResolutionStatus(input.status);
|
|
64515
66148
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
64516
66149
|
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
64517
66150
|
const warnings = [];
|
|
@@ -64530,7 +66163,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64530
66163
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64531
66164
|
local_only: true,
|
|
64532
66165
|
dry_run: true,
|
|
64533
|
-
processed_at:
|
|
66166
|
+
processed_at: timestamp5,
|
|
64534
66167
|
action: candidates.length > 0 ? "preview" : "noop",
|
|
64535
66168
|
task_id: input.task_id,
|
|
64536
66169
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
@@ -64553,10 +66186,10 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64553
66186
|
const tx = d.transaction(() => {
|
|
64554
66187
|
for (const finding2 of candidates) {
|
|
64555
66188
|
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
64556
|
-
|
|
64557
|
-
|
|
66189
|
+
status3,
|
|
66190
|
+
timestamp5,
|
|
64558
66191
|
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
64559
|
-
|
|
66192
|
+
timestamp5,
|
|
64560
66193
|
finding2.id
|
|
64561
66194
|
]);
|
|
64562
66195
|
}
|
|
@@ -64568,8 +66201,8 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
64568
66201
|
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
64569
66202
|
local_only: true,
|
|
64570
66203
|
dry_run: false,
|
|
64571
|
-
processed_at:
|
|
64572
|
-
action: updated.length > 0 ?
|
|
66204
|
+
processed_at: timestamp5,
|
|
66205
|
+
action: updated.length > 0 ? status3 : "noop",
|
|
64573
66206
|
task_id: input.task_id,
|
|
64574
66207
|
source: input.source ? redactOptional(input.source, 120) : null,
|
|
64575
66208
|
run_id: runId,
|
|
@@ -65462,7 +67095,7 @@ init_tasks();
|
|
|
65462
67095
|
init_task_relations();
|
|
65463
67096
|
init_config2();
|
|
65464
67097
|
var LOCAL_CAPACITY_SCHEMA_VERSION = 1;
|
|
65465
|
-
function
|
|
67098
|
+
function timestamp5() {
|
|
65466
67099
|
return new Date().toISOString();
|
|
65467
67100
|
}
|
|
65468
67101
|
function todayIso() {
|
|
@@ -65546,7 +67179,7 @@ function upsertCapacityProfile(input) {
|
|
|
65546
67179
|
const store = readStore2();
|
|
65547
67180
|
const key2 = profileKey(agentId, projectId);
|
|
65548
67181
|
const existing = store.profiles[key2];
|
|
65549
|
-
const now4 =
|
|
67182
|
+
const now4 = timestamp5();
|
|
65550
67183
|
const profile = {
|
|
65551
67184
|
id: existing?.id ?? key2,
|
|
65552
67185
|
agent_id: agentId,
|
|
@@ -65627,7 +67260,7 @@ function getPlanningForecast(input = {}, db) {
|
|
|
65627
67260
|
return {
|
|
65628
67261
|
schema_version: LOCAL_CAPACITY_SCHEMA_VERSION,
|
|
65629
67262
|
local_only: true,
|
|
65630
|
-
generated_at:
|
|
67263
|
+
generated_at: timestamp5(),
|
|
65631
67264
|
filters: {
|
|
65632
67265
|
project_id: input.project_id ?? null,
|
|
65633
67266
|
plan_id: input.plan_id ?? null,
|
|
@@ -66015,6 +67648,8 @@ export {
|
|
|
66015
67648
|
testTerminalNotificationRule,
|
|
66016
67649
|
testLocalEventHook,
|
|
66017
67650
|
tasksFromTemplate,
|
|
67651
|
+
taskSubtreeTransferRollbackRequestDigest,
|
|
67652
|
+
taskSubtreeTransferRequestDigest,
|
|
66018
67653
|
taskManifestRequestDigest,
|
|
66019
67654
|
taskManifestCompensationRequestDigest,
|
|
66020
67655
|
taskFromTemplate,
|
|
@@ -66037,6 +67672,7 @@ export {
|
|
|
66037
67672
|
startTaskRun,
|
|
66038
67673
|
startTask,
|
|
66039
67674
|
startFocusSession,
|
|
67675
|
+
sqliteTodosTaskSubtreeTransferSchemaSql,
|
|
66040
67676
|
sqliteTodosTaskManifestSchemaSql,
|
|
66041
67677
|
sqliteTodosProjectRegistrationSchemaSql,
|
|
66042
67678
|
softDeleteArtifact,
|
|
@@ -66228,6 +67864,7 @@ export {
|
|
|
66228
67864
|
previewInstalledTemplate,
|
|
66229
67865
|
previewInboxIntake,
|
|
66230
67866
|
previewBuiltinTemplate,
|
|
67867
|
+
postgresTodosTaskSubtreeTransferSchemaSql,
|
|
66231
67868
|
postgresTodosTaskManifestSchemaSql,
|
|
66232
67869
|
postgresTodosSyncSchemaSql,
|
|
66233
67870
|
postgresTodosProjectRegistrationSchemaSql,
|
|
@@ -66236,6 +67873,9 @@ export {
|
|
|
66236
67873
|
planPlanProjectLink,
|
|
66237
67874
|
pauseFocusSession,
|
|
66238
67875
|
patrolTasks,
|
|
67876
|
+
parseTodosTaskSubtreeTransferRollback,
|
|
67877
|
+
parseTodosTaskSubtreeTransferInspect,
|
|
67878
|
+
parseTodosTaskSubtreeTransferApply,
|
|
66239
67879
|
parseTodosTaskManifestCompensation,
|
|
66240
67880
|
parseTodosTaskManifestBindingLookup,
|
|
66241
67881
|
parseTodosTaskManifest,
|
|
@@ -66243,7 +67883,6 @@ export {
|
|
|
66243
67883
|
parseTodosAiOutputSchema,
|
|
66244
67884
|
parseTodosAiJson,
|
|
66245
67885
|
parseTmuxTarget,
|
|
66246
|
-
parseStorageMode,
|
|
66247
67886
|
parseStorageBackend,
|
|
66248
67887
|
parseRecurrenceRule,
|
|
66249
67888
|
parseQuietHours,
|
|
@@ -66394,7 +68033,6 @@ export {
|
|
|
66394
68033
|
issueToTask,
|
|
66395
68034
|
isWorktreePath,
|
|
66396
68035
|
isValidRecurrenceRule,
|
|
66397
|
-
isTodosRemoteStorageEnabled,
|
|
66398
68036
|
isTodosPostgresBackend,
|
|
66399
68037
|
isTodosAiUpdateTaskResult,
|
|
66400
68038
|
isTodosAiRuntimeEvent,
|
|
@@ -66434,6 +68072,7 @@ export {
|
|
|
66434
68072
|
importActivityLog,
|
|
66435
68073
|
hasSecretFindings,
|
|
66436
68074
|
hasActiveApiKeys,
|
|
68075
|
+
handleTodosTaskSubtreeTransferHttpRequest,
|
|
66437
68076
|
handleTodosTaskManifestHttpRequest,
|
|
66438
68077
|
handleTodosProjectRegistrationHttpRequest,
|
|
66439
68078
|
getWorkspaceTrustStatus,
|
|
@@ -66447,7 +68086,6 @@ export {
|
|
|
66447
68086
|
getUnlockImpact,
|
|
66448
68087
|
getTraceStats,
|
|
66449
68088
|
getTopologyDocs,
|
|
66450
|
-
getTodosStorageMode,
|
|
66451
68089
|
getTodosStorageEnvName,
|
|
66452
68090
|
getTodosStorageDatabaseUrl,
|
|
66453
68091
|
getTodosStorageDatabaseEnv,
|
|
@@ -66492,7 +68130,6 @@ export {
|
|
|
66492
68130
|
getTaskBoard,
|
|
66493
68131
|
getTask,
|
|
66494
68132
|
getStoredHandoffAsPacket,
|
|
66495
|
-
getStorageMode,
|
|
66496
68133
|
getStorageDatabaseUrl,
|
|
66497
68134
|
getStorageDatabaseEnv,
|
|
66498
68135
|
getStatus,
|
|
@@ -66751,6 +68388,9 @@ export {
|
|
|
66751
68388
|
detectInboxSourceType,
|
|
66752
68389
|
detectCyclesFromEdges,
|
|
66753
68390
|
describeTerminalNotificationRule,
|
|
68391
|
+
deriveTodosTaskSubtreeTransferRollbackPreconditionDigest,
|
|
68392
|
+
deriveTodosTaskSubtreeTransferIdempotencyKey,
|
|
68393
|
+
deriveTodosTaskSubtreeTransferApplyPreconditionDigest,
|
|
66754
68394
|
deriveTodosTaskManifestIdempotencyKey,
|
|
66755
68395
|
deriveTodosTaskManifestCompensationPreconditionDigest,
|
|
66756
68396
|
deriveTodosTaskManifestApplyPreconditionDigest,
|
|
@@ -66787,6 +68427,7 @@ export {
|
|
|
66787
68427
|
createVerificationEvidence,
|
|
66788
68428
|
createUserScaffold,
|
|
66789
68429
|
createTuiDashboardSnapshot,
|
|
68430
|
+
createTodosTaskSubtreeTransferHttpClient,
|
|
66790
68431
|
createTodosTaskManifestHttpClient,
|
|
66791
68432
|
createTodosStorageAdapter,
|
|
66792
68433
|
createTodosS3ArtifactStore,
|
|
@@ -66801,6 +68442,7 @@ export {
|
|
|
66801
68442
|
createTaskList,
|
|
66802
68443
|
createTaskBoard,
|
|
66803
68444
|
createTask,
|
|
68445
|
+
createSqliteTodosTaskSubtreeTransferAuthority,
|
|
66804
68446
|
createSqliteTodosTaskManifestAuthority,
|
|
66805
68447
|
createSessionRecoveryHandoff,
|
|
66806
68448
|
createSession,
|
|
@@ -66816,6 +68458,7 @@ export {
|
|
|
66816
68458
|
createReminder,
|
|
66817
68459
|
createReleaseCompatibilityReport,
|
|
66818
68460
|
createProject,
|
|
68461
|
+
createPostgresTodosTaskSubtreeTransferAuthority,
|
|
66819
68462
|
createPostgresTodosTaskManifestAuthority,
|
|
66820
68463
|
createPostgresTodosSyncStore,
|
|
66821
68464
|
createPostgresTodosStorageAdapter,
|
|
@@ -66984,6 +68627,8 @@ export {
|
|
|
66984
68627
|
VersionConflictError,
|
|
66985
68628
|
VERIFICATION_EVIDENCE_SCHEMA,
|
|
66986
68629
|
USER_SCAFFOLD_SCHEMA,
|
|
68630
|
+
TodosTaskSubtreeTransferHttpClient,
|
|
68631
|
+
TodosTaskSubtreeTransferError,
|
|
66987
68632
|
TodosTaskManifestHttpClient,
|
|
66988
68633
|
TodosTaskManifestError,
|
|
66989
68634
|
TodosProjectRegistrationHttpClient,
|
|
@@ -66995,6 +68640,8 @@ export {
|
|
|
66995
68640
|
TaskNotFoundError,
|
|
66996
68641
|
TaskListNotFoundError,
|
|
66997
68642
|
TUI_DASHBOARD_VIEWS,
|
|
68643
|
+
TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
68644
|
+
TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
66998
68645
|
TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
66999
68646
|
TODOS_TASK_MANIFEST_SCHEMA_VERSION,
|
|
67000
68647
|
TODOS_TASK_MANIFEST_ROUTE,
|
|
@@ -67101,6 +68748,7 @@ export {
|
|
|
67101
68748
|
PostgresTodosProjectRegistrationBackend,
|
|
67102
68749
|
PlanProjectLinkError,
|
|
67103
68750
|
PlanNotFoundError,
|
|
68751
|
+
PackageOwnedTodosTaskSubtreeTransferAuthority,
|
|
67104
68752
|
PackageOwnedTodosTaskManifestAuthority,
|
|
67105
68753
|
PackageOwnedTodosProjectRegistrationAuthority,
|
|
67106
68754
|
PROJECT_DEPENDENCY_GRAPH_SCHEMA,
|