@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
|
@@ -336,7 +336,7 @@ var init_types = __esm(() => {
|
|
|
336
336
|
// src/lib/paths.ts
|
|
337
337
|
import { existsSync } from "fs";
|
|
338
338
|
import { homedir } from "os";
|
|
339
|
-
import { join as join2, resolve } from "path";
|
|
339
|
+
import { isAbsolute, join as join2, resolve } from "path";
|
|
340
340
|
import { homedir as pathsResolverHomedir } from "os";
|
|
341
341
|
import { join as pathsResolverJoin } from "path";
|
|
342
342
|
function pathsResolverAssertApp(app) {
|
|
@@ -393,8 +393,14 @@ function dataDir(options) {
|
|
|
393
393
|
function effectiveHome(env = process.env) {
|
|
394
394
|
return env.HOME || env.USERPROFILE || homedir();
|
|
395
395
|
}
|
|
396
|
+
function hasnaHomeRoot(env = process.env) {
|
|
397
|
+
const override = env["HASNA_HOME"]?.trim();
|
|
398
|
+
if (override && isAbsolute(override))
|
|
399
|
+
return override;
|
|
400
|
+
return join2(effectiveHome(env), ".hasna");
|
|
401
|
+
}
|
|
396
402
|
function legacyHomeDir(env = process.env) {
|
|
397
|
-
return join2(
|
|
403
|
+
return join2(hasnaHomeRoot(env), "todos");
|
|
398
404
|
}
|
|
399
405
|
function resolverHome(env = process.env) {
|
|
400
406
|
return dataDir({ app: "todos", home: effectiveHome(env), env });
|
|
@@ -4995,20 +5001,17 @@ function ensureAgentIdentitySchema(db) {
|
|
|
4995
5001
|
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
4996
5002
|
END;
|
|
4997
5003
|
|
|
5004
|
+
-- Drop first so an older definition is replaced rather than silently
|
|
5005
|
+
-- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
|
|
5006
|
+
-- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
|
|
5007
|
+
-- do different jobs and both are kept: the DROP is what refreshes a
|
|
5008
|
+
-- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
|
|
5009
|
+
-- on its own \u2014 so a caller that reaches it without a preceding DROP, in
|
|
5010
|
+
-- its own connection or transaction, gets a no-op instead of
|
|
5011
|
+
-- "trigger ... already exists".
|
|
4998
5012
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
4999
|
-
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
5000
|
-
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
5001
|
-
ON agent_identity_source_mappings
|
|
5002
|
-
BEGIN
|
|
5003
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
5004
|
-
END;
|
|
5005
|
-
|
|
5006
5013
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
5007
|
-
|
|
5008
|
-
BEFORE DELETE ON agent_identity_source_mappings
|
|
5009
|
-
BEGIN
|
|
5010
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
5011
|
-
END;
|
|
5014
|
+
${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
|
|
5012
5015
|
`);
|
|
5013
5016
|
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
5014
5017
|
});
|
|
@@ -5048,7 +5051,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
|
5048
5051
|
function recordAgentAlias(agentId, label, db) {
|
|
5049
5052
|
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
5050
5053
|
}
|
|
5051
|
-
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65
|
|
5054
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
|
|
5055
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
5056
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
5057
|
+
ON agent_identity_source_mappings
|
|
5058
|
+
BEGIN
|
|
5059
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
5060
|
+
END;
|
|
5061
|
+
|
|
5062
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
5063
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
5064
|
+
BEGIN
|
|
5065
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
5066
|
+
END;
|
|
5067
|
+
`;
|
|
5052
5068
|
var init_identity_mapping = __esm(() => {
|
|
5053
5069
|
init_types();
|
|
5054
5070
|
init_types();
|
|
@@ -5365,15 +5381,18 @@ __export(exports_database, {
|
|
|
5365
5381
|
resolvePartialId: () => resolvePartialId,
|
|
5366
5382
|
resolveAssignedToAliases: () => resolveAssignedToAliases,
|
|
5367
5383
|
resetDatabase: () => resetDatabase,
|
|
5384
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
5368
5385
|
now: () => now,
|
|
5369
5386
|
lowerInClause: () => lowerInClause,
|
|
5370
5387
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
5371
5388
|
isLockExpired: () => isLockExpired,
|
|
5389
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
5372
5390
|
getDatabasePath: () => getDatabasePath,
|
|
5373
5391
|
getDatabase: () => getDatabase,
|
|
5374
5392
|
closeDatabase: () => closeDatabase,
|
|
5375
5393
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
5376
5394
|
assignedToAliasSet: () => assignedToAliasSet,
|
|
5395
|
+
allowLocalStore: () => allowLocalStore,
|
|
5377
5396
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
5378
5397
|
});
|
|
5379
5398
|
import { Database } from "bun:sqlite";
|
|
@@ -5490,7 +5509,18 @@ function maybeInstallShadowCapture(db) {
|
|
|
5490
5509
|
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5491
5510
|
}
|
|
5492
5511
|
}
|
|
5512
|
+
function refuseLocalStore(message) {
|
|
5513
|
+
localStoreRefusal = message;
|
|
5514
|
+
}
|
|
5515
|
+
function allowLocalStore() {
|
|
5516
|
+
localStoreRefusal = null;
|
|
5517
|
+
}
|
|
5518
|
+
function isLocalStoreRefused() {
|
|
5519
|
+
return localStoreRefusal !== null;
|
|
5520
|
+
}
|
|
5493
5521
|
function getDatabase(dbPath) {
|
|
5522
|
+
if (localStoreRefusal !== null)
|
|
5523
|
+
throw new Error(localStoreRefusal);
|
|
5494
5524
|
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
5495
5525
|
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
5496
5526
|
}
|
|
@@ -5631,7 +5661,7 @@ function lowerInClause(column, values, params) {
|
|
|
5631
5661
|
params.push(...values.map((v) => v.toLowerCase()));
|
|
5632
5662
|
return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
|
|
5633
5663
|
}
|
|
5634
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
5664
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
|
|
5635
5665
|
var init_database = __esm(() => {
|
|
5636
5666
|
init_local_opt_in();
|
|
5637
5667
|
init_schema();
|
|
@@ -10679,7 +10709,9 @@ function listTasks2(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 listTasks2(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
|
logTaskChange2(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
|
logTaskChange2(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
|
+
logTaskChange2(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
|
logTaskChange2(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
11040
11081
|
if (input.approved_by !== undefined)
|
|
@@ -13735,7 +13776,7 @@ import { createHash as createHash8 } from "crypto";
|
|
|
13735
13776
|
// package.json
|
|
13736
13777
|
var package_default = {
|
|
13737
13778
|
name: "@hasna/todos",
|
|
13738
|
-
version: "0.
|
|
13779
|
+
version: "0.17.0",
|
|
13739
13780
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
13740
13781
|
type: "module",
|
|
13741
13782
|
main: "dist/index.js",
|
|
@@ -13848,7 +13889,7 @@ var package_default = {
|
|
|
13848
13889
|
url: "https://github.com/hasna/apps.git",
|
|
13849
13890
|
directory: "apps/todos"
|
|
13850
13891
|
},
|
|
13851
|
-
homepage: "https://github.com/hasna/apps",
|
|
13892
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
|
|
13852
13893
|
bugs: {
|
|
13853
13894
|
url: "https://github.com/hasna/apps/issues"
|
|
13854
13895
|
},
|
|
@@ -16128,6 +16169,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
16128
16169
|
count: (filter = {}) => store.countTasks(filter),
|
|
16129
16170
|
update: (id, input, context) => updateTask(id, input, store, context),
|
|
16130
16171
|
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
16172
|
+
bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
|
|
16173
|
+
bulkDeleteAtomic: (ids, force, context) => bulkDeleteTasksAtomic(ids, force, store, context),
|
|
16131
16174
|
start: (id, agentId) => startTask(id, agentId, store),
|
|
16132
16175
|
complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
|
|
16133
16176
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
|
|
@@ -16144,6 +16187,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
16144
16187
|
add: (taskId, dependsOn, context) => addDependency(taskId, dependsOn, store, context),
|
|
16145
16188
|
remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, store),
|
|
16146
16189
|
list: (taskId) => listDependencies(taskId, store),
|
|
16190
|
+
listPage: (page) => store.listDependencyPage(page),
|
|
16147
16191
|
listAll: () => store.list("dependencies")
|
|
16148
16192
|
},
|
|
16149
16193
|
verifications: {
|
|
@@ -16209,6 +16253,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
16209
16253
|
get: (id) => store.get("agents", id),
|
|
16210
16254
|
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
16211
16255
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
16256
|
+
listPage: (options2) => store.listAgentPage(options2),
|
|
16212
16257
|
update: (id, input) => updateAgent(id, input, store),
|
|
16213
16258
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
16214
16259
|
release: (idOrName, sessionId, context) => releaseAgent(idOrName, sessionId, store, context)
|
|
@@ -16266,8 +16311,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
16266
16311
|
getCommentsPage: async (taskId, options2) => {
|
|
16267
16312
|
return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
16268
16313
|
},
|
|
16269
|
-
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) =>
|
|
16270
|
-
|
|
16314
|
+
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)),
|
|
16315
|
+
getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
|
|
16316
|
+
getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
|
|
16271
16317
|
},
|
|
16272
16318
|
machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
|
|
16273
16319
|
atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
|
|
@@ -16304,6 +16350,8 @@ class PostgresJsonRecordStore {
|
|
|
16304
16350
|
}
|
|
16305
16351
|
async withDependencyGraphTransaction(fn) {
|
|
16306
16352
|
await this.ensureSchema();
|
|
16353
|
+
if (this.projectIntegrityLocked)
|
|
16354
|
+
return fn(this);
|
|
16307
16355
|
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
16308
16356
|
const scoped = new PostgresJsonRecordStore({ ...this.options, client });
|
|
16309
16357
|
scoped.schemaReady = Promise.resolve();
|
|
@@ -16450,6 +16498,128 @@ class PostgresJsonRecordStore {
|
|
|
16450
16498
|
async list(type) {
|
|
16451
16499
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
16452
16500
|
}
|
|
16501
|
+
async listAgentPage(options) {
|
|
16502
|
+
const { limit, offset } = options;
|
|
16503
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
16504
|
+
throw new Error("Postgres agent page limit must be an integer from 1 to 500");
|
|
16505
|
+
}
|
|
16506
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
16507
|
+
throw new Error("Postgres agent page offset must be a non-negative integer");
|
|
16508
|
+
}
|
|
16509
|
+
await this.ensureSchema();
|
|
16510
|
+
const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
|
|
16511
|
+
SELECT object_id, payload
|
|
16512
|
+
FROM ${this.tableName}
|
|
16513
|
+
WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
|
|
16514
|
+
AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
|
|
16515
|
+
), page AS (
|
|
16516
|
+
SELECT object_id, payload FROM filtered
|
|
16517
|
+
ORDER BY LOWER(payload->>'name'), object_id
|
|
16518
|
+
LIMIT $3 OFFSET $4
|
|
16519
|
+
)
|
|
16520
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
16521
|
+
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]);
|
|
16522
|
+
const row = result.rows[0];
|
|
16523
|
+
const total = Number(row?.total);
|
|
16524
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
|
|
16525
|
+
throw new Error("Postgres agent page returned invalid count or agents");
|
|
16526
|
+
}
|
|
16527
|
+
if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
|
|
16528
|
+
throw new Error("Postgres agent page exceeded its requested bounds");
|
|
16529
|
+
}
|
|
16530
|
+
return { agents: row.agents.map((value) => payloadRecord2(value)), total };
|
|
16531
|
+
}
|
|
16532
|
+
async listRecentActivity(limit) {
|
|
16533
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
16534
|
+
throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
|
|
16535
|
+
}
|
|
16536
|
+
await this.ensureSchema();
|
|
16537
|
+
const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
|
|
16538
|
+
FROM ${this.tableName}
|
|
16539
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
16540
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
16541
|
+
LIMIT $2`, [this.service, limit]);
|
|
16542
|
+
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
16543
|
+
}
|
|
16544
|
+
async listTaskHistoryPage(taskId, options) {
|
|
16545
|
+
const { limit, offset } = options;
|
|
16546
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
16547
|
+
throw new Error("Postgres task history limit must be an integer from 1 to 500");
|
|
16548
|
+
}
|
|
16549
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
16550
|
+
throw new Error("Postgres task history offset must be a non-negative integer");
|
|
16551
|
+
}
|
|
16552
|
+
await this.ensureSchema();
|
|
16553
|
+
const direction = options.order === "asc" ? "ASC" : "DESC";
|
|
16554
|
+
const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
|
|
16555
|
+
SELECT object_id, payload, payload->>'created_at' AS created_at
|
|
16556
|
+
FROM ${this.tableName}
|
|
16557
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
16558
|
+
AND payload->>'task_id' = $2
|
|
16559
|
+
AND ($3::text IS NULL OR payload->>'created_at' >= $3)
|
|
16560
|
+
AND ($4::text IS NULL OR payload->>'created_at' <= $4)
|
|
16561
|
+
), page AS (
|
|
16562
|
+
SELECT object_id, payload, created_at FROM filtered
|
|
16563
|
+
ORDER BY created_at ${direction}, object_id ${direction}
|
|
16564
|
+
LIMIT $5 OFFSET $6
|
|
16565
|
+
)
|
|
16566
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
16567
|
+
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]);
|
|
16568
|
+
const row = result.rows[0];
|
|
16569
|
+
const total = Number(row?.total);
|
|
16570
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
|
|
16571
|
+
throw new Error("Postgres task history page returned invalid count or history");
|
|
16572
|
+
}
|
|
16573
|
+
if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
|
|
16574
|
+
throw new Error("Postgres task history page exceeded its requested bounds");
|
|
16575
|
+
}
|
|
16576
|
+
return { history: row.history.map((value) => payloadRecord2(value)), total };
|
|
16577
|
+
}
|
|
16578
|
+
async listDependencyPage(options) {
|
|
16579
|
+
const { limit, offset } = options;
|
|
16580
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
16581
|
+
throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
|
|
16582
|
+
}
|
|
16583
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
16584
|
+
throw new Error("Postgres dependency page offset must be a non-negative integer");
|
|
16585
|
+
}
|
|
16586
|
+
await this.ensureSchema();
|
|
16587
|
+
const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
|
|
16588
|
+
SELECT payload, updated_at, object_id
|
|
16589
|
+
FROM ${this.tableName}
|
|
16590
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
|
|
16591
|
+
ORDER BY updated_at ASC, object_id ASC
|
|
16592
|
+
LIMIT $2 OFFSET $3
|
|
16593
|
+
)
|
|
16594
|
+
SELECT
|
|
16595
|
+
(SELECT COUNT(*)::text FROM ${this.tableName}
|
|
16596
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
|
|
16597
|
+
COALESCE(
|
|
16598
|
+
jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
|
|
16599
|
+
FILTER (WHERE page.payload IS NOT NULL),
|
|
16600
|
+
'[]'::jsonb
|
|
16601
|
+
) AS dependencies
|
|
16602
|
+
FROM page`, [this.service, limit, offset]);
|
|
16603
|
+
const row = result.rows[0];
|
|
16604
|
+
const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
|
|
16605
|
+
if (totalText === null || !/^\d+$/.test(totalText)) {
|
|
16606
|
+
throw new Error("Postgres dependency page returned an invalid total");
|
|
16607
|
+
}
|
|
16608
|
+
const total = Number(totalText);
|
|
16609
|
+
if (!Number.isSafeInteger(total)) {
|
|
16610
|
+
throw new Error("Postgres dependency page total exceeds the safe integer range");
|
|
16611
|
+
}
|
|
16612
|
+
if (!Array.isArray(row?.dependencies)) {
|
|
16613
|
+
throw new Error("Postgres dependency page returned an invalid dependencies array");
|
|
16614
|
+
}
|
|
16615
|
+
if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
|
|
16616
|
+
throw new Error("Postgres dependency page returned rows outside the requested bound");
|
|
16617
|
+
}
|
|
16618
|
+
return {
|
|
16619
|
+
dependencies: row.dependencies.map((value) => payloadRecord2(value)),
|
|
16620
|
+
total
|
|
16621
|
+
};
|
|
16622
|
+
}
|
|
16453
16623
|
async listComments(taskId, options = {}) {
|
|
16454
16624
|
await this.ensureSchema();
|
|
16455
16625
|
const limit = options.limit ?? 100;
|
|
@@ -16525,7 +16695,9 @@ class PostgresJsonRecordStore {
|
|
|
16525
16695
|
return "1=0";
|
|
16526
16696
|
return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
|
|
16527
16697
|
};
|
|
16528
|
-
if (filter.
|
|
16698
|
+
if (filter.archived_only === true)
|
|
16699
|
+
conds.push(`(payload->>'archived_at' IS NOT NULL)`);
|
|
16700
|
+
else if (filter.include_archived === false)
|
|
16529
16701
|
conds.push(`(payload->>'archived_at' IS NULL)`);
|
|
16530
16702
|
if (filter.ids)
|
|
16531
16703
|
conds.push(inClause("payload->>'id'", filter.ids));
|
|
@@ -16582,7 +16754,7 @@ class PostgresJsonRecordStore {
|
|
|
16582
16754
|
async listTasks(filter) {
|
|
16583
16755
|
await this.ensureSchema();
|
|
16584
16756
|
const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
|
|
16585
|
-
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;
|
|
16757
|
+
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;
|
|
16586
16758
|
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
|
|
16587
16759
|
if (filter.limit !== undefined) {
|
|
16588
16760
|
params.push(filter.limit);
|
|
@@ -16802,6 +16974,8 @@ class PostgresJsonRecordStore {
|
|
|
16802
16974
|
if (planIds.length === 0 && !parentGuard)
|
|
16803
16975
|
return this.upsert("tasks", value, context);
|
|
16804
16976
|
await this.ensureSchema();
|
|
16977
|
+
if (!queryClient && this.projectIntegrityLocked)
|
|
16978
|
+
queryClient = this.options.client;
|
|
16805
16979
|
if (!queryClient) {
|
|
16806
16980
|
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
16807
16981
|
}
|
|
@@ -17339,9 +17513,11 @@ class PostgresJsonRecordStore {
|
|
|
17339
17513
|
}
|
|
17340
17514
|
async deleteTaskHierarchy(id, context = {}) {
|
|
17341
17515
|
await this.ensureSchema();
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17516
|
+
if (!this.projectIntegrityLocked) {
|
|
17517
|
+
return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
|
|
17518
|
+
}
|
|
17519
|
+
const timestamp = new Date().toISOString();
|
|
17520
|
+
const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
17345
17521
|
task_tree(object_id, path, cycle) AS (
|
|
17346
17522
|
SELECT task.object_id, ARRAY[task.object_id], false
|
|
17347
17523
|
FROM ${this.tableName} AS task
|
|
@@ -17403,13 +17579,12 @@ class PostgresJsonRecordStore {
|
|
|
17403
17579
|
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
17404
17580
|
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
17405
17581
|
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
17406
|
-
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
});
|
|
17582
|
+
this.service,
|
|
17583
|
+
id,
|
|
17584
|
+
timestamp,
|
|
17585
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
17586
|
+
]);
|
|
17587
|
+
return Boolean(result.rows[0]?.found);
|
|
17413
17588
|
}
|
|
17414
17589
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
17415
17590
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
@@ -17865,6 +18040,102 @@ class PostgresJsonRecordStore {
|
|
|
17865
18040
|
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
17866
18041
|
}
|
|
17867
18042
|
}
|
|
18043
|
+
async function bulkCreateTasksAtomic(inputs, store, context) {
|
|
18044
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
18045
|
+
const tempIds = new Map;
|
|
18046
|
+
const created = [];
|
|
18047
|
+
const dependencies = [];
|
|
18048
|
+
for (const input of inputs) {
|
|
18049
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
18050
|
+
const task = await createTask(taskInput, scoped, context);
|
|
18051
|
+
if (temp_id)
|
|
18052
|
+
tempIds.set(temp_id, task.id);
|
|
18053
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
18054
|
+
}
|
|
18055
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
18056
|
+
const input = inputs[index];
|
|
18057
|
+
const taskId = created[index].id;
|
|
18058
|
+
const seenDependencies = new Set;
|
|
18059
|
+
for (const reference of input.depends_on ?? []) {
|
|
18060
|
+
const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
|
|
18061
|
+
const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
|
|
18062
|
+
if (!dependencyId)
|
|
18063
|
+
throw new TaskNotFoundError(reference);
|
|
18064
|
+
if (seenDependencies.has(dependencyId)) {
|
|
18065
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
18066
|
+
}
|
|
18067
|
+
seenDependencies.add(dependencyId);
|
|
18068
|
+
dependencies.push(await addDependency(taskId, dependencyId, scoped, context));
|
|
18069
|
+
}
|
|
18070
|
+
}
|
|
18071
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
18072
|
+
});
|
|
18073
|
+
}
|
|
18074
|
+
async function bulkDeleteTasksAtomic(ids, force, store, context) {
|
|
18075
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
18076
|
+
const resolved = [];
|
|
18077
|
+
for (const reference of ids) {
|
|
18078
|
+
resolved.push({
|
|
18079
|
+
reference,
|
|
18080
|
+
task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
|
|
18081
|
+
});
|
|
18082
|
+
}
|
|
18083
|
+
const seen = new Set;
|
|
18084
|
+
for (const item of resolved) {
|
|
18085
|
+
if (!item.task)
|
|
18086
|
+
continue;
|
|
18087
|
+
if (seen.has(item.task.id)) {
|
|
18088
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
18089
|
+
}
|
|
18090
|
+
seen.add(item.task.id);
|
|
18091
|
+
}
|
|
18092
|
+
const childState = new Map;
|
|
18093
|
+
for (const item of resolved) {
|
|
18094
|
+
if (!item.task)
|
|
18095
|
+
continue;
|
|
18096
|
+
const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
|
|
18097
|
+
childState.set(item.task.id, children.length > 0);
|
|
18098
|
+
}
|
|
18099
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
18100
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
18101
|
+
const roots = [];
|
|
18102
|
+
for (const item of planned) {
|
|
18103
|
+
if (!force) {
|
|
18104
|
+
roots.push(item);
|
|
18105
|
+
continue;
|
|
18106
|
+
}
|
|
18107
|
+
let parentId = item.task.parent_id;
|
|
18108
|
+
let covered = false;
|
|
18109
|
+
while (parentId) {
|
|
18110
|
+
if (plannedIds.has(parentId)) {
|
|
18111
|
+
covered = true;
|
|
18112
|
+
break;
|
|
18113
|
+
}
|
|
18114
|
+
parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
|
|
18115
|
+
}
|
|
18116
|
+
if (!covered)
|
|
18117
|
+
roots.push(item);
|
|
18118
|
+
}
|
|
18119
|
+
for (const item of roots) {
|
|
18120
|
+
if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
|
|
18121
|
+
throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
return {
|
|
18125
|
+
schema_version: 1,
|
|
18126
|
+
atomic: true,
|
|
18127
|
+
force,
|
|
18128
|
+
results: resolved.map((item) => {
|
|
18129
|
+
if (!item.task)
|
|
18130
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
18131
|
+
if (!force && childState.get(item.task.id)) {
|
|
18132
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
18133
|
+
}
|
|
18134
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
18135
|
+
})
|
|
18136
|
+
};
|
|
18137
|
+
});
|
|
18138
|
+
}
|
|
17868
18139
|
async function createTask(input, store, context) {
|
|
17869
18140
|
const timestamp = new Date().toISOString();
|
|
17870
18141
|
const taskId = randomUUID2();
|
|
@@ -17955,12 +18226,16 @@ async function updateTask(id, input, store, context) {
|
|
|
17955
18226
|
}
|
|
17956
18227
|
}
|
|
17957
18228
|
}
|
|
18229
|
+
const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
|
|
18230
|
+
const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
|
|
18231
|
+
const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId(effectiveProjectIdForShortId, store, context) : null;
|
|
17958
18232
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
17959
18233
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
17960
18234
|
const task = {
|
|
17961
18235
|
...existing,
|
|
17962
18236
|
...definedPatch(input),
|
|
17963
18237
|
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
18238
|
+
short_id: backfilledShortId ?? existing.short_id,
|
|
17964
18239
|
version: existing.version + 1,
|
|
17965
18240
|
updated_at: new Date().toISOString(),
|
|
17966
18241
|
tags: input.tags ?? existing.tags,
|
|
@@ -19652,6 +19927,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
19652
19927
|
|
|
19653
19928
|
// src/storage/local-sqlite.ts
|
|
19654
19929
|
init_tasks();
|
|
19930
|
+
init_task_graph();
|
|
19655
19931
|
init_projects();
|
|
19656
19932
|
init_plans();
|
|
19657
19933
|
|
|
@@ -20210,6 +20486,19 @@ function listAgents(opts, db) {
|
|
|
20210
20486
|
}
|
|
20211
20487
|
return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
|
|
20212
20488
|
}
|
|
20489
|
+
function listAgentsPage(options, db) {
|
|
20490
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
|
|
20491
|
+
throw new Error("Agent page limit must be an integer from 1 to 500");
|
|
20492
|
+
}
|
|
20493
|
+
if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
|
|
20494
|
+
throw new Error("Agent page offset must be a non-negative integer");
|
|
20495
|
+
}
|
|
20496
|
+
const d = db || getDatabase();
|
|
20497
|
+
const where = options.include_archived ? "" : "WHERE status = 'active'";
|
|
20498
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
|
|
20499
|
+
const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
|
|
20500
|
+
return { agents, total };
|
|
20501
|
+
}
|
|
20213
20502
|
function updateAgentActivity(id, db) {
|
|
20214
20503
|
const d = db || getDatabase();
|
|
20215
20504
|
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
@@ -21048,6 +21337,98 @@ function exportSqliteMachines(db) {
|
|
|
21048
21337
|
}
|
|
21049
21338
|
|
|
21050
21339
|
// src/storage/local-sqlite.ts
|
|
21340
|
+
function bulkCreateAtomicSqlite(db, inputs) {
|
|
21341
|
+
return db.transaction(() => {
|
|
21342
|
+
const tempIds = new Map;
|
|
21343
|
+
const created = [];
|
|
21344
|
+
const dependencies = [];
|
|
21345
|
+
for (const input of inputs) {
|
|
21346
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
21347
|
+
const task = createTask2(taskInput, db);
|
|
21348
|
+
if (temp_id)
|
|
21349
|
+
tempIds.set(temp_id, task.id);
|
|
21350
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
21351
|
+
}
|
|
21352
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
21353
|
+
const input = inputs[index];
|
|
21354
|
+
const taskId = created[index].id;
|
|
21355
|
+
const seenDependencies = new Set;
|
|
21356
|
+
for (const reference of input.depends_on ?? []) {
|
|
21357
|
+
const dependencyId2 = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
|
|
21358
|
+
if (!dependencyId2)
|
|
21359
|
+
throw new TaskNotFoundError(reference);
|
|
21360
|
+
if (seenDependencies.has(dependencyId2)) {
|
|
21361
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId2}`);
|
|
21362
|
+
}
|
|
21363
|
+
seenDependencies.add(dependencyId2);
|
|
21364
|
+
addDependency2(taskId, dependencyId2, db);
|
|
21365
|
+
dependencies.push({ task_id: taskId, depends_on: dependencyId2 });
|
|
21366
|
+
}
|
|
21367
|
+
}
|
|
21368
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
21369
|
+
})();
|
|
21370
|
+
}
|
|
21371
|
+
function deleteTaskHierarchySqlite(db, rootId) {
|
|
21372
|
+
const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
|
|
21373
|
+
SELECT id, 0 FROM tasks WHERE id = ?
|
|
21374
|
+
UNION ALL
|
|
21375
|
+
SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
|
|
21376
|
+
) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
|
|
21377
|
+
if (rows.length === 0)
|
|
21378
|
+
throw new TaskNotFoundError(rootId);
|
|
21379
|
+
for (const row of rows) {
|
|
21380
|
+
if (!deleteTask(row.id, db))
|
|
21381
|
+
throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
|
|
21382
|
+
}
|
|
21383
|
+
}
|
|
21384
|
+
function bulkDeleteAtomicSqlite(db, ids, force) {
|
|
21385
|
+
return db.transaction(() => {
|
|
21386
|
+
const resolved = ids.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
|
|
21387
|
+
const seen = new Set;
|
|
21388
|
+
for (const item of resolved) {
|
|
21389
|
+
if (!item.task)
|
|
21390
|
+
continue;
|
|
21391
|
+
if (seen.has(item.task.id)) {
|
|
21392
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
21393
|
+
}
|
|
21394
|
+
seen.add(item.task.id);
|
|
21395
|
+
}
|
|
21396
|
+
const childState = new Map;
|
|
21397
|
+
for (const item of resolved) {
|
|
21398
|
+
if (!item.task)
|
|
21399
|
+
continue;
|
|
21400
|
+
childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
|
|
21401
|
+
}
|
|
21402
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
21403
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
21404
|
+
const roots = planned.filter((item) => {
|
|
21405
|
+
if (!force)
|
|
21406
|
+
return true;
|
|
21407
|
+
let parentId = item.task.parent_id;
|
|
21408
|
+
while (parentId) {
|
|
21409
|
+
if (plannedIds.has(parentId))
|
|
21410
|
+
return false;
|
|
21411
|
+
parentId = getTask(parentId, db)?.parent_id ?? null;
|
|
21412
|
+
}
|
|
21413
|
+
return true;
|
|
21414
|
+
});
|
|
21415
|
+
for (const item of roots)
|
|
21416
|
+
deleteTaskHierarchySqlite(db, item.task.id);
|
|
21417
|
+
return {
|
|
21418
|
+
schema_version: 1,
|
|
21419
|
+
atomic: true,
|
|
21420
|
+
force,
|
|
21421
|
+
results: resolved.map((item) => {
|
|
21422
|
+
if (!item.task)
|
|
21423
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
21424
|
+
if (!force && childState.get(item.task.id)) {
|
|
21425
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
21426
|
+
}
|
|
21427
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
21428
|
+
})
|
|
21429
|
+
};
|
|
21430
|
+
})();
|
|
21431
|
+
}
|
|
21051
21432
|
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;
|
|
21052
21433
|
function resolveTaskRefLocal(db, ref) {
|
|
21053
21434
|
const raw = ref.trim().toLowerCase();
|
|
@@ -21144,6 +21525,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
21144
21525
|
},
|
|
21145
21526
|
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
21146
21527
|
delete: (id) => deleteTask(id, database()),
|
|
21528
|
+
bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
|
|
21529
|
+
bulkDeleteAtomic: (ids, force, _context) => bulkDeleteAtomicSqlite(database(), ids, force),
|
|
21147
21530
|
start: (id, agentId) => startTask2(id, agentId, database()),
|
|
21148
21531
|
complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
|
|
21149
21532
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, database()),
|
|
@@ -21152,6 +21535,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
21152
21535
|
getActiveWork: (filters) => getActiveWork2(filters, database()),
|
|
21153
21536
|
getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
21154
21537
|
},
|
|
21538
|
+
dependencies: {
|
|
21539
|
+
add: (taskId, dependsOn) => {
|
|
21540
|
+
addDependency2(taskId, dependsOn, database());
|
|
21541
|
+
return { task_id: taskId, depends_on: dependsOn };
|
|
21542
|
+
},
|
|
21543
|
+
remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, database()),
|
|
21544
|
+
list: (taskId) => {
|
|
21545
|
+
const dependencies = getTaskDependencies(taskId, database());
|
|
21546
|
+
const blocks = getTaskDependents(taskId, database());
|
|
21547
|
+
return { dependencies, blocks, blocked_by: blocks };
|
|
21548
|
+
},
|
|
21549
|
+
listPage: ({ limit, offset }) => {
|
|
21550
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
21551
|
+
throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
|
|
21552
|
+
}
|
|
21553
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
21554
|
+
throw new Error("SQLite dependency page offset must be a non-negative integer");
|
|
21555
|
+
}
|
|
21556
|
+
const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
|
|
21557
|
+
const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
|
|
21558
|
+
return { dependencies, total: totalRow.total };
|
|
21559
|
+
},
|
|
21560
|
+
listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
|
|
21561
|
+
},
|
|
21155
21562
|
projects: {
|
|
21156
21563
|
create: (input) => createProject2(input, database()),
|
|
21157
21564
|
get: (id) => getProject(id, database()),
|
|
@@ -21195,6 +21602,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
21195
21602
|
get: (id) => getAgent(id, database()),
|
|
21196
21603
|
getByName: (name) => getAgentByName(name, database()),
|
|
21197
21604
|
list: (options2) => listAgents(options2, database()),
|
|
21605
|
+
listPage: (options2) => listAgentsPage(options2, database()),
|
|
21198
21606
|
update: (id, input) => updateAgent2(id, input, database())
|
|
21199
21607
|
},
|
|
21200
21608
|
taskLists: {
|
|
@@ -21233,6 +21641,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
21233
21641
|
return comments;
|
|
21234
21642
|
},
|
|
21235
21643
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
21644
|
+
getTaskHistoryPage: (taskId, options2) => {
|
|
21645
|
+
if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
|
|
21646
|
+
throw new Error("Task history limit must be an integer from 1 to 500");
|
|
21647
|
+
}
|
|
21648
|
+
if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
|
|
21649
|
+
throw new Error("Task history offset must be a non-negative integer");
|
|
21650
|
+
}
|
|
21651
|
+
const conditions = ["task_id = ?"];
|
|
21652
|
+
const values = [taskId];
|
|
21653
|
+
if (options2.since) {
|
|
21654
|
+
conditions.push("created_at >= ?");
|
|
21655
|
+
values.push(options2.since);
|
|
21656
|
+
}
|
|
21657
|
+
if (options2.until) {
|
|
21658
|
+
conditions.push("created_at <= ?");
|
|
21659
|
+
values.push(options2.until);
|
|
21660
|
+
}
|
|
21661
|
+
const where = conditions.join(" AND ");
|
|
21662
|
+
const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
|
|
21663
|
+
const direction = options2.order === "asc" ? "ASC" : "DESC";
|
|
21664
|
+
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);
|
|
21665
|
+
return { history, total };
|
|
21666
|
+
},
|
|
21236
21667
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
21237
21668
|
},
|
|
21238
21669
|
sync: {
|