@hasna/todos 0.11.71 → 0.11.73
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/README.md +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2162 -1450
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +6526 -6258
- package/dist/db/api-keys.d.ts +9 -0
- package/dist/db/api-keys.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +1 -0
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-status.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10121 -9282
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/lib/task-route-contract.d.ts +2 -1
- package/dist/lib/task-route-contract.d.ts.map +1 -1
- package/dist/lib/task-route-sources.d.ts +68 -0
- package/dist/lib/task-route-sources.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1869 -1650
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp.js +6 -8
- package/dist/registry.js +6526 -6258
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +6 -4
- package/dist/server/index.js +1871 -1652
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +3324 -3029
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -1807,6 +1807,11 @@ function ensureSchema(db) {
|
|
|
1807
1807
|
ensureColumn("tasks", "priority_score", "INTEGER");
|
|
1808
1808
|
ensureColumn("tasks", "priority_reason", "TEXT");
|
|
1809
1809
|
ensureColumn("tasks", "archived_at", "TEXT");
|
|
1810
|
+
ensureColumn("tasks", "runner_id", "TEXT");
|
|
1811
|
+
ensureColumn("tasks", "runner_started_at", "TEXT");
|
|
1812
|
+
ensureColumn("tasks", "runner_completed_at", "TEXT");
|
|
1813
|
+
ensureColumn("tasks", "current_step", "TEXT");
|
|
1814
|
+
ensureColumn("tasks", "total_steps", "INTEGER");
|
|
1810
1815
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
1811
1816
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
1812
1817
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -1814,6 +1819,10 @@ function ensureSchema(db) {
|
|
|
1814
1819
|
ensureColumn("agents", "level", "TEXT");
|
|
1815
1820
|
ensureColumn("agents", "org_id", "TEXT");
|
|
1816
1821
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1822
|
+
ensureColumn("agents", "session_id", "TEXT");
|
|
1823
|
+
ensureColumn("agents", "working_dir", "TEXT");
|
|
1824
|
+
ensureColumn("agents", "active_project_id", "TEXT");
|
|
1825
|
+
ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
|
|
1817
1826
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1818
1827
|
ensureColumn("plans", "slug", "TEXT");
|
|
1819
1828
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
@@ -2731,34 +2740,40 @@ function ensureDir(filePath) {
|
|
|
2731
2740
|
mkdirSync(dir, { recursive: true });
|
|
2732
2741
|
}
|
|
2733
2742
|
}
|
|
2743
|
+
function openDatabase(path) {
|
|
2744
|
+
ensureDir(path);
|
|
2745
|
+
const db = new Database(path);
|
|
2746
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
2747
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
2748
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
2749
|
+
runMigrations(db);
|
|
2750
|
+
backfillTaskTags(db);
|
|
2751
|
+
backfillMachineId(db);
|
|
2752
|
+
return db;
|
|
2753
|
+
}
|
|
2734
2754
|
function getDatabase(dbPath) {
|
|
2735
2755
|
const path = dbPath || getDbPath();
|
|
2736
2756
|
if (_db && _dbPath === path)
|
|
2737
2757
|
return _db;
|
|
2738
|
-
|
|
2739
|
-
_db.close();
|
|
2740
|
-
_db = null;
|
|
2741
|
-
_dbPath = null;
|
|
2742
|
-
}
|
|
2743
|
-
ensureDir(path);
|
|
2744
|
-
_db = new Database(path);
|
|
2758
|
+
_db = openDatabase(path);
|
|
2745
2759
|
_dbPath = path;
|
|
2746
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
2747
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
2748
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
2749
|
-
runMigrations(_db);
|
|
2750
|
-
backfillTaskTags(_db);
|
|
2751
|
-
backfillMachineId(_db);
|
|
2752
2760
|
return _db;
|
|
2753
2761
|
}
|
|
2754
2762
|
function closeDatabase() {
|
|
2755
2763
|
if (_db) {
|
|
2756
|
-
|
|
2764
|
+
try {
|
|
2765
|
+
_db.close();
|
|
2766
|
+
} catch {}
|
|
2757
2767
|
_db = null;
|
|
2758
2768
|
_dbPath = null;
|
|
2759
2769
|
}
|
|
2760
2770
|
}
|
|
2761
2771
|
function resetDatabase() {
|
|
2772
|
+
if (_db) {
|
|
2773
|
+
try {
|
|
2774
|
+
_db.close();
|
|
2775
|
+
} catch {}
|
|
2776
|
+
}
|
|
2762
2777
|
_db = null;
|
|
2763
2778
|
_dbPath = null;
|
|
2764
2779
|
}
|
|
@@ -9706,8 +9721,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
9706
9721
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
9707
9722
|
if (explicit !== undefined)
|
|
9708
9723
|
return explicit;
|
|
9709
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
9710
|
-
return true;
|
|
9711
9724
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
9712
9725
|
if (taskListDefault !== undefined)
|
|
9713
9726
|
return taskListDefault;
|
|
@@ -9726,8 +9739,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
9726
9739
|
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
9727
9740
|
};
|
|
9728
9741
|
}
|
|
9729
|
-
function
|
|
9730
|
-
|
|
9742
|
+
function metadataStringField(record, keys) {
|
|
9743
|
+
if (!record)
|
|
9744
|
+
return;
|
|
9745
|
+
for (const key of keys) {
|
|
9746
|
+
const value = record[key];
|
|
9747
|
+
if (typeof value === "string" && value.trim())
|
|
9748
|
+
return value.trim();
|
|
9749
|
+
}
|
|
9750
|
+
return;
|
|
9751
|
+
}
|
|
9752
|
+
function projectKindFromMetadata(...records) {
|
|
9753
|
+
for (const record of records) {
|
|
9754
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
9755
|
+
if (value)
|
|
9756
|
+
return value;
|
|
9757
|
+
}
|
|
9758
|
+
return null;
|
|
9759
|
+
}
|
|
9760
|
+
function classifyProjectKind(_path, metadata) {
|
|
9761
|
+
return projectKindFromMetadata(metadata);
|
|
9731
9762
|
}
|
|
9732
9763
|
function isWorktreePath(path) {
|
|
9733
9764
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -9800,7 +9831,6 @@ function taskEventMetadata(task) {
|
|
|
9800
9831
|
metadata.project_canonical_path = projectPath;
|
|
9801
9832
|
}
|
|
9802
9833
|
if (projectPath) {
|
|
9803
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
9804
9834
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
9805
9835
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
9806
9836
|
}
|
|
@@ -9812,6 +9842,10 @@ function taskEventMetadata(task) {
|
|
|
9812
9842
|
metadata.task_list_project_id = taskList.project_id;
|
|
9813
9843
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
9814
9844
|
}
|
|
9845
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
9846
|
+
if (projectKind) {
|
|
9847
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
9848
|
+
}
|
|
9815
9849
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
9816
9850
|
if (routeEnabled !== undefined) {
|
|
9817
9851
|
metadata.route_enabled = routeEnabled;
|
|
@@ -10366,1767 +10400,1858 @@ var init_checklists = __esm(() => {
|
|
|
10366
10400
|
init_database();
|
|
10367
10401
|
});
|
|
10368
10402
|
|
|
10369
|
-
// src/
|
|
10370
|
-
function
|
|
10403
|
+
// src/lib/recurrence.ts
|
|
10404
|
+
function parseRecurrenceRule(rule) {
|
|
10405
|
+
const normalized = rule.trim().toLowerCase();
|
|
10406
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
10407
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
10408
|
+
}
|
|
10409
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
10410
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
10411
|
+
}
|
|
10412
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
10413
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
10414
|
+
}
|
|
10415
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
10416
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
10417
|
+
}
|
|
10418
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
10419
|
+
if (intervalMatch) {
|
|
10420
|
+
return {
|
|
10421
|
+
type: "interval",
|
|
10422
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
10423
|
+
unit: intervalMatch[2]
|
|
10424
|
+
};
|
|
10425
|
+
}
|
|
10426
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
10427
|
+
if (daysMatch) {
|
|
10428
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
10429
|
+
const days = [];
|
|
10430
|
+
for (const part of dayParts) {
|
|
10431
|
+
const dayNum = DAY_NAMES[part];
|
|
10432
|
+
if (dayNum !== undefined) {
|
|
10433
|
+
days.push(dayNum);
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
if (days.length > 0) {
|
|
10437
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
10438
|
+
}
|
|
10439
|
+
}
|
|
10440
|
+
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
10441
|
+
}
|
|
10442
|
+
function isValidRecurrenceRule(rule) {
|
|
10443
|
+
try {
|
|
10444
|
+
parseRecurrenceRule(rule);
|
|
10445
|
+
return true;
|
|
10446
|
+
} catch {
|
|
10447
|
+
return false;
|
|
10448
|
+
}
|
|
10449
|
+
}
|
|
10450
|
+
function nextOccurrence(rule, from) {
|
|
10451
|
+
const parsed = parseRecurrenceRule(rule);
|
|
10452
|
+
const base = from || new Date;
|
|
10453
|
+
if (parsed.type === "interval") {
|
|
10454
|
+
const next = new Date(base);
|
|
10455
|
+
if (parsed.unit === "day") {
|
|
10456
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
10457
|
+
} else if (parsed.unit === "week") {
|
|
10458
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
10459
|
+
} else if (parsed.unit === "month") {
|
|
10460
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
10461
|
+
}
|
|
10462
|
+
return next.toISOString();
|
|
10463
|
+
}
|
|
10464
|
+
if (parsed.type === "specific_days") {
|
|
10465
|
+
const currentDay = base.getDay();
|
|
10466
|
+
const days = parsed.days;
|
|
10467
|
+
let daysToAdd = Infinity;
|
|
10468
|
+
for (const day of days) {
|
|
10469
|
+
let diff = day - currentDay;
|
|
10470
|
+
if (diff <= 0)
|
|
10471
|
+
diff += 7;
|
|
10472
|
+
if (diff < daysToAdd)
|
|
10473
|
+
daysToAdd = diff;
|
|
10474
|
+
}
|
|
10475
|
+
const next = new Date(base);
|
|
10476
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
10477
|
+
return next.toISOString();
|
|
10478
|
+
}
|
|
10479
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
10480
|
+
}
|
|
10481
|
+
var DAY_NAMES;
|
|
10482
|
+
var init_recurrence = __esm(() => {
|
|
10483
|
+
DAY_NAMES = {
|
|
10484
|
+
sunday: 0,
|
|
10485
|
+
sun: 0,
|
|
10486
|
+
monday: 1,
|
|
10487
|
+
mon: 1,
|
|
10488
|
+
tuesday: 2,
|
|
10489
|
+
tue: 2,
|
|
10490
|
+
wednesday: 3,
|
|
10491
|
+
wed: 3,
|
|
10492
|
+
thursday: 4,
|
|
10493
|
+
thu: 4,
|
|
10494
|
+
friday: 5,
|
|
10495
|
+
fri: 5,
|
|
10496
|
+
saturday: 6,
|
|
10497
|
+
sat: 6
|
|
10498
|
+
};
|
|
10499
|
+
});
|
|
10500
|
+
|
|
10501
|
+
// src/db/templates.ts
|
|
10502
|
+
var exports_templates = {};
|
|
10503
|
+
__export(exports_templates, {
|
|
10504
|
+
updateTemplate: () => updateTemplate,
|
|
10505
|
+
tasksFromTemplate: () => tasksFromTemplate,
|
|
10506
|
+
taskFromTemplate: () => taskFromTemplate,
|
|
10507
|
+
resolveVariables: () => resolveVariables,
|
|
10508
|
+
previewTemplate: () => previewTemplate,
|
|
10509
|
+
listTemplates: () => listTemplates,
|
|
10510
|
+
listTemplateVersions: () => listTemplateVersions,
|
|
10511
|
+
importTemplate: () => importTemplate,
|
|
10512
|
+
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
10513
|
+
getTemplateVersion: () => getTemplateVersion,
|
|
10514
|
+
getTemplateTasks: () => getTemplateTasks,
|
|
10515
|
+
getTemplate: () => getTemplate,
|
|
10516
|
+
exportTemplate: () => exportTemplate,
|
|
10517
|
+
evaluateCondition: () => evaluateCondition,
|
|
10518
|
+
deleteTemplate: () => deleteTemplate,
|
|
10519
|
+
createTemplate: () => createTemplate,
|
|
10520
|
+
addTemplateTasks: () => addTemplateTasks
|
|
10521
|
+
});
|
|
10522
|
+
function rowToTemplate(row) {
|
|
10371
10523
|
return {
|
|
10372
10524
|
...row,
|
|
10373
10525
|
tags: JSON.parse(row.tags || "[]"),
|
|
10526
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
10374
10527
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
requires_approval: !!row.requires_approval
|
|
10528
|
+
priority: row.priority || "medium",
|
|
10529
|
+
version: row.version ?? 1
|
|
10378
10530
|
};
|
|
10379
10531
|
}
|
|
10380
|
-
function
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
10391
|
-
insertTaskTags(taskId, tags, db);
|
|
10532
|
+
function rowToTemplateTask(row) {
|
|
10533
|
+
return {
|
|
10534
|
+
...row,
|
|
10535
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
10536
|
+
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
10537
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
10538
|
+
priority: row.priority || "medium",
|
|
10539
|
+
condition: row.condition ?? null,
|
|
10540
|
+
include_template_id: row.include_template_id ?? null
|
|
10541
|
+
};
|
|
10392
10542
|
}
|
|
10393
|
-
function
|
|
10394
|
-
|
|
10395
|
-
return;
|
|
10396
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
10397
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
10398
|
-
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
10399
|
-
}
|
|
10400
|
-
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
10401
|
-
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
10402
|
-
}
|
|
10543
|
+
function resolveTemplateId(id, d) {
|
|
10544
|
+
return resolvePartialId(d, "task_templates", id);
|
|
10403
10545
|
}
|
|
10404
|
-
function
|
|
10546
|
+
function createTemplate(input, db) {
|
|
10405
10547
|
const d = db || getDatabase();
|
|
10406
|
-
const
|
|
10407
|
-
const tags = input.tags || [];
|
|
10548
|
+
const id = uuid();
|
|
10408
10549
|
const machineId = currentStorageMachineId(d);
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
input.priority || "medium",
|
|
10427
|
-
input.agent_id || null,
|
|
10428
|
-
input.assigned_to || null,
|
|
10429
|
-
input.session_id || null,
|
|
10430
|
-
input.working_dir || null,
|
|
10431
|
-
JSON.stringify(tags),
|
|
10432
|
-
JSON.stringify(input.metadata || {}),
|
|
10433
|
-
timestamp,
|
|
10434
|
-
timestamp,
|
|
10435
|
-
input.due_at || null,
|
|
10436
|
-
input.estimated_minutes || null,
|
|
10437
|
-
input.sla_minutes ?? null,
|
|
10438
|
-
input.confidence ?? null,
|
|
10439
|
-
input.retry_count ?? 0,
|
|
10440
|
-
input.max_retries ?? 3,
|
|
10441
|
-
input.retry_after ?? null,
|
|
10442
|
-
input.requires_approval ? 1 : 0,
|
|
10443
|
-
null,
|
|
10444
|
-
null,
|
|
10445
|
-
input.recurrence_rule || null,
|
|
10446
|
-
input.recurrence_parent_id || null,
|
|
10447
|
-
input.spawns_template_id || null,
|
|
10448
|
-
input.reason || null,
|
|
10449
|
-
input.spawned_from_session || null,
|
|
10450
|
-
assignedBy || null,
|
|
10451
|
-
assignedFromProject || null,
|
|
10452
|
-
input.task_type || null,
|
|
10453
|
-
machineId
|
|
10454
|
-
]);
|
|
10455
|
-
break;
|
|
10456
|
-
} catch (e) {
|
|
10457
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
10458
|
-
id = uuid();
|
|
10459
|
-
continue;
|
|
10460
|
-
}
|
|
10461
|
-
throw e;
|
|
10462
|
-
}
|
|
10463
|
-
}
|
|
10464
|
-
if (tags.length > 0) {
|
|
10465
|
-
insertTaskTags(id, tags, d);
|
|
10550
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
|
|
10551
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10552
|
+
id,
|
|
10553
|
+
input.name,
|
|
10554
|
+
input.title_pattern,
|
|
10555
|
+
input.description || null,
|
|
10556
|
+
input.priority || "medium",
|
|
10557
|
+
JSON.stringify(input.tags || []),
|
|
10558
|
+
JSON.stringify(input.variables || []),
|
|
10559
|
+
input.project_id || null,
|
|
10560
|
+
input.plan_id || null,
|
|
10561
|
+
JSON.stringify(input.metadata || {}),
|
|
10562
|
+
now(),
|
|
10563
|
+
machineId
|
|
10564
|
+
]);
|
|
10565
|
+
if (input.tasks && input.tasks.length > 0) {
|
|
10566
|
+
addTemplateTasks(id, input.tasks, d);
|
|
10466
10567
|
}
|
|
10467
|
-
|
|
10468
|
-
const payload = taskEventData(task);
|
|
10469
|
-
const databasePath = databasePathFromDatabase(d);
|
|
10470
|
-
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
10471
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
10472
|
-
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
10473
|
-
return task;
|
|
10568
|
+
return getTemplate(id, d);
|
|
10474
10569
|
}
|
|
10475
|
-
function
|
|
10570
|
+
function getTemplate(id, db) {
|
|
10476
10571
|
const d = db || getDatabase();
|
|
10477
|
-
const
|
|
10478
|
-
if (!
|
|
10572
|
+
const resolved = resolveTemplateId(id, d);
|
|
10573
|
+
if (!resolved)
|
|
10479
10574
|
return null;
|
|
10480
|
-
|
|
10575
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
10576
|
+
return row ? rowToTemplate(row) : null;
|
|
10481
10577
|
}
|
|
10482
|
-
function
|
|
10578
|
+
function listTemplates(db) {
|
|
10483
10579
|
const d = db || getDatabase();
|
|
10484
|
-
|
|
10485
|
-
if (!task)
|
|
10486
|
-
return null;
|
|
10487
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
10488
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
10489
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
10490
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
10491
|
-
WHERE td.task_id = ?`).all(id);
|
|
10492
|
-
const dependencies = depRows.map(rowToTask);
|
|
10493
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
10494
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
10495
|
-
WHERE td.depends_on = ?`).all(id);
|
|
10496
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
10497
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
10498
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
10499
|
-
const checklist = getChecklist(id, d);
|
|
10500
|
-
return {
|
|
10501
|
-
...task,
|
|
10502
|
-
subtasks,
|
|
10503
|
-
dependencies,
|
|
10504
|
-
blocked_by,
|
|
10505
|
-
comments,
|
|
10506
|
-
parent,
|
|
10507
|
-
checklist
|
|
10508
|
-
};
|
|
10580
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
10509
10581
|
}
|
|
10510
|
-
function
|
|
10582
|
+
function deleteTemplate(id, db) {
|
|
10511
10583
|
const d = db || getDatabase();
|
|
10512
|
-
const
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
const
|
|
10516
|
-
if (
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
}
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
if (
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
conditions.push("priority = ?");
|
|
10547
|
-
params.push(filter.priority);
|
|
10548
|
-
}
|
|
10549
|
-
}
|
|
10550
|
-
if (filter.assigned_to) {
|
|
10551
|
-
conditions.push("assigned_to = ?");
|
|
10552
|
-
params.push(filter.assigned_to);
|
|
10553
|
-
}
|
|
10554
|
-
if (filter.agent_id) {
|
|
10555
|
-
conditions.push("agent_id = ?");
|
|
10556
|
-
params.push(filter.agent_id);
|
|
10584
|
+
const resolved = resolveTemplateId(id, d);
|
|
10585
|
+
if (!resolved)
|
|
10586
|
+
return false;
|
|
10587
|
+
const template = getTemplate(resolved, d);
|
|
10588
|
+
if (!template)
|
|
10589
|
+
return false;
|
|
10590
|
+
recordStorageTombstone({
|
|
10591
|
+
object_type: "templates",
|
|
10592
|
+
object_id: resolved,
|
|
10593
|
+
payload: template,
|
|
10594
|
+
version: template.version
|
|
10595
|
+
}, d);
|
|
10596
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
10597
|
+
}
|
|
10598
|
+
function updateTemplate(id, updates, db) {
|
|
10599
|
+
const d = db || getDatabase();
|
|
10600
|
+
const resolved = resolveTemplateId(id, d);
|
|
10601
|
+
if (!resolved)
|
|
10602
|
+
return null;
|
|
10603
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
10604
|
+
if (current) {
|
|
10605
|
+
const snapshot = JSON.stringify({
|
|
10606
|
+
name: current.name,
|
|
10607
|
+
title_pattern: current.title_pattern,
|
|
10608
|
+
description: current.description,
|
|
10609
|
+
priority: current.priority,
|
|
10610
|
+
tags: current.tags,
|
|
10611
|
+
variables: current.variables,
|
|
10612
|
+
project_id: current.project_id,
|
|
10613
|
+
plan_id: current.plan_id,
|
|
10614
|
+
metadata: current.metadata,
|
|
10615
|
+
tasks: current.tasks
|
|
10616
|
+
});
|
|
10617
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
10557
10618
|
}
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10619
|
+
const sets = ["version = version + 1"];
|
|
10620
|
+
const values = [];
|
|
10621
|
+
if (updates.name !== undefined) {
|
|
10622
|
+
sets.push("name = ?");
|
|
10623
|
+
values.push(updates.name);
|
|
10561
10624
|
}
|
|
10562
|
-
if (
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
params.push(...filter.tags);
|
|
10625
|
+
if (updates.title_pattern !== undefined) {
|
|
10626
|
+
sets.push("title_pattern = ?");
|
|
10627
|
+
values.push(updates.title_pattern);
|
|
10566
10628
|
}
|
|
10567
|
-
if (
|
|
10568
|
-
|
|
10569
|
-
|
|
10629
|
+
if (updates.description !== undefined) {
|
|
10630
|
+
sets.push("description = ?");
|
|
10631
|
+
values.push(updates.description);
|
|
10570
10632
|
}
|
|
10571
|
-
if (
|
|
10572
|
-
|
|
10573
|
-
|
|
10633
|
+
if (updates.priority !== undefined) {
|
|
10634
|
+
sets.push("priority = ?");
|
|
10635
|
+
values.push(updates.priority);
|
|
10574
10636
|
}
|
|
10575
|
-
if (
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
conditions.push("recurrence_rule IS NULL");
|
|
10637
|
+
if (updates.tags !== undefined) {
|
|
10638
|
+
sets.push("tags = ?");
|
|
10639
|
+
values.push(JSON.stringify(updates.tags));
|
|
10579
10640
|
}
|
|
10580
|
-
if (
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
params.push(...filter.task_type);
|
|
10584
|
-
} else {
|
|
10585
|
-
conditions.push("task_type = ?");
|
|
10586
|
-
params.push(filter.task_type);
|
|
10587
|
-
}
|
|
10641
|
+
if (updates.variables !== undefined) {
|
|
10642
|
+
sets.push("variables = ?");
|
|
10643
|
+
values.push(JSON.stringify(updates.variables));
|
|
10588
10644
|
}
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
try {
|
|
10593
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
10594
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
10595
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
10596
|
-
} catch {}
|
|
10645
|
+
if (updates.project_id !== undefined) {
|
|
10646
|
+
sets.push("project_id = ?");
|
|
10647
|
+
values.push(updates.project_id);
|
|
10597
10648
|
}
|
|
10598
|
-
if (
|
|
10599
|
-
|
|
10649
|
+
if (updates.plan_id !== undefined) {
|
|
10650
|
+
sets.push("plan_id = ?");
|
|
10651
|
+
values.push(updates.plan_id);
|
|
10600
10652
|
}
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
limitClause = " LIMIT ?";
|
|
10605
|
-
params.push(filter.limit);
|
|
10606
|
-
if (!filter.cursor && filter.offset) {
|
|
10607
|
-
limitClause += " OFFSET ?";
|
|
10608
|
-
params.push(filter.offset);
|
|
10609
|
-
}
|
|
10653
|
+
if (updates.metadata !== undefined) {
|
|
10654
|
+
sets.push("metadata = ?");
|
|
10655
|
+
values.push(JSON.stringify(updates.metadata));
|
|
10610
10656
|
}
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
function getTaskByFingerprint(fingerprint, db) {
|
|
10615
|
-
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
10616
|
-
return tasks[0] ?? null;
|
|
10657
|
+
values.push(resolved);
|
|
10658
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
10659
|
+
return getTemplate(resolved, d);
|
|
10617
10660
|
}
|
|
10618
|
-
function
|
|
10661
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
10662
|
+
const t = getTemplate(templateId, db);
|
|
10663
|
+
if (!t)
|
|
10664
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
10665
|
+
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
10619
10666
|
return {
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10667
|
+
title: cleanOverrides.title || t.title_pattern,
|
|
10668
|
+
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
10669
|
+
priority: cleanOverrides.priority ?? t.priority,
|
|
10670
|
+
tags: cleanOverrides.tags ?? t.tags,
|
|
10671
|
+
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
10672
|
+
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
10673
|
+
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
10674
|
+
...cleanOverrides
|
|
10623
10675
|
};
|
|
10624
10676
|
}
|
|
10625
|
-
function
|
|
10626
|
-
const d = db || getDatabase();
|
|
10627
|
-
const fingerprint = input.fingerprint.trim();
|
|
10628
|
-
if (!fingerprint)
|
|
10629
|
-
throw new Error("fingerprint is required");
|
|
10630
|
-
const existing = getTaskByFingerprint(fingerprint, d);
|
|
10631
|
-
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
10632
|
-
if (!existing) {
|
|
10633
|
-
const task2 = createTask({ ...input, metadata }, d);
|
|
10634
|
-
return { task: task2, created: true };
|
|
10635
|
-
}
|
|
10636
|
-
const task = updateTask(existing.id, {
|
|
10637
|
-
version: existing.version,
|
|
10638
|
-
title: input.title,
|
|
10639
|
-
description: input.description,
|
|
10640
|
-
status: input.status,
|
|
10641
|
-
priority: input.priority,
|
|
10642
|
-
project_id: input.project_id,
|
|
10643
|
-
assigned_to: input.assigned_to,
|
|
10644
|
-
working_dir: input.working_dir,
|
|
10645
|
-
plan_id: input.plan_id,
|
|
10646
|
-
task_list_id: input.task_list_id,
|
|
10647
|
-
tags: input.tags,
|
|
10648
|
-
metadata,
|
|
10649
|
-
due_at: input.due_at,
|
|
10650
|
-
estimated_minutes: input.estimated_minutes,
|
|
10651
|
-
sla_minutes: input.sla_minutes,
|
|
10652
|
-
confidence: input.confidence,
|
|
10653
|
-
retry_count: input.retry_count,
|
|
10654
|
-
max_retries: input.max_retries,
|
|
10655
|
-
retry_after: input.retry_after,
|
|
10656
|
-
requires_approval: input.requires_approval,
|
|
10657
|
-
recurrence_rule: input.recurrence_rule,
|
|
10658
|
-
task_type: input.task_type
|
|
10659
|
-
}, d);
|
|
10660
|
-
return { task, created: false };
|
|
10661
|
-
}
|
|
10662
|
-
function countTasks(filter = {}, db) {
|
|
10677
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
10663
10678
|
const d = db || getDatabase();
|
|
10664
|
-
const
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
if (filter.priority) {
|
|
10692
|
-
if (Array.isArray(filter.priority)) {
|
|
10693
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
10694
|
-
params.push(...filter.priority);
|
|
10695
|
-
} else {
|
|
10696
|
-
conditions.push("priority = ?");
|
|
10697
|
-
params.push(filter.priority);
|
|
10698
|
-
}
|
|
10699
|
-
}
|
|
10700
|
-
if (filter.assigned_to) {
|
|
10701
|
-
conditions.push("assigned_to = ?");
|
|
10702
|
-
params.push(filter.assigned_to);
|
|
10703
|
-
}
|
|
10704
|
-
if (filter.agent_id) {
|
|
10705
|
-
conditions.push("agent_id = ?");
|
|
10706
|
-
params.push(filter.agent_id);
|
|
10707
|
-
}
|
|
10708
|
-
if (filter.session_id) {
|
|
10709
|
-
conditions.push("session_id = ?");
|
|
10710
|
-
params.push(filter.session_id);
|
|
10711
|
-
}
|
|
10712
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
10713
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
10714
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
10715
|
-
params.push(...filter.tags);
|
|
10716
|
-
}
|
|
10717
|
-
if (filter.plan_id) {
|
|
10718
|
-
conditions.push("plan_id = ?");
|
|
10719
|
-
params.push(filter.plan_id);
|
|
10720
|
-
}
|
|
10721
|
-
if (filter.task_list_id) {
|
|
10722
|
-
conditions.push("task_list_id = ?");
|
|
10723
|
-
params.push(filter.task_list_id);
|
|
10724
|
-
}
|
|
10725
|
-
addMetadataConditions(filter.metadata, conditions, params);
|
|
10726
|
-
if (!filter.include_archived) {
|
|
10727
|
-
conditions.push("archived_at IS NULL");
|
|
10679
|
+
const template = getTemplate(templateId, d);
|
|
10680
|
+
if (!template)
|
|
10681
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
10682
|
+
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
10683
|
+
const results = [];
|
|
10684
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
10685
|
+
const task = tasks[i];
|
|
10686
|
+
const id = uuid();
|
|
10687
|
+
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
10688
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10689
|
+
id,
|
|
10690
|
+
templateId,
|
|
10691
|
+
i,
|
|
10692
|
+
task.title_pattern,
|
|
10693
|
+
task.description || null,
|
|
10694
|
+
task.priority || "medium",
|
|
10695
|
+
JSON.stringify(task.tags || []),
|
|
10696
|
+
task.task_type || null,
|
|
10697
|
+
task.condition || null,
|
|
10698
|
+
task.include_template_id || null,
|
|
10699
|
+
JSON.stringify(task.depends_on || []),
|
|
10700
|
+
JSON.stringify(task.metadata || {}),
|
|
10701
|
+
now()
|
|
10702
|
+
]);
|
|
10703
|
+
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
10704
|
+
if (row)
|
|
10705
|
+
results.push(rowToTemplateTask(row));
|
|
10728
10706
|
}
|
|
10729
|
-
|
|
10730
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
10731
|
-
return row.count;
|
|
10707
|
+
return results;
|
|
10732
10708
|
}
|
|
10733
|
-
function
|
|
10709
|
+
function getTemplateWithTasks(id, db) {
|
|
10734
10710
|
const d = db || getDatabase();
|
|
10735
|
-
const
|
|
10736
|
-
if (!
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
}
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
const
|
|
10744
|
-
const
|
|
10745
|
-
if (
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
if (input.status === "completed") {
|
|
10760
|
-
sets.push("completed_at = ?");
|
|
10761
|
-
params.push(completionTimestamp);
|
|
10762
|
-
}
|
|
10763
|
-
}
|
|
10764
|
-
if (input.priority !== undefined) {
|
|
10765
|
-
sets.push("priority = ?");
|
|
10766
|
-
params.push(input.priority);
|
|
10767
|
-
}
|
|
10768
|
-
if (input.project_id !== undefined) {
|
|
10769
|
-
sets.push("project_id = ?");
|
|
10770
|
-
params.push(input.project_id);
|
|
10771
|
-
}
|
|
10772
|
-
if (input.assigned_to !== undefined) {
|
|
10773
|
-
sets.push("assigned_to = ?");
|
|
10774
|
-
params.push(input.assigned_to);
|
|
10775
|
-
}
|
|
10776
|
-
if (input.working_dir !== undefined) {
|
|
10777
|
-
sets.push("working_dir = ?");
|
|
10778
|
-
params.push(input.working_dir);
|
|
10779
|
-
}
|
|
10780
|
-
if (input.tags !== undefined) {
|
|
10781
|
-
sets.push("tags = ?");
|
|
10782
|
-
params.push(JSON.stringify(input.tags));
|
|
10783
|
-
}
|
|
10784
|
-
if (input.metadata !== undefined) {
|
|
10785
|
-
sets.push("metadata = ?");
|
|
10786
|
-
params.push(JSON.stringify(input.metadata));
|
|
10787
|
-
}
|
|
10788
|
-
if (input.plan_id !== undefined) {
|
|
10789
|
-
sets.push("plan_id = ?");
|
|
10790
|
-
params.push(input.plan_id);
|
|
10791
|
-
}
|
|
10792
|
-
if (input.task_list_id !== undefined) {
|
|
10793
|
-
sets.push("task_list_id = ?");
|
|
10794
|
-
params.push(input.task_list_id);
|
|
10795
|
-
}
|
|
10796
|
-
if (input.due_at !== undefined) {
|
|
10797
|
-
sets.push("due_at = ?");
|
|
10798
|
-
params.push(input.due_at);
|
|
10799
|
-
}
|
|
10800
|
-
if (input.estimated_minutes !== undefined) {
|
|
10801
|
-
sets.push("estimated_minutes = ?");
|
|
10802
|
-
params.push(input.estimated_minutes);
|
|
10803
|
-
}
|
|
10804
|
-
if (input.sla_minutes !== undefined) {
|
|
10805
|
-
sets.push("sla_minutes = ?");
|
|
10806
|
-
params.push(input.sla_minutes);
|
|
10807
|
-
}
|
|
10808
|
-
if (input.actual_minutes !== undefined) {
|
|
10809
|
-
sets.push("actual_minutes = ?");
|
|
10810
|
-
params.push(input.actual_minutes);
|
|
10811
|
-
}
|
|
10812
|
-
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
10813
|
-
sets.push("completed_at = ?");
|
|
10814
|
-
params.push(input.completed_at);
|
|
10815
|
-
}
|
|
10816
|
-
if (input.confidence !== undefined) {
|
|
10817
|
-
sets.push("confidence = ?");
|
|
10818
|
-
params.push(input.confidence);
|
|
10819
|
-
}
|
|
10820
|
-
if (input.retry_count !== undefined) {
|
|
10821
|
-
sets.push("retry_count = ?");
|
|
10822
|
-
params.push(input.retry_count);
|
|
10823
|
-
}
|
|
10824
|
-
if (input.max_retries !== undefined) {
|
|
10825
|
-
sets.push("max_retries = ?");
|
|
10826
|
-
params.push(input.max_retries);
|
|
10827
|
-
}
|
|
10828
|
-
if (input.retry_after !== undefined) {
|
|
10829
|
-
sets.push("retry_after = ?");
|
|
10830
|
-
params.push(input.retry_after);
|
|
10831
|
-
}
|
|
10832
|
-
if (input.requires_approval !== undefined) {
|
|
10833
|
-
sets.push("requires_approval = ?");
|
|
10834
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
10835
|
-
}
|
|
10836
|
-
if (input.approved_by !== undefined) {
|
|
10837
|
-
sets.push("approved_by = ?");
|
|
10838
|
-
params.push(input.approved_by);
|
|
10839
|
-
sets.push("approved_at = ?");
|
|
10840
|
-
params.push(now());
|
|
10841
|
-
}
|
|
10842
|
-
if (input.recurrence_rule !== undefined) {
|
|
10843
|
-
sets.push("recurrence_rule = ?");
|
|
10844
|
-
params.push(input.recurrence_rule);
|
|
10711
|
+
const template = getTemplate(id, d);
|
|
10712
|
+
if (!template)
|
|
10713
|
+
return null;
|
|
10714
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
10715
|
+
const tasks = rows.map(rowToTemplateTask);
|
|
10716
|
+
return { ...template, tasks };
|
|
10717
|
+
}
|
|
10718
|
+
function getTemplateTasks(templateId, db) {
|
|
10719
|
+
const d = db || getDatabase();
|
|
10720
|
+
const resolved = resolveTemplateId(templateId, d);
|
|
10721
|
+
if (!resolved)
|
|
10722
|
+
return [];
|
|
10723
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
10724
|
+
return rows.map(rowToTemplateTask);
|
|
10725
|
+
}
|
|
10726
|
+
function evaluateCondition(condition, variables) {
|
|
10727
|
+
if (!condition || condition.trim() === "")
|
|
10728
|
+
return true;
|
|
10729
|
+
const trimmed = condition.trim();
|
|
10730
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
10731
|
+
if (eqMatch) {
|
|
10732
|
+
const varName = eqMatch[1];
|
|
10733
|
+
const expected = eqMatch[2].trim();
|
|
10734
|
+
return (variables[varName] ?? "") === expected;
|
|
10845
10735
|
}
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10736
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
10737
|
+
if (neqMatch) {
|
|
10738
|
+
const varName = neqMatch[1];
|
|
10739
|
+
const expected = neqMatch[2].trim();
|
|
10740
|
+
return (variables[varName] ?? "") !== expected;
|
|
10849
10741
|
}
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
const
|
|
10854
|
-
|
|
10742
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
10743
|
+
if (falsyMatch) {
|
|
10744
|
+
const varName = falsyMatch[1];
|
|
10745
|
+
const val = variables[varName];
|
|
10746
|
+
return !val || val === "" || val === "false";
|
|
10855
10747
|
}
|
|
10856
|
-
|
|
10857
|
-
|
|
10748
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
10749
|
+
if (truthyMatch) {
|
|
10750
|
+
const varName = truthyMatch[1];
|
|
10751
|
+
const val = variables[varName];
|
|
10752
|
+
return !!val && val !== "" && val !== "false";
|
|
10858
10753
|
}
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
if (
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
10754
|
+
return true;
|
|
10755
|
+
}
|
|
10756
|
+
function exportTemplate(id, db) {
|
|
10757
|
+
const d = db || getDatabase();
|
|
10758
|
+
const template = getTemplateWithTasks(id, d);
|
|
10759
|
+
if (!template)
|
|
10760
|
+
throw new Error(`Template not found: ${id}`);
|
|
10761
|
+
return {
|
|
10762
|
+
name: template.name,
|
|
10763
|
+
title_pattern: template.title_pattern,
|
|
10764
|
+
description: template.description,
|
|
10765
|
+
priority: template.priority,
|
|
10766
|
+
tags: template.tags,
|
|
10767
|
+
variables: template.variables,
|
|
10768
|
+
project_id: template.project_id,
|
|
10769
|
+
plan_id: template.plan_id,
|
|
10770
|
+
metadata: template.metadata,
|
|
10771
|
+
tasks: template.tasks.map((t) => ({
|
|
10772
|
+
position: t.position,
|
|
10773
|
+
title_pattern: t.title_pattern,
|
|
10774
|
+
description: t.description,
|
|
10775
|
+
priority: t.priority,
|
|
10776
|
+
tags: t.tags,
|
|
10777
|
+
task_type: t.task_type,
|
|
10778
|
+
condition: t.condition,
|
|
10779
|
+
include_template_id: t.include_template_id,
|
|
10780
|
+
depends_on_positions: t.depends_on_positions,
|
|
10781
|
+
metadata: t.metadata
|
|
10782
|
+
}))
|
|
10889
10783
|
};
|
|
10890
|
-
const databasePath = databasePathFromDatabase(d);
|
|
10891
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
10892
|
-
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
10893
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
10894
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
10895
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
10896
|
-
}
|
|
10897
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
10898
|
-
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
10899
|
-
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
10900
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
10901
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
10902
|
-
}
|
|
10903
|
-
if (input.approved_by !== undefined) {
|
|
10904
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
10905
|
-
}
|
|
10906
|
-
const updatePayload = taskEventData(updatedTask);
|
|
10907
|
-
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
10908
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
10909
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
10910
|
-
return updatedTask;
|
|
10911
10784
|
}
|
|
10912
|
-
function
|
|
10785
|
+
function importTemplate(json, db) {
|
|
10913
10786
|
const d = db || getDatabase();
|
|
10914
|
-
const
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10787
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
10788
|
+
title_pattern: t.title_pattern,
|
|
10789
|
+
description: t.description ?? undefined,
|
|
10790
|
+
priority: t.priority,
|
|
10791
|
+
tags: t.tags,
|
|
10792
|
+
task_type: t.task_type ?? undefined,
|
|
10793
|
+
condition: t.condition ?? undefined,
|
|
10794
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
10795
|
+
depends_on: t.depends_on_positions,
|
|
10796
|
+
metadata: t.metadata
|
|
10797
|
+
}));
|
|
10798
|
+
return createTemplate({
|
|
10799
|
+
name: json.name,
|
|
10800
|
+
title_pattern: json.title_pattern,
|
|
10801
|
+
description: json.description ?? undefined,
|
|
10802
|
+
priority: json.priority,
|
|
10803
|
+
tags: json.tags,
|
|
10804
|
+
variables: json.variables,
|
|
10805
|
+
project_id: json.project_id ?? undefined,
|
|
10806
|
+
plan_id: json.plan_id ?? undefined,
|
|
10807
|
+
metadata: json.metadata,
|
|
10808
|
+
tasks: taskInputs
|
|
10922
10809
|
}, d);
|
|
10923
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
10924
|
-
return result.changes > 0;
|
|
10925
10810
|
}
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10811
|
+
function getTemplateVersion(id, version, db) {
|
|
10812
|
+
const d = db || getDatabase();
|
|
10813
|
+
const resolved = resolveTemplateId(id, d);
|
|
10814
|
+
if (!resolved)
|
|
10815
|
+
return null;
|
|
10816
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
10817
|
+
return row || null;
|
|
10818
|
+
}
|
|
10819
|
+
function listTemplateVersions(id, db) {
|
|
10820
|
+
const d = db || getDatabase();
|
|
10821
|
+
const resolved = resolveTemplateId(id, d);
|
|
10822
|
+
if (!resolved)
|
|
10823
|
+
return [];
|
|
10824
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
10825
|
+
}
|
|
10826
|
+
function resolveVariables(templateVars, provided) {
|
|
10827
|
+
const merged = { ...provided };
|
|
10828
|
+
for (const v of templateVars) {
|
|
10829
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
10830
|
+
merged[v.name] = v.default;
|
|
10831
|
+
}
|
|
10944
10832
|
}
|
|
10945
|
-
|
|
10946
|
-
|
|
10833
|
+
const missing = [];
|
|
10834
|
+
for (const v of templateVars) {
|
|
10835
|
+
if (v.required && merged[v.name] === undefined) {
|
|
10836
|
+
missing.push(v.name);
|
|
10837
|
+
}
|
|
10947
10838
|
}
|
|
10948
|
-
if (
|
|
10949
|
-
|
|
10839
|
+
if (missing.length > 0) {
|
|
10840
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
10950
10841
|
}
|
|
10951
|
-
|
|
10952
|
-
|
|
10842
|
+
return merged;
|
|
10843
|
+
}
|
|
10844
|
+
function substituteVars(text, variables) {
|
|
10845
|
+
let result = text;
|
|
10846
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
10847
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
10953
10848
|
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
};
|
|
10849
|
+
return result;
|
|
10850
|
+
}
|
|
10851
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
10852
|
+
const d = db || getDatabase();
|
|
10853
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
10854
|
+
if (!template)
|
|
10855
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
10856
|
+
const visited = _visitedTemplateIds || new Set;
|
|
10857
|
+
if (visited.has(template.id)) {
|
|
10858
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
10961
10859
|
}
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
const
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10860
|
+
visited.add(template.id);
|
|
10861
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
10862
|
+
if (template.tasks.length === 0) {
|
|
10863
|
+
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
10864
|
+
const task = createTask(input, d);
|
|
10865
|
+
return [task];
|
|
10866
|
+
}
|
|
10867
|
+
const createdTasks = [];
|
|
10868
|
+
const positionToId = new Map;
|
|
10869
|
+
const skippedPositions = new Set;
|
|
10870
|
+
for (const tt of template.tasks) {
|
|
10871
|
+
if (tt.include_template_id) {
|
|
10872
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
10873
|
+
createdTasks.push(...includedTasks);
|
|
10874
|
+
if (includedTasks.length > 0) {
|
|
10875
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
10876
|
+
} else {
|
|
10877
|
+
skippedPositions.add(tt.position);
|
|
10970
10878
|
}
|
|
10879
|
+
continue;
|
|
10971
10880
|
}
|
|
10972
|
-
if (
|
|
10973
|
-
|
|
10881
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
10882
|
+
skippedPositions.add(tt.position);
|
|
10883
|
+
continue;
|
|
10974
10884
|
}
|
|
10885
|
+
let title = tt.title_pattern;
|
|
10886
|
+
let desc = tt.description;
|
|
10887
|
+
title = substituteVars(title, resolved);
|
|
10888
|
+
if (desc)
|
|
10889
|
+
desc = substituteVars(desc, resolved);
|
|
10890
|
+
const task = createTask({
|
|
10891
|
+
title,
|
|
10892
|
+
description: desc ?? undefined,
|
|
10893
|
+
priority: tt.priority,
|
|
10894
|
+
tags: tt.tags,
|
|
10895
|
+
task_type: tt.task_type ?? undefined,
|
|
10896
|
+
project_id: projectId,
|
|
10897
|
+
task_list_id: taskListId,
|
|
10898
|
+
metadata: tt.metadata
|
|
10899
|
+
}, d);
|
|
10900
|
+
createdTasks.push(task);
|
|
10901
|
+
positionToId.set(tt.position, task.id);
|
|
10975
10902
|
}
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
const next = new Date(base);
|
|
10991
|
-
if (parsed.unit === "day") {
|
|
10992
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
10993
|
-
} else if (parsed.unit === "week") {
|
|
10994
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
10995
|
-
} else if (parsed.unit === "month") {
|
|
10996
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
10903
|
+
for (const tt of template.tasks) {
|
|
10904
|
+
if (skippedPositions.has(tt.position))
|
|
10905
|
+
continue;
|
|
10906
|
+
if (tt.include_template_id)
|
|
10907
|
+
continue;
|
|
10908
|
+
const deps = tt.depends_on_positions;
|
|
10909
|
+
for (const depPos of deps) {
|
|
10910
|
+
if (skippedPositions.has(depPos))
|
|
10911
|
+
continue;
|
|
10912
|
+
const taskId = positionToId.get(tt.position);
|
|
10913
|
+
const depId = positionToId.get(depPos);
|
|
10914
|
+
if (taskId && depId) {
|
|
10915
|
+
addDependency(taskId, depId, d);
|
|
10916
|
+
}
|
|
10997
10917
|
}
|
|
10998
|
-
return next.toISOString();
|
|
10999
10918
|
}
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
10919
|
+
return createdTasks;
|
|
10920
|
+
}
|
|
10921
|
+
function previewTemplate(templateId, variables, db) {
|
|
10922
|
+
const d = db || getDatabase();
|
|
10923
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
10924
|
+
if (!template)
|
|
10925
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
10926
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
10927
|
+
const tasks = [];
|
|
10928
|
+
if (template.tasks.length === 0) {
|
|
10929
|
+
tasks.push({
|
|
10930
|
+
position: 0,
|
|
10931
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
10932
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
10933
|
+
priority: template.priority,
|
|
10934
|
+
tags: template.tags,
|
|
10935
|
+
task_type: null,
|
|
10936
|
+
depends_on_positions: []
|
|
10937
|
+
});
|
|
10938
|
+
} else {
|
|
10939
|
+
for (const tt of template.tasks) {
|
|
10940
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
10941
|
+
continue;
|
|
10942
|
+
tasks.push({
|
|
10943
|
+
position: tt.position,
|
|
10944
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
10945
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
10946
|
+
priority: tt.priority,
|
|
10947
|
+
tags: tt.tags,
|
|
10948
|
+
task_type: tt.task_type,
|
|
10949
|
+
depends_on_positions: tt.depends_on_positions
|
|
10950
|
+
});
|
|
11010
10951
|
}
|
|
11011
|
-
const next = new Date(base);
|
|
11012
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
11013
|
-
return next.toISOString();
|
|
11014
10952
|
}
|
|
11015
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
11016
|
-
}
|
|
11017
|
-
var DAY_NAMES;
|
|
11018
|
-
var init_recurrence = __esm(() => {
|
|
11019
|
-
DAY_NAMES = {
|
|
11020
|
-
sunday: 0,
|
|
11021
|
-
sun: 0,
|
|
11022
|
-
monday: 1,
|
|
11023
|
-
mon: 1,
|
|
11024
|
-
tuesday: 2,
|
|
11025
|
-
tue: 2,
|
|
11026
|
-
wednesday: 3,
|
|
11027
|
-
wed: 3,
|
|
11028
|
-
thursday: 4,
|
|
11029
|
-
thu: 4,
|
|
11030
|
-
friday: 5,
|
|
11031
|
-
fri: 5,
|
|
11032
|
-
saturday: 6,
|
|
11033
|
-
sat: 6
|
|
11034
|
-
};
|
|
11035
|
-
});
|
|
11036
|
-
|
|
11037
|
-
// src/db/templates.ts
|
|
11038
|
-
var exports_templates = {};
|
|
11039
|
-
__export(exports_templates, {
|
|
11040
|
-
updateTemplate: () => updateTemplate,
|
|
11041
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
11042
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
11043
|
-
resolveVariables: () => resolveVariables,
|
|
11044
|
-
previewTemplate: () => previewTemplate,
|
|
11045
|
-
listTemplates: () => listTemplates,
|
|
11046
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
11047
|
-
importTemplate: () => importTemplate,
|
|
11048
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
11049
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
11050
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
11051
|
-
getTemplate: () => getTemplate,
|
|
11052
|
-
exportTemplate: () => exportTemplate,
|
|
11053
|
-
evaluateCondition: () => evaluateCondition,
|
|
11054
|
-
deleteTemplate: () => deleteTemplate,
|
|
11055
|
-
createTemplate: () => createTemplate,
|
|
11056
|
-
addTemplateTasks: () => addTemplateTasks
|
|
11057
|
-
});
|
|
11058
|
-
function rowToTemplate(row) {
|
|
11059
|
-
return {
|
|
11060
|
-
...row,
|
|
11061
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
11062
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
11063
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
11064
|
-
priority: row.priority || "medium",
|
|
11065
|
-
version: row.version ?? 1
|
|
11066
|
-
};
|
|
11067
|
-
}
|
|
11068
|
-
function rowToTemplateTask(row) {
|
|
11069
10953
|
return {
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
include_template_id: row.include_template_id ?? null
|
|
10954
|
+
template_id: template.id,
|
|
10955
|
+
template_name: template.name,
|
|
10956
|
+
description: template.description,
|
|
10957
|
+
variables: template.variables,
|
|
10958
|
+
resolved_variables: resolved,
|
|
10959
|
+
tasks
|
|
11077
10960
|
};
|
|
11078
10961
|
}
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
10962
|
+
var init_templates = __esm(() => {
|
|
10963
|
+
init_database();
|
|
10964
|
+
init_tasks();
|
|
10965
|
+
init_storage_tombstones();
|
|
10966
|
+
});
|
|
10967
|
+
|
|
10968
|
+
// src/db/task-graph.ts
|
|
10969
|
+
function addDependency(taskId, dependsOn, db) {
|
|
11083
10970
|
const d = db || getDatabase();
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
input.title_pattern,
|
|
11091
|
-
input.description || null,
|
|
11092
|
-
input.priority || "medium",
|
|
11093
|
-
JSON.stringify(input.tags || []),
|
|
11094
|
-
JSON.stringify(input.variables || []),
|
|
11095
|
-
input.project_id || null,
|
|
11096
|
-
input.plan_id || null,
|
|
11097
|
-
JSON.stringify(input.metadata || {}),
|
|
11098
|
-
now(),
|
|
11099
|
-
machineId
|
|
11100
|
-
]);
|
|
11101
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
11102
|
-
addTemplateTasks(id, input.tasks, d);
|
|
10971
|
+
if (!getTask(taskId, d))
|
|
10972
|
+
throw new TaskNotFoundError(taskId);
|
|
10973
|
+
if (!getTask(dependsOn, d))
|
|
10974
|
+
throw new TaskNotFoundError(dependsOn);
|
|
10975
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
10976
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
11103
10977
|
}
|
|
11104
|
-
|
|
10978
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
11105
10979
|
}
|
|
11106
|
-
function
|
|
10980
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
11107
10981
|
const d = db || getDatabase();
|
|
11108
|
-
const
|
|
11109
|
-
|
|
11110
|
-
return null;
|
|
11111
|
-
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
11112
|
-
return row ? rowToTemplate(row) : null;
|
|
10982
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
10983
|
+
return result.changes > 0;
|
|
11113
10984
|
}
|
|
11114
|
-
function
|
|
10985
|
+
function getTaskDependencies(taskId, db) {
|
|
11115
10986
|
const d = db || getDatabase();
|
|
11116
|
-
return d.query("SELECT * FROM
|
|
10987
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
11117
10988
|
}
|
|
11118
|
-
function
|
|
10989
|
+
function getTaskDependents(taskId, db) {
|
|
11119
10990
|
const d = db || getDatabase();
|
|
11120
|
-
|
|
11121
|
-
if (!resolved)
|
|
11122
|
-
return false;
|
|
11123
|
-
const template = getTemplate(resolved, d);
|
|
11124
|
-
if (!template)
|
|
11125
|
-
return false;
|
|
11126
|
-
recordStorageTombstone({
|
|
11127
|
-
object_type: "templates",
|
|
11128
|
-
object_id: resolved,
|
|
11129
|
-
payload: template,
|
|
11130
|
-
version: template.version
|
|
11131
|
-
}, d);
|
|
11132
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
10991
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
11133
10992
|
}
|
|
11134
|
-
function
|
|
10993
|
+
function cloneTask(taskId, overrides, db) {
|
|
11135
10994
|
const d = db || getDatabase();
|
|
11136
|
-
const
|
|
11137
|
-
if (!
|
|
11138
|
-
|
|
11139
|
-
const
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
10995
|
+
const source = getTask(taskId, d);
|
|
10996
|
+
if (!source)
|
|
10997
|
+
throw new TaskNotFoundError(taskId);
|
|
10998
|
+
const input = {
|
|
10999
|
+
title: overrides?.title ?? source.title,
|
|
11000
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
11001
|
+
priority: overrides?.priority ?? source.priority,
|
|
11002
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
11003
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
11004
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
11005
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
11006
|
+
status: overrides?.status ?? "pending",
|
|
11007
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
11008
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
11009
|
+
tags: overrides?.tags ?? source.tags,
|
|
11010
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
11011
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
11012
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
11013
|
+
};
|
|
11014
|
+
return createTask(input, d);
|
|
11015
|
+
}
|
|
11016
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
11017
|
+
const d = db || getDatabase();
|
|
11018
|
+
const task = getTask(taskId, d);
|
|
11019
|
+
if (!task)
|
|
11020
|
+
throw new TaskNotFoundError(taskId);
|
|
11021
|
+
function toNode(t) {
|
|
11022
|
+
const deps = getTaskDependencies(t.id, d);
|
|
11023
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
11024
|
+
const depTask = getTask(dep.depends_on, d);
|
|
11025
|
+
return depTask && depTask.status !== "completed";
|
|
11152
11026
|
});
|
|
11153
|
-
|
|
11154
|
-
}
|
|
11155
|
-
const sets = ["version = version + 1"];
|
|
11156
|
-
const values = [];
|
|
11157
|
-
if (updates.name !== undefined) {
|
|
11158
|
-
sets.push("name = ?");
|
|
11159
|
-
values.push(updates.name);
|
|
11160
|
-
}
|
|
11161
|
-
if (updates.title_pattern !== undefined) {
|
|
11162
|
-
sets.push("title_pattern = ?");
|
|
11163
|
-
values.push(updates.title_pattern);
|
|
11164
|
-
}
|
|
11165
|
-
if (updates.description !== undefined) {
|
|
11166
|
-
sets.push("description = ?");
|
|
11167
|
-
values.push(updates.description);
|
|
11027
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
11168
11028
|
}
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11029
|
+
function buildUp(id, visited) {
|
|
11030
|
+
if (visited.has(id))
|
|
11031
|
+
return [];
|
|
11032
|
+
visited.add(id);
|
|
11033
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
11034
|
+
return deps.map((dep) => {
|
|
11035
|
+
const depTask = getTask(dep.depends_on, d);
|
|
11036
|
+
if (!depTask)
|
|
11037
|
+
return null;
|
|
11038
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
11039
|
+
}).filter(Boolean);
|
|
11172
11040
|
}
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11041
|
+
function buildDown(id, visited) {
|
|
11042
|
+
if (visited.has(id))
|
|
11043
|
+
return [];
|
|
11044
|
+
visited.add(id);
|
|
11045
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
11046
|
+
return dependents.map((dep) => {
|
|
11047
|
+
const depTask = getTask(dep.task_id, d);
|
|
11048
|
+
if (!depTask)
|
|
11049
|
+
return null;
|
|
11050
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
11051
|
+
}).filter(Boolean);
|
|
11176
11052
|
}
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11053
|
+
const rootNode = toNode(task);
|
|
11054
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
11055
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
11056
|
+
return { task: rootNode, depends_on, blocks };
|
|
11057
|
+
}
|
|
11058
|
+
function moveTask(taskId, target, db) {
|
|
11059
|
+
const d = db || getDatabase();
|
|
11060
|
+
const task = getTask(taskId, d);
|
|
11061
|
+
if (!task)
|
|
11062
|
+
throw new TaskNotFoundError(taskId);
|
|
11063
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
11064
|
+
const params = [now()];
|
|
11065
|
+
if (target.task_list_id !== undefined) {
|
|
11066
|
+
sets.push("task_list_id = ?");
|
|
11067
|
+
params.push(target.task_list_id);
|
|
11180
11068
|
}
|
|
11181
|
-
if (
|
|
11069
|
+
if (target.project_id !== undefined) {
|
|
11182
11070
|
sets.push("project_id = ?");
|
|
11183
|
-
|
|
11071
|
+
params.push(target.project_id);
|
|
11184
11072
|
}
|
|
11185
|
-
if (
|
|
11073
|
+
if (target.plan_id !== undefined) {
|
|
11186
11074
|
sets.push("plan_id = ?");
|
|
11187
|
-
|
|
11075
|
+
params.push(target.plan_id);
|
|
11188
11076
|
}
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11077
|
+
params.push(taskId);
|
|
11078
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
11079
|
+
return getTask(taskId, d);
|
|
11080
|
+
}
|
|
11081
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
11082
|
+
const visited = new Set;
|
|
11083
|
+
const queue = [dependsOn];
|
|
11084
|
+
while (queue.length > 0) {
|
|
11085
|
+
const current = queue.shift();
|
|
11086
|
+
if (current === taskId)
|
|
11087
|
+
return true;
|
|
11088
|
+
if (visited.has(current))
|
|
11089
|
+
continue;
|
|
11090
|
+
visited.add(current);
|
|
11091
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
11092
|
+
for (const dep of deps) {
|
|
11093
|
+
queue.push(dep.depends_on);
|
|
11094
|
+
}
|
|
11192
11095
|
}
|
|
11193
|
-
|
|
11194
|
-
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
11195
|
-
return getTemplate(resolved, d);
|
|
11096
|
+
return false;
|
|
11196
11097
|
}
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11098
|
+
var init_task_graph = __esm(() => {
|
|
11099
|
+
init_types();
|
|
11100
|
+
init_database();
|
|
11101
|
+
init_task_crud();
|
|
11102
|
+
});
|
|
11103
|
+
|
|
11104
|
+
// src/db/task-lifecycle.ts
|
|
11105
|
+
var exports_task_lifecycle = {};
|
|
11106
|
+
__export(exports_task_lifecycle, {
|
|
11107
|
+
unlockTask: () => unlockTask,
|
|
11108
|
+
stealTask: () => stealTask,
|
|
11109
|
+
startTask: () => startTask,
|
|
11110
|
+
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
11111
|
+
lockTask: () => lockTask,
|
|
11112
|
+
getTasksChangedSince: () => getTasksChangedSince,
|
|
11113
|
+
getTaskLockStatus: () => getTaskLockStatus,
|
|
11114
|
+
getStaleTasks: () => getStaleTasks,
|
|
11115
|
+
getNextTask: () => getNextTask,
|
|
11116
|
+
getBlockingDeps: () => getBlockingDeps,
|
|
11117
|
+
getActiveWork: () => getActiveWork,
|
|
11118
|
+
failTask: () => failTask,
|
|
11119
|
+
completeTask: () => completeTask,
|
|
11120
|
+
claimOrSteal: () => claimOrSteal,
|
|
11121
|
+
claimNextTask: () => claimNextTask
|
|
11122
|
+
});
|
|
11123
|
+
function lockExpiresAt(lockedAt) {
|
|
11124
|
+
if (!lockedAt)
|
|
11125
|
+
return null;
|
|
11126
|
+
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
11212
11127
|
}
|
|
11213
|
-
function
|
|
11128
|
+
function assertStartable(task, agentId) {
|
|
11129
|
+
if (task.status === "pending")
|
|
11130
|
+
return;
|
|
11131
|
+
if (task.status === "in_progress")
|
|
11132
|
+
return;
|
|
11133
|
+
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
11134
|
+
}
|
|
11135
|
+
function getBlockingDeps(id, db) {
|
|
11214
11136
|
const d = db || getDatabase();
|
|
11215
|
-
const
|
|
11216
|
-
if (
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
const
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
11224
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
11225
|
-
id,
|
|
11226
|
-
templateId,
|
|
11227
|
-
i,
|
|
11228
|
-
task.title_pattern,
|
|
11229
|
-
task.description || null,
|
|
11230
|
-
task.priority || "medium",
|
|
11231
|
-
JSON.stringify(task.tags || []),
|
|
11232
|
-
task.task_type || null,
|
|
11233
|
-
task.condition || null,
|
|
11234
|
-
task.include_template_id || null,
|
|
11235
|
-
JSON.stringify(task.depends_on || []),
|
|
11236
|
-
JSON.stringify(task.metadata || {}),
|
|
11237
|
-
now()
|
|
11238
|
-
]);
|
|
11239
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
11240
|
-
if (row)
|
|
11241
|
-
results.push(rowToTemplateTask(row));
|
|
11137
|
+
const deps = getTaskDependencies(id, d);
|
|
11138
|
+
if (deps.length === 0)
|
|
11139
|
+
return [];
|
|
11140
|
+
const blocking = [];
|
|
11141
|
+
for (const dep of deps) {
|
|
11142
|
+
const task = getTask(dep.depends_on, d);
|
|
11143
|
+
if (task && task.status !== "completed")
|
|
11144
|
+
blocking.push(task);
|
|
11242
11145
|
}
|
|
11243
|
-
return
|
|
11146
|
+
return blocking;
|
|
11244
11147
|
}
|
|
11245
|
-
function
|
|
11148
|
+
function startTask(id, agentId, db) {
|
|
11246
11149
|
const d = db || getDatabase();
|
|
11247
|
-
const
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11150
|
+
const databasePath = databasePathFromDatabase(d);
|
|
11151
|
+
const task = getTask(id, d);
|
|
11152
|
+
if (!task)
|
|
11153
|
+
throw new TaskNotFoundError(id);
|
|
11154
|
+
assertStartable(task, agentId);
|
|
11155
|
+
const blocking = getBlockingDeps(id, d);
|
|
11156
|
+
if (blocking.length > 0) {
|
|
11157
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
11158
|
+
emitLocalEventHooksQuiet({
|
|
11159
|
+
type: "task.blocked",
|
|
11160
|
+
payload: {
|
|
11161
|
+
id,
|
|
11162
|
+
agent_id: agentId,
|
|
11163
|
+
title: task.title,
|
|
11164
|
+
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
11165
|
+
},
|
|
11166
|
+
databasePath
|
|
11167
|
+
});
|
|
11168
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
11169
|
+
}
|
|
11170
|
+
const cutoff = lockExpiryCutoff();
|
|
11171
|
+
const timestamp = now();
|
|
11172
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
11173
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
11174
|
+
if (result.changes === 0) {
|
|
11175
|
+
const current = getTask(id, d);
|
|
11176
|
+
if (!current)
|
|
11177
|
+
throw new TaskNotFoundError(id);
|
|
11178
|
+
assertStartable(current, agentId);
|
|
11179
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
11180
|
+
throw new LockError(id, current.locked_by);
|
|
11181
|
+
}
|
|
11182
|
+
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
11183
|
+
}
|
|
11184
|
+
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
11185
|
+
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
11186
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
11187
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
11188
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
11189
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
11190
|
+
return startedTask;
|
|
11253
11191
|
}
|
|
11254
|
-
function
|
|
11192
|
+
function completeTask(id, agentId, db, options) {
|
|
11255
11193
|
const d = db || getDatabase();
|
|
11256
|
-
const
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
function evaluateCondition(condition, variables) {
|
|
11263
|
-
if (!condition || condition.trim() === "")
|
|
11264
|
-
return true;
|
|
11265
|
-
const trimmed = condition.trim();
|
|
11266
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
11267
|
-
if (eqMatch) {
|
|
11268
|
-
const varName = eqMatch[1];
|
|
11269
|
-
const expected = eqMatch[2].trim();
|
|
11270
|
-
return (variables[varName] ?? "") === expected;
|
|
11194
|
+
const databasePath = databasePathFromDatabase(d);
|
|
11195
|
+
const task = getTask(id, d);
|
|
11196
|
+
if (!task)
|
|
11197
|
+
throw new TaskNotFoundError(id);
|
|
11198
|
+
if (task.status === "completed") {
|
|
11199
|
+
return task;
|
|
11271
11200
|
}
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
const varName = neqMatch[1];
|
|
11275
|
-
const expected = neqMatch[2].trim();
|
|
11276
|
-
return (variables[varName] ?? "") !== expected;
|
|
11201
|
+
if (task.status === "cancelled") {
|
|
11202
|
+
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
11277
11203
|
}
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
const varName = falsyMatch[1];
|
|
11281
|
-
const val = variables[varName];
|
|
11282
|
-
return !val || val === "" || val === "false";
|
|
11204
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
11205
|
+
throw new LockError(id, task.locked_by);
|
|
11283
11206
|
}
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11207
|
+
checkCompletionGuard(task, agentId || null, d);
|
|
11208
|
+
const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
|
|
11209
|
+
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
11210
|
+
const completionMeta = {};
|
|
11211
|
+
if (hasEvidence)
|
|
11212
|
+
completionMeta._evidence = evidence;
|
|
11213
|
+
if (options?.confidence !== undefined) {
|
|
11214
|
+
completionMeta._completion = { confidence: options.confidence };
|
|
11215
|
+
}
|
|
11216
|
+
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
11217
|
+
const timestamp = options?.completed_at || now();
|
|
11218
|
+
const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
|
|
11219
|
+
const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
|
|
11220
|
+
const finalVersion = versionBeforeStatus + 1;
|
|
11221
|
+
const tx = d.transaction(() => {
|
|
11222
|
+
if (hasMeta) {
|
|
11223
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
11224
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
11225
|
+
if (metaResult.changes === 0) {
|
|
11226
|
+
const current = getTask(id, d);
|
|
11227
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
11231
|
+
WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
|
|
11232
|
+
if (statusResult.changes === 0) {
|
|
11233
|
+
const current = getTask(id, d);
|
|
11234
|
+
throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
|
|
11235
|
+
}
|
|
11236
|
+
});
|
|
11237
|
+
tx();
|
|
11238
|
+
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
11239
|
+
const completedTaskForEvent = {
|
|
11240
|
+
...task,
|
|
11241
|
+
status: "completed",
|
|
11242
|
+
locked_by: null,
|
|
11243
|
+
locked_at: null,
|
|
11244
|
+
completed_at: timestamp,
|
|
11245
|
+
confidence,
|
|
11246
|
+
version: finalVersion,
|
|
11247
|
+
updated_at: timestamp,
|
|
11248
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
11249
|
+
};
|
|
11250
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
11251
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
11252
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
11253
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
11254
|
+
let spawnedTask = null;
|
|
11255
|
+
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
11256
|
+
try {
|
|
11257
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
11258
|
+
} catch (e) {
|
|
11259
|
+
spawnedTask = null;
|
|
11260
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
11261
|
+
}
|
|
11262
|
+
}
|
|
11263
|
+
let spawnedFromTemplate = null;
|
|
11264
|
+
if (task.spawns_template_id) {
|
|
11265
|
+
const spawnDepth = task.metadata?._spawn_depth || 0;
|
|
11266
|
+
if (spawnDepth >= MAX_SPAWN_DEPTH) {
|
|
11267
|
+
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
11268
|
+
} else {
|
|
11269
|
+
try {
|
|
11270
|
+
const input = taskFromTemplate(task.spawns_template_id, {
|
|
11271
|
+
project_id: task.project_id ?? undefined,
|
|
11272
|
+
plan_id: task.plan_id ?? undefined,
|
|
11273
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
11274
|
+
assigned_to: task.assigned_to ?? undefined
|
|
11275
|
+
}, d);
|
|
11276
|
+
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
11277
|
+
spawnedFromTemplate = createTask(input, d);
|
|
11278
|
+
} catch {}
|
|
11279
|
+
}
|
|
11280
|
+
}
|
|
11281
|
+
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
11282
|
+
if (spawnedTask) {
|
|
11283
|
+
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
11284
|
+
}
|
|
11285
|
+
if (spawnedFromTemplate) {
|
|
11286
|
+
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
11287
|
+
}
|
|
11288
|
+
const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
|
|
11289
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
11290
|
+
WHERE td.depends_on = ? AND t.status = 'pending'
|
|
11291
|
+
AND NOT EXISTS (
|
|
11292
|
+
SELECT 1 FROM task_dependencies td2
|
|
11293
|
+
JOIN tasks dep2 ON dep2.id = td2.depends_on
|
|
11294
|
+
WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
|
|
11295
|
+
)`).all(id, id);
|
|
11296
|
+
if (unblockedDeps.length > 0) {
|
|
11297
|
+
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
11298
|
+
for (const dep of unblockedDeps) {
|
|
11299
|
+
const depTask = getTask(dep.id, d);
|
|
11300
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
11301
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
11302
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
11303
|
+
if (depTask)
|
|
11304
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
11305
|
+
}
|
|
11306
|
+
}
|
|
11307
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
|
|
11308
|
+
}
|
|
11309
|
+
function lockTask(id, agentId, db) {
|
|
11310
|
+
const d = db || getDatabase();
|
|
11311
|
+
const task = getTask(id, d);
|
|
11312
|
+
if (!task)
|
|
11313
|
+
throw new TaskNotFoundError(id);
|
|
11314
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
11315
|
+
return {
|
|
11316
|
+
success: false,
|
|
11317
|
+
error: `Task is ${task.status} and cannot be locked`
|
|
11318
|
+
};
|
|
11319
|
+
}
|
|
11320
|
+
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
11321
|
+
const timestamp2 = now();
|
|
11322
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
11323
|
+
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
11324
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
11325
|
+
}
|
|
11326
|
+
const cutoff = lockExpiryCutoff();
|
|
11327
|
+
const timestamp = now();
|
|
11328
|
+
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
11329
|
+
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
11330
|
+
if (result.changes === 0) {
|
|
11331
|
+
const current = getTask(id, d);
|
|
11332
|
+
if (!current)
|
|
11333
|
+
throw new TaskNotFoundError(id);
|
|
11334
|
+
if (current.status === "completed" || current.status === "cancelled") {
|
|
11335
|
+
return {
|
|
11336
|
+
success: false,
|
|
11337
|
+
error: `Task is ${current.status} and cannot be locked`
|
|
11338
|
+
};
|
|
11339
|
+
}
|
|
11340
|
+
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
11341
|
+
return {
|
|
11342
|
+
success: false,
|
|
11343
|
+
locked_by: current.locked_by,
|
|
11344
|
+
locked_at: current.locked_at,
|
|
11345
|
+
error: `Task is locked by ${current.locked_by}`
|
|
11346
|
+
};
|
|
11347
|
+
}
|
|
11348
|
+
return {
|
|
11349
|
+
success: false,
|
|
11350
|
+
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
11351
|
+
};
|
|
11289
11352
|
}
|
|
11353
|
+
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
11354
|
+
return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
|
|
11355
|
+
}
|
|
11356
|
+
function unlockTask(id, agentId, db) {
|
|
11357
|
+
const d = db || getDatabase();
|
|
11358
|
+
const task = getTask(id, d);
|
|
11359
|
+
if (!task)
|
|
11360
|
+
throw new TaskNotFoundError(id);
|
|
11361
|
+
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
11362
|
+
throw new LockError(id, task.locked_by);
|
|
11363
|
+
}
|
|
11364
|
+
const timestamp = now();
|
|
11365
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
11366
|
+
WHERE id = ?`, [timestamp, id]);
|
|
11290
11367
|
return true;
|
|
11291
11368
|
}
|
|
11292
|
-
function
|
|
11369
|
+
function getTaskLockStatus(id, db) {
|
|
11293
11370
|
const d = db || getDatabase();
|
|
11294
|
-
const
|
|
11295
|
-
if (!
|
|
11296
|
-
throw new
|
|
11371
|
+
const task = getTask(id, d);
|
|
11372
|
+
if (!task)
|
|
11373
|
+
throw new TaskNotFoundError(id);
|
|
11374
|
+
const expired = isLockExpired(task.locked_at);
|
|
11297
11375
|
return {
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
project_id: template.project_id,
|
|
11305
|
-
plan_id: template.plan_id,
|
|
11306
|
-
metadata: template.metadata,
|
|
11307
|
-
tasks: template.tasks.map((t) => ({
|
|
11308
|
-
position: t.position,
|
|
11309
|
-
title_pattern: t.title_pattern,
|
|
11310
|
-
description: t.description,
|
|
11311
|
-
priority: t.priority,
|
|
11312
|
-
tags: t.tags,
|
|
11313
|
-
task_type: t.task_type,
|
|
11314
|
-
condition: t.condition,
|
|
11315
|
-
include_template_id: t.include_template_id,
|
|
11316
|
-
depends_on_positions: t.depends_on_positions,
|
|
11317
|
-
metadata: t.metadata
|
|
11318
|
-
}))
|
|
11376
|
+
task_id: id,
|
|
11377
|
+
locked: !!task.locked_by && !expired,
|
|
11378
|
+
locked_by: task.locked_by,
|
|
11379
|
+
locked_at: task.locked_at,
|
|
11380
|
+
expires_at: lockExpiresAt(task.locked_at),
|
|
11381
|
+
expired
|
|
11319
11382
|
};
|
|
11320
11383
|
}
|
|
11321
|
-
function
|
|
11322
|
-
const d = db || getDatabase();
|
|
11323
|
-
const taskInputs = (json.tasks || []).map((t) => ({
|
|
11324
|
-
title_pattern: t.title_pattern,
|
|
11325
|
-
description: t.description ?? undefined,
|
|
11326
|
-
priority: t.priority,
|
|
11327
|
-
tags: t.tags,
|
|
11328
|
-
task_type: t.task_type ?? undefined,
|
|
11329
|
-
condition: t.condition ?? undefined,
|
|
11330
|
-
include_template_id: t.include_template_id ?? undefined,
|
|
11331
|
-
depends_on: t.depends_on_positions,
|
|
11332
|
-
metadata: t.metadata
|
|
11333
|
-
}));
|
|
11334
|
-
return createTemplate({
|
|
11335
|
-
name: json.name,
|
|
11336
|
-
title_pattern: json.title_pattern,
|
|
11337
|
-
description: json.description ?? undefined,
|
|
11338
|
-
priority: json.priority,
|
|
11339
|
-
tags: json.tags,
|
|
11340
|
-
variables: json.variables,
|
|
11341
|
-
project_id: json.project_id ?? undefined,
|
|
11342
|
-
plan_id: json.plan_id ?? undefined,
|
|
11343
|
-
metadata: json.metadata,
|
|
11344
|
-
tasks: taskInputs
|
|
11345
|
-
}, d);
|
|
11346
|
-
}
|
|
11347
|
-
function getTemplateVersion(id, version, db) {
|
|
11384
|
+
function claimNextTask(agentId, filters, db) {
|
|
11348
11385
|
const d = db || getDatabase();
|
|
11349
|
-
const
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11386
|
+
const MAX_ATTEMPTS = 25;
|
|
11387
|
+
const tried = new Set;
|
|
11388
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
11389
|
+
const outcome = d.transaction(() => {
|
|
11390
|
+
const task = getNextTask(agentId, filters, d);
|
|
11391
|
+
if (!task)
|
|
11392
|
+
return { done: true, task: null };
|
|
11393
|
+
if (tried.has(task.id))
|
|
11394
|
+
return { done: true, task: null };
|
|
11395
|
+
tried.add(task.id);
|
|
11396
|
+
try {
|
|
11397
|
+
return { done: true, task: startTask(task.id, agentId, d) };
|
|
11398
|
+
} catch {
|
|
11399
|
+
return { done: false, task: null };
|
|
11400
|
+
}
|
|
11401
|
+
})();
|
|
11402
|
+
if (outcome.done)
|
|
11403
|
+
return outcome.task;
|
|
11404
|
+
}
|
|
11405
|
+
return null;
|
|
11354
11406
|
}
|
|
11355
|
-
function
|
|
11407
|
+
function getNextTask(agentId, filters, db) {
|
|
11356
11408
|
const d = db || getDatabase();
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
const merged = { ...provided };
|
|
11364
|
-
for (const v of templateVars) {
|
|
11365
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
11366
|
-
merged[v.name] = v.default;
|
|
11367
|
-
}
|
|
11409
|
+
clearExpiredLocks(d);
|
|
11410
|
+
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
11411
|
+
const params = [lockExpiryCutoff()];
|
|
11412
|
+
if (filters?.project_id) {
|
|
11413
|
+
conditions.push("project_id = ?");
|
|
11414
|
+
params.push(filters.project_id);
|
|
11368
11415
|
}
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
missing.push(v.name);
|
|
11373
|
-
}
|
|
11416
|
+
if (filters?.task_list_id) {
|
|
11417
|
+
conditions.push("task_list_id = ?");
|
|
11418
|
+
params.push(filters.task_list_id);
|
|
11374
11419
|
}
|
|
11375
|
-
if (
|
|
11376
|
-
|
|
11420
|
+
if (filters?.plan_id) {
|
|
11421
|
+
conditions.push("plan_id = ?");
|
|
11422
|
+
params.push(filters.plan_id);
|
|
11377
11423
|
}
|
|
11378
|
-
|
|
11424
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
11425
|
+
const placeholders = filters.tags.map(() => "?").join(",");
|
|
11426
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
11427
|
+
params.push(...filters.tags);
|
|
11428
|
+
}
|
|
11429
|
+
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
|
|
11430
|
+
const where = conditions.join(" AND ");
|
|
11431
|
+
let recentProjectIds = [];
|
|
11432
|
+
if (agentId) {
|
|
11433
|
+
const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
|
|
11434
|
+
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
11435
|
+
}
|
|
11436
|
+
let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
|
|
11437
|
+
if (agentId) {
|
|
11438
|
+
sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
|
|
11439
|
+
params.push(agentId);
|
|
11440
|
+
}
|
|
11441
|
+
if (recentProjectIds.length > 0) {
|
|
11442
|
+
const placeholders = recentProjectIds.map(() => "?").join(",");
|
|
11443
|
+
sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
|
|
11444
|
+
params.push(...recentProjectIds);
|
|
11445
|
+
}
|
|
11446
|
+
sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
|
|
11447
|
+
const row = d.query(sql).get(...params);
|
|
11448
|
+
return row ? rowToTask(row) : null;
|
|
11379
11449
|
}
|
|
11380
|
-
function
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11450
|
+
function getActiveWork(filters, db) {
|
|
11451
|
+
const d = db || getDatabase();
|
|
11452
|
+
clearExpiredLocks(d);
|
|
11453
|
+
const conditions = ["status = 'in_progress'"];
|
|
11454
|
+
const params = [];
|
|
11455
|
+
if (filters?.project_id) {
|
|
11456
|
+
conditions.push("project_id = ?");
|
|
11457
|
+
params.push(filters.project_id);
|
|
11384
11458
|
}
|
|
11385
|
-
|
|
11459
|
+
if (filters?.task_list_id) {
|
|
11460
|
+
conditions.push("task_list_id = ?");
|
|
11461
|
+
params.push(filters.task_list_id);
|
|
11462
|
+
}
|
|
11463
|
+
const where = conditions.join(" AND ");
|
|
11464
|
+
const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
|
|
11465
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
11466
|
+
updated_at DESC`).all(...params);
|
|
11467
|
+
return rows;
|
|
11386
11468
|
}
|
|
11387
|
-
function
|
|
11469
|
+
function getTasksChangedSince(since, filters, db) {
|
|
11388
11470
|
const d = db || getDatabase();
|
|
11389
|
-
const
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
11471
|
+
const conditions = ["updated_at > ?"];
|
|
11472
|
+
const params = [since];
|
|
11473
|
+
if (filters?.project_id) {
|
|
11474
|
+
conditions.push("project_id = ?");
|
|
11475
|
+
params.push(filters.project_id);
|
|
11395
11476
|
}
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
11400
|
-
const task = createTask(input, d);
|
|
11401
|
-
return [task];
|
|
11477
|
+
if (filters?.task_list_id) {
|
|
11478
|
+
conditions.push("task_list_id = ?");
|
|
11479
|
+
params.push(filters.task_list_id);
|
|
11402
11480
|
}
|
|
11403
|
-
const
|
|
11404
|
-
const
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11481
|
+
const where = conditions.join(" AND ");
|
|
11482
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
11483
|
+
return rows.map(rowToTask);
|
|
11484
|
+
}
|
|
11485
|
+
function failTask(id, agentId, reason, options, db) {
|
|
11486
|
+
const d = db || getDatabase();
|
|
11487
|
+
const databasePath = databasePathFromDatabase(d);
|
|
11488
|
+
const task = getTask(id, d);
|
|
11489
|
+
if (!task)
|
|
11490
|
+
throw new TaskNotFoundError(id);
|
|
11491
|
+
const meta = {
|
|
11492
|
+
...task.metadata,
|
|
11493
|
+
_failure: {
|
|
11494
|
+
reason: reason || "Unknown failure",
|
|
11495
|
+
error_code: options?.error_code || null,
|
|
11496
|
+
failed_by: agentId || null,
|
|
11497
|
+
failed_at: now(),
|
|
11498
|
+
retry_requested: options?.retry || false
|
|
11499
|
+
}
|
|
11500
|
+
};
|
|
11501
|
+
const timestamp = now();
|
|
11502
|
+
const failTx = d.transaction(() => {
|
|
11503
|
+
const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
11504
|
+
WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
|
|
11505
|
+
if (res.changes === 0) {
|
|
11506
|
+
const current = getTask(id, d);
|
|
11507
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
11508
|
+
}
|
|
11509
|
+
});
|
|
11510
|
+
failTx();
|
|
11511
|
+
const failedTask = {
|
|
11512
|
+
...task,
|
|
11513
|
+
status: "failed",
|
|
11514
|
+
locked_by: null,
|
|
11515
|
+
locked_at: null,
|
|
11516
|
+
metadata: meta,
|
|
11517
|
+
version: task.version + 1,
|
|
11518
|
+
updated_at: timestamp
|
|
11519
|
+
};
|
|
11520
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
11521
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
11522
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
11523
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
11524
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
|
|
11525
|
+
let retryTask;
|
|
11526
|
+
if (options?.retry) {
|
|
11527
|
+
const retryCount = (task.retry_count || 0) + 1;
|
|
11528
|
+
const maxRetries = task.max_retries || 3;
|
|
11529
|
+
if (retryCount > maxRetries) {
|
|
11530
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
11531
|
+
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
11532
|
+
id
|
|
11533
|
+
]);
|
|
11534
|
+
} else {
|
|
11535
|
+
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
11536
|
+
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
11537
|
+
let title = task.title;
|
|
11538
|
+
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
11539
|
+
title = title.slice(task.short_id.length + 2);
|
|
11414
11540
|
}
|
|
11415
|
-
|
|
11541
|
+
retryTask = createTask({
|
|
11542
|
+
title,
|
|
11543
|
+
description: task.description ?? undefined,
|
|
11544
|
+
priority: task.priority,
|
|
11545
|
+
project_id: task.project_id ?? undefined,
|
|
11546
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
11547
|
+
plan_id: task.plan_id ?? undefined,
|
|
11548
|
+
assigned_to: task.assigned_to ?? undefined,
|
|
11549
|
+
tags: task.tags,
|
|
11550
|
+
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
11551
|
+
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
11552
|
+
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
11553
|
+
due_at: retryAfter
|
|
11554
|
+
}, d);
|
|
11555
|
+
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
11416
11556
|
}
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11557
|
+
}
|
|
11558
|
+
return { task: failedTask, retryTask };
|
|
11559
|
+
}
|
|
11560
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
11561
|
+
const d = db || getDatabase();
|
|
11562
|
+
const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
|
|
11563
|
+
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
11564
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
11565
|
+
const conditions = [
|
|
11566
|
+
"status = 'in_progress'",
|
|
11567
|
+
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
11568
|
+
];
|
|
11569
|
+
const params = [cutoff, cutoff];
|
|
11570
|
+
if (effectiveFilters?.project_id) {
|
|
11571
|
+
conditions.push("project_id = ?");
|
|
11572
|
+
params.push(effectiveFilters.project_id);
|
|
11573
|
+
}
|
|
11574
|
+
if (effectiveFilters?.task_list_id) {
|
|
11575
|
+
conditions.push("task_list_id = ?");
|
|
11576
|
+
params.push(effectiveFilters.task_list_id);
|
|
11577
|
+
}
|
|
11578
|
+
const where = conditions.join(" AND ");
|
|
11579
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
11580
|
+
return rows.map(rowToTask);
|
|
11581
|
+
}
|
|
11582
|
+
function stealTask(agentId, opts, db) {
|
|
11583
|
+
const d = db || getDatabase();
|
|
11584
|
+
const databasePath = databasePathFromDatabase(d);
|
|
11585
|
+
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
11586
|
+
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
11587
|
+
if (staleTasks.length === 0)
|
|
11588
|
+
return null;
|
|
11589
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
11590
|
+
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
11591
|
+
const target = staleTasks[0];
|
|
11592
|
+
const timestamp = now();
|
|
11593
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
11594
|
+
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
11595
|
+
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
|
|
11596
|
+
if (result.changes === 0)
|
|
11597
|
+
return null;
|
|
11598
|
+
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
11599
|
+
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
11600
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
11601
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
11602
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
11603
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
11604
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
11605
|
+
return stolenTask;
|
|
11606
|
+
}
|
|
11607
|
+
function claimOrSteal(agentId, filters, db) {
|
|
11608
|
+
const d = db || getDatabase();
|
|
11609
|
+
const tx = d.transaction(() => {
|
|
11610
|
+
const next = getNextTask(agentId, filters, d);
|
|
11611
|
+
if (next) {
|
|
11612
|
+
const started = startTask(next.id, agentId, d);
|
|
11613
|
+
return { task: started, stolen: false };
|
|
11420
11614
|
}
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
metadata: tt.metadata
|
|
11435
|
-
}, d);
|
|
11436
|
-
createdTasks.push(task);
|
|
11437
|
-
positionToId.set(tt.position, task.id);
|
|
11615
|
+
const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
|
|
11616
|
+
if (stolen)
|
|
11617
|
+
return { task: stolen, stolen: true };
|
|
11618
|
+
return null;
|
|
11619
|
+
});
|
|
11620
|
+
return tx();
|
|
11621
|
+
}
|
|
11622
|
+
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
11623
|
+
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
11624
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
11625
|
+
let title = completedTask.title;
|
|
11626
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
11627
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
11438
11628
|
}
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11629
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
11630
|
+
return createTask({
|
|
11631
|
+
title,
|
|
11632
|
+
description: completedTask.description ?? undefined,
|
|
11633
|
+
priority: completedTask.priority,
|
|
11634
|
+
project_id: completedTask.project_id ?? undefined,
|
|
11635
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
11636
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
11637
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
11638
|
+
tags: completedTask.tags,
|
|
11639
|
+
metadata: completedTask.metadata,
|
|
11640
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
11641
|
+
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
11642
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
11643
|
+
recurrence_parent_id: recurrenceParentId,
|
|
11644
|
+
due_at: dueAt
|
|
11645
|
+
}, db);
|
|
11646
|
+
}
|
|
11647
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
11648
|
+
var init_task_lifecycle = __esm(() => {
|
|
11649
|
+
init_types();
|
|
11650
|
+
init_database();
|
|
11651
|
+
init_completion_guard();
|
|
11652
|
+
init_event_emission_safety();
|
|
11653
|
+
init_event_hooks();
|
|
11654
|
+
init_shared_events();
|
|
11655
|
+
init_audit();
|
|
11656
|
+
init_recurrence();
|
|
11657
|
+
init_webhooks();
|
|
11658
|
+
init_templates();
|
|
11659
|
+
init_task_crud();
|
|
11660
|
+
init_task_graph();
|
|
11661
|
+
});
|
|
11662
|
+
|
|
11663
|
+
// src/db/task-crud.ts
|
|
11664
|
+
function rowToTask(row) {
|
|
11665
|
+
return {
|
|
11666
|
+
...row,
|
|
11667
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
11668
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
11669
|
+
status: row.status,
|
|
11670
|
+
priority: row.priority,
|
|
11671
|
+
requires_approval: !!row.requires_approval
|
|
11672
|
+
};
|
|
11673
|
+
}
|
|
11674
|
+
function insertTaskTags(taskId, tags, db) {
|
|
11675
|
+
if (tags.length === 0)
|
|
11676
|
+
return;
|
|
11677
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
11678
|
+
for (const tag of tags) {
|
|
11679
|
+
if (tag)
|
|
11680
|
+
stmt.run(taskId, tag);
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
11684
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
11685
|
+
insertTaskTags(taskId, tags, db);
|
|
11686
|
+
}
|
|
11687
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
11688
|
+
if (!metadata)
|
|
11689
|
+
return;
|
|
11690
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
11691
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
11692
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
11453
11693
|
}
|
|
11694
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
11695
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
11454
11696
|
}
|
|
11455
|
-
return createdTasks;
|
|
11456
11697
|
}
|
|
11457
|
-
function
|
|
11698
|
+
function createTask(input, db) {
|
|
11458
11699
|
const d = db || getDatabase();
|
|
11459
|
-
const
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
const
|
|
11463
|
-
const
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11700
|
+
const timestamp = now();
|
|
11701
|
+
const tags = input.tags || [];
|
|
11702
|
+
const machineId = currentStorageMachineId(d);
|
|
11703
|
+
const assignedBy = input.assigned_by || input.agent_id;
|
|
11704
|
+
const assignedFromProject = input.assigned_from_project || null;
|
|
11705
|
+
let id = uuid();
|
|
11706
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
11707
|
+
try {
|
|
11708
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
|
|
11709
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
11710
|
+
id,
|
|
11711
|
+
null,
|
|
11712
|
+
input.project_id || null,
|
|
11713
|
+
input.parent_id || null,
|
|
11714
|
+
input.plan_id || null,
|
|
11715
|
+
input.task_list_id || null,
|
|
11716
|
+
input.cycle_id || null,
|
|
11717
|
+
input.title,
|
|
11718
|
+
input.description || null,
|
|
11719
|
+
input.status || "pending",
|
|
11720
|
+
input.priority || "medium",
|
|
11721
|
+
input.agent_id || null,
|
|
11722
|
+
input.assigned_to || null,
|
|
11723
|
+
input.session_id || null,
|
|
11724
|
+
input.working_dir || null,
|
|
11725
|
+
JSON.stringify(tags),
|
|
11726
|
+
JSON.stringify(input.metadata || {}),
|
|
11727
|
+
timestamp,
|
|
11728
|
+
timestamp,
|
|
11729
|
+
input.due_at || null,
|
|
11730
|
+
input.estimated_minutes || null,
|
|
11731
|
+
input.sla_minutes ?? null,
|
|
11732
|
+
input.confidence ?? null,
|
|
11733
|
+
input.retry_count ?? 0,
|
|
11734
|
+
input.max_retries ?? 3,
|
|
11735
|
+
input.retry_after ?? null,
|
|
11736
|
+
input.requires_approval ? 1 : 0,
|
|
11737
|
+
null,
|
|
11738
|
+
null,
|
|
11739
|
+
input.recurrence_rule || null,
|
|
11740
|
+
input.recurrence_parent_id || null,
|
|
11741
|
+
input.spawns_template_id || null,
|
|
11742
|
+
input.reason || null,
|
|
11743
|
+
input.spawned_from_session || null,
|
|
11744
|
+
assignedBy || null,
|
|
11745
|
+
assignedFromProject || null,
|
|
11746
|
+
input.task_type || null,
|
|
11747
|
+
machineId
|
|
11748
|
+
]);
|
|
11749
|
+
break;
|
|
11750
|
+
} catch (e) {
|
|
11751
|
+
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
11752
|
+
id = uuid();
|
|
11477
11753
|
continue;
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
title: substituteVars(tt.title_pattern, resolved),
|
|
11481
|
-
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
11482
|
-
priority: tt.priority,
|
|
11483
|
-
tags: tt.tags,
|
|
11484
|
-
task_type: tt.task_type,
|
|
11485
|
-
depends_on_positions: tt.depends_on_positions
|
|
11486
|
-
});
|
|
11754
|
+
}
|
|
11755
|
+
throw e;
|
|
11487
11756
|
}
|
|
11488
11757
|
}
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
template_name: template.name,
|
|
11492
|
-
description: template.description,
|
|
11493
|
-
variables: template.variables,
|
|
11494
|
-
resolved_variables: resolved,
|
|
11495
|
-
tasks
|
|
11496
|
-
};
|
|
11497
|
-
}
|
|
11498
|
-
var init_templates = __esm(() => {
|
|
11499
|
-
init_database();
|
|
11500
|
-
init_tasks();
|
|
11501
|
-
init_storage_tombstones();
|
|
11502
|
-
});
|
|
11503
|
-
|
|
11504
|
-
// src/db/task-graph.ts
|
|
11505
|
-
function addDependency(taskId, dependsOn, db) {
|
|
11506
|
-
const d = db || getDatabase();
|
|
11507
|
-
if (!getTask(taskId, d))
|
|
11508
|
-
throw new TaskNotFoundError(taskId);
|
|
11509
|
-
if (!getTask(dependsOn, d))
|
|
11510
|
-
throw new TaskNotFoundError(dependsOn);
|
|
11511
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
11512
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
11758
|
+
if (tags.length > 0) {
|
|
11759
|
+
insertTaskTags(id, tags, d);
|
|
11513
11760
|
}
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11520
|
-
|
|
11521
|
-
function getTaskDependencies(taskId, db) {
|
|
11522
|
-
const d = db || getDatabase();
|
|
11523
|
-
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
11761
|
+
const task = getTask(id, d);
|
|
11762
|
+
const payload = taskEventData(task);
|
|
11763
|
+
const databasePath = databasePathFromDatabase(d);
|
|
11764
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
11765
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
11766
|
+
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
11767
|
+
return task;
|
|
11524
11768
|
}
|
|
11525
|
-
function
|
|
11769
|
+
function getTask(id, db) {
|
|
11526
11770
|
const d = db || getDatabase();
|
|
11527
|
-
|
|
11771
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
11772
|
+
if (!row)
|
|
11773
|
+
return null;
|
|
11774
|
+
return rowToTask(row);
|
|
11528
11775
|
}
|
|
11529
|
-
function
|
|
11776
|
+
function getTaskWithRelations(id, db) {
|
|
11530
11777
|
const d = db || getDatabase();
|
|
11531
|
-
const
|
|
11532
|
-
if (!
|
|
11533
|
-
|
|
11534
|
-
const
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11778
|
+
const task = getTask(id, d);
|
|
11779
|
+
if (!task)
|
|
11780
|
+
return null;
|
|
11781
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
11782
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
11783
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
11784
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
11785
|
+
WHERE td.task_id = ?`).all(id);
|
|
11786
|
+
const dependencies = depRows.map(rowToTask);
|
|
11787
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
11788
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
11789
|
+
WHERE td.depends_on = ?`).all(id);
|
|
11790
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
11791
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
11792
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
11793
|
+
const checklist = getChecklist(id, d);
|
|
11794
|
+
return {
|
|
11795
|
+
...task,
|
|
11796
|
+
subtasks,
|
|
11797
|
+
dependencies,
|
|
11798
|
+
blocked_by,
|
|
11799
|
+
comments,
|
|
11800
|
+
parent,
|
|
11801
|
+
checklist
|
|
11549
11802
|
};
|
|
11550
|
-
return createTask(input, d);
|
|
11551
11803
|
}
|
|
11552
|
-
function
|
|
11804
|
+
function listTasks(filter = {}, db) {
|
|
11553
11805
|
const d = db || getDatabase();
|
|
11554
|
-
const
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
return depTask && depTask.status !== "completed";
|
|
11562
|
-
});
|
|
11563
|
-
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
11806
|
+
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
11807
|
+
clearExpiredLocks2(d);
|
|
11808
|
+
const conditions = [];
|
|
11809
|
+
const params = [];
|
|
11810
|
+
if (filter.project_id) {
|
|
11811
|
+
conditions.push("project_id = ?");
|
|
11812
|
+
params.push(filter.project_id);
|
|
11564
11813
|
}
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
visited.add(id);
|
|
11569
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
11570
|
-
return deps.map((dep) => {
|
|
11571
|
-
const depTask = getTask(dep.depends_on, d);
|
|
11572
|
-
if (!depTask)
|
|
11573
|
-
return null;
|
|
11574
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
11575
|
-
}).filter(Boolean);
|
|
11814
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
11815
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
11816
|
+
params.push(...filter.ids);
|
|
11576
11817
|
}
|
|
11577
|
-
|
|
11578
|
-
if (
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
if (!depTask)
|
|
11585
|
-
return null;
|
|
11586
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
11587
|
-
}).filter(Boolean);
|
|
11818
|
+
if (filter.parent_id !== undefined) {
|
|
11819
|
+
if (filter.parent_id === null) {
|
|
11820
|
+
conditions.push("parent_id IS NULL");
|
|
11821
|
+
} else {
|
|
11822
|
+
conditions.push("parent_id = ?");
|
|
11823
|
+
params.push(filter.parent_id);
|
|
11824
|
+
}
|
|
11588
11825
|
}
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
}
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
if (!task)
|
|
11598
|
-
throw new TaskNotFoundError(taskId);
|
|
11599
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
11600
|
-
const params = [now()];
|
|
11601
|
-
if (target.task_list_id !== undefined) {
|
|
11602
|
-
sets.push("task_list_id = ?");
|
|
11603
|
-
params.push(target.task_list_id);
|
|
11826
|
+
if (filter.status) {
|
|
11827
|
+
if (Array.isArray(filter.status)) {
|
|
11828
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
11829
|
+
params.push(...filter.status);
|
|
11830
|
+
} else {
|
|
11831
|
+
conditions.push("status = ?");
|
|
11832
|
+
params.push(filter.status);
|
|
11833
|
+
}
|
|
11604
11834
|
}
|
|
11605
|
-
if (
|
|
11606
|
-
|
|
11607
|
-
|
|
11835
|
+
if (filter.priority) {
|
|
11836
|
+
if (Array.isArray(filter.priority)) {
|
|
11837
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
11838
|
+
params.push(...filter.priority);
|
|
11839
|
+
} else {
|
|
11840
|
+
conditions.push("priority = ?");
|
|
11841
|
+
params.push(filter.priority);
|
|
11842
|
+
}
|
|
11608
11843
|
}
|
|
11609
|
-
if (
|
|
11610
|
-
|
|
11611
|
-
params.push(
|
|
11844
|
+
if (filter.assigned_to) {
|
|
11845
|
+
conditions.push("assigned_to = ?");
|
|
11846
|
+
params.push(filter.assigned_to);
|
|
11612
11847
|
}
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
}
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11848
|
+
if (filter.agent_id) {
|
|
11849
|
+
conditions.push("agent_id = ?");
|
|
11850
|
+
params.push(filter.agent_id);
|
|
11851
|
+
}
|
|
11852
|
+
if (filter.session_id) {
|
|
11853
|
+
conditions.push("session_id = ?");
|
|
11854
|
+
params.push(filter.session_id);
|
|
11855
|
+
}
|
|
11856
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
11857
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
11858
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
11859
|
+
params.push(...filter.tags);
|
|
11860
|
+
}
|
|
11861
|
+
if (filter.plan_id) {
|
|
11862
|
+
conditions.push("plan_id = ?");
|
|
11863
|
+
params.push(filter.plan_id);
|
|
11864
|
+
}
|
|
11865
|
+
if (filter.task_list_id) {
|
|
11866
|
+
conditions.push("task_list_id = ?");
|
|
11867
|
+
params.push(filter.task_list_id);
|
|
11868
|
+
}
|
|
11869
|
+
if (filter.has_recurrence === true) {
|
|
11870
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
11871
|
+
} else if (filter.has_recurrence === false) {
|
|
11872
|
+
conditions.push("recurrence_rule IS NULL");
|
|
11873
|
+
}
|
|
11874
|
+
if (filter.task_type) {
|
|
11875
|
+
if (Array.isArray(filter.task_type)) {
|
|
11876
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
11877
|
+
params.push(...filter.task_type);
|
|
11878
|
+
} else {
|
|
11879
|
+
conditions.push("task_type = ?");
|
|
11880
|
+
params.push(filter.task_type);
|
|
11630
11881
|
}
|
|
11631
11882
|
}
|
|
11632
|
-
|
|
11883
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
11884
|
+
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
11885
|
+
if (filter.cursor) {
|
|
11886
|
+
try {
|
|
11887
|
+
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
11888
|
+
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
11889
|
+
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
11890
|
+
} catch {}
|
|
11891
|
+
}
|
|
11892
|
+
if (!filter.include_archived) {
|
|
11893
|
+
conditions.push("archived_at IS NULL");
|
|
11894
|
+
}
|
|
11895
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
11896
|
+
let limitClause = "";
|
|
11897
|
+
if (filter.limit) {
|
|
11898
|
+
limitClause = " LIMIT ?";
|
|
11899
|
+
params.push(filter.limit);
|
|
11900
|
+
if (!filter.cursor && filter.offset) {
|
|
11901
|
+
limitClause += " OFFSET ?";
|
|
11902
|
+
params.push(filter.offset);
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
|
|
11906
|
+
return rows.map(rowToTask);
|
|
11633
11907
|
}
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
init_task_crud();
|
|
11638
|
-
});
|
|
11639
|
-
|
|
11640
|
-
// src/db/task-lifecycle.ts
|
|
11641
|
-
function lockExpiresAt(lockedAt) {
|
|
11642
|
-
if (!lockedAt)
|
|
11643
|
-
return null;
|
|
11644
|
-
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
11908
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
11909
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
11910
|
+
return tasks[0] ?? null;
|
|
11645
11911
|
}
|
|
11646
|
-
function
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11912
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
11913
|
+
return {
|
|
11914
|
+
...current,
|
|
11915
|
+
...next ?? {},
|
|
11916
|
+
fingerprint
|
|
11917
|
+
};
|
|
11652
11918
|
}
|
|
11653
|
-
function
|
|
11919
|
+
function upsertTaskByFingerprint(input, db) {
|
|
11654
11920
|
const d = db || getDatabase();
|
|
11655
|
-
const
|
|
11656
|
-
if (
|
|
11657
|
-
|
|
11658
|
-
const
|
|
11659
|
-
|
|
11660
|
-
const
|
|
11661
|
-
if (
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11921
|
+
const fingerprint = input.fingerprint.trim();
|
|
11922
|
+
if (!fingerprint)
|
|
11923
|
+
throw new Error("fingerprint is required");
|
|
11924
|
+
const tx = d.transaction(() => {
|
|
11925
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
11926
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
11927
|
+
if (!existing) {
|
|
11928
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
11929
|
+
return { task: task2, created: true };
|
|
11930
|
+
}
|
|
11931
|
+
const task = updateTask(existing.id, {
|
|
11932
|
+
version: existing.version,
|
|
11933
|
+
title: input.title,
|
|
11934
|
+
description: input.description,
|
|
11935
|
+
status: input.status,
|
|
11936
|
+
priority: input.priority,
|
|
11937
|
+
project_id: input.project_id,
|
|
11938
|
+
assigned_to: input.assigned_to,
|
|
11939
|
+
working_dir: input.working_dir,
|
|
11940
|
+
plan_id: input.plan_id,
|
|
11941
|
+
task_list_id: input.task_list_id,
|
|
11942
|
+
tags: input.tags,
|
|
11943
|
+
metadata,
|
|
11944
|
+
due_at: input.due_at,
|
|
11945
|
+
estimated_minutes: input.estimated_minutes,
|
|
11946
|
+
sla_minutes: input.sla_minutes,
|
|
11947
|
+
confidence: input.confidence,
|
|
11948
|
+
retry_count: input.retry_count,
|
|
11949
|
+
max_retries: input.max_retries,
|
|
11950
|
+
retry_after: input.retry_after,
|
|
11951
|
+
requires_approval: input.requires_approval,
|
|
11952
|
+
recurrence_rule: input.recurrence_rule,
|
|
11953
|
+
task_type: input.task_type
|
|
11954
|
+
}, d);
|
|
11955
|
+
return { task, created: false };
|
|
11956
|
+
});
|
|
11957
|
+
return tx();
|
|
11665
11958
|
}
|
|
11666
|
-
function
|
|
11959
|
+
function countTasks(filter = {}, db) {
|
|
11667
11960
|
const d = db || getDatabase();
|
|
11668
|
-
const
|
|
11669
|
-
const
|
|
11670
|
-
if (
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
const blocking = getBlockingDeps(id, d);
|
|
11674
|
-
if (blocking.length > 0) {
|
|
11675
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
11676
|
-
emitLocalEventHooksQuiet({
|
|
11677
|
-
type: "task.blocked",
|
|
11678
|
-
payload: {
|
|
11679
|
-
id,
|
|
11680
|
-
agent_id: agentId,
|
|
11681
|
-
title: task.title,
|
|
11682
|
-
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
11683
|
-
},
|
|
11684
|
-
databasePath
|
|
11685
|
-
});
|
|
11686
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
11687
|
-
}
|
|
11688
|
-
const cutoff = lockExpiryCutoff();
|
|
11689
|
-
const timestamp = now();
|
|
11690
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
11691
|
-
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
11692
|
-
if (result.changes === 0) {
|
|
11693
|
-
const current = getTask(id, d);
|
|
11694
|
-
if (!current)
|
|
11695
|
-
throw new TaskNotFoundError(id);
|
|
11696
|
-
assertStartable(current, agentId);
|
|
11697
|
-
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
11698
|
-
throw new LockError(id, current.locked_by);
|
|
11699
|
-
}
|
|
11700
|
-
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
11961
|
+
const conditions = [];
|
|
11962
|
+
const params = [];
|
|
11963
|
+
if (filter.project_id) {
|
|
11964
|
+
conditions.push("project_id = ?");
|
|
11965
|
+
params.push(filter.project_id);
|
|
11701
11966
|
}
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
11706
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
11707
|
-
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
11708
|
-
return startedTask;
|
|
11709
|
-
}
|
|
11710
|
-
function completeTask(id, agentId, db, options) {
|
|
11711
|
-
const d = db || getDatabase();
|
|
11712
|
-
const databasePath = databasePathFromDatabase(d);
|
|
11713
|
-
const task = getTask(id, d);
|
|
11714
|
-
if (!task)
|
|
11715
|
-
throw new TaskNotFoundError(id);
|
|
11716
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
11717
|
-
throw new LockError(id, task.locked_by);
|
|
11967
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
11968
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
11969
|
+
params.push(...filter.ids);
|
|
11718
11970
|
}
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
completionMeta._completion = { confidence: options.confidence };
|
|
11971
|
+
if (filter.parent_id !== undefined) {
|
|
11972
|
+
if (filter.parent_id === null) {
|
|
11973
|
+
conditions.push("parent_id IS NULL");
|
|
11974
|
+
} else {
|
|
11975
|
+
conditions.push("parent_id = ?");
|
|
11976
|
+
params.push(filter.parent_id);
|
|
11977
|
+
}
|
|
11727
11978
|
}
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
if (metaResult.changes === 0) {
|
|
11736
|
-
const current = getTask(id, d);
|
|
11737
|
-
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
11738
|
-
}
|
|
11979
|
+
if (filter.status) {
|
|
11980
|
+
if (Array.isArray(filter.status)) {
|
|
11981
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
11982
|
+
params.push(...filter.status);
|
|
11983
|
+
} else {
|
|
11984
|
+
conditions.push("status = ?");
|
|
11985
|
+
params.push(filter.status);
|
|
11739
11986
|
}
|
|
11740
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
11741
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
11742
|
-
});
|
|
11743
|
-
tx();
|
|
11744
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
11745
|
-
const completedTaskForEvent = {
|
|
11746
|
-
...task,
|
|
11747
|
-
status: "completed",
|
|
11748
|
-
locked_by: null,
|
|
11749
|
-
locked_at: null,
|
|
11750
|
-
completed_at: timestamp,
|
|
11751
|
-
confidence,
|
|
11752
|
-
version: task.version + 1,
|
|
11753
|
-
updated_at: timestamp,
|
|
11754
|
-
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
11755
|
-
};
|
|
11756
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
11757
|
-
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
11758
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
11759
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
11760
|
-
let spawnedTask = null;
|
|
11761
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
11762
|
-
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
11763
11987
|
}
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
11988
|
+
if (filter.priority) {
|
|
11989
|
+
if (Array.isArray(filter.priority)) {
|
|
11990
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
11991
|
+
params.push(...filter.priority);
|
|
11769
11992
|
} else {
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
project_id: task.project_id ?? undefined,
|
|
11773
|
-
plan_id: task.plan_id ?? undefined,
|
|
11774
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
11775
|
-
assigned_to: task.assigned_to ?? undefined
|
|
11776
|
-
}, d);
|
|
11777
|
-
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
11778
|
-
spawnedFromTemplate = createTask(input, d);
|
|
11779
|
-
} catch {}
|
|
11993
|
+
conditions.push("priority = ?");
|
|
11994
|
+
params.push(filter.priority);
|
|
11780
11995
|
}
|
|
11781
11996
|
}
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11997
|
+
if (filter.assigned_to) {
|
|
11998
|
+
conditions.push("assigned_to = ?");
|
|
11999
|
+
params.push(filter.assigned_to);
|
|
11785
12000
|
}
|
|
11786
|
-
if (
|
|
11787
|
-
|
|
12001
|
+
if (filter.agent_id) {
|
|
12002
|
+
conditions.push("agent_id = ?");
|
|
12003
|
+
params.push(filter.agent_id);
|
|
11788
12004
|
}
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
|
|
12005
|
+
if (filter.session_id) {
|
|
12006
|
+
conditions.push("session_id = ?");
|
|
12007
|
+
params.push(filter.session_id);
|
|
12008
|
+
}
|
|
12009
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
12010
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
12011
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
12012
|
+
params.push(...filter.tags);
|
|
12013
|
+
}
|
|
12014
|
+
if (filter.plan_id) {
|
|
12015
|
+
conditions.push("plan_id = ?");
|
|
12016
|
+
params.push(filter.plan_id);
|
|
12017
|
+
}
|
|
12018
|
+
if (filter.task_list_id) {
|
|
12019
|
+
conditions.push("task_list_id = ?");
|
|
12020
|
+
params.push(filter.task_list_id);
|
|
12021
|
+
}
|
|
12022
|
+
if (filter.has_recurrence === true) {
|
|
12023
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
12024
|
+
} else if (filter.has_recurrence === false) {
|
|
12025
|
+
conditions.push("recurrence_rule IS NULL");
|
|
12026
|
+
}
|
|
12027
|
+
if (filter.task_type) {
|
|
12028
|
+
if (Array.isArray(filter.task_type)) {
|
|
12029
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
12030
|
+
params.push(...filter.task_type);
|
|
12031
|
+
} else {
|
|
12032
|
+
conditions.push("task_type = ?");
|
|
12033
|
+
params.push(filter.task_type);
|
|
11806
12034
|
}
|
|
11807
12035
|
}
|
|
11808
|
-
|
|
12036
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
12037
|
+
if (!filter.include_archived) {
|
|
12038
|
+
conditions.push("archived_at IS NULL");
|
|
12039
|
+
}
|
|
12040
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
12041
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
12042
|
+
return row.count;
|
|
11809
12043
|
}
|
|
11810
|
-
function
|
|
12044
|
+
function updateTask(id, input, db) {
|
|
11811
12045
|
const d = db || getDatabase();
|
|
11812
12046
|
const task = getTask(id, d);
|
|
11813
12047
|
if (!task)
|
|
11814
12048
|
throw new TaskNotFoundError(id);
|
|
11815
|
-
if (task.
|
|
11816
|
-
|
|
11817
|
-
success: false,
|
|
11818
|
-
error: `Task is ${task.status} and cannot be locked`
|
|
11819
|
-
};
|
|
11820
|
-
}
|
|
11821
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
11822
|
-
const timestamp2 = now();
|
|
11823
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
11824
|
-
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
11825
|
-
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
12049
|
+
if (task.version !== input.version) {
|
|
12050
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
11826
12051
|
}
|
|
11827
|
-
const cutoff = lockExpiryCutoff();
|
|
11828
12052
|
const timestamp = now();
|
|
11829
|
-
const
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
12053
|
+
const completionTimestamp = input.completed_at ?? timestamp;
|
|
12054
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
12055
|
+
const params = [timestamp];
|
|
12056
|
+
if (input.title !== undefined) {
|
|
12057
|
+
sets.push("title = ?");
|
|
12058
|
+
params.push(input.title);
|
|
12059
|
+
}
|
|
12060
|
+
if (input.description !== undefined) {
|
|
12061
|
+
sets.push("description = ?");
|
|
12062
|
+
params.push(input.description);
|
|
12063
|
+
}
|
|
12064
|
+
if (input.status !== undefined) {
|
|
12065
|
+
if (input.status === "completed") {
|
|
12066
|
+
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
11840
12067
|
}
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
12068
|
+
sets.push("status = ?");
|
|
12069
|
+
params.push(input.status);
|
|
12070
|
+
if (input.status === "completed") {
|
|
12071
|
+
sets.push("completed_at = ?");
|
|
12072
|
+
params.push(completionTimestamp);
|
|
12073
|
+
sets.push("locked_by = NULL");
|
|
12074
|
+
sets.push("locked_at = NULL");
|
|
12075
|
+
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
12076
|
+
sets.push("completed_at = NULL");
|
|
11848
12077
|
}
|
|
11849
|
-
return {
|
|
11850
|
-
success: false,
|
|
11851
|
-
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
11852
|
-
};
|
|
11853
12078
|
}
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
function unlockTask(id, agentId, db) {
|
|
11858
|
-
const d = db || getDatabase();
|
|
11859
|
-
const task = getTask(id, d);
|
|
11860
|
-
if (!task)
|
|
11861
|
-
throw new TaskNotFoundError(id);
|
|
11862
|
-
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
11863
|
-
throw new LockError(id, task.locked_by);
|
|
12079
|
+
if (input.priority !== undefined) {
|
|
12080
|
+
sets.push("priority = ?");
|
|
12081
|
+
params.push(input.priority);
|
|
11864
12082
|
}
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
return true;
|
|
11869
|
-
}
|
|
11870
|
-
function getTaskLockStatus(id, db) {
|
|
11871
|
-
const d = db || getDatabase();
|
|
11872
|
-
const task = getTask(id, d);
|
|
11873
|
-
if (!task)
|
|
11874
|
-
throw new TaskNotFoundError(id);
|
|
11875
|
-
const expired = isLockExpired(task.locked_at);
|
|
11876
|
-
return {
|
|
11877
|
-
task_id: id,
|
|
11878
|
-
locked: !!task.locked_by && !expired,
|
|
11879
|
-
locked_by: task.locked_by,
|
|
11880
|
-
locked_at: task.locked_at,
|
|
11881
|
-
expires_at: lockExpiresAt(task.locked_at),
|
|
11882
|
-
expired
|
|
11883
|
-
};
|
|
11884
|
-
}
|
|
11885
|
-
function claimNextTask(agentId, filters, db) {
|
|
11886
|
-
const d = db || getDatabase();
|
|
11887
|
-
const tx = d.transaction(() => {
|
|
11888
|
-
const task = getNextTask(agentId, filters, d);
|
|
11889
|
-
if (!task)
|
|
11890
|
-
return null;
|
|
11891
|
-
return startTask(task.id, agentId, d);
|
|
11892
|
-
});
|
|
11893
|
-
return tx();
|
|
11894
|
-
}
|
|
11895
|
-
function getNextTask(agentId, filters, db) {
|
|
11896
|
-
const d = db || getDatabase();
|
|
11897
|
-
clearExpiredLocks(d);
|
|
11898
|
-
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
11899
|
-
const params = [lockExpiryCutoff()];
|
|
11900
|
-
if (filters?.project_id) {
|
|
11901
|
-
conditions.push("project_id = ?");
|
|
11902
|
-
params.push(filters.project_id);
|
|
12083
|
+
if (input.project_id !== undefined) {
|
|
12084
|
+
sets.push("project_id = ?");
|
|
12085
|
+
params.push(input.project_id);
|
|
11903
12086
|
}
|
|
11904
|
-
if (
|
|
11905
|
-
|
|
11906
|
-
params.push(
|
|
12087
|
+
if (input.assigned_to !== undefined) {
|
|
12088
|
+
sets.push("assigned_to = ?");
|
|
12089
|
+
params.push(input.assigned_to);
|
|
11907
12090
|
}
|
|
11908
|
-
if (
|
|
11909
|
-
|
|
11910
|
-
params.push(
|
|
12091
|
+
if (input.working_dir !== undefined) {
|
|
12092
|
+
sets.push("working_dir = ?");
|
|
12093
|
+
params.push(input.working_dir);
|
|
11911
12094
|
}
|
|
11912
|
-
if (
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
params.push(...filters.tags);
|
|
12095
|
+
if (input.tags !== undefined) {
|
|
12096
|
+
sets.push("tags = ?");
|
|
12097
|
+
params.push(JSON.stringify(input.tags));
|
|
11916
12098
|
}
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
if (agentId) {
|
|
11921
|
-
const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
|
|
11922
|
-
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
12099
|
+
if (input.metadata !== undefined) {
|
|
12100
|
+
sets.push("metadata = ?");
|
|
12101
|
+
params.push(JSON.stringify(input.metadata));
|
|
11923
12102
|
}
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
params.push(agentId);
|
|
12103
|
+
if (input.plan_id !== undefined) {
|
|
12104
|
+
sets.push("plan_id = ?");
|
|
12105
|
+
params.push(input.plan_id);
|
|
11928
12106
|
}
|
|
11929
|
-
if (
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
params.push(...recentProjectIds);
|
|
12107
|
+
if (input.task_list_id !== undefined) {
|
|
12108
|
+
sets.push("task_list_id = ?");
|
|
12109
|
+
params.push(input.task_list_id);
|
|
11933
12110
|
}
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
}
|
|
11938
|
-
function getActiveWork(filters, db) {
|
|
11939
|
-
const d = db || getDatabase();
|
|
11940
|
-
clearExpiredLocks(d);
|
|
11941
|
-
const conditions = ["status = 'in_progress'"];
|
|
11942
|
-
const params = [];
|
|
11943
|
-
if (filters?.project_id) {
|
|
11944
|
-
conditions.push("project_id = ?");
|
|
11945
|
-
params.push(filters.project_id);
|
|
12111
|
+
if (input.due_at !== undefined) {
|
|
12112
|
+
sets.push("due_at = ?");
|
|
12113
|
+
params.push(input.due_at);
|
|
11946
12114
|
}
|
|
11947
|
-
if (
|
|
11948
|
-
|
|
11949
|
-
params.push(
|
|
12115
|
+
if (input.estimated_minutes !== undefined) {
|
|
12116
|
+
sets.push("estimated_minutes = ?");
|
|
12117
|
+
params.push(input.estimated_minutes);
|
|
11950
12118
|
}
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
updated_at DESC`).all(...params);
|
|
11955
|
-
return rows;
|
|
11956
|
-
}
|
|
11957
|
-
function getTasksChangedSince(since, filters, db) {
|
|
11958
|
-
const d = db || getDatabase();
|
|
11959
|
-
const conditions = ["updated_at > ?"];
|
|
11960
|
-
const params = [since];
|
|
11961
|
-
if (filters?.project_id) {
|
|
11962
|
-
conditions.push("project_id = ?");
|
|
11963
|
-
params.push(filters.project_id);
|
|
12119
|
+
if (input.sla_minutes !== undefined) {
|
|
12120
|
+
sets.push("sla_minutes = ?");
|
|
12121
|
+
params.push(input.sla_minutes);
|
|
11964
12122
|
}
|
|
11965
|
-
if (
|
|
11966
|
-
|
|
11967
|
-
params.push(
|
|
12123
|
+
if (input.actual_minutes !== undefined) {
|
|
12124
|
+
sets.push("actual_minutes = ?");
|
|
12125
|
+
params.push(input.actual_minutes);
|
|
11968
12126
|
}
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
}
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
if (
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
12127
|
+
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
12128
|
+
sets.push("completed_at = ?");
|
|
12129
|
+
params.push(input.completed_at);
|
|
12130
|
+
}
|
|
12131
|
+
if (input.confidence !== undefined) {
|
|
12132
|
+
sets.push("confidence = ?");
|
|
12133
|
+
params.push(input.confidence);
|
|
12134
|
+
}
|
|
12135
|
+
if (input.retry_count !== undefined) {
|
|
12136
|
+
sets.push("retry_count = ?");
|
|
12137
|
+
params.push(input.retry_count);
|
|
12138
|
+
}
|
|
12139
|
+
if (input.max_retries !== undefined) {
|
|
12140
|
+
sets.push("max_retries = ?");
|
|
12141
|
+
params.push(input.max_retries);
|
|
12142
|
+
}
|
|
12143
|
+
if (input.retry_after !== undefined) {
|
|
12144
|
+
sets.push("retry_after = ?");
|
|
12145
|
+
params.push(input.retry_after);
|
|
12146
|
+
}
|
|
12147
|
+
if (input.requires_approval !== undefined) {
|
|
12148
|
+
sets.push("requires_approval = ?");
|
|
12149
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
12150
|
+
}
|
|
12151
|
+
if (input.approved_by !== undefined) {
|
|
12152
|
+
sets.push("approved_by = ?");
|
|
12153
|
+
params.push(input.approved_by);
|
|
12154
|
+
sets.push("approved_at = ?");
|
|
12155
|
+
params.push(now());
|
|
12156
|
+
}
|
|
12157
|
+
if (input.recurrence_rule !== undefined) {
|
|
12158
|
+
sets.push("recurrence_rule = ?");
|
|
12159
|
+
params.push(input.recurrence_rule);
|
|
12160
|
+
}
|
|
12161
|
+
if (input.task_type !== undefined) {
|
|
12162
|
+
sets.push("task_type = ?");
|
|
12163
|
+
params.push(input.task_type ?? null);
|
|
12164
|
+
}
|
|
12165
|
+
params.push(id, input.version);
|
|
12166
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
12167
|
+
if (result.changes === 0) {
|
|
12168
|
+
const current = getTask(id, d);
|
|
12169
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
12170
|
+
}
|
|
12171
|
+
if (input.tags !== undefined) {
|
|
12172
|
+
replaceTaskTags(id, input.tags, d);
|
|
12173
|
+
}
|
|
12174
|
+
const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
|
|
12175
|
+
if (transitionedToCompleted && task.recurrence_rule) {
|
|
12176
|
+
try {
|
|
12177
|
+
const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
|
|
12178
|
+
spawnNextRecurrence2(task, d, completionTimestamp);
|
|
12179
|
+
} catch (e) {
|
|
12180
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
11987
12181
|
}
|
|
11988
|
-
}
|
|
11989
|
-
const
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
12182
|
+
}
|
|
12183
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
12184
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
12185
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
12186
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
12187
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
12188
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
12189
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
12190
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
12191
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
12192
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
12193
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
12194
|
+
if (input.approved_by !== undefined)
|
|
12195
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
12196
|
+
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
12197
|
+
const completedNow = input.status === "completed";
|
|
12198
|
+
const updatedTask = {
|
|
11993
12199
|
...task,
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
metadata: meta,
|
|
12200
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
12201
|
+
tags: input.tags ?? task.tags,
|
|
12202
|
+
metadata: input.metadata ?? task.metadata,
|
|
11998
12203
|
version: task.version + 1,
|
|
11999
|
-
updated_at: timestamp
|
|
12204
|
+
updated_at: timestamp,
|
|
12205
|
+
locked_by: completedNow ? null : task.locked_by,
|
|
12206
|
+
locked_at: completedNow ? null : task.locked_at,
|
|
12207
|
+
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
12208
|
+
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
12209
|
+
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
12210
|
+
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
12211
|
+
retry_count: input.retry_count ?? task.retry_count,
|
|
12212
|
+
max_retries: input.max_retries ?? task.max_retries,
|
|
12213
|
+
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
12214
|
+
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
12215
|
+
approved_by: input.approved_by ?? task.approved_by,
|
|
12216
|
+
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
12000
12217
|
};
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
if (options?.retry) {
|
|
12008
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
12009
|
-
const maxRetries = task.max_retries || 3;
|
|
12010
|
-
if (retryCount > maxRetries) {
|
|
12011
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
12012
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
12013
|
-
id
|
|
12014
|
-
]);
|
|
12015
|
-
} else {
|
|
12016
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
12017
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
12018
|
-
let title = task.title;
|
|
12019
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
12020
|
-
title = title.slice(task.short_id.length + 2);
|
|
12021
|
-
}
|
|
12022
|
-
retryTask = createTask({
|
|
12023
|
-
title,
|
|
12024
|
-
description: task.description ?? undefined,
|
|
12025
|
-
priority: task.priority,
|
|
12026
|
-
project_id: task.project_id ?? undefined,
|
|
12027
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
12028
|
-
plan_id: task.plan_id ?? undefined,
|
|
12029
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
12030
|
-
tags: task.tags,
|
|
12031
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
12032
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
12033
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
12034
|
-
due_at: retryAfter
|
|
12035
|
-
}, d);
|
|
12036
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
12037
|
-
}
|
|
12218
|
+
const databasePath = databasePathFromDatabase(d);
|
|
12219
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
12220
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
12221
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
12222
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
12223
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
12038
12224
|
}
|
|
12039
|
-
|
|
12040
|
-
}
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
12045
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
12046
|
-
const conditions = [
|
|
12047
|
-
"status = 'in_progress'",
|
|
12048
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
12049
|
-
];
|
|
12050
|
-
const params = [cutoff, cutoff];
|
|
12051
|
-
if (effectiveFilters?.project_id) {
|
|
12052
|
-
conditions.push("project_id = ?");
|
|
12053
|
-
params.push(effectiveFilters.project_id);
|
|
12225
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
12226
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
12227
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
12228
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
12229
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
12054
12230
|
}
|
|
12055
|
-
if (
|
|
12056
|
-
|
|
12057
|
-
params.push(effectiveFilters.task_list_id);
|
|
12231
|
+
if (input.approved_by !== undefined) {
|
|
12232
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
12058
12233
|
}
|
|
12059
|
-
const
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
}
|
|
12063
|
-
|
|
12064
|
-
const d = db || getDatabase();
|
|
12065
|
-
const databasePath = databasePathFromDatabase(d);
|
|
12066
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
12067
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
12068
|
-
if (staleTasks.length === 0)
|
|
12069
|
-
return null;
|
|
12070
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
12071
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
12072
|
-
const target = staleTasks[0];
|
|
12073
|
-
const timestamp = now();
|
|
12074
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
12075
|
-
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
12076
|
-
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
|
|
12077
|
-
if (result.changes === 0)
|
|
12078
|
-
return null;
|
|
12079
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
12080
|
-
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
12081
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
12082
|
-
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
12083
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
12084
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
12085
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
12086
|
-
return stolenTask;
|
|
12234
|
+
const updatePayload = taskEventData(updatedTask);
|
|
12235
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
12236
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
12237
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
12238
|
+
return updatedTask;
|
|
12087
12239
|
}
|
|
12088
|
-
function
|
|
12240
|
+
function deleteTask(id, db) {
|
|
12089
12241
|
const d = db || getDatabase();
|
|
12090
|
-
const
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
return tx();
|
|
12102
|
-
}
|
|
12103
|
-
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
12104
|
-
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
12105
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
12106
|
-
let title = completedTask.title;
|
|
12107
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
12108
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
12109
|
-
}
|
|
12110
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
12111
|
-
return createTask({
|
|
12112
|
-
title,
|
|
12113
|
-
description: completedTask.description ?? undefined,
|
|
12114
|
-
priority: completedTask.priority,
|
|
12115
|
-
project_id: completedTask.project_id ?? undefined,
|
|
12116
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
12117
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
12118
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
12119
|
-
tags: completedTask.tags,
|
|
12120
|
-
metadata: completedTask.metadata,
|
|
12121
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
12122
|
-
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
12123
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
12124
|
-
recurrence_parent_id: recurrenceParentId,
|
|
12125
|
-
due_at: dueAt
|
|
12126
|
-
}, db);
|
|
12242
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
12243
|
+
if (!row)
|
|
12244
|
+
return false;
|
|
12245
|
+
recordStorageTombstone({
|
|
12246
|
+
object_type: "tasks",
|
|
12247
|
+
object_id: id,
|
|
12248
|
+
payload: rowToTask(row),
|
|
12249
|
+
version: row.version
|
|
12250
|
+
}, d);
|
|
12251
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
12252
|
+
return result.changes > 0;
|
|
12127
12253
|
}
|
|
12128
|
-
var
|
|
12129
|
-
var init_task_lifecycle = __esm(() => {
|
|
12254
|
+
var init_task_crud = __esm(() => {
|
|
12130
12255
|
init_types();
|
|
12131
12256
|
init_database();
|
|
12132
12257
|
init_completion_guard();
|
|
@@ -12134,11 +12259,9 @@ var init_task_lifecycle = __esm(() => {
|
|
|
12134
12259
|
init_event_hooks();
|
|
12135
12260
|
init_shared_events();
|
|
12136
12261
|
init_audit();
|
|
12137
|
-
init_recurrence();
|
|
12138
12262
|
init_webhooks();
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
init_task_graph();
|
|
12263
|
+
init_checklists();
|
|
12264
|
+
init_storage_tombstones();
|
|
12142
12265
|
});
|
|
12143
12266
|
|
|
12144
12267
|
// src/db/task-status.ts
|
|
@@ -12228,6 +12351,15 @@ function setTaskStatus(id, status, _agentId, db) {
|
|
|
12228
12351
|
throw new TaskNotFoundError(id);
|
|
12229
12352
|
if (task.status === status)
|
|
12230
12353
|
return task;
|
|
12354
|
+
if (status === "completed") {
|
|
12355
|
+
try {
|
|
12356
|
+
return completeTask(id, _agentId, d);
|
|
12357
|
+
} catch (e) {
|
|
12358
|
+
if (e instanceof VersionConflictError && attempt < 2)
|
|
12359
|
+
continue;
|
|
12360
|
+
throw e;
|
|
12361
|
+
}
|
|
12362
|
+
}
|
|
12231
12363
|
try {
|
|
12232
12364
|
return updateTask(id, { status, version: task.version }, d);
|
|
12233
12365
|
} catch (e) {
|
|
@@ -15613,10 +15745,8 @@ var init_token_utils = __esm(() => {
|
|
|
15613
15745
|
"cancel_task",
|
|
15614
15746
|
"check_task_done_contract",
|
|
15615
15747
|
"claim_task",
|
|
15616
|
-
"clone_task",
|
|
15617
15748
|
"delete_task",
|
|
15618
15749
|
"extend_task",
|
|
15619
|
-
"get_active_work",
|
|
15620
15750
|
"get_archived_tasks",
|
|
15621
15751
|
"get_blocked_tasks",
|
|
15622
15752
|
"get_blocking_tasks",
|
|
@@ -15652,7 +15782,8 @@ var init_token_utils = __esm(() => {
|
|
|
15652
15782
|
"task_context",
|
|
15653
15783
|
"unlock_task",
|
|
15654
15784
|
"unarchive_task",
|
|
15655
|
-
"update_task"
|
|
15785
|
+
"update_task",
|
|
15786
|
+
"upsert_task"
|
|
15656
15787
|
],
|
|
15657
15788
|
projects: [
|
|
15658
15789
|
"bootstrap_project",
|
|
@@ -15911,12 +16042,8 @@ var init_token_utils = __esm(() => {
|
|
|
15911
16042
|
"delete_tag",
|
|
15912
16043
|
"get_label",
|
|
15913
16044
|
"get_activity_timeline",
|
|
15914
|
-
"get_recent_activity",
|
|
15915
16045
|
"get_tag",
|
|
15916
16046
|
"get_task_fields",
|
|
15917
|
-
"get_task_graph",
|
|
15918
|
-
"get_task_history",
|
|
15919
|
-
"get_task_stats",
|
|
15920
16047
|
"list_workflow_states",
|
|
15921
16048
|
"list_labels",
|
|
15922
16049
|
"list_tags",
|
|
@@ -15927,6 +16054,10 @@ var init_token_utils = __esm(() => {
|
|
|
15927
16054
|
"describe_tools",
|
|
15928
16055
|
"set_task_workflow_state",
|
|
15929
16056
|
"set_task_fields",
|
|
16057
|
+
"assign_label_to_task",
|
|
16058
|
+
"create_custom_field",
|
|
16059
|
+
"set_task_custom_field",
|
|
16060
|
+
"set_task_priority_meta",
|
|
15930
16061
|
"update_label",
|
|
15931
16062
|
"update_tag"
|
|
15932
16063
|
],
|
|
@@ -15952,7 +16083,6 @@ var init_token_utils = __esm(() => {
|
|
|
15952
16083
|
"update_template",
|
|
15953
16084
|
"write_template_library"
|
|
15954
16085
|
],
|
|
15955
|
-
webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
|
|
15956
16086
|
machines: [
|
|
15957
16087
|
"machines_archive",
|
|
15958
16088
|
"machines_delete",
|
|
@@ -38526,6 +38656,11 @@ function safeEqualHex(a, b) {
|
|
|
38526
38656
|
return false;
|
|
38527
38657
|
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
38528
38658
|
}
|
|
38659
|
+
function safeEqualStrings(a, b) {
|
|
38660
|
+
const ah = createHash12("sha256").update(a, "utf8").digest();
|
|
38661
|
+
const bh = createHash12("sha256").update(b, "utf8").digest();
|
|
38662
|
+
return timingSafeEqual3(ah, bh);
|
|
38663
|
+
}
|
|
38529
38664
|
function hasActiveApiKeys(db) {
|
|
38530
38665
|
const d = db || getDatabase();
|
|
38531
38666
|
const row = d.query("SELECT COUNT(*) AS count FROM api_keys WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)").get(now());
|
|
@@ -38698,6 +38833,37 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
38698
38833
|
return fallback;
|
|
38699
38834
|
return Math.min(parsed, max);
|
|
38700
38835
|
}
|
|
38836
|
+
function mapTaskError(e, json2) {
|
|
38837
|
+
if (e instanceof VersionConflictError) {
|
|
38838
|
+
return json2({
|
|
38839
|
+
error: e.message,
|
|
38840
|
+
code: VersionConflictError.code,
|
|
38841
|
+
expected_version: e.expectedVersion,
|
|
38842
|
+
current_version: e.actualVersion
|
|
38843
|
+
}, 409);
|
|
38844
|
+
}
|
|
38845
|
+
if (e instanceof TaskNotFoundError) {
|
|
38846
|
+
return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
|
|
38847
|
+
}
|
|
38848
|
+
if (e instanceof LockError) {
|
|
38849
|
+
return json2({ error: e.message, code: LockError.code }, 409);
|
|
38850
|
+
}
|
|
38851
|
+
if (e instanceof CompletionGuardError) {
|
|
38852
|
+
return json2({
|
|
38853
|
+
error: e.message,
|
|
38854
|
+
code: CompletionGuardError.code,
|
|
38855
|
+
retry_after: e.retryAfterSeconds ?? null
|
|
38856
|
+
}, 409);
|
|
38857
|
+
}
|
|
38858
|
+
if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
|
|
38859
|
+
return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
|
|
38860
|
+
}
|
|
38861
|
+
return null;
|
|
38862
|
+
}
|
|
38863
|
+
function countRecurringTasks() {
|
|
38864
|
+
const row = getDatabase().query("SELECT COUNT(*) as count FROM tasks WHERE recurrence_rule IS NOT NULL AND recurrence_rule != '' AND archived_at IS NULL").get();
|
|
38865
|
+
return row?.count ?? 0;
|
|
38866
|
+
}
|
|
38701
38867
|
function handleSseEvents(_req, url, ctx) {
|
|
38702
38868
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
38703
38869
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
@@ -38776,35 +38942,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
38776
38942
|
});
|
|
38777
38943
|
}
|
|
38778
38944
|
function handleHealth(_ctx, json2) {
|
|
38779
|
-
const
|
|
38780
|
-
const
|
|
38781
|
-
const
|
|
38782
|
-
return json2({
|
|
38945
|
+
const stats2 = getTaskStats();
|
|
38946
|
+
const staleCount = getStaleTasks(30).length;
|
|
38947
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
38948
|
+
return json2({
|
|
38949
|
+
status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
|
|
38950
|
+
tasks: stats2.total,
|
|
38951
|
+
stale: staleCount,
|
|
38952
|
+
overdue_recurring: overdueRecurring,
|
|
38953
|
+
timestamp: new Date().toISOString()
|
|
38954
|
+
});
|
|
38783
38955
|
}
|
|
38784
38956
|
function handleHeadlessBoundary(_ctx, json2) {
|
|
38785
38957
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
38786
38958
|
return json2(getHeadlessBoundaryManifest2());
|
|
38787
38959
|
}
|
|
38788
38960
|
function handleStats(_ctx, json2) {
|
|
38789
|
-
const
|
|
38961
|
+
const stats2 = getTaskStats();
|
|
38962
|
+
const byStatus = stats2.by_status;
|
|
38790
38963
|
const projects = listProjects();
|
|
38791
38964
|
const agents = listAgents();
|
|
38792
|
-
const
|
|
38793
|
-
const
|
|
38794
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
38795
|
-
const recurringTasks = all.filter((t) => t.recurrence_rule).length;
|
|
38965
|
+
const staleCount = getStaleTasks(30).length;
|
|
38966
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
38796
38967
|
return json2({
|
|
38797
|
-
total_tasks:
|
|
38798
|
-
pending:
|
|
38799
|
-
in_progress:
|
|
38800
|
-
completed:
|
|
38801
|
-
failed:
|
|
38802
|
-
cancelled:
|
|
38968
|
+
total_tasks: stats2.total,
|
|
38969
|
+
pending: byStatus["pending"] ?? 0,
|
|
38970
|
+
in_progress: byStatus["in_progress"] ?? 0,
|
|
38971
|
+
completed: byStatus["completed"] ?? 0,
|
|
38972
|
+
failed: byStatus["failed"] ?? 0,
|
|
38973
|
+
cancelled: byStatus["cancelled"] ?? 0,
|
|
38803
38974
|
projects: projects.length,
|
|
38804
38975
|
agents: agents.length,
|
|
38805
|
-
stale_count:
|
|
38976
|
+
stale_count: staleCount,
|
|
38806
38977
|
overdue_recurring: overdueRecurring,
|
|
38807
|
-
recurring_tasks:
|
|
38978
|
+
recurring_tasks: countRecurringTasks()
|
|
38808
38979
|
});
|
|
38809
38980
|
}
|
|
38810
38981
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
@@ -38883,27 +39054,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
38883
39054
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
38884
39055
|
if (format === "csv") {
|
|
38885
39056
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
38886
|
-
const
|
|
38887
|
-
const val = t[h];
|
|
39057
|
+
const csvCell = (val) => {
|
|
38888
39058
|
if (val === null || val === undefined)
|
|
38889
39059
|
return "";
|
|
38890
|
-
|
|
38891
|
-
|
|
38892
|
-
|
|
38893
|
-
|
|
39060
|
+
let str = String(val);
|
|
39061
|
+
if (/^[=+\-@\t\r]/.test(str))
|
|
39062
|
+
str = `'${str}`;
|
|
39063
|
+
if (str.includes(",") || str.includes('"') || str.includes(`
|
|
39064
|
+
`) || str.includes("\r")) {
|
|
39065
|
+
str = `"${str.replace(/"/g, '""')}"`;
|
|
39066
|
+
}
|
|
39067
|
+
return str;
|
|
39068
|
+
};
|
|
39069
|
+
const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
|
|
38894
39070
|
const csv = [headers.join(","), ...rows].join(`
|
|
38895
39071
|
`);
|
|
38896
39072
|
return new Response(csv, {
|
|
38897
39073
|
headers: {
|
|
38898
39074
|
"Content-Type": "text/csv",
|
|
38899
|
-
"Content-Disposition": "attachment; filename=tasks.csv"
|
|
39075
|
+
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
39076
|
+
...SECURITY_HEADERS
|
|
38900
39077
|
}
|
|
38901
39078
|
});
|
|
38902
39079
|
}
|
|
38903
39080
|
return new Response(JSON.stringify(summaries, null, 2), {
|
|
38904
39081
|
headers: {
|
|
38905
39082
|
"Content-Type": "application/json",
|
|
38906
|
-
"Content-Disposition": "attachment; filename=tasks.json"
|
|
39083
|
+
"Content-Disposition": "attachment; filename=tasks.json",
|
|
39084
|
+
...SECURITY_HEADERS
|
|
38907
39085
|
}
|
|
38908
39086
|
});
|
|
38909
39087
|
}
|
|
@@ -39077,12 +39255,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
39077
39255
|
if (ALLOWED.has(key))
|
|
39078
39256
|
safeBody[key] = value;
|
|
39079
39257
|
}
|
|
39258
|
+
const clientVersion = typeof body["version"] === "number" ? body["version"] : task2.version;
|
|
39080
39259
|
const updated = updateTask(id, {
|
|
39081
39260
|
...safeBody,
|
|
39082
|
-
version:
|
|
39261
|
+
version: clientVersion
|
|
39083
39262
|
});
|
|
39084
39263
|
return json2(taskToSummary2(updated));
|
|
39085
39264
|
} catch (e) {
|
|
39265
|
+
const mapped = mapTaskError(e, json2);
|
|
39266
|
+
if (mapped)
|
|
39267
|
+
return mapped;
|
|
39086
39268
|
return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
39087
39269
|
}
|
|
39088
39270
|
}
|
|
@@ -39098,6 +39280,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
|
|
|
39098
39280
|
ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "started", agent_id: "dashboard", project_id: task2.project_id });
|
|
39099
39281
|
return json2(taskToSummary2(task2));
|
|
39100
39282
|
} catch (e) {
|
|
39283
|
+
const mapped = mapTaskError(e, json2);
|
|
39284
|
+
if (mapped)
|
|
39285
|
+
return mapped;
|
|
39101
39286
|
return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
39102
39287
|
}
|
|
39103
39288
|
}
|
|
@@ -39117,6 +39302,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
|
|
|
39117
39302
|
ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "completed", agent_id: "dashboard", project_id: task2.project_id });
|
|
39118
39303
|
return json2(taskToSummary2(task2));
|
|
39119
39304
|
} catch (e) {
|
|
39305
|
+
const mapped = mapTaskError(e, json2);
|
|
39306
|
+
if (mapped)
|
|
39307
|
+
return mapped;
|
|
39120
39308
|
return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
39121
39309
|
}
|
|
39122
39310
|
}
|
|
@@ -39441,6 +39629,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
39441
39629
|
}
|
|
39442
39630
|
var init_routes = __esm(() => {
|
|
39443
39631
|
init_tasks();
|
|
39632
|
+
init_database();
|
|
39633
|
+
init_types();
|
|
39444
39634
|
init_projects();
|
|
39445
39635
|
init_agents();
|
|
39446
39636
|
init_plans();
|
|
@@ -39498,7 +39688,7 @@ function checkAuth(req, apiKey) {
|
|
|
39498
39688
|
if (!apiKey && !generatedKeysEnabled)
|
|
39499
39689
|
return null;
|
|
39500
39690
|
const provided = getProvidedApiKey(req);
|
|
39501
|
-
const matchesEnvKey = Boolean(apiKey && provided && provided
|
|
39691
|
+
const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
|
|
39502
39692
|
const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
|
|
39503
39693
|
if (!matchesEnvKey && !matchesGeneratedKey) {
|
|
39504
39694
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
@@ -39508,6 +39698,15 @@ function checkAuth(req, apiKey) {
|
|
|
39508
39698
|
}
|
|
39509
39699
|
return null;
|
|
39510
39700
|
}
|
|
39701
|
+
function resolveClientIp(req, server) {
|
|
39702
|
+
const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
|
|
39703
|
+
if (trustProxy) {
|
|
39704
|
+
const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
|
|
39705
|
+
if (forwarded)
|
|
39706
|
+
return forwarded;
|
|
39707
|
+
}
|
|
39708
|
+
return server.requestIP(req)?.address || "unknown";
|
|
39709
|
+
}
|
|
39511
39710
|
function checkRateLimit(ip) {
|
|
39512
39711
|
const now4 = Date.now();
|
|
39513
39712
|
const entry = rateLimitMap.get(ip);
|
|
@@ -39635,7 +39834,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
39635
39834
|
const server = Bun.serve({
|
|
39636
39835
|
port,
|
|
39637
39836
|
hostname: hostname3,
|
|
39638
|
-
async fetch(req) {
|
|
39837
|
+
async fetch(req, server2) {
|
|
39639
39838
|
const url = new URL(req.url);
|
|
39640
39839
|
const path = url.pathname;
|
|
39641
39840
|
const method = req.method;
|
|
@@ -39647,15 +39846,6 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
39647
39846
|
Vary: "Origin"
|
|
39648
39847
|
} : undefined;
|
|
39649
39848
|
const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
|
|
39650
|
-
if (path === "/health" && method === "GET") {
|
|
39651
|
-
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
39652
|
-
return healthResponse2("todos");
|
|
39653
|
-
}
|
|
39654
|
-
if (path === "/mcp") {
|
|
39655
|
-
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
39656
|
-
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
39657
|
-
return handleMcpHttpRequest2(req, buildServer2);
|
|
39658
|
-
}
|
|
39659
39849
|
if (method === "OPTIONS") {
|
|
39660
39850
|
return new Response(null, {
|
|
39661
39851
|
headers: corsHeaders || {
|
|
@@ -39663,7 +39853,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
39663
39853
|
}
|
|
39664
39854
|
});
|
|
39665
39855
|
}
|
|
39666
|
-
const ip = req
|
|
39856
|
+
const ip = resolveClientIp(req, server2);
|
|
39667
39857
|
const rl = checkRateLimit(ip);
|
|
39668
39858
|
if (!rl.allowed) {
|
|
39669
39859
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
@@ -39671,6 +39861,18 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
39671
39861
|
headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
|
|
39672
39862
|
});
|
|
39673
39863
|
}
|
|
39864
|
+
if (path === "/health" && method === "GET") {
|
|
39865
|
+
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
39866
|
+
return healthResponse2("todos");
|
|
39867
|
+
}
|
|
39868
|
+
if (path === "/mcp") {
|
|
39869
|
+
const authError = checkAuth(req, apiKey);
|
|
39870
|
+
if (authError)
|
|
39871
|
+
return authError;
|
|
39872
|
+
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
39873
|
+
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
39874
|
+
return handleMcpHttpRequest2(req, buildServer2);
|
|
39875
|
+
}
|
|
39674
39876
|
if (path.startsWith("/api/")) {
|
|
39675
39877
|
const authError = checkAuth(req, apiKey);
|
|
39676
39878
|
if (authError)
|
|
@@ -39950,14 +40152,16 @@ function printHelp() {
|
|
|
39950
40152
|
Start the @hasna/todos MCP server.
|
|
39951
40153
|
|
|
39952
40154
|
Options:
|
|
39953
|
-
--stdio Use stdio transport
|
|
39954
|
-
--
|
|
40155
|
+
--stdio Use stdio transport (default)
|
|
40156
|
+
--http Use Streamable HTTP transport
|
|
40157
|
+
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
39955
40158
|
-V, --version output the version number
|
|
39956
40159
|
-h, --help display help for command
|
|
39957
40160
|
|
|
39958
40161
|
Environment:
|
|
39959
|
-
|
|
39960
|
-
|
|
40162
|
+
MCP_STDIO=1 Force stdio transport
|
|
40163
|
+
MCP_HTTP=1 Use Streamable HTTP transport
|
|
40164
|
+
MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
|
|
39961
40165
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
39962
40166
|
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
|
|
39963
40167
|
}
|
|
@@ -40046,8 +40250,22 @@ function formatError(error) {
|
|
|
40046
40250
|
function resolveId(partialId, table = "tasks") {
|
|
40047
40251
|
const db = getDatabase();
|
|
40048
40252
|
const id = resolvePartialId(db, table, partialId);
|
|
40049
|
-
if (!id)
|
|
40050
|
-
|
|
40253
|
+
if (!id) {
|
|
40254
|
+
switch (table) {
|
|
40255
|
+
case "tasks":
|
|
40256
|
+
throw new TaskNotFoundError(partialId);
|
|
40257
|
+
case "projects":
|
|
40258
|
+
throw new ProjectNotFoundError(partialId);
|
|
40259
|
+
case "plans":
|
|
40260
|
+
throw new PlanNotFoundError(partialId);
|
|
40261
|
+
case "task_lists":
|
|
40262
|
+
throw new TaskListNotFoundError(partialId);
|
|
40263
|
+
case "agents":
|
|
40264
|
+
throw new AgentNotFoundError(partialId);
|
|
40265
|
+
default:
|
|
40266
|
+
throw new TaskNotFoundError(partialId);
|
|
40267
|
+
}
|
|
40268
|
+
}
|
|
40051
40269
|
return id;
|
|
40052
40270
|
}
|
|
40053
40271
|
function formatTask(task2) {
|
|
@@ -40136,8 +40354,9 @@ function buildServer() {
|
|
|
40136
40354
|
return server;
|
|
40137
40355
|
}
|
|
40138
40356
|
async function main() {
|
|
40139
|
-
const {
|
|
40140
|
-
|
|
40357
|
+
const { isHttpMode: isHttpMode2, resolveHttpPort: resolveHttpPort2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
40358
|
+
const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
|
|
40359
|
+
if (!isHttpMode2() && !portRequested) {
|
|
40141
40360
|
const server = buildServer();
|
|
40142
40361
|
const transport = new StdioServerTransport;
|
|
40143
40362
|
await server.connect(transport);
|