@hasna/todos 0.16.0 → 0.17.0
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/CHANGELOG.md +109 -0
- package/README.md +59 -22
- package/dist/cli/cloud-router.d.ts +63 -21
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/template-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2591 -503
- package/dist/cli/stage-a.d.ts +2 -12
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/template-remote.d.ts +90 -0
- package/dist/cli/template-remote.d.ts.map +1 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +77 -30
- package/dist/db/agents.d.ts +8 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts +6 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +25 -0
- package/dist/db/identity-mapping.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.js +496 -58
- package/dist/lib/feature-manifest.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +14 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +11 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +52 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +15162 -30695
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/templates.d.ts.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/project-registration.js +466 -35
- package/dist/registry.js +77 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/client.d.ts +3 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +48 -12
- package/dist/sdk/resolve.d.ts +23 -13
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +17 -2
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +115 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +3972 -916
- package/dist/server/openapi.d.ts +837 -50
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/rate-limit-config.d.ts +6 -0
- package/dist/server/rate-limit-config.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +74 -3
- 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.js +464 -33
- package/dist/task-manifest.js +8 -2
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/mcp/tools/code-tools.d.ts +0 -15
- package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/storage.js
CHANGED
|
@@ -4324,20 +4324,17 @@ function ensureAgentIdentitySchema(db) {
|
|
|
4324
4324
|
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
4325
4325
|
END;
|
|
4326
4326
|
|
|
4327
|
+
-- Drop first so an older definition is replaced rather than silently
|
|
4328
|
+
-- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
|
|
4329
|
+
-- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
|
|
4330
|
+
-- do different jobs and both are kept: the DROP is what refreshes a
|
|
4331
|
+
-- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
|
|
4332
|
+
-- on its own \u2014 so a caller that reaches it without a preceding DROP, in
|
|
4333
|
+
-- its own connection or transaction, gets a no-op instead of
|
|
4334
|
+
-- "trigger ... already exists".
|
|
4327
4335
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
4328
|
-
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
4329
|
-
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4330
|
-
ON agent_identity_source_mappings
|
|
4331
|
-
BEGIN
|
|
4332
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4333
|
-
END;
|
|
4334
|
-
|
|
4335
4336
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
4336
|
-
|
|
4337
|
-
BEFORE DELETE ON agent_identity_source_mappings
|
|
4338
|
-
BEGIN
|
|
4339
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4340
|
-
END;
|
|
4337
|
+
${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
|
|
4341
4338
|
`);
|
|
4342
4339
|
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
4343
4340
|
});
|
|
@@ -4377,7 +4374,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
|
4377
4374
|
function recordAgentAlias(agentId, label, db) {
|
|
4378
4375
|
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
4379
4376
|
}
|
|
4380
|
-
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65
|
|
4377
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
|
|
4378
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
4379
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4380
|
+
ON agent_identity_source_mappings
|
|
4381
|
+
BEGIN
|
|
4382
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4383
|
+
END;
|
|
4384
|
+
|
|
4385
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
4386
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
4387
|
+
BEGIN
|
|
4388
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4389
|
+
END;
|
|
4390
|
+
`;
|
|
4381
4391
|
var init_identity_mapping = __esm(() => {
|
|
4382
4392
|
init_types();
|
|
4383
4393
|
init_types();
|
|
@@ -4402,7 +4412,7 @@ var init_identity_mapping = __esm(() => {
|
|
|
4402
4412
|
// src/lib/paths.ts
|
|
4403
4413
|
import { existsSync as existsSync2 } from "fs";
|
|
4404
4414
|
import { homedir } from "os";
|
|
4405
|
-
import { join, resolve as resolve2 } from "path";
|
|
4415
|
+
import { isAbsolute, join, resolve as resolve2 } from "path";
|
|
4406
4416
|
import { homedir as pathsResolverHomedir } from "os";
|
|
4407
4417
|
import { join as pathsResolverJoin } from "path";
|
|
4408
4418
|
function pathsResolverAssertApp(app) {
|
|
@@ -4459,8 +4469,14 @@ function dataDir(options) {
|
|
|
4459
4469
|
function effectiveHome(env2 = process.env) {
|
|
4460
4470
|
return env2.HOME || env2.USERPROFILE || homedir();
|
|
4461
4471
|
}
|
|
4472
|
+
function hasnaHomeRoot(env2 = process.env) {
|
|
4473
|
+
const override = env2["HASNA_HOME"]?.trim();
|
|
4474
|
+
if (override && isAbsolute(override))
|
|
4475
|
+
return override;
|
|
4476
|
+
return join(effectiveHome(env2), ".hasna");
|
|
4477
|
+
}
|
|
4462
4478
|
function legacyHomeDir(env2 = process.env) {
|
|
4463
|
-
return join(
|
|
4479
|
+
return join(hasnaHomeRoot(env2), "todos");
|
|
4464
4480
|
}
|
|
4465
4481
|
function resolverHome(env2 = process.env) {
|
|
4466
4482
|
return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
|
|
@@ -4672,15 +4688,18 @@ __export(exports_database, {
|
|
|
4672
4688
|
resolvePartialId: () => resolvePartialId,
|
|
4673
4689
|
resolveAssignedToAliases: () => resolveAssignedToAliases,
|
|
4674
4690
|
resetDatabase: () => resetDatabase,
|
|
4691
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4675
4692
|
now: () => now,
|
|
4676
4693
|
lowerInClause: () => lowerInClause,
|
|
4677
4694
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
4678
4695
|
isLockExpired: () => isLockExpired,
|
|
4696
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4679
4697
|
getDatabasePath: () => getDatabasePath,
|
|
4680
4698
|
getDatabase: () => getDatabase,
|
|
4681
4699
|
closeDatabase: () => closeDatabase,
|
|
4682
4700
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
4683
4701
|
assignedToAliasSet: () => assignedToAliasSet,
|
|
4702
|
+
allowLocalStore: () => allowLocalStore,
|
|
4684
4703
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4685
4704
|
});
|
|
4686
4705
|
import { Database } from "bun:sqlite";
|
|
@@ -4797,7 +4816,18 @@ function maybeInstallShadowCapture(db) {
|
|
|
4797
4816
|
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4798
4817
|
}
|
|
4799
4818
|
}
|
|
4819
|
+
function refuseLocalStore(message) {
|
|
4820
|
+
localStoreRefusal = message;
|
|
4821
|
+
}
|
|
4822
|
+
function allowLocalStore() {
|
|
4823
|
+
localStoreRefusal = null;
|
|
4824
|
+
}
|
|
4825
|
+
function isLocalStoreRefused() {
|
|
4826
|
+
return localStoreRefusal !== null;
|
|
4827
|
+
}
|
|
4800
4828
|
function getDatabase(dbPath) {
|
|
4829
|
+
if (localStoreRefusal !== null)
|
|
4830
|
+
throw new Error(localStoreRefusal);
|
|
4801
4831
|
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
4802
4832
|
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
4803
4833
|
}
|
|
@@ -4938,7 +4968,7 @@ function lowerInClause(column, values, params) {
|
|
|
4938
4968
|
params.push(...values.map((v) => v.toLowerCase()));
|
|
4939
4969
|
return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
|
|
4940
4970
|
}
|
|
4941
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
4971
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
|
|
4942
4972
|
var init_database = __esm(() => {
|
|
4943
4973
|
init_local_opt_in();
|
|
4944
4974
|
init_schema();
|
|
@@ -10679,7 +10709,9 @@ function listTasks(filter = {}, db) {
|
|
|
10679
10709
|
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
10680
10710
|
} catch {}
|
|
10681
10711
|
}
|
|
10682
|
-
if (
|
|
10712
|
+
if (filter.archived_only) {
|
|
10713
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10714
|
+
} else if (!filter.include_archived) {
|
|
10683
10715
|
conditions.push("archived_at IS NULL");
|
|
10684
10716
|
}
|
|
10685
10717
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10692,7 +10724,8 @@ function listTasks(filter = {}, db) {
|
|
|
10692
10724
|
params.push(filter.offset);
|
|
10693
10725
|
}
|
|
10694
10726
|
}
|
|
10695
|
-
const
|
|
10727
|
+
const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
|
|
10728
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
|
|
10696
10729
|
return rows.map(rowToTask2);
|
|
10697
10730
|
}
|
|
10698
10731
|
function getTaskByFingerprint(fingerprint, db) {
|
|
@@ -10835,7 +10868,9 @@ function countTasks(filter = {}, db) {
|
|
|
10835
10868
|
}
|
|
10836
10869
|
}
|
|
10837
10870
|
addMetadataConditions(filter.metadata, conditions, params);
|
|
10838
|
-
if (
|
|
10871
|
+
if (filter.archived_only) {
|
|
10872
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10873
|
+
} else if (!filter.include_archived) {
|
|
10839
10874
|
conditions.push("archived_at IS NULL");
|
|
10840
10875
|
}
|
|
10841
10876
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10972,6 +11007,10 @@ function updateTaskStored(id, input, db) {
|
|
|
10972
11007
|
sets.push("completed_at = ?");
|
|
10973
11008
|
params.push(input.completed_at);
|
|
10974
11009
|
}
|
|
11010
|
+
if (input.archived_at !== undefined) {
|
|
11011
|
+
sets.push("archived_at = ?");
|
|
11012
|
+
params.push(input.archived_at);
|
|
11013
|
+
}
|
|
10975
11014
|
if (input.confidence !== undefined) {
|
|
10976
11015
|
sets.push("confidence = ?");
|
|
10977
11016
|
params.push(input.confidence);
|
|
@@ -11035,6 +11074,8 @@ function updateTaskStored(id, input, db) {
|
|
|
11035
11074
|
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
11036
11075
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
11037
11076
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
11077
|
+
if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
|
|
11078
|
+
logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
|
|
11038
11079
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
11039
11080
|
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
11040
11081
|
if (input.approved_by !== undefined)
|
|
@@ -13891,6 +13932,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
13891
13932
|
|
|
13892
13933
|
// src/storage/local-sqlite.ts
|
|
13893
13934
|
init_tasks();
|
|
13935
|
+
init_task_graph();
|
|
13894
13936
|
init_projects();
|
|
13895
13937
|
init_plans();
|
|
13896
13938
|
|
|
@@ -14454,6 +14496,19 @@ function listAgents(opts, db) {
|
|
|
14454
14496
|
}
|
|
14455
14497
|
return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
|
|
14456
14498
|
}
|
|
14499
|
+
function listAgentsPage(options, db) {
|
|
14500
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
|
|
14501
|
+
throw new Error("Agent page limit must be an integer from 1 to 500");
|
|
14502
|
+
}
|
|
14503
|
+
if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
|
|
14504
|
+
throw new Error("Agent page offset must be a non-negative integer");
|
|
14505
|
+
}
|
|
14506
|
+
const d = db || getDatabase();
|
|
14507
|
+
const where = options.include_archived ? "" : "WHERE status = 'active'";
|
|
14508
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
|
|
14509
|
+
const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
|
|
14510
|
+
return { agents, total };
|
|
14511
|
+
}
|
|
14457
14512
|
function updateAgentActivity(id, db) {
|
|
14458
14513
|
const d = db || getDatabase();
|
|
14459
14514
|
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
@@ -15782,6 +15837,98 @@ function exportSqliteMachines(db) {
|
|
|
15782
15837
|
}
|
|
15783
15838
|
|
|
15784
15839
|
// src/storage/local-sqlite.ts
|
|
15840
|
+
function bulkCreateAtomicSqlite(db, inputs) {
|
|
15841
|
+
return db.transaction(() => {
|
|
15842
|
+
const tempIds = new Map;
|
|
15843
|
+
const created = [];
|
|
15844
|
+
const dependencies = [];
|
|
15845
|
+
for (const input of inputs) {
|
|
15846
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
15847
|
+
const task = createTask(taskInput, db);
|
|
15848
|
+
if (temp_id)
|
|
15849
|
+
tempIds.set(temp_id, task.id);
|
|
15850
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
15851
|
+
}
|
|
15852
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
15853
|
+
const input = inputs[index];
|
|
15854
|
+
const taskId = created[index].id;
|
|
15855
|
+
const seenDependencies = new Set;
|
|
15856
|
+
for (const reference of input.depends_on ?? []) {
|
|
15857
|
+
const dependencyId = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
|
|
15858
|
+
if (!dependencyId)
|
|
15859
|
+
throw new TaskNotFoundError(reference);
|
|
15860
|
+
if (seenDependencies.has(dependencyId)) {
|
|
15861
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
15862
|
+
}
|
|
15863
|
+
seenDependencies.add(dependencyId);
|
|
15864
|
+
addDependency(taskId, dependencyId, db);
|
|
15865
|
+
dependencies.push({ task_id: taskId, depends_on: dependencyId });
|
|
15866
|
+
}
|
|
15867
|
+
}
|
|
15868
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
15869
|
+
})();
|
|
15870
|
+
}
|
|
15871
|
+
function deleteTaskHierarchySqlite(db, rootId) {
|
|
15872
|
+
const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
|
|
15873
|
+
SELECT id, 0 FROM tasks WHERE id = ?
|
|
15874
|
+
UNION ALL
|
|
15875
|
+
SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
|
|
15876
|
+
) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
|
|
15877
|
+
if (rows.length === 0)
|
|
15878
|
+
throw new TaskNotFoundError(rootId);
|
|
15879
|
+
for (const row of rows) {
|
|
15880
|
+
if (!deleteTask(row.id, db))
|
|
15881
|
+
throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
function bulkDeleteAtomicSqlite(db, ids, force) {
|
|
15885
|
+
return db.transaction(() => {
|
|
15886
|
+
const resolved = ids.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
|
|
15887
|
+
const seen = new Set;
|
|
15888
|
+
for (const item of resolved) {
|
|
15889
|
+
if (!item.task)
|
|
15890
|
+
continue;
|
|
15891
|
+
if (seen.has(item.task.id)) {
|
|
15892
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
15893
|
+
}
|
|
15894
|
+
seen.add(item.task.id);
|
|
15895
|
+
}
|
|
15896
|
+
const childState = new Map;
|
|
15897
|
+
for (const item of resolved) {
|
|
15898
|
+
if (!item.task)
|
|
15899
|
+
continue;
|
|
15900
|
+
childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
|
|
15901
|
+
}
|
|
15902
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
15903
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
15904
|
+
const roots = planned.filter((item) => {
|
|
15905
|
+
if (!force)
|
|
15906
|
+
return true;
|
|
15907
|
+
let parentId = item.task.parent_id;
|
|
15908
|
+
while (parentId) {
|
|
15909
|
+
if (plannedIds.has(parentId))
|
|
15910
|
+
return false;
|
|
15911
|
+
parentId = getTask(parentId, db)?.parent_id ?? null;
|
|
15912
|
+
}
|
|
15913
|
+
return true;
|
|
15914
|
+
});
|
|
15915
|
+
for (const item of roots)
|
|
15916
|
+
deleteTaskHierarchySqlite(db, item.task.id);
|
|
15917
|
+
return {
|
|
15918
|
+
schema_version: 1,
|
|
15919
|
+
atomic: true,
|
|
15920
|
+
force,
|
|
15921
|
+
results: resolved.map((item) => {
|
|
15922
|
+
if (!item.task)
|
|
15923
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
15924
|
+
if (!force && childState.get(item.task.id)) {
|
|
15925
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
15926
|
+
}
|
|
15927
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
15928
|
+
})
|
|
15929
|
+
};
|
|
15930
|
+
})();
|
|
15931
|
+
}
|
|
15785
15932
|
var TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15786
15933
|
function resolveTaskRefLocal(db, ref) {
|
|
15787
15934
|
const raw = ref.trim().toLowerCase();
|
|
@@ -15878,6 +16025,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15878
16025
|
},
|
|
15879
16026
|
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
15880
16027
|
delete: (id) => deleteTask(id, database()),
|
|
16028
|
+
bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
|
|
16029
|
+
bulkDeleteAtomic: (ids, force, _context) => bulkDeleteAtomicSqlite(database(), ids, force),
|
|
15881
16030
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
15882
16031
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
15883
16032
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
|
|
@@ -15886,6 +16035,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15886
16035
|
getActiveWork: (filters) => getActiveWork(filters, database()),
|
|
15887
16036
|
getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
15888
16037
|
},
|
|
16038
|
+
dependencies: {
|
|
16039
|
+
add: (taskId, dependsOn) => {
|
|
16040
|
+
addDependency(taskId, dependsOn, database());
|
|
16041
|
+
return { task_id: taskId, depends_on: dependsOn };
|
|
16042
|
+
},
|
|
16043
|
+
remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, database()),
|
|
16044
|
+
list: (taskId) => {
|
|
16045
|
+
const dependencies = getTaskDependencies(taskId, database());
|
|
16046
|
+
const blocks = getTaskDependents(taskId, database());
|
|
16047
|
+
return { dependencies, blocks, blocked_by: blocks };
|
|
16048
|
+
},
|
|
16049
|
+
listPage: ({ limit, offset }) => {
|
|
16050
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
16051
|
+
throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
|
|
16052
|
+
}
|
|
16053
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
16054
|
+
throw new Error("SQLite dependency page offset must be a non-negative integer");
|
|
16055
|
+
}
|
|
16056
|
+
const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
|
|
16057
|
+
const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
|
|
16058
|
+
return { dependencies, total: totalRow.total };
|
|
16059
|
+
},
|
|
16060
|
+
listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
|
|
16061
|
+
},
|
|
15889
16062
|
projects: {
|
|
15890
16063
|
create: (input) => createProject(input, database()),
|
|
15891
16064
|
get: (id) => getProject(id, database()),
|
|
@@ -15929,6 +16102,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15929
16102
|
get: (id) => getAgent(id, database()),
|
|
15930
16103
|
getByName: (name) => getAgentByName(name, database()),
|
|
15931
16104
|
list: (options2) => listAgents(options2, database()),
|
|
16105
|
+
listPage: (options2) => listAgentsPage(options2, database()),
|
|
15932
16106
|
update: (id, input) => updateAgent(id, input, database())
|
|
15933
16107
|
},
|
|
15934
16108
|
taskLists: {
|
|
@@ -15967,6 +16141,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15967
16141
|
return comments;
|
|
15968
16142
|
},
|
|
15969
16143
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
16144
|
+
getTaskHistoryPage: (taskId, options2) => {
|
|
16145
|
+
if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
|
|
16146
|
+
throw new Error("Task history limit must be an integer from 1 to 500");
|
|
16147
|
+
}
|
|
16148
|
+
if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
|
|
16149
|
+
throw new Error("Task history offset must be a non-negative integer");
|
|
16150
|
+
}
|
|
16151
|
+
const conditions = ["task_id = ?"];
|
|
16152
|
+
const values = [taskId];
|
|
16153
|
+
if (options2.since) {
|
|
16154
|
+
conditions.push("created_at >= ?");
|
|
16155
|
+
values.push(options2.since);
|
|
16156
|
+
}
|
|
16157
|
+
if (options2.until) {
|
|
16158
|
+
conditions.push("created_at <= ?");
|
|
16159
|
+
values.push(options2.until);
|
|
16160
|
+
}
|
|
16161
|
+
const where = conditions.join(" AND ");
|
|
16162
|
+
const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
|
|
16163
|
+
const direction = options2.order === "asc" ? "ASC" : "DESC";
|
|
16164
|
+
const history = database().query(`SELECT * FROM task_history WHERE ${where} ORDER BY created_at ${direction}, id ${direction} LIMIT ? OFFSET ?`).all(...values, options2.limit, options2.offset);
|
|
16165
|
+
return { history, total };
|
|
16166
|
+
},
|
|
15970
16167
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
15971
16168
|
},
|
|
15972
16169
|
sync: {
|
|
@@ -17733,6 +17930,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
17733
17930
|
count: (filter = {}) => store.countTasks(filter),
|
|
17734
17931
|
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
17735
17932
|
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
17933
|
+
bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
|
|
17934
|
+
bulkDeleteAtomic: (ids, force, context) => bulkDeleteTasksAtomic(ids, force, store, context),
|
|
17736
17935
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
17737
17936
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
17738
17937
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -17749,6 +17948,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
17749
17948
|
add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
|
|
17750
17949
|
remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, store),
|
|
17751
17950
|
list: (taskId) => listDependencies(taskId, store),
|
|
17951
|
+
listPage: (page) => store.listDependencyPage(page),
|
|
17752
17952
|
listAll: () => store.list("dependencies")
|
|
17753
17953
|
},
|
|
17754
17954
|
verifications: {
|
|
@@ -17814,6 +18014,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
17814
18014
|
get: (id) => store.get("agents", id),
|
|
17815
18015
|
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
17816
18016
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
18017
|
+
listPage: (options2) => store.listAgentPage(options2),
|
|
17817
18018
|
update: (id, input) => updateAgent2(id, input, store),
|
|
17818
18019
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
17819
18020
|
release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
|
|
@@ -17871,8 +18072,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
17871
18072
|
getCommentsPage: async (taskId, options2) => {
|
|
17872
18073
|
return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
17873
18074
|
},
|
|
17874
|
-
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) =>
|
|
17875
|
-
|
|
18075
|
+
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)),
|
|
18076
|
+
getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
|
|
18077
|
+
getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
|
|
17876
18078
|
},
|
|
17877
18079
|
machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
|
|
17878
18080
|
atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
|
|
@@ -17909,6 +18111,8 @@ class PostgresJsonRecordStore {
|
|
|
17909
18111
|
}
|
|
17910
18112
|
async withDependencyGraphTransaction(fn) {
|
|
17911
18113
|
await this.ensureSchema();
|
|
18114
|
+
if (this.projectIntegrityLocked)
|
|
18115
|
+
return fn(this);
|
|
17912
18116
|
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
17913
18117
|
const scoped = new PostgresJsonRecordStore({ ...this.options, client });
|
|
17914
18118
|
scoped.schemaReady = Promise.resolve();
|
|
@@ -18055,6 +18259,128 @@ class PostgresJsonRecordStore {
|
|
|
18055
18259
|
async list(type) {
|
|
18056
18260
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
18057
18261
|
}
|
|
18262
|
+
async listAgentPage(options) {
|
|
18263
|
+
const { limit, offset } = options;
|
|
18264
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
18265
|
+
throw new Error("Postgres agent page limit must be an integer from 1 to 500");
|
|
18266
|
+
}
|
|
18267
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
18268
|
+
throw new Error("Postgres agent page offset must be a non-negative integer");
|
|
18269
|
+
}
|
|
18270
|
+
await this.ensureSchema();
|
|
18271
|
+
const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
|
|
18272
|
+
SELECT object_id, payload
|
|
18273
|
+
FROM ${this.tableName}
|
|
18274
|
+
WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
|
|
18275
|
+
AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
|
|
18276
|
+
), page AS (
|
|
18277
|
+
SELECT object_id, payload FROM filtered
|
|
18278
|
+
ORDER BY LOWER(payload->>'name'), object_id
|
|
18279
|
+
LIMIT $3 OFFSET $4
|
|
18280
|
+
)
|
|
18281
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
18282
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY LOWER(payload->>'name'), object_id) FROM page), '[]'::jsonb) AS agents`, [this.service, options.include_archived === true, limit, offset]);
|
|
18283
|
+
const row = result.rows[0];
|
|
18284
|
+
const total = Number(row?.total);
|
|
18285
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
|
|
18286
|
+
throw new Error("Postgres agent page returned invalid count or agents");
|
|
18287
|
+
}
|
|
18288
|
+
if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
|
|
18289
|
+
throw new Error("Postgres agent page exceeded its requested bounds");
|
|
18290
|
+
}
|
|
18291
|
+
return { agents: row.agents.map((value) => payloadRecord2(value)), total };
|
|
18292
|
+
}
|
|
18293
|
+
async listRecentActivity(limit) {
|
|
18294
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
18295
|
+
throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
|
|
18296
|
+
}
|
|
18297
|
+
await this.ensureSchema();
|
|
18298
|
+
const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
|
|
18299
|
+
FROM ${this.tableName}
|
|
18300
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
18301
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
18302
|
+
LIMIT $2`, [this.service, limit]);
|
|
18303
|
+
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
18304
|
+
}
|
|
18305
|
+
async listTaskHistoryPage(taskId, options) {
|
|
18306
|
+
const { limit, offset } = options;
|
|
18307
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
18308
|
+
throw new Error("Postgres task history limit must be an integer from 1 to 500");
|
|
18309
|
+
}
|
|
18310
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
18311
|
+
throw new Error("Postgres task history offset must be a non-negative integer");
|
|
18312
|
+
}
|
|
18313
|
+
await this.ensureSchema();
|
|
18314
|
+
const direction = options.order === "asc" ? "ASC" : "DESC";
|
|
18315
|
+
const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
|
|
18316
|
+
SELECT object_id, payload, payload->>'created_at' AS created_at
|
|
18317
|
+
FROM ${this.tableName}
|
|
18318
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
18319
|
+
AND payload->>'task_id' = $2
|
|
18320
|
+
AND ($3::text IS NULL OR payload->>'created_at' >= $3)
|
|
18321
|
+
AND ($4::text IS NULL OR payload->>'created_at' <= $4)
|
|
18322
|
+
), page AS (
|
|
18323
|
+
SELECT object_id, payload, created_at FROM filtered
|
|
18324
|
+
ORDER BY created_at ${direction}, object_id ${direction}
|
|
18325
|
+
LIMIT $5 OFFSET $6
|
|
18326
|
+
)
|
|
18327
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
18328
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY created_at ${direction}, object_id ${direction}) FROM page), '[]'::jsonb) AS history`, [this.service, taskId, options.since ?? null, options.until ?? null, limit, offset]);
|
|
18329
|
+
const row = result.rows[0];
|
|
18330
|
+
const total = Number(row?.total);
|
|
18331
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
|
|
18332
|
+
throw new Error("Postgres task history page returned invalid count or history");
|
|
18333
|
+
}
|
|
18334
|
+
if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
|
|
18335
|
+
throw new Error("Postgres task history page exceeded its requested bounds");
|
|
18336
|
+
}
|
|
18337
|
+
return { history: row.history.map((value) => payloadRecord2(value)), total };
|
|
18338
|
+
}
|
|
18339
|
+
async listDependencyPage(options) {
|
|
18340
|
+
const { limit, offset } = options;
|
|
18341
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
18342
|
+
throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
|
|
18343
|
+
}
|
|
18344
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
18345
|
+
throw new Error("Postgres dependency page offset must be a non-negative integer");
|
|
18346
|
+
}
|
|
18347
|
+
await this.ensureSchema();
|
|
18348
|
+
const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
|
|
18349
|
+
SELECT payload, updated_at, object_id
|
|
18350
|
+
FROM ${this.tableName}
|
|
18351
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
|
|
18352
|
+
ORDER BY updated_at ASC, object_id ASC
|
|
18353
|
+
LIMIT $2 OFFSET $3
|
|
18354
|
+
)
|
|
18355
|
+
SELECT
|
|
18356
|
+
(SELECT COUNT(*)::text FROM ${this.tableName}
|
|
18357
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
|
|
18358
|
+
COALESCE(
|
|
18359
|
+
jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
|
|
18360
|
+
FILTER (WHERE page.payload IS NOT NULL),
|
|
18361
|
+
'[]'::jsonb
|
|
18362
|
+
) AS dependencies
|
|
18363
|
+
FROM page`, [this.service, limit, offset]);
|
|
18364
|
+
const row = result.rows[0];
|
|
18365
|
+
const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
|
|
18366
|
+
if (totalText === null || !/^\d+$/.test(totalText)) {
|
|
18367
|
+
throw new Error("Postgres dependency page returned an invalid total");
|
|
18368
|
+
}
|
|
18369
|
+
const total = Number(totalText);
|
|
18370
|
+
if (!Number.isSafeInteger(total)) {
|
|
18371
|
+
throw new Error("Postgres dependency page total exceeds the safe integer range");
|
|
18372
|
+
}
|
|
18373
|
+
if (!Array.isArray(row?.dependencies)) {
|
|
18374
|
+
throw new Error("Postgres dependency page returned an invalid dependencies array");
|
|
18375
|
+
}
|
|
18376
|
+
if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
|
|
18377
|
+
throw new Error("Postgres dependency page returned rows outside the requested bound");
|
|
18378
|
+
}
|
|
18379
|
+
return {
|
|
18380
|
+
dependencies: row.dependencies.map((value) => payloadRecord2(value)),
|
|
18381
|
+
total
|
|
18382
|
+
};
|
|
18383
|
+
}
|
|
18058
18384
|
async listComments(taskId, options = {}) {
|
|
18059
18385
|
await this.ensureSchema();
|
|
18060
18386
|
const limit = options.limit ?? 100;
|
|
@@ -18130,7 +18456,9 @@ class PostgresJsonRecordStore {
|
|
|
18130
18456
|
return "1=0";
|
|
18131
18457
|
return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
|
|
18132
18458
|
};
|
|
18133
|
-
if (filter.
|
|
18459
|
+
if (filter.archived_only === true)
|
|
18460
|
+
conds.push(`(payload->>'archived_at' IS NOT NULL)`);
|
|
18461
|
+
else if (filter.include_archived === false)
|
|
18134
18462
|
conds.push(`(payload->>'archived_at' IS NULL)`);
|
|
18135
18463
|
if (filter.ids)
|
|
18136
18464
|
conds.push(inClause("payload->>'id'", filter.ids));
|
|
@@ -18187,7 +18515,7 @@ class PostgresJsonRecordStore {
|
|
|
18187
18515
|
async listTasks(filter) {
|
|
18188
18516
|
await this.ensureSchema();
|
|
18189
18517
|
const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
|
|
18190
|
-
const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
18518
|
+
const orderBy = filter.archived_only ? `ORDER BY payload->>'archived_at' DESC, object_id DESC` : queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
18191
18519
|
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
|
|
18192
18520
|
if (filter.limit !== undefined) {
|
|
18193
18521
|
params.push(filter.limit);
|
|
@@ -18407,6 +18735,8 @@ class PostgresJsonRecordStore {
|
|
|
18407
18735
|
if (planIds.length === 0 && !parentGuard)
|
|
18408
18736
|
return this.upsert("tasks", value, context);
|
|
18409
18737
|
await this.ensureSchema();
|
|
18738
|
+
if (!queryClient && this.projectIntegrityLocked)
|
|
18739
|
+
queryClient = this.options.client;
|
|
18410
18740
|
if (!queryClient) {
|
|
18411
18741
|
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
18412
18742
|
}
|
|
@@ -18944,9 +19274,11 @@ class PostgresJsonRecordStore {
|
|
|
18944
19274
|
}
|
|
18945
19275
|
async deleteTaskHierarchy(id, context = {}) {
|
|
18946
19276
|
await this.ensureSchema();
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
19277
|
+
if (!this.projectIntegrityLocked) {
|
|
19278
|
+
return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
|
|
19279
|
+
}
|
|
19280
|
+
const timestamp2 = new Date().toISOString();
|
|
19281
|
+
const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
18950
19282
|
task_tree(object_id, path, cycle) AS (
|
|
18951
19283
|
SELECT task.object_id, ARRAY[task.object_id], false
|
|
18952
19284
|
FROM ${this.tableName} AS task
|
|
@@ -19008,13 +19340,12 @@ class PostgresJsonRecordStore {
|
|
|
19008
19340
|
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
19009
19341
|
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
19010
19342
|
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
});
|
|
19343
|
+
this.service,
|
|
19344
|
+
id,
|
|
19345
|
+
timestamp2,
|
|
19346
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
19347
|
+
]);
|
|
19348
|
+
return Boolean(result.rows[0]?.found);
|
|
19018
19349
|
}
|
|
19019
19350
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
19020
19351
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
@@ -19470,6 +19801,102 @@ class PostgresJsonRecordStore {
|
|
|
19470
19801
|
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
19471
19802
|
}
|
|
19472
19803
|
}
|
|
19804
|
+
async function bulkCreateTasksAtomic(inputs, store, context) {
|
|
19805
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
19806
|
+
const tempIds = new Map;
|
|
19807
|
+
const created = [];
|
|
19808
|
+
const dependencies = [];
|
|
19809
|
+
for (const input of inputs) {
|
|
19810
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
19811
|
+
const task = await createTask2(taskInput, scoped, context);
|
|
19812
|
+
if (temp_id)
|
|
19813
|
+
tempIds.set(temp_id, task.id);
|
|
19814
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
19815
|
+
}
|
|
19816
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
19817
|
+
const input = inputs[index];
|
|
19818
|
+
const taskId = created[index].id;
|
|
19819
|
+
const seenDependencies = new Set;
|
|
19820
|
+
for (const reference of input.depends_on ?? []) {
|
|
19821
|
+
const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
|
|
19822
|
+
const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
|
|
19823
|
+
if (!dependencyId)
|
|
19824
|
+
throw new TaskNotFoundError(reference);
|
|
19825
|
+
if (seenDependencies.has(dependencyId)) {
|
|
19826
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
19827
|
+
}
|
|
19828
|
+
seenDependencies.add(dependencyId);
|
|
19829
|
+
dependencies.push(await addDependency2(taskId, dependencyId, scoped, context));
|
|
19830
|
+
}
|
|
19831
|
+
}
|
|
19832
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
19833
|
+
});
|
|
19834
|
+
}
|
|
19835
|
+
async function bulkDeleteTasksAtomic(ids, force, store, context) {
|
|
19836
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
19837
|
+
const resolved = [];
|
|
19838
|
+
for (const reference of ids) {
|
|
19839
|
+
resolved.push({
|
|
19840
|
+
reference,
|
|
19841
|
+
task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
|
|
19842
|
+
});
|
|
19843
|
+
}
|
|
19844
|
+
const seen = new Set;
|
|
19845
|
+
for (const item of resolved) {
|
|
19846
|
+
if (!item.task)
|
|
19847
|
+
continue;
|
|
19848
|
+
if (seen.has(item.task.id)) {
|
|
19849
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
19850
|
+
}
|
|
19851
|
+
seen.add(item.task.id);
|
|
19852
|
+
}
|
|
19853
|
+
const childState = new Map;
|
|
19854
|
+
for (const item of resolved) {
|
|
19855
|
+
if (!item.task)
|
|
19856
|
+
continue;
|
|
19857
|
+
const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
|
|
19858
|
+
childState.set(item.task.id, children.length > 0);
|
|
19859
|
+
}
|
|
19860
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
19861
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
19862
|
+
const roots = [];
|
|
19863
|
+
for (const item of planned) {
|
|
19864
|
+
if (!force) {
|
|
19865
|
+
roots.push(item);
|
|
19866
|
+
continue;
|
|
19867
|
+
}
|
|
19868
|
+
let parentId = item.task.parent_id;
|
|
19869
|
+
let covered = false;
|
|
19870
|
+
while (parentId) {
|
|
19871
|
+
if (plannedIds.has(parentId)) {
|
|
19872
|
+
covered = true;
|
|
19873
|
+
break;
|
|
19874
|
+
}
|
|
19875
|
+
parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
|
|
19876
|
+
}
|
|
19877
|
+
if (!covered)
|
|
19878
|
+
roots.push(item);
|
|
19879
|
+
}
|
|
19880
|
+
for (const item of roots) {
|
|
19881
|
+
if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
|
|
19882
|
+
throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
|
|
19883
|
+
}
|
|
19884
|
+
}
|
|
19885
|
+
return {
|
|
19886
|
+
schema_version: 1,
|
|
19887
|
+
atomic: true,
|
|
19888
|
+
force,
|
|
19889
|
+
results: resolved.map((item) => {
|
|
19890
|
+
if (!item.task)
|
|
19891
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
19892
|
+
if (!force && childState.get(item.task.id)) {
|
|
19893
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
19894
|
+
}
|
|
19895
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
19896
|
+
})
|
|
19897
|
+
};
|
|
19898
|
+
});
|
|
19899
|
+
}
|
|
19473
19900
|
async function createTask2(input, store, context) {
|
|
19474
19901
|
const timestamp2 = new Date().toISOString();
|
|
19475
19902
|
const taskId = randomUUID4();
|
|
@@ -19560,12 +19987,16 @@ async function updateTask2(id, input, store, context) {
|
|
|
19560
19987
|
}
|
|
19561
19988
|
}
|
|
19562
19989
|
}
|
|
19990
|
+
const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
|
|
19991
|
+
const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
|
|
19992
|
+
const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId2(effectiveProjectIdForShortId, store, context) : null;
|
|
19563
19993
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
19564
19994
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
19565
19995
|
const task = {
|
|
19566
19996
|
...existing,
|
|
19567
19997
|
...definedPatch(input),
|
|
19568
19998
|
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
19999
|
+
short_id: backfilledShortId ?? existing.short_id,
|
|
19569
20000
|
version: existing.version + 1,
|
|
19570
20001
|
updated_at: new Date().toISOString(),
|
|
19571
20002
|
tags: input.tags ?? existing.tags,
|