@hasna/todos 0.11.72 → 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 +2027 -1706
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +6500 -6251
- 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.js +8929 -8433
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1838 -1638
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp.js +6 -8
- package/dist/registry.js +6500 -6251
- 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 +1838 -1638
- 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 +3298 -3022
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1856,6 +1856,11 @@ function ensureSchema(db) {
|
|
|
1856
1856
|
ensureColumn("tasks", "priority_score", "INTEGER");
|
|
1857
1857
|
ensureColumn("tasks", "priority_reason", "TEXT");
|
|
1858
1858
|
ensureColumn("tasks", "archived_at", "TEXT");
|
|
1859
|
+
ensureColumn("tasks", "runner_id", "TEXT");
|
|
1860
|
+
ensureColumn("tasks", "runner_started_at", "TEXT");
|
|
1861
|
+
ensureColumn("tasks", "runner_completed_at", "TEXT");
|
|
1862
|
+
ensureColumn("tasks", "current_step", "TEXT");
|
|
1863
|
+
ensureColumn("tasks", "total_steps", "INTEGER");
|
|
1859
1864
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
1860
1865
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
1861
1866
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -1863,6 +1868,10 @@ function ensureSchema(db) {
|
|
|
1863
1868
|
ensureColumn("agents", "level", "TEXT");
|
|
1864
1869
|
ensureColumn("agents", "org_id", "TEXT");
|
|
1865
1870
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1871
|
+
ensureColumn("agents", "session_id", "TEXT");
|
|
1872
|
+
ensureColumn("agents", "working_dir", "TEXT");
|
|
1873
|
+
ensureColumn("agents", "active_project_id", "TEXT");
|
|
1874
|
+
ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
|
|
1866
1875
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1867
1876
|
ensureColumn("plans", "slug", "TEXT");
|
|
1868
1877
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
@@ -2780,34 +2789,40 @@ function ensureDir(filePath) {
|
|
|
2780
2789
|
mkdirSync(dir, { recursive: true });
|
|
2781
2790
|
}
|
|
2782
2791
|
}
|
|
2792
|
+
function openDatabase(path) {
|
|
2793
|
+
ensureDir(path);
|
|
2794
|
+
const db = new Database(path);
|
|
2795
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
2796
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
2797
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
2798
|
+
runMigrations(db);
|
|
2799
|
+
backfillTaskTags(db);
|
|
2800
|
+
backfillMachineId(db);
|
|
2801
|
+
return db;
|
|
2802
|
+
}
|
|
2783
2803
|
function getDatabase(dbPath) {
|
|
2784
2804
|
const path = dbPath || getDbPath();
|
|
2785
2805
|
if (_db && _dbPath === path)
|
|
2786
2806
|
return _db;
|
|
2787
|
-
|
|
2788
|
-
_db.close();
|
|
2789
|
-
_db = null;
|
|
2790
|
-
_dbPath = null;
|
|
2791
|
-
}
|
|
2792
|
-
ensureDir(path);
|
|
2793
|
-
_db = new Database(path);
|
|
2807
|
+
_db = openDatabase(path);
|
|
2794
2808
|
_dbPath = path;
|
|
2795
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
2796
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
2797
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
2798
|
-
runMigrations(_db);
|
|
2799
|
-
backfillTaskTags(_db);
|
|
2800
|
-
backfillMachineId(_db);
|
|
2801
2809
|
return _db;
|
|
2802
2810
|
}
|
|
2803
2811
|
function closeDatabase() {
|
|
2804
2812
|
if (_db) {
|
|
2805
|
-
|
|
2813
|
+
try {
|
|
2814
|
+
_db.close();
|
|
2815
|
+
} catch {}
|
|
2806
2816
|
_db = null;
|
|
2807
2817
|
_dbPath = null;
|
|
2808
2818
|
}
|
|
2809
2819
|
}
|
|
2810
2820
|
function resetDatabase() {
|
|
2821
|
+
if (_db) {
|
|
2822
|
+
try {
|
|
2823
|
+
_db.close();
|
|
2824
|
+
} catch {}
|
|
2825
|
+
}
|
|
2811
2826
|
_db = null;
|
|
2812
2827
|
_dbPath = null;
|
|
2813
2828
|
}
|
|
@@ -2912,6 +2927,11 @@ function safeEqualHex(a, b) {
|
|
|
2912
2927
|
return false;
|
|
2913
2928
|
return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
2914
2929
|
}
|
|
2930
|
+
function safeEqualStrings(a, b) {
|
|
2931
|
+
const ah = createHash("sha256").update(a, "utf8").digest();
|
|
2932
|
+
const bh = createHash("sha256").update(b, "utf8").digest();
|
|
2933
|
+
return timingSafeEqual(ah, bh);
|
|
2934
|
+
}
|
|
2915
2935
|
function hasActiveApiKeys(db) {
|
|
2916
2936
|
const d = db || getDatabase();
|
|
2917
2937
|
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());
|
|
@@ -5739,1767 +5759,1858 @@ var init_checklists = __esm(() => {
|
|
|
5739
5759
|
init_database();
|
|
5740
5760
|
});
|
|
5741
5761
|
|
|
5742
|
-
// src/
|
|
5743
|
-
function
|
|
5762
|
+
// src/lib/recurrence.ts
|
|
5763
|
+
function parseRecurrenceRule(rule) {
|
|
5764
|
+
const normalized = rule.trim().toLowerCase();
|
|
5765
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
5766
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
5767
|
+
}
|
|
5768
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
5769
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
5770
|
+
}
|
|
5771
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
5772
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
5773
|
+
}
|
|
5774
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
5775
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
5776
|
+
}
|
|
5777
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
5778
|
+
if (intervalMatch) {
|
|
5779
|
+
return {
|
|
5780
|
+
type: "interval",
|
|
5781
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
5782
|
+
unit: intervalMatch[2]
|
|
5783
|
+
};
|
|
5784
|
+
}
|
|
5785
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
5786
|
+
if (daysMatch) {
|
|
5787
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
5788
|
+
const days = [];
|
|
5789
|
+
for (const part of dayParts) {
|
|
5790
|
+
const dayNum = DAY_NAMES[part];
|
|
5791
|
+
if (dayNum !== undefined) {
|
|
5792
|
+
days.push(dayNum);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
if (days.length > 0) {
|
|
5796
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
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"`);
|
|
5800
|
+
}
|
|
5801
|
+
function isValidRecurrenceRule(rule) {
|
|
5802
|
+
try {
|
|
5803
|
+
parseRecurrenceRule(rule);
|
|
5804
|
+
return true;
|
|
5805
|
+
} catch {
|
|
5806
|
+
return false;
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
function nextOccurrence(rule, from) {
|
|
5810
|
+
const parsed = parseRecurrenceRule(rule);
|
|
5811
|
+
const base = from || new Date;
|
|
5812
|
+
if (parsed.type === "interval") {
|
|
5813
|
+
const next = new Date(base);
|
|
5814
|
+
if (parsed.unit === "day") {
|
|
5815
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
5816
|
+
} else if (parsed.unit === "week") {
|
|
5817
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
5818
|
+
} else if (parsed.unit === "month") {
|
|
5819
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
5820
|
+
}
|
|
5821
|
+
return next.toISOString();
|
|
5822
|
+
}
|
|
5823
|
+
if (parsed.type === "specific_days") {
|
|
5824
|
+
const currentDay = base.getDay();
|
|
5825
|
+
const days = parsed.days;
|
|
5826
|
+
let daysToAdd = Infinity;
|
|
5827
|
+
for (const day of days) {
|
|
5828
|
+
let diff = day - currentDay;
|
|
5829
|
+
if (diff <= 0)
|
|
5830
|
+
diff += 7;
|
|
5831
|
+
if (diff < daysToAdd)
|
|
5832
|
+
daysToAdd = diff;
|
|
5833
|
+
}
|
|
5834
|
+
const next = new Date(base);
|
|
5835
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
5836
|
+
return next.toISOString();
|
|
5837
|
+
}
|
|
5838
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
5839
|
+
}
|
|
5840
|
+
var DAY_NAMES;
|
|
5841
|
+
var init_recurrence = __esm(() => {
|
|
5842
|
+
DAY_NAMES = {
|
|
5843
|
+
sunday: 0,
|
|
5844
|
+
sun: 0,
|
|
5845
|
+
monday: 1,
|
|
5846
|
+
mon: 1,
|
|
5847
|
+
tuesday: 2,
|
|
5848
|
+
tue: 2,
|
|
5849
|
+
wednesday: 3,
|
|
5850
|
+
wed: 3,
|
|
5851
|
+
thursday: 4,
|
|
5852
|
+
thu: 4,
|
|
5853
|
+
friday: 5,
|
|
5854
|
+
fri: 5,
|
|
5855
|
+
saturday: 6,
|
|
5856
|
+
sat: 6
|
|
5857
|
+
};
|
|
5858
|
+
});
|
|
5859
|
+
|
|
5860
|
+
// src/db/templates.ts
|
|
5861
|
+
var exports_templates = {};
|
|
5862
|
+
__export(exports_templates, {
|
|
5863
|
+
updateTemplate: () => updateTemplate,
|
|
5864
|
+
tasksFromTemplate: () => tasksFromTemplate,
|
|
5865
|
+
taskFromTemplate: () => taskFromTemplate,
|
|
5866
|
+
resolveVariables: () => resolveVariables,
|
|
5867
|
+
previewTemplate: () => previewTemplate,
|
|
5868
|
+
listTemplates: () => listTemplates,
|
|
5869
|
+
listTemplateVersions: () => listTemplateVersions,
|
|
5870
|
+
importTemplate: () => importTemplate,
|
|
5871
|
+
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
5872
|
+
getTemplateVersion: () => getTemplateVersion,
|
|
5873
|
+
getTemplateTasks: () => getTemplateTasks,
|
|
5874
|
+
getTemplate: () => getTemplate,
|
|
5875
|
+
exportTemplate: () => exportTemplate,
|
|
5876
|
+
evaluateCondition: () => evaluateCondition,
|
|
5877
|
+
deleteTemplate: () => deleteTemplate,
|
|
5878
|
+
createTemplate: () => createTemplate,
|
|
5879
|
+
addTemplateTasks: () => addTemplateTasks
|
|
5880
|
+
});
|
|
5881
|
+
function rowToTemplate(row) {
|
|
5744
5882
|
return {
|
|
5745
5883
|
...row,
|
|
5746
5884
|
tags: JSON.parse(row.tags || "[]"),
|
|
5885
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
5747
5886
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
requires_approval: !!row.requires_approval
|
|
5887
|
+
priority: row.priority || "medium",
|
|
5888
|
+
version: row.version ?? 1
|
|
5751
5889
|
};
|
|
5752
5890
|
}
|
|
5753
|
-
function
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5764
|
-
insertTaskTags(taskId, tags, db);
|
|
5891
|
+
function rowToTemplateTask(row) {
|
|
5892
|
+
return {
|
|
5893
|
+
...row,
|
|
5894
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
5895
|
+
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
5896
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
5897
|
+
priority: row.priority || "medium",
|
|
5898
|
+
condition: row.condition ?? null,
|
|
5899
|
+
include_template_id: row.include_template_id ?? null
|
|
5900
|
+
};
|
|
5765
5901
|
}
|
|
5766
|
-
function
|
|
5767
|
-
|
|
5768
|
-
return;
|
|
5769
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
5770
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5771
|
-
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5772
|
-
}
|
|
5773
|
-
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5774
|
-
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5775
|
-
}
|
|
5902
|
+
function resolveTemplateId(id, d) {
|
|
5903
|
+
return resolvePartialId(d, "task_templates", id);
|
|
5776
5904
|
}
|
|
5777
|
-
function
|
|
5905
|
+
function createTemplate(input, db) {
|
|
5778
5906
|
const d = db || getDatabase();
|
|
5779
|
-
const
|
|
5780
|
-
const tags = input.tags || [];
|
|
5907
|
+
const id = uuid();
|
|
5781
5908
|
const machineId = currentStorageMachineId(d);
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
input.priority || "medium",
|
|
5800
|
-
input.agent_id || null,
|
|
5801
|
-
input.assigned_to || null,
|
|
5802
|
-
input.session_id || null,
|
|
5803
|
-
input.working_dir || null,
|
|
5804
|
-
JSON.stringify(tags),
|
|
5805
|
-
JSON.stringify(input.metadata || {}),
|
|
5806
|
-
timestamp,
|
|
5807
|
-
timestamp,
|
|
5808
|
-
input.due_at || null,
|
|
5809
|
-
input.estimated_minutes || null,
|
|
5810
|
-
input.sla_minutes ?? null,
|
|
5811
|
-
input.confidence ?? null,
|
|
5812
|
-
input.retry_count ?? 0,
|
|
5813
|
-
input.max_retries ?? 3,
|
|
5814
|
-
input.retry_after ?? null,
|
|
5815
|
-
input.requires_approval ? 1 : 0,
|
|
5816
|
-
null,
|
|
5817
|
-
null,
|
|
5818
|
-
input.recurrence_rule || null,
|
|
5819
|
-
input.recurrence_parent_id || null,
|
|
5820
|
-
input.spawns_template_id || null,
|
|
5821
|
-
input.reason || null,
|
|
5822
|
-
input.spawned_from_session || null,
|
|
5823
|
-
assignedBy || null,
|
|
5824
|
-
assignedFromProject || null,
|
|
5825
|
-
input.task_type || null,
|
|
5826
|
-
machineId
|
|
5827
|
-
]);
|
|
5828
|
-
break;
|
|
5829
|
-
} catch (e) {
|
|
5830
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
5831
|
-
id = uuid();
|
|
5832
|
-
continue;
|
|
5833
|
-
}
|
|
5834
|
-
throw e;
|
|
5835
|
-
}
|
|
5836
|
-
}
|
|
5837
|
-
if (tags.length > 0) {
|
|
5838
|
-
insertTaskTags(id, tags, d);
|
|
5909
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
|
|
5910
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5911
|
+
id,
|
|
5912
|
+
input.name,
|
|
5913
|
+
input.title_pattern,
|
|
5914
|
+
input.description || null,
|
|
5915
|
+
input.priority || "medium",
|
|
5916
|
+
JSON.stringify(input.tags || []),
|
|
5917
|
+
JSON.stringify(input.variables || []),
|
|
5918
|
+
input.project_id || null,
|
|
5919
|
+
input.plan_id || null,
|
|
5920
|
+
JSON.stringify(input.metadata || {}),
|
|
5921
|
+
now(),
|
|
5922
|
+
machineId
|
|
5923
|
+
]);
|
|
5924
|
+
if (input.tasks && input.tasks.length > 0) {
|
|
5925
|
+
addTemplateTasks(id, input.tasks, d);
|
|
5839
5926
|
}
|
|
5840
|
-
|
|
5841
|
-
const payload = taskEventData(task);
|
|
5842
|
-
const databasePath = databasePathFromDatabase(d);
|
|
5843
|
-
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5844
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
5845
|
-
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
5846
|
-
return task;
|
|
5927
|
+
return getTemplate(id, d);
|
|
5847
5928
|
}
|
|
5848
|
-
function
|
|
5929
|
+
function getTemplate(id, db) {
|
|
5849
5930
|
const d = db || getDatabase();
|
|
5850
|
-
const
|
|
5851
|
-
if (!
|
|
5931
|
+
const resolved = resolveTemplateId(id, d);
|
|
5932
|
+
if (!resolved)
|
|
5852
5933
|
return null;
|
|
5853
|
-
|
|
5934
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
5935
|
+
return row ? rowToTemplate(row) : null;
|
|
5854
5936
|
}
|
|
5855
|
-
function
|
|
5937
|
+
function listTemplates(db) {
|
|
5856
5938
|
const d = db || getDatabase();
|
|
5857
|
-
|
|
5858
|
-
if (!task)
|
|
5859
|
-
return null;
|
|
5860
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
5861
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
5862
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
5863
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
5864
|
-
WHERE td.task_id = ?`).all(id);
|
|
5865
|
-
const dependencies = depRows.map(rowToTask);
|
|
5866
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
5867
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
5868
|
-
WHERE td.depends_on = ?`).all(id);
|
|
5869
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
5870
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
5871
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
5872
|
-
const checklist = getChecklist(id, d);
|
|
5873
|
-
return {
|
|
5874
|
-
...task,
|
|
5875
|
-
subtasks,
|
|
5876
|
-
dependencies,
|
|
5877
|
-
blocked_by,
|
|
5878
|
-
comments,
|
|
5879
|
-
parent,
|
|
5880
|
-
checklist
|
|
5881
|
-
};
|
|
5939
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
5882
5940
|
}
|
|
5883
|
-
function
|
|
5941
|
+
function deleteTemplate(id, db) {
|
|
5884
5942
|
const d = db || getDatabase();
|
|
5885
|
-
const
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
const
|
|
5889
|
-
if (
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
if (
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
conditions.push("priority = ?");
|
|
5920
|
-
params.push(filter.priority);
|
|
5921
|
-
}
|
|
5922
|
-
}
|
|
5923
|
-
if (filter.assigned_to) {
|
|
5924
|
-
conditions.push("assigned_to = ?");
|
|
5925
|
-
params.push(filter.assigned_to);
|
|
5926
|
-
}
|
|
5927
|
-
if (filter.agent_id) {
|
|
5928
|
-
conditions.push("agent_id = ?");
|
|
5929
|
-
params.push(filter.agent_id);
|
|
5943
|
+
const resolved = resolveTemplateId(id, d);
|
|
5944
|
+
if (!resolved)
|
|
5945
|
+
return false;
|
|
5946
|
+
const template = getTemplate(resolved, d);
|
|
5947
|
+
if (!template)
|
|
5948
|
+
return false;
|
|
5949
|
+
recordStorageTombstone({
|
|
5950
|
+
object_type: "templates",
|
|
5951
|
+
object_id: resolved,
|
|
5952
|
+
payload: template,
|
|
5953
|
+
version: template.version
|
|
5954
|
+
}, d);
|
|
5955
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
5956
|
+
}
|
|
5957
|
+
function updateTemplate(id, updates, db) {
|
|
5958
|
+
const d = db || getDatabase();
|
|
5959
|
+
const resolved = resolveTemplateId(id, d);
|
|
5960
|
+
if (!resolved)
|
|
5961
|
+
return null;
|
|
5962
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
5963
|
+
if (current) {
|
|
5964
|
+
const snapshot = JSON.stringify({
|
|
5965
|
+
name: current.name,
|
|
5966
|
+
title_pattern: current.title_pattern,
|
|
5967
|
+
description: current.description,
|
|
5968
|
+
priority: current.priority,
|
|
5969
|
+
tags: current.tags,
|
|
5970
|
+
variables: current.variables,
|
|
5971
|
+
project_id: current.project_id,
|
|
5972
|
+
plan_id: current.plan_id,
|
|
5973
|
+
metadata: current.metadata,
|
|
5974
|
+
tasks: current.tasks
|
|
5975
|
+
});
|
|
5976
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
5930
5977
|
}
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5978
|
+
const sets = ["version = version + 1"];
|
|
5979
|
+
const values = [];
|
|
5980
|
+
if (updates.name !== undefined) {
|
|
5981
|
+
sets.push("name = ?");
|
|
5982
|
+
values.push(updates.name);
|
|
5934
5983
|
}
|
|
5935
|
-
if (
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
params.push(...filter.tags);
|
|
5984
|
+
if (updates.title_pattern !== undefined) {
|
|
5985
|
+
sets.push("title_pattern = ?");
|
|
5986
|
+
values.push(updates.title_pattern);
|
|
5939
5987
|
}
|
|
5940
|
-
if (
|
|
5941
|
-
|
|
5942
|
-
|
|
5988
|
+
if (updates.description !== undefined) {
|
|
5989
|
+
sets.push("description = ?");
|
|
5990
|
+
values.push(updates.description);
|
|
5943
5991
|
}
|
|
5944
|
-
if (
|
|
5945
|
-
|
|
5946
|
-
|
|
5992
|
+
if (updates.priority !== undefined) {
|
|
5993
|
+
sets.push("priority = ?");
|
|
5994
|
+
values.push(updates.priority);
|
|
5947
5995
|
}
|
|
5948
|
-
if (
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
conditions.push("recurrence_rule IS NULL");
|
|
5996
|
+
if (updates.tags !== undefined) {
|
|
5997
|
+
sets.push("tags = ?");
|
|
5998
|
+
values.push(JSON.stringify(updates.tags));
|
|
5952
5999
|
}
|
|
5953
|
-
if (
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
params.push(...filter.task_type);
|
|
5957
|
-
} else {
|
|
5958
|
-
conditions.push("task_type = ?");
|
|
5959
|
-
params.push(filter.task_type);
|
|
5960
|
-
}
|
|
6000
|
+
if (updates.variables !== undefined) {
|
|
6001
|
+
sets.push("variables = ?");
|
|
6002
|
+
values.push(JSON.stringify(updates.variables));
|
|
5961
6003
|
}
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
try {
|
|
5966
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
5967
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
5968
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
5969
|
-
} catch {}
|
|
6004
|
+
if (updates.project_id !== undefined) {
|
|
6005
|
+
sets.push("project_id = ?");
|
|
6006
|
+
values.push(updates.project_id);
|
|
5970
6007
|
}
|
|
5971
|
-
if (
|
|
5972
|
-
|
|
6008
|
+
if (updates.plan_id !== undefined) {
|
|
6009
|
+
sets.push("plan_id = ?");
|
|
6010
|
+
values.push(updates.plan_id);
|
|
5973
6011
|
}
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
limitClause = " LIMIT ?";
|
|
5978
|
-
params.push(filter.limit);
|
|
5979
|
-
if (!filter.cursor && filter.offset) {
|
|
5980
|
-
limitClause += " OFFSET ?";
|
|
5981
|
-
params.push(filter.offset);
|
|
5982
|
-
}
|
|
6012
|
+
if (updates.metadata !== undefined) {
|
|
6013
|
+
sets.push("metadata = ?");
|
|
6014
|
+
values.push(JSON.stringify(updates.metadata));
|
|
5983
6015
|
}
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
function getTaskByFingerprint(fingerprint, db) {
|
|
5988
|
-
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5989
|
-
return tasks[0] ?? null;
|
|
6016
|
+
values.push(resolved);
|
|
6017
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
6018
|
+
return getTemplate(resolved, d);
|
|
5990
6019
|
}
|
|
5991
|
-
function
|
|
6020
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
6021
|
+
const t = getTemplate(templateId, db);
|
|
6022
|
+
if (!t)
|
|
6023
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
6024
|
+
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
5992
6025
|
return {
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
6026
|
+
title: cleanOverrides.title || t.title_pattern,
|
|
6027
|
+
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
6028
|
+
priority: cleanOverrides.priority ?? t.priority,
|
|
6029
|
+
tags: cleanOverrides.tags ?? t.tags,
|
|
6030
|
+
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
6031
|
+
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
6032
|
+
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
6033
|
+
...cleanOverrides
|
|
5996
6034
|
};
|
|
5997
6035
|
}
|
|
5998
|
-
function
|
|
6036
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
5999
6037
|
const d = db || getDatabase();
|
|
6000
|
-
const
|
|
6001
|
-
if (!
|
|
6002
|
-
throw new Error(
|
|
6003
|
-
|
|
6004
|
-
const
|
|
6005
|
-
|
|
6006
|
-
const
|
|
6007
|
-
|
|
6038
|
+
const template = getTemplate(templateId, d);
|
|
6039
|
+
if (!template)
|
|
6040
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
6041
|
+
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
6042
|
+
const results = [];
|
|
6043
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
6044
|
+
const task = tasks[i];
|
|
6045
|
+
const id = uuid();
|
|
6046
|
+
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)
|
|
6047
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6048
|
+
id,
|
|
6049
|
+
templateId,
|
|
6050
|
+
i,
|
|
6051
|
+
task.title_pattern,
|
|
6052
|
+
task.description || null,
|
|
6053
|
+
task.priority || "medium",
|
|
6054
|
+
JSON.stringify(task.tags || []),
|
|
6055
|
+
task.task_type || null,
|
|
6056
|
+
task.condition || null,
|
|
6057
|
+
task.include_template_id || null,
|
|
6058
|
+
JSON.stringify(task.depends_on || []),
|
|
6059
|
+
JSON.stringify(task.metadata || {}),
|
|
6060
|
+
now()
|
|
6061
|
+
]);
|
|
6062
|
+
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
6063
|
+
if (row)
|
|
6064
|
+
results.push(rowToTemplateTask(row));
|
|
6008
6065
|
}
|
|
6009
|
-
|
|
6010
|
-
version: existing.version,
|
|
6011
|
-
title: input.title,
|
|
6012
|
-
description: input.description,
|
|
6013
|
-
status: input.status,
|
|
6014
|
-
priority: input.priority,
|
|
6015
|
-
project_id: input.project_id,
|
|
6016
|
-
assigned_to: input.assigned_to,
|
|
6017
|
-
working_dir: input.working_dir,
|
|
6018
|
-
plan_id: input.plan_id,
|
|
6019
|
-
task_list_id: input.task_list_id,
|
|
6020
|
-
tags: input.tags,
|
|
6021
|
-
metadata,
|
|
6022
|
-
due_at: input.due_at,
|
|
6023
|
-
estimated_minutes: input.estimated_minutes,
|
|
6024
|
-
sla_minutes: input.sla_minutes,
|
|
6025
|
-
confidence: input.confidence,
|
|
6026
|
-
retry_count: input.retry_count,
|
|
6027
|
-
max_retries: input.max_retries,
|
|
6028
|
-
retry_after: input.retry_after,
|
|
6029
|
-
requires_approval: input.requires_approval,
|
|
6030
|
-
recurrence_rule: input.recurrence_rule,
|
|
6031
|
-
task_type: input.task_type
|
|
6032
|
-
}, d);
|
|
6033
|
-
return { task, created: false };
|
|
6066
|
+
return results;
|
|
6034
6067
|
}
|
|
6035
|
-
function
|
|
6068
|
+
function getTemplateWithTasks(id, db) {
|
|
6036
6069
|
const d = db || getDatabase();
|
|
6037
|
-
const
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6070
|
+
const template = getTemplate(id, d);
|
|
6071
|
+
if (!template)
|
|
6072
|
+
return null;
|
|
6073
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
6074
|
+
const tasks = rows.map(rowToTemplateTask);
|
|
6075
|
+
return { ...template, tasks };
|
|
6076
|
+
}
|
|
6077
|
+
function getTemplateTasks(templateId, db) {
|
|
6078
|
+
const d = db || getDatabase();
|
|
6079
|
+
const resolved = resolveTemplateId(templateId, d);
|
|
6080
|
+
if (!resolved)
|
|
6081
|
+
return [];
|
|
6082
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
6083
|
+
return rows.map(rowToTemplateTask);
|
|
6084
|
+
}
|
|
6085
|
+
function evaluateCondition(condition, variables) {
|
|
6086
|
+
if (!condition || condition.trim() === "")
|
|
6087
|
+
return true;
|
|
6088
|
+
const trimmed = condition.trim();
|
|
6089
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
6090
|
+
if (eqMatch) {
|
|
6091
|
+
const varName = eqMatch[1];
|
|
6092
|
+
const expected = eqMatch[2].trim();
|
|
6093
|
+
return (variables[varName] ?? "") === expected;
|
|
6042
6094
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6095
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
6096
|
+
if (neqMatch) {
|
|
6097
|
+
const varName = neqMatch[1];
|
|
6098
|
+
const expected = neqMatch[2].trim();
|
|
6099
|
+
return (variables[varName] ?? "") !== expected;
|
|
6046
6100
|
}
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
params.push(filter.parent_id);
|
|
6053
|
-
}
|
|
6101
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
6102
|
+
if (falsyMatch) {
|
|
6103
|
+
const varName = falsyMatch[1];
|
|
6104
|
+
const val = variables[varName];
|
|
6105
|
+
return !val || val === "" || val === "false";
|
|
6054
6106
|
}
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
conditions.push("status = ?");
|
|
6061
|
-
params.push(filter.status);
|
|
6062
|
-
}
|
|
6107
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
6108
|
+
if (truthyMatch) {
|
|
6109
|
+
const varName = truthyMatch[1];
|
|
6110
|
+
const val = variables[varName];
|
|
6111
|
+
return !!val && val !== "" && val !== "false";
|
|
6063
6112
|
}
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6113
|
+
return true;
|
|
6114
|
+
}
|
|
6115
|
+
function exportTemplate(id, db) {
|
|
6116
|
+
const d = db || getDatabase();
|
|
6117
|
+
const template = getTemplateWithTasks(id, d);
|
|
6118
|
+
if (!template)
|
|
6119
|
+
throw new Error(`Template not found: ${id}`);
|
|
6120
|
+
return {
|
|
6121
|
+
name: template.name,
|
|
6122
|
+
title_pattern: template.title_pattern,
|
|
6123
|
+
description: template.description,
|
|
6124
|
+
priority: template.priority,
|
|
6125
|
+
tags: template.tags,
|
|
6126
|
+
variables: template.variables,
|
|
6127
|
+
project_id: template.project_id,
|
|
6128
|
+
plan_id: template.plan_id,
|
|
6129
|
+
metadata: template.metadata,
|
|
6130
|
+
tasks: template.tasks.map((t) => ({
|
|
6131
|
+
position: t.position,
|
|
6132
|
+
title_pattern: t.title_pattern,
|
|
6133
|
+
description: t.description,
|
|
6134
|
+
priority: t.priority,
|
|
6135
|
+
tags: t.tags,
|
|
6136
|
+
task_type: t.task_type,
|
|
6137
|
+
condition: t.condition,
|
|
6138
|
+
include_template_id: t.include_template_id,
|
|
6139
|
+
depends_on_positions: t.depends_on_positions,
|
|
6140
|
+
metadata: t.metadata
|
|
6141
|
+
}))
|
|
6142
|
+
};
|
|
6143
|
+
}
|
|
6144
|
+
function importTemplate(json, db) {
|
|
6145
|
+
const d = db || getDatabase();
|
|
6146
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
6147
|
+
title_pattern: t.title_pattern,
|
|
6148
|
+
description: t.description ?? undefined,
|
|
6149
|
+
priority: t.priority,
|
|
6150
|
+
tags: t.tags,
|
|
6151
|
+
task_type: t.task_type ?? undefined,
|
|
6152
|
+
condition: t.condition ?? undefined,
|
|
6153
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
6154
|
+
depends_on: t.depends_on_positions,
|
|
6155
|
+
metadata: t.metadata
|
|
6156
|
+
}));
|
|
6157
|
+
return createTemplate({
|
|
6158
|
+
name: json.name,
|
|
6159
|
+
title_pattern: json.title_pattern,
|
|
6160
|
+
description: json.description ?? undefined,
|
|
6161
|
+
priority: json.priority,
|
|
6162
|
+
tags: json.tags,
|
|
6163
|
+
variables: json.variables,
|
|
6164
|
+
project_id: json.project_id ?? undefined,
|
|
6165
|
+
plan_id: json.plan_id ?? undefined,
|
|
6166
|
+
metadata: json.metadata,
|
|
6167
|
+
tasks: taskInputs
|
|
6168
|
+
}, d);
|
|
6169
|
+
}
|
|
6170
|
+
function getTemplateVersion(id, version, db) {
|
|
6171
|
+
const d = db || getDatabase();
|
|
6172
|
+
const resolved = resolveTemplateId(id, d);
|
|
6173
|
+
if (!resolved)
|
|
6174
|
+
return null;
|
|
6175
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
6176
|
+
return row || null;
|
|
6177
|
+
}
|
|
6178
|
+
function listTemplateVersions(id, db) {
|
|
6179
|
+
const d = db || getDatabase();
|
|
6180
|
+
const resolved = resolveTemplateId(id, d);
|
|
6181
|
+
if (!resolved)
|
|
6182
|
+
return [];
|
|
6183
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
6184
|
+
}
|
|
6185
|
+
function resolveVariables(templateVars, provided) {
|
|
6186
|
+
const merged = { ...provided };
|
|
6187
|
+
for (const v of templateVars) {
|
|
6188
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
6189
|
+
merged[v.name] = v.default;
|
|
6071
6190
|
}
|
|
6072
6191
|
}
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
conditions.push("agent_id = ?");
|
|
6079
|
-
params.push(filter.agent_id);
|
|
6080
|
-
}
|
|
6081
|
-
if (filter.session_id) {
|
|
6082
|
-
conditions.push("session_id = ?");
|
|
6083
|
-
params.push(filter.session_id);
|
|
6084
|
-
}
|
|
6085
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
6086
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
6087
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
6088
|
-
params.push(...filter.tags);
|
|
6089
|
-
}
|
|
6090
|
-
if (filter.plan_id) {
|
|
6091
|
-
conditions.push("plan_id = ?");
|
|
6092
|
-
params.push(filter.plan_id);
|
|
6192
|
+
const missing = [];
|
|
6193
|
+
for (const v of templateVars) {
|
|
6194
|
+
if (v.required && merged[v.name] === undefined) {
|
|
6195
|
+
missing.push(v.name);
|
|
6196
|
+
}
|
|
6093
6197
|
}
|
|
6094
|
-
if (
|
|
6095
|
-
|
|
6096
|
-
params.push(filter.task_list_id);
|
|
6198
|
+
if (missing.length > 0) {
|
|
6199
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
6097
6200
|
}
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6201
|
+
return merged;
|
|
6202
|
+
}
|
|
6203
|
+
function substituteVars(text, variables) {
|
|
6204
|
+
let result = text;
|
|
6205
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
6206
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
6101
6207
|
}
|
|
6102
|
-
|
|
6103
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
6104
|
-
return row.count;
|
|
6208
|
+
return result;
|
|
6105
6209
|
}
|
|
6106
|
-
function
|
|
6210
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
6107
6211
|
const d = db || getDatabase();
|
|
6108
|
-
const
|
|
6109
|
-
if (!
|
|
6110
|
-
throw new
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
const timestamp = now();
|
|
6115
|
-
const completionTimestamp = input.completed_at ?? timestamp;
|
|
6116
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
6117
|
-
const params = [timestamp];
|
|
6118
|
-
if (input.title !== undefined) {
|
|
6119
|
-
sets.push("title = ?");
|
|
6120
|
-
params.push(input.title);
|
|
6212
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
6213
|
+
if (!template)
|
|
6214
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
6215
|
+
const visited = _visitedTemplateIds || new Set;
|
|
6216
|
+
if (visited.has(template.id)) {
|
|
6217
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
6121
6218
|
}
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6219
|
+
visited.add(template.id);
|
|
6220
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
6221
|
+
if (template.tasks.length === 0) {
|
|
6222
|
+
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
6223
|
+
const task = createTask(input, d);
|
|
6224
|
+
return [task];
|
|
6125
6225
|
}
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6226
|
+
const createdTasks = [];
|
|
6227
|
+
const positionToId = new Map;
|
|
6228
|
+
const skippedPositions = new Set;
|
|
6229
|
+
for (const tt of template.tasks) {
|
|
6230
|
+
if (tt.include_template_id) {
|
|
6231
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
6232
|
+
createdTasks.push(...includedTasks);
|
|
6233
|
+
if (includedTasks.length > 0) {
|
|
6234
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
6235
|
+
} else {
|
|
6236
|
+
skippedPositions.add(tt.position);
|
|
6237
|
+
}
|
|
6238
|
+
continue;
|
|
6129
6239
|
}
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
sets.push("completed_at = ?");
|
|
6134
|
-
params.push(completionTimestamp);
|
|
6240
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
6241
|
+
skippedPositions.add(tt.position);
|
|
6242
|
+
continue;
|
|
6135
6243
|
}
|
|
6244
|
+
let title = tt.title_pattern;
|
|
6245
|
+
let desc = tt.description;
|
|
6246
|
+
title = substituteVars(title, resolved);
|
|
6247
|
+
if (desc)
|
|
6248
|
+
desc = substituteVars(desc, resolved);
|
|
6249
|
+
const task = createTask({
|
|
6250
|
+
title,
|
|
6251
|
+
description: desc ?? undefined,
|
|
6252
|
+
priority: tt.priority,
|
|
6253
|
+
tags: tt.tags,
|
|
6254
|
+
task_type: tt.task_type ?? undefined,
|
|
6255
|
+
project_id: projectId,
|
|
6256
|
+
task_list_id: taskListId,
|
|
6257
|
+
metadata: tt.metadata
|
|
6258
|
+
}, d);
|
|
6259
|
+
createdTasks.push(task);
|
|
6260
|
+
positionToId.set(tt.position, task.id);
|
|
6136
6261
|
}
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6262
|
+
for (const tt of template.tasks) {
|
|
6263
|
+
if (skippedPositions.has(tt.position))
|
|
6264
|
+
continue;
|
|
6265
|
+
if (tt.include_template_id)
|
|
6266
|
+
continue;
|
|
6267
|
+
const deps = tt.depends_on_positions;
|
|
6268
|
+
for (const depPos of deps) {
|
|
6269
|
+
if (skippedPositions.has(depPos))
|
|
6270
|
+
continue;
|
|
6271
|
+
const taskId = positionToId.get(tt.position);
|
|
6272
|
+
const depId = positionToId.get(depPos);
|
|
6273
|
+
if (taskId && depId) {
|
|
6274
|
+
addDependency(taskId, depId, d);
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6144
6277
|
}
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6278
|
+
return createdTasks;
|
|
6279
|
+
}
|
|
6280
|
+
function previewTemplate(templateId, variables, db) {
|
|
6281
|
+
const d = db || getDatabase();
|
|
6282
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
6283
|
+
if (!template)
|
|
6284
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
6285
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
6286
|
+
const tasks = [];
|
|
6287
|
+
if (template.tasks.length === 0) {
|
|
6288
|
+
tasks.push({
|
|
6289
|
+
position: 0,
|
|
6290
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
6291
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
6292
|
+
priority: template.priority,
|
|
6293
|
+
tags: template.tags,
|
|
6294
|
+
task_type: null,
|
|
6295
|
+
depends_on_positions: []
|
|
6296
|
+
});
|
|
6297
|
+
} else {
|
|
6298
|
+
for (const tt of template.tasks) {
|
|
6299
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
6300
|
+
continue;
|
|
6301
|
+
tasks.push({
|
|
6302
|
+
position: tt.position,
|
|
6303
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
6304
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
6305
|
+
priority: tt.priority,
|
|
6306
|
+
tags: tt.tags,
|
|
6307
|
+
task_type: tt.task_type,
|
|
6308
|
+
depends_on_positions: tt.depends_on_positions
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6148
6311
|
}
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6312
|
+
return {
|
|
6313
|
+
template_id: template.id,
|
|
6314
|
+
template_name: template.name,
|
|
6315
|
+
description: template.description,
|
|
6316
|
+
variables: template.variables,
|
|
6317
|
+
resolved_variables: resolved,
|
|
6318
|
+
tasks
|
|
6319
|
+
};
|
|
6320
|
+
}
|
|
6321
|
+
var init_templates = __esm(() => {
|
|
6322
|
+
init_database();
|
|
6323
|
+
init_tasks();
|
|
6324
|
+
init_storage_tombstones();
|
|
6325
|
+
});
|
|
6326
|
+
|
|
6327
|
+
// src/db/task-graph.ts
|
|
6328
|
+
function addDependency(taskId, dependsOn, db) {
|
|
6329
|
+
const d = db || getDatabase();
|
|
6330
|
+
if (!getTask(taskId, d))
|
|
6331
|
+
throw new TaskNotFoundError(taskId);
|
|
6332
|
+
if (!getTask(dependsOn, d))
|
|
6333
|
+
throw new TaskNotFoundError(dependsOn);
|
|
6334
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
6335
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
6152
6336
|
}
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6337
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
6338
|
+
}
|
|
6339
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
6340
|
+
const d = db || getDatabase();
|
|
6341
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
6342
|
+
return result.changes > 0;
|
|
6343
|
+
}
|
|
6344
|
+
function getTaskDependencies(taskId, db) {
|
|
6345
|
+
const d = db || getDatabase();
|
|
6346
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
6347
|
+
}
|
|
6348
|
+
function getTaskDependents(taskId, db) {
|
|
6349
|
+
const d = db || getDatabase();
|
|
6350
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
6351
|
+
}
|
|
6352
|
+
function cloneTask(taskId, overrides, db) {
|
|
6353
|
+
const d = db || getDatabase();
|
|
6354
|
+
const source = getTask(taskId, d);
|
|
6355
|
+
if (!source)
|
|
6356
|
+
throw new TaskNotFoundError(taskId);
|
|
6357
|
+
const input = {
|
|
6358
|
+
title: overrides?.title ?? source.title,
|
|
6359
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
6360
|
+
priority: overrides?.priority ?? source.priority,
|
|
6361
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
6362
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
6363
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
6364
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
6365
|
+
status: overrides?.status ?? "pending",
|
|
6366
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
6367
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
6368
|
+
tags: overrides?.tags ?? source.tags,
|
|
6369
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
6370
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
6371
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
6372
|
+
};
|
|
6373
|
+
return createTask(input, d);
|
|
6374
|
+
}
|
|
6375
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
6376
|
+
const d = db || getDatabase();
|
|
6377
|
+
const task = getTask(taskId, d);
|
|
6378
|
+
if (!task)
|
|
6379
|
+
throw new TaskNotFoundError(taskId);
|
|
6380
|
+
function toNode(t) {
|
|
6381
|
+
const deps = getTaskDependencies(t.id, d);
|
|
6382
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
6383
|
+
const depTask = getTask(dep.depends_on, d);
|
|
6384
|
+
return depTask && depTask.status !== "completed";
|
|
6385
|
+
});
|
|
6386
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
6156
6387
|
}
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6388
|
+
function buildUp(id, visited) {
|
|
6389
|
+
if (visited.has(id))
|
|
6390
|
+
return [];
|
|
6391
|
+
visited.add(id);
|
|
6392
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
6393
|
+
return deps.map((dep) => {
|
|
6394
|
+
const depTask = getTask(dep.depends_on, d);
|
|
6395
|
+
if (!depTask)
|
|
6396
|
+
return null;
|
|
6397
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
6398
|
+
}).filter(Boolean);
|
|
6160
6399
|
}
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6400
|
+
function buildDown(id, visited) {
|
|
6401
|
+
if (visited.has(id))
|
|
6402
|
+
return [];
|
|
6403
|
+
visited.add(id);
|
|
6404
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
6405
|
+
return dependents.map((dep) => {
|
|
6406
|
+
const depTask = getTask(dep.task_id, d);
|
|
6407
|
+
if (!depTask)
|
|
6408
|
+
return null;
|
|
6409
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
6410
|
+
}).filter(Boolean);
|
|
6164
6411
|
}
|
|
6165
|
-
|
|
6412
|
+
const rootNode = toNode(task);
|
|
6413
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
6414
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
6415
|
+
return { task: rootNode, depends_on, blocks };
|
|
6416
|
+
}
|
|
6417
|
+
function moveTask(taskId, target, db) {
|
|
6418
|
+
const d = db || getDatabase();
|
|
6419
|
+
const task = getTask(taskId, d);
|
|
6420
|
+
if (!task)
|
|
6421
|
+
throw new TaskNotFoundError(taskId);
|
|
6422
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
6423
|
+
const params = [now()];
|
|
6424
|
+
if (target.task_list_id !== undefined) {
|
|
6166
6425
|
sets.push("task_list_id = ?");
|
|
6167
|
-
params.push(
|
|
6168
|
-
}
|
|
6169
|
-
if (input.due_at !== undefined) {
|
|
6170
|
-
sets.push("due_at = ?");
|
|
6171
|
-
params.push(input.due_at);
|
|
6172
|
-
}
|
|
6173
|
-
if (input.estimated_minutes !== undefined) {
|
|
6174
|
-
sets.push("estimated_minutes = ?");
|
|
6175
|
-
params.push(input.estimated_minutes);
|
|
6176
|
-
}
|
|
6177
|
-
if (input.sla_minutes !== undefined) {
|
|
6178
|
-
sets.push("sla_minutes = ?");
|
|
6179
|
-
params.push(input.sla_minutes);
|
|
6180
|
-
}
|
|
6181
|
-
if (input.actual_minutes !== undefined) {
|
|
6182
|
-
sets.push("actual_minutes = ?");
|
|
6183
|
-
params.push(input.actual_minutes);
|
|
6184
|
-
}
|
|
6185
|
-
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
6186
|
-
sets.push("completed_at = ?");
|
|
6187
|
-
params.push(input.completed_at);
|
|
6188
|
-
}
|
|
6189
|
-
if (input.confidence !== undefined) {
|
|
6190
|
-
sets.push("confidence = ?");
|
|
6191
|
-
params.push(input.confidence);
|
|
6192
|
-
}
|
|
6193
|
-
if (input.retry_count !== undefined) {
|
|
6194
|
-
sets.push("retry_count = ?");
|
|
6195
|
-
params.push(input.retry_count);
|
|
6426
|
+
params.push(target.task_list_id);
|
|
6196
6427
|
}
|
|
6197
|
-
if (
|
|
6198
|
-
sets.push("
|
|
6199
|
-
params.push(
|
|
6428
|
+
if (target.project_id !== undefined) {
|
|
6429
|
+
sets.push("project_id = ?");
|
|
6430
|
+
params.push(target.project_id);
|
|
6200
6431
|
}
|
|
6201
|
-
if (
|
|
6202
|
-
sets.push("
|
|
6203
|
-
params.push(
|
|
6432
|
+
if (target.plan_id !== undefined) {
|
|
6433
|
+
sets.push("plan_id = ?");
|
|
6434
|
+
params.push(target.plan_id);
|
|
6204
6435
|
}
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6436
|
+
params.push(taskId);
|
|
6437
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
6438
|
+
return getTask(taskId, d);
|
|
6439
|
+
}
|
|
6440
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
6441
|
+
const visited = new Set;
|
|
6442
|
+
const queue = [dependsOn];
|
|
6443
|
+
while (queue.length > 0) {
|
|
6444
|
+
const current = queue.shift();
|
|
6445
|
+
if (current === taskId)
|
|
6446
|
+
return true;
|
|
6447
|
+
if (visited.has(current))
|
|
6448
|
+
continue;
|
|
6449
|
+
visited.add(current);
|
|
6450
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
6451
|
+
for (const dep of deps) {
|
|
6452
|
+
queue.push(dep.depends_on);
|
|
6453
|
+
}
|
|
6208
6454
|
}
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6455
|
+
return false;
|
|
6456
|
+
}
|
|
6457
|
+
var init_task_graph = __esm(() => {
|
|
6458
|
+
init_types();
|
|
6459
|
+
init_database();
|
|
6460
|
+
init_task_crud();
|
|
6461
|
+
});
|
|
6462
|
+
|
|
6463
|
+
// src/db/task-lifecycle.ts
|
|
6464
|
+
var exports_task_lifecycle = {};
|
|
6465
|
+
__export(exports_task_lifecycle, {
|
|
6466
|
+
unlockTask: () => unlockTask,
|
|
6467
|
+
stealTask: () => stealTask,
|
|
6468
|
+
startTask: () => startTask,
|
|
6469
|
+
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
6470
|
+
lockTask: () => lockTask,
|
|
6471
|
+
getTasksChangedSince: () => getTasksChangedSince,
|
|
6472
|
+
getTaskLockStatus: () => getTaskLockStatus,
|
|
6473
|
+
getStaleTasks: () => getStaleTasks,
|
|
6474
|
+
getNextTask: () => getNextTask,
|
|
6475
|
+
getBlockingDeps: () => getBlockingDeps,
|
|
6476
|
+
getActiveWork: () => getActiveWork,
|
|
6477
|
+
failTask: () => failTask,
|
|
6478
|
+
completeTask: () => completeTask,
|
|
6479
|
+
claimOrSteal: () => claimOrSteal,
|
|
6480
|
+
claimNextTask: () => claimNextTask
|
|
6481
|
+
});
|
|
6482
|
+
function lockExpiresAt(lockedAt) {
|
|
6483
|
+
if (!lockedAt)
|
|
6484
|
+
return null;
|
|
6485
|
+
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
6486
|
+
}
|
|
6487
|
+
function assertStartable(task, agentId) {
|
|
6488
|
+
if (task.status === "pending")
|
|
6489
|
+
return;
|
|
6490
|
+
if (task.status === "in_progress")
|
|
6491
|
+
return;
|
|
6492
|
+
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
6493
|
+
}
|
|
6494
|
+
function getBlockingDeps(id, db) {
|
|
6495
|
+
const d = db || getDatabase();
|
|
6496
|
+
const deps = getTaskDependencies(id, d);
|
|
6497
|
+
if (deps.length === 0)
|
|
6498
|
+
return [];
|
|
6499
|
+
const blocking = [];
|
|
6500
|
+
for (const dep of deps) {
|
|
6501
|
+
const task = getTask(dep.depends_on, d);
|
|
6502
|
+
if (task && task.status !== "completed")
|
|
6503
|
+
blocking.push(task);
|
|
6218
6504
|
}
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6505
|
+
return blocking;
|
|
6506
|
+
}
|
|
6507
|
+
function startTask(id, agentId, db) {
|
|
6508
|
+
const d = db || getDatabase();
|
|
6509
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6510
|
+
const task = getTask(id, d);
|
|
6511
|
+
if (!task)
|
|
6512
|
+
throw new TaskNotFoundError(id);
|
|
6513
|
+
assertStartable(task, agentId);
|
|
6514
|
+
const blocking = getBlockingDeps(id, d);
|
|
6515
|
+
if (blocking.length > 0) {
|
|
6516
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
6517
|
+
emitLocalEventHooksQuiet({
|
|
6518
|
+
type: "task.blocked",
|
|
6519
|
+
payload: {
|
|
6520
|
+
id,
|
|
6521
|
+
agent_id: agentId,
|
|
6522
|
+
title: task.title,
|
|
6523
|
+
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
6524
|
+
},
|
|
6525
|
+
databasePath
|
|
6526
|
+
});
|
|
6527
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
6222
6528
|
}
|
|
6223
|
-
|
|
6224
|
-
const
|
|
6529
|
+
const cutoff = lockExpiryCutoff();
|
|
6530
|
+
const timestamp = now();
|
|
6531
|
+
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 = ?
|
|
6532
|
+
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]);
|
|
6225
6533
|
if (result.changes === 0) {
|
|
6226
6534
|
const current = getTask(id, d);
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6535
|
+
if (!current)
|
|
6536
|
+
throw new TaskNotFoundError(id);
|
|
6537
|
+
assertStartable(current, agentId);
|
|
6538
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
6539
|
+
throw new LockError(id, current.locked_by);
|
|
6540
|
+
}
|
|
6541
|
+
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
6231
6542
|
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
6243
|
-
if (input.approved_by !== undefined)
|
|
6244
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
6245
|
-
const updatedTask = {
|
|
6246
|
-
...task,
|
|
6247
|
-
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
6248
|
-
tags: input.tags ?? task.tags,
|
|
6249
|
-
metadata: input.metadata ?? task.metadata,
|
|
6250
|
-
version: task.version + 1,
|
|
6251
|
-
updated_at: timestamp,
|
|
6252
|
-
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
6253
|
-
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
6254
|
-
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
6255
|
-
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
6256
|
-
retry_count: input.retry_count ?? task.retry_count,
|
|
6257
|
-
max_retries: input.max_retries ?? task.max_retries,
|
|
6258
|
-
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
6259
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
6260
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
6261
|
-
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
6262
|
-
};
|
|
6543
|
+
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
6544
|
+
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 };
|
|
6545
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
6546
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
6547
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
6548
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
6549
|
+
return startedTask;
|
|
6550
|
+
}
|
|
6551
|
+
function completeTask(id, agentId, db, options) {
|
|
6552
|
+
const d = db || getDatabase();
|
|
6263
6553
|
const databasePath = databasePathFromDatabase(d);
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6554
|
+
const task = getTask(id, d);
|
|
6555
|
+
if (!task)
|
|
6556
|
+
throw new TaskNotFoundError(id);
|
|
6557
|
+
if (task.status === "completed") {
|
|
6558
|
+
return task;
|
|
6269
6559
|
}
|
|
6270
|
-
if (
|
|
6271
|
-
|
|
6272
|
-
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
6273
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
6274
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
6560
|
+
if (task.status === "cancelled") {
|
|
6561
|
+
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
6275
6562
|
}
|
|
6276
|
-
if (
|
|
6277
|
-
|
|
6563
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
6564
|
+
throw new LockError(id, task.locked_by);
|
|
6278
6565
|
}
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
6288
|
-
if (!row)
|
|
6289
|
-
return false;
|
|
6290
|
-
recordStorageTombstone({
|
|
6291
|
-
object_type: "tasks",
|
|
6292
|
-
object_id: id,
|
|
6293
|
-
payload: rowToTask(row),
|
|
6294
|
-
version: row.version
|
|
6295
|
-
}, d);
|
|
6296
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
6297
|
-
return result.changes > 0;
|
|
6298
|
-
}
|
|
6299
|
-
var init_task_crud = __esm(() => {
|
|
6300
|
-
init_types();
|
|
6301
|
-
init_database();
|
|
6302
|
-
init_completion_guard();
|
|
6303
|
-
init_event_emission_safety();
|
|
6304
|
-
init_event_hooks();
|
|
6305
|
-
init_shared_events();
|
|
6306
|
-
init_audit();
|
|
6307
|
-
init_webhooks();
|
|
6308
|
-
init_checklists();
|
|
6309
|
-
init_storage_tombstones();
|
|
6310
|
-
});
|
|
6311
|
-
|
|
6312
|
-
// src/lib/recurrence.ts
|
|
6313
|
-
function parseRecurrenceRule(rule) {
|
|
6314
|
-
const normalized = rule.trim().toLowerCase();
|
|
6315
|
-
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
6316
|
-
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
6566
|
+
checkCompletionGuard(task, agentId || null, d);
|
|
6567
|
+
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;
|
|
6568
|
+
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
6569
|
+
const completionMeta = {};
|
|
6570
|
+
if (hasEvidence)
|
|
6571
|
+
completionMeta._evidence = evidence;
|
|
6572
|
+
if (options?.confidence !== undefined) {
|
|
6573
|
+
completionMeta._completion = { confidence: options.confidence };
|
|
6317
6574
|
}
|
|
6318
|
-
|
|
6319
|
-
|
|
6575
|
+
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
6576
|
+
const timestamp = options?.completed_at || now();
|
|
6577
|
+
const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
|
|
6578
|
+
const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
|
|
6579
|
+
const finalVersion = versionBeforeStatus + 1;
|
|
6580
|
+
const tx = d.transaction(() => {
|
|
6581
|
+
if (hasMeta) {
|
|
6582
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
6583
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
6584
|
+
if (metaResult.changes === 0) {
|
|
6585
|
+
const current = getTask(id, d);
|
|
6586
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
6590
|
+
WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
|
|
6591
|
+
if (statusResult.changes === 0) {
|
|
6592
|
+
const current = getTask(id, d);
|
|
6593
|
+
throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
|
|
6594
|
+
}
|
|
6595
|
+
});
|
|
6596
|
+
tx();
|
|
6597
|
+
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
6598
|
+
const completedTaskForEvent = {
|
|
6599
|
+
...task,
|
|
6600
|
+
status: "completed",
|
|
6601
|
+
locked_by: null,
|
|
6602
|
+
locked_at: null,
|
|
6603
|
+
completed_at: timestamp,
|
|
6604
|
+
confidence,
|
|
6605
|
+
version: finalVersion,
|
|
6606
|
+
updated_at: timestamp,
|
|
6607
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
6608
|
+
};
|
|
6609
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
6610
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
6611
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
6612
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
6613
|
+
let spawnedTask = null;
|
|
6614
|
+
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
6615
|
+
try {
|
|
6616
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
6617
|
+
} catch (e) {
|
|
6618
|
+
spawnedTask = null;
|
|
6619
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
6620
|
+
}
|
|
6320
6621
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6622
|
+
let spawnedFromTemplate = null;
|
|
6623
|
+
if (task.spawns_template_id) {
|
|
6624
|
+
const spawnDepth = task.metadata?._spawn_depth || 0;
|
|
6625
|
+
if (spawnDepth >= MAX_SPAWN_DEPTH) {
|
|
6626
|
+
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
6627
|
+
} else {
|
|
6628
|
+
try {
|
|
6629
|
+
const input = taskFromTemplate(task.spawns_template_id, {
|
|
6630
|
+
project_id: task.project_id ?? undefined,
|
|
6631
|
+
plan_id: task.plan_id ?? undefined,
|
|
6632
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
6633
|
+
assigned_to: task.assigned_to ?? undefined
|
|
6634
|
+
}, d);
|
|
6635
|
+
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
6636
|
+
spawnedFromTemplate = createTask(input, d);
|
|
6637
|
+
} catch {}
|
|
6638
|
+
}
|
|
6323
6639
|
}
|
|
6324
|
-
|
|
6325
|
-
|
|
6640
|
+
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
6641
|
+
if (spawnedTask) {
|
|
6642
|
+
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
6326
6643
|
}
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
return {
|
|
6330
|
-
type: "interval",
|
|
6331
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
6332
|
-
unit: intervalMatch[2]
|
|
6333
|
-
};
|
|
6644
|
+
if (spawnedFromTemplate) {
|
|
6645
|
+
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
6334
6646
|
}
|
|
6335
|
-
const
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
}
|
|
6345
|
-
|
|
6346
|
-
|
|
6647
|
+
const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
|
|
6648
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
6649
|
+
WHERE td.depends_on = ? AND t.status = 'pending'
|
|
6650
|
+
AND NOT EXISTS (
|
|
6651
|
+
SELECT 1 FROM task_dependencies td2
|
|
6652
|
+
JOIN tasks dep2 ON dep2.id = td2.depends_on
|
|
6653
|
+
WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
|
|
6654
|
+
)`).all(id, id);
|
|
6655
|
+
if (unblockedDeps.length > 0) {
|
|
6656
|
+
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
6657
|
+
for (const dep of unblockedDeps) {
|
|
6658
|
+
const depTask = getTask(dep.id, d);
|
|
6659
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
6660
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
6661
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
6662
|
+
if (depTask)
|
|
6663
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
6347
6664
|
}
|
|
6348
6665
|
}
|
|
6349
|
-
|
|
6666
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
|
|
6350
6667
|
}
|
|
6351
|
-
function
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6668
|
+
function lockTask(id, agentId, db) {
|
|
6669
|
+
const d = db || getDatabase();
|
|
6670
|
+
const task = getTask(id, d);
|
|
6671
|
+
if (!task)
|
|
6672
|
+
throw new TaskNotFoundError(id);
|
|
6673
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
6674
|
+
return {
|
|
6675
|
+
success: false,
|
|
6676
|
+
error: `Task is ${task.status} and cannot be locked`
|
|
6677
|
+
};
|
|
6357
6678
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
const next = new Date(base);
|
|
6364
|
-
if (parsed.unit === "day") {
|
|
6365
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
6366
|
-
} else if (parsed.unit === "week") {
|
|
6367
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
6368
|
-
} else if (parsed.unit === "month") {
|
|
6369
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
6370
|
-
}
|
|
6371
|
-
return next.toISOString();
|
|
6679
|
+
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
6680
|
+
const timestamp2 = now();
|
|
6681
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
6682
|
+
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
6683
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
6372
6684
|
}
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6685
|
+
const cutoff = lockExpiryCutoff();
|
|
6686
|
+
const timestamp = now();
|
|
6687
|
+
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
6688
|
+
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]);
|
|
6689
|
+
if (result.changes === 0) {
|
|
6690
|
+
const current = getTask(id, d);
|
|
6691
|
+
if (!current)
|
|
6692
|
+
throw new TaskNotFoundError(id);
|
|
6693
|
+
if (current.status === "completed" || current.status === "cancelled") {
|
|
6694
|
+
return {
|
|
6695
|
+
success: false,
|
|
6696
|
+
error: `Task is ${current.status} and cannot be locked`
|
|
6697
|
+
};
|
|
6383
6698
|
}
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6699
|
+
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
6700
|
+
return {
|
|
6701
|
+
success: false,
|
|
6702
|
+
locked_by: current.locked_by,
|
|
6703
|
+
locked_at: current.locked_at,
|
|
6704
|
+
error: `Task is locked by ${current.locked_by}`
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
return {
|
|
6708
|
+
success: false,
|
|
6709
|
+
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
6710
|
+
};
|
|
6387
6711
|
}
|
|
6388
|
-
|
|
6389
|
-
}
|
|
6390
|
-
var DAY_NAMES;
|
|
6391
|
-
var init_recurrence = __esm(() => {
|
|
6392
|
-
DAY_NAMES = {
|
|
6393
|
-
sunday: 0,
|
|
6394
|
-
sun: 0,
|
|
6395
|
-
monday: 1,
|
|
6396
|
-
mon: 1,
|
|
6397
|
-
tuesday: 2,
|
|
6398
|
-
tue: 2,
|
|
6399
|
-
wednesday: 3,
|
|
6400
|
-
wed: 3,
|
|
6401
|
-
thursday: 4,
|
|
6402
|
-
thu: 4,
|
|
6403
|
-
friday: 5,
|
|
6404
|
-
fri: 5,
|
|
6405
|
-
saturday: 6,
|
|
6406
|
-
sat: 6
|
|
6407
|
-
};
|
|
6408
|
-
});
|
|
6409
|
-
|
|
6410
|
-
// src/db/templates.ts
|
|
6411
|
-
var exports_templates = {};
|
|
6412
|
-
__export(exports_templates, {
|
|
6413
|
-
updateTemplate: () => updateTemplate,
|
|
6414
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
6415
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
6416
|
-
resolveVariables: () => resolveVariables,
|
|
6417
|
-
previewTemplate: () => previewTemplate,
|
|
6418
|
-
listTemplates: () => listTemplates,
|
|
6419
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
6420
|
-
importTemplate: () => importTemplate,
|
|
6421
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
6422
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
6423
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
6424
|
-
getTemplate: () => getTemplate,
|
|
6425
|
-
exportTemplate: () => exportTemplate,
|
|
6426
|
-
evaluateCondition: () => evaluateCondition,
|
|
6427
|
-
deleteTemplate: () => deleteTemplate,
|
|
6428
|
-
createTemplate: () => createTemplate,
|
|
6429
|
-
addTemplateTasks: () => addTemplateTasks
|
|
6430
|
-
});
|
|
6431
|
-
function rowToTemplate(row) {
|
|
6432
|
-
return {
|
|
6433
|
-
...row,
|
|
6434
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
6435
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
6436
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
6437
|
-
priority: row.priority || "medium",
|
|
6438
|
-
version: row.version ?? 1
|
|
6439
|
-
};
|
|
6440
|
-
}
|
|
6441
|
-
function rowToTemplateTask(row) {
|
|
6442
|
-
return {
|
|
6443
|
-
...row,
|
|
6444
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
6445
|
-
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
6446
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
6447
|
-
priority: row.priority || "medium",
|
|
6448
|
-
condition: row.condition ?? null,
|
|
6449
|
-
include_template_id: row.include_template_id ?? null
|
|
6450
|
-
};
|
|
6451
|
-
}
|
|
6452
|
-
function resolveTemplateId(id, d) {
|
|
6453
|
-
return resolvePartialId(d, "task_templates", id);
|
|
6712
|
+
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
6713
|
+
return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
|
|
6454
6714
|
}
|
|
6455
|
-
function
|
|
6715
|
+
function unlockTask(id, agentId, db) {
|
|
6456
6716
|
const d = db || getDatabase();
|
|
6457
|
-
const
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
id,
|
|
6462
|
-
input.name,
|
|
6463
|
-
input.title_pattern,
|
|
6464
|
-
input.description || null,
|
|
6465
|
-
input.priority || "medium",
|
|
6466
|
-
JSON.stringify(input.tags || []),
|
|
6467
|
-
JSON.stringify(input.variables || []),
|
|
6468
|
-
input.project_id || null,
|
|
6469
|
-
input.plan_id || null,
|
|
6470
|
-
JSON.stringify(input.metadata || {}),
|
|
6471
|
-
now(),
|
|
6472
|
-
machineId
|
|
6473
|
-
]);
|
|
6474
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
6475
|
-
addTemplateTasks(id, input.tasks, d);
|
|
6717
|
+
const task = getTask(id, d);
|
|
6718
|
+
if (!task)
|
|
6719
|
+
throw new TaskNotFoundError(id);
|
|
6720
|
+
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
6721
|
+
throw new LockError(id, task.locked_by);
|
|
6476
6722
|
}
|
|
6477
|
-
|
|
6723
|
+
const timestamp = now();
|
|
6724
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
6725
|
+
WHERE id = ?`, [timestamp, id]);
|
|
6726
|
+
return true;
|
|
6478
6727
|
}
|
|
6479
|
-
function
|
|
6728
|
+
function getTaskLockStatus(id, db) {
|
|
6480
6729
|
const d = db || getDatabase();
|
|
6481
|
-
const
|
|
6482
|
-
if (!
|
|
6483
|
-
|
|
6484
|
-
const
|
|
6485
|
-
return
|
|
6730
|
+
const task = getTask(id, d);
|
|
6731
|
+
if (!task)
|
|
6732
|
+
throw new TaskNotFoundError(id);
|
|
6733
|
+
const expired = isLockExpired(task.locked_at);
|
|
6734
|
+
return {
|
|
6735
|
+
task_id: id,
|
|
6736
|
+
locked: !!task.locked_by && !expired,
|
|
6737
|
+
locked_by: task.locked_by,
|
|
6738
|
+
locked_at: task.locked_at,
|
|
6739
|
+
expires_at: lockExpiresAt(task.locked_at),
|
|
6740
|
+
expired
|
|
6741
|
+
};
|
|
6486
6742
|
}
|
|
6487
|
-
function
|
|
6743
|
+
function claimNextTask(agentId, filters, db) {
|
|
6488
6744
|
const d = db || getDatabase();
|
|
6489
|
-
|
|
6745
|
+
const MAX_ATTEMPTS = 25;
|
|
6746
|
+
const tried = new Set;
|
|
6747
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
6748
|
+
const outcome = d.transaction(() => {
|
|
6749
|
+
const task = getNextTask(agentId, filters, d);
|
|
6750
|
+
if (!task)
|
|
6751
|
+
return { done: true, task: null };
|
|
6752
|
+
if (tried.has(task.id))
|
|
6753
|
+
return { done: true, task: null };
|
|
6754
|
+
tried.add(task.id);
|
|
6755
|
+
try {
|
|
6756
|
+
return { done: true, task: startTask(task.id, agentId, d) };
|
|
6757
|
+
} catch {
|
|
6758
|
+
return { done: false, task: null };
|
|
6759
|
+
}
|
|
6760
|
+
})();
|
|
6761
|
+
if (outcome.done)
|
|
6762
|
+
return outcome.task;
|
|
6763
|
+
}
|
|
6764
|
+
return null;
|
|
6490
6765
|
}
|
|
6491
|
-
function
|
|
6766
|
+
function getNextTask(agentId, filters, db) {
|
|
6492
6767
|
const d = db || getDatabase();
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
recordStorageTombstone({
|
|
6500
|
-
object_type: "templates",
|
|
6501
|
-
object_id: resolved,
|
|
6502
|
-
payload: template,
|
|
6503
|
-
version: template.version
|
|
6504
|
-
}, d);
|
|
6505
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
6506
|
-
}
|
|
6507
|
-
function updateTemplate(id, updates, db) {
|
|
6508
|
-
const d = db || getDatabase();
|
|
6509
|
-
const resolved = resolveTemplateId(id, d);
|
|
6510
|
-
if (!resolved)
|
|
6511
|
-
return null;
|
|
6512
|
-
const current = getTemplateWithTasks(resolved, d);
|
|
6513
|
-
if (current) {
|
|
6514
|
-
const snapshot = JSON.stringify({
|
|
6515
|
-
name: current.name,
|
|
6516
|
-
title_pattern: current.title_pattern,
|
|
6517
|
-
description: current.description,
|
|
6518
|
-
priority: current.priority,
|
|
6519
|
-
tags: current.tags,
|
|
6520
|
-
variables: current.variables,
|
|
6521
|
-
project_id: current.project_id,
|
|
6522
|
-
plan_id: current.plan_id,
|
|
6523
|
-
metadata: current.metadata,
|
|
6524
|
-
tasks: current.tasks
|
|
6525
|
-
});
|
|
6526
|
-
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
6527
|
-
}
|
|
6528
|
-
const sets = ["version = version + 1"];
|
|
6529
|
-
const values = [];
|
|
6530
|
-
if (updates.name !== undefined) {
|
|
6531
|
-
sets.push("name = ?");
|
|
6532
|
-
values.push(updates.name);
|
|
6533
|
-
}
|
|
6534
|
-
if (updates.title_pattern !== undefined) {
|
|
6535
|
-
sets.push("title_pattern = ?");
|
|
6536
|
-
values.push(updates.title_pattern);
|
|
6537
|
-
}
|
|
6538
|
-
if (updates.description !== undefined) {
|
|
6539
|
-
sets.push("description = ?");
|
|
6540
|
-
values.push(updates.description);
|
|
6768
|
+
clearExpiredLocks(d);
|
|
6769
|
+
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
6770
|
+
const params = [lockExpiryCutoff()];
|
|
6771
|
+
if (filters?.project_id) {
|
|
6772
|
+
conditions.push("project_id = ?");
|
|
6773
|
+
params.push(filters.project_id);
|
|
6541
6774
|
}
|
|
6542
|
-
if (
|
|
6543
|
-
|
|
6544
|
-
|
|
6775
|
+
if (filters?.task_list_id) {
|
|
6776
|
+
conditions.push("task_list_id = ?");
|
|
6777
|
+
params.push(filters.task_list_id);
|
|
6545
6778
|
}
|
|
6546
|
-
if (
|
|
6547
|
-
|
|
6548
|
-
|
|
6779
|
+
if (filters?.plan_id) {
|
|
6780
|
+
conditions.push("plan_id = ?");
|
|
6781
|
+
params.push(filters.plan_id);
|
|
6549
6782
|
}
|
|
6550
|
-
if (
|
|
6551
|
-
|
|
6552
|
-
|
|
6783
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
6784
|
+
const placeholders = filters.tags.map(() => "?").join(",");
|
|
6785
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
6786
|
+
params.push(...filters.tags);
|
|
6553
6787
|
}
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6788
|
+
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')");
|
|
6789
|
+
const where = conditions.join(" AND ");
|
|
6790
|
+
let recentProjectIds = [];
|
|
6791
|
+
if (agentId) {
|
|
6792
|
+
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);
|
|
6793
|
+
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
6557
6794
|
}
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6795
|
+
let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
|
|
6796
|
+
if (agentId) {
|
|
6797
|
+
sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
|
|
6798
|
+
params.push(agentId);
|
|
6561
6799
|
}
|
|
6562
|
-
if (
|
|
6563
|
-
|
|
6564
|
-
|
|
6800
|
+
if (recentProjectIds.length > 0) {
|
|
6801
|
+
const placeholders = recentProjectIds.map(() => "?").join(",");
|
|
6802
|
+
sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
|
|
6803
|
+
params.push(...recentProjectIds);
|
|
6565
6804
|
}
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
return
|
|
6569
|
-
}
|
|
6570
|
-
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
6571
|
-
const t = getTemplate(templateId, db);
|
|
6572
|
-
if (!t)
|
|
6573
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
6574
|
-
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
6575
|
-
return {
|
|
6576
|
-
title: cleanOverrides.title || t.title_pattern,
|
|
6577
|
-
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
6578
|
-
priority: cleanOverrides.priority ?? t.priority,
|
|
6579
|
-
tags: cleanOverrides.tags ?? t.tags,
|
|
6580
|
-
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
6581
|
-
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
6582
|
-
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
6583
|
-
...cleanOverrides
|
|
6584
|
-
};
|
|
6805
|
+
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`;
|
|
6806
|
+
const row = d.query(sql).get(...params);
|
|
6807
|
+
return row ? rowToTask(row) : null;
|
|
6585
6808
|
}
|
|
6586
|
-
function
|
|
6809
|
+
function getActiveWork(filters, db) {
|
|
6587
6810
|
const d = db || getDatabase();
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
const task = tasks[i];
|
|
6595
|
-
const id = uuid();
|
|
6596
|
-
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)
|
|
6597
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6598
|
-
id,
|
|
6599
|
-
templateId,
|
|
6600
|
-
i,
|
|
6601
|
-
task.title_pattern,
|
|
6602
|
-
task.description || null,
|
|
6603
|
-
task.priority || "medium",
|
|
6604
|
-
JSON.stringify(task.tags || []),
|
|
6605
|
-
task.task_type || null,
|
|
6606
|
-
task.condition || null,
|
|
6607
|
-
task.include_template_id || null,
|
|
6608
|
-
JSON.stringify(task.depends_on || []),
|
|
6609
|
-
JSON.stringify(task.metadata || {}),
|
|
6610
|
-
now()
|
|
6611
|
-
]);
|
|
6612
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
6613
|
-
if (row)
|
|
6614
|
-
results.push(rowToTemplateTask(row));
|
|
6811
|
+
clearExpiredLocks(d);
|
|
6812
|
+
const conditions = ["status = 'in_progress'"];
|
|
6813
|
+
const params = [];
|
|
6814
|
+
if (filters?.project_id) {
|
|
6815
|
+
conditions.push("project_id = ?");
|
|
6816
|
+
params.push(filters.project_id);
|
|
6615
6817
|
}
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
const
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
return { ...template, tasks };
|
|
6818
|
+
if (filters?.task_list_id) {
|
|
6819
|
+
conditions.push("task_list_id = ?");
|
|
6820
|
+
params.push(filters.task_list_id);
|
|
6821
|
+
}
|
|
6822
|
+
const where = conditions.join(" AND ");
|
|
6823
|
+
const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
|
|
6824
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
6825
|
+
updated_at DESC`).all(...params);
|
|
6826
|
+
return rows;
|
|
6626
6827
|
}
|
|
6627
|
-
function
|
|
6828
|
+
function getTasksChangedSince(since, filters, db) {
|
|
6628
6829
|
const d = db || getDatabase();
|
|
6629
|
-
const
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
}
|
|
6635
|
-
function evaluateCondition(condition, variables) {
|
|
6636
|
-
if (!condition || condition.trim() === "")
|
|
6637
|
-
return true;
|
|
6638
|
-
const trimmed = condition.trim();
|
|
6639
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
6640
|
-
if (eqMatch) {
|
|
6641
|
-
const varName = eqMatch[1];
|
|
6642
|
-
const expected = eqMatch[2].trim();
|
|
6643
|
-
return (variables[varName] ?? "") === expected;
|
|
6644
|
-
}
|
|
6645
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
6646
|
-
if (neqMatch) {
|
|
6647
|
-
const varName = neqMatch[1];
|
|
6648
|
-
const expected = neqMatch[2].trim();
|
|
6649
|
-
return (variables[varName] ?? "") !== expected;
|
|
6650
|
-
}
|
|
6651
|
-
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
6652
|
-
if (falsyMatch) {
|
|
6653
|
-
const varName = falsyMatch[1];
|
|
6654
|
-
const val = variables[varName];
|
|
6655
|
-
return !val || val === "" || val === "false";
|
|
6830
|
+
const conditions = ["updated_at > ?"];
|
|
6831
|
+
const params = [since];
|
|
6832
|
+
if (filters?.project_id) {
|
|
6833
|
+
conditions.push("project_id = ?");
|
|
6834
|
+
params.push(filters.project_id);
|
|
6656
6835
|
}
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
const val = variables[varName];
|
|
6661
|
-
return !!val && val !== "" && val !== "false";
|
|
6836
|
+
if (filters?.task_list_id) {
|
|
6837
|
+
conditions.push("task_list_id = ?");
|
|
6838
|
+
params.push(filters.task_list_id);
|
|
6662
6839
|
}
|
|
6663
|
-
|
|
6840
|
+
const where = conditions.join(" AND ");
|
|
6841
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
6842
|
+
return rows.map(rowToTask);
|
|
6664
6843
|
}
|
|
6665
|
-
function
|
|
6844
|
+
function failTask(id, agentId, reason, options, db) {
|
|
6666
6845
|
const d = db || getDatabase();
|
|
6667
|
-
const
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6846
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6847
|
+
const task = getTask(id, d);
|
|
6848
|
+
if (!task)
|
|
6849
|
+
throw new TaskNotFoundError(id);
|
|
6850
|
+
const meta = {
|
|
6851
|
+
...task.metadata,
|
|
6852
|
+
_failure: {
|
|
6853
|
+
reason: reason || "Unknown failure",
|
|
6854
|
+
error_code: options?.error_code || null,
|
|
6855
|
+
failed_by: agentId || null,
|
|
6856
|
+
failed_at: now(),
|
|
6857
|
+
retry_requested: options?.retry || false
|
|
6858
|
+
}
|
|
6859
|
+
};
|
|
6860
|
+
const timestamp = now();
|
|
6861
|
+
const failTx = d.transaction(() => {
|
|
6862
|
+
const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
6863
|
+
WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
|
|
6864
|
+
if (res.changes === 0) {
|
|
6865
|
+
const current = getTask(id, d);
|
|
6866
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
6867
|
+
}
|
|
6868
|
+
});
|
|
6869
|
+
failTx();
|
|
6870
|
+
const failedTask = {
|
|
6871
|
+
...task,
|
|
6872
|
+
status: "failed",
|
|
6873
|
+
locked_by: null,
|
|
6874
|
+
locked_at: null,
|
|
6875
|
+
metadata: meta,
|
|
6876
|
+
version: task.version + 1,
|
|
6877
|
+
updated_at: timestamp
|
|
6692
6878
|
};
|
|
6879
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6880
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
6881
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
6882
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
6883
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
|
|
6884
|
+
let retryTask;
|
|
6885
|
+
if (options?.retry) {
|
|
6886
|
+
const retryCount = (task.retry_count || 0) + 1;
|
|
6887
|
+
const maxRetries = task.max_retries || 3;
|
|
6888
|
+
if (retryCount > maxRetries) {
|
|
6889
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
6890
|
+
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
6891
|
+
id
|
|
6892
|
+
]);
|
|
6893
|
+
} else {
|
|
6894
|
+
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
6895
|
+
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
6896
|
+
let title = task.title;
|
|
6897
|
+
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
6898
|
+
title = title.slice(task.short_id.length + 2);
|
|
6899
|
+
}
|
|
6900
|
+
retryTask = createTask({
|
|
6901
|
+
title,
|
|
6902
|
+
description: task.description ?? undefined,
|
|
6903
|
+
priority: task.priority,
|
|
6904
|
+
project_id: task.project_id ?? undefined,
|
|
6905
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
6906
|
+
plan_id: task.plan_id ?? undefined,
|
|
6907
|
+
assigned_to: task.assigned_to ?? undefined,
|
|
6908
|
+
tags: task.tags,
|
|
6909
|
+
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
6910
|
+
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
6911
|
+
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
6912
|
+
due_at: retryAfter
|
|
6913
|
+
}, d);
|
|
6914
|
+
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
return { task: failedTask, retryTask };
|
|
6693
6918
|
}
|
|
6694
|
-
function
|
|
6919
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
6695
6920
|
const d = db || getDatabase();
|
|
6696
|
-
const
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
plan_id: json.plan_id ?? undefined,
|
|
6716
|
-
metadata: json.metadata,
|
|
6717
|
-
tasks: taskInputs
|
|
6718
|
-
}, d);
|
|
6921
|
+
const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
|
|
6922
|
+
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
6923
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
6924
|
+
const conditions = [
|
|
6925
|
+
"status = 'in_progress'",
|
|
6926
|
+
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
6927
|
+
];
|
|
6928
|
+
const params = [cutoff, cutoff];
|
|
6929
|
+
if (effectiveFilters?.project_id) {
|
|
6930
|
+
conditions.push("project_id = ?");
|
|
6931
|
+
params.push(effectiveFilters.project_id);
|
|
6932
|
+
}
|
|
6933
|
+
if (effectiveFilters?.task_list_id) {
|
|
6934
|
+
conditions.push("task_list_id = ?");
|
|
6935
|
+
params.push(effectiveFilters.task_list_id);
|
|
6936
|
+
}
|
|
6937
|
+
const where = conditions.join(" AND ");
|
|
6938
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
6939
|
+
return rows.map(rowToTask);
|
|
6719
6940
|
}
|
|
6720
|
-
function
|
|
6941
|
+
function stealTask(agentId, opts, db) {
|
|
6721
6942
|
const d = db || getDatabase();
|
|
6722
|
-
const
|
|
6723
|
-
|
|
6943
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6944
|
+
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
6945
|
+
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
6946
|
+
if (staleTasks.length === 0)
|
|
6724
6947
|
return null;
|
|
6725
|
-
const
|
|
6726
|
-
|
|
6948
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
6949
|
+
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
6950
|
+
const target = staleTasks[0];
|
|
6951
|
+
const timestamp = now();
|
|
6952
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
6953
|
+
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
6954
|
+
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]);
|
|
6955
|
+
if (result.changes === 0)
|
|
6956
|
+
return null;
|
|
6957
|
+
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
6958
|
+
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
6959
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
6960
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
6961
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6962
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
6963
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
6964
|
+
return stolenTask;
|
|
6727
6965
|
}
|
|
6728
|
-
function
|
|
6966
|
+
function claimOrSteal(agentId, filters, db) {
|
|
6729
6967
|
const d = db || getDatabase();
|
|
6730
|
-
const
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
}
|
|
6735
|
-
function resolveVariables(templateVars, provided) {
|
|
6736
|
-
const merged = { ...provided };
|
|
6737
|
-
for (const v of templateVars) {
|
|
6738
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
6739
|
-
merged[v.name] = v.default;
|
|
6740
|
-
}
|
|
6741
|
-
}
|
|
6742
|
-
const missing = [];
|
|
6743
|
-
for (const v of templateVars) {
|
|
6744
|
-
if (v.required && merged[v.name] === undefined) {
|
|
6745
|
-
missing.push(v.name);
|
|
6968
|
+
const tx = d.transaction(() => {
|
|
6969
|
+
const next = getNextTask(agentId, filters, d);
|
|
6970
|
+
if (next) {
|
|
6971
|
+
const started = startTask(next.id, agentId, d);
|
|
6972
|
+
return { task: started, stolen: false };
|
|
6746
6973
|
}
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6974
|
+
const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
|
|
6975
|
+
if (stolen)
|
|
6976
|
+
return { task: stolen, stolen: true };
|
|
6977
|
+
return null;
|
|
6978
|
+
});
|
|
6979
|
+
return tx();
|
|
6752
6980
|
}
|
|
6753
|
-
function
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6981
|
+
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
6982
|
+
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
6983
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
6984
|
+
let title = completedTask.title;
|
|
6985
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
6986
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
6757
6987
|
}
|
|
6758
|
-
|
|
6988
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
6989
|
+
return createTask({
|
|
6990
|
+
title,
|
|
6991
|
+
description: completedTask.description ?? undefined,
|
|
6992
|
+
priority: completedTask.priority,
|
|
6993
|
+
project_id: completedTask.project_id ?? undefined,
|
|
6994
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
6995
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
6996
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
6997
|
+
tags: completedTask.tags,
|
|
6998
|
+
metadata: completedTask.metadata,
|
|
6999
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
7000
|
+
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
7001
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
7002
|
+
recurrence_parent_id: recurrenceParentId,
|
|
7003
|
+
due_at: dueAt
|
|
7004
|
+
}, db);
|
|
6759
7005
|
}
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
let title = tt.title_pattern;
|
|
6795
|
-
let desc = tt.description;
|
|
6796
|
-
title = substituteVars(title, resolved);
|
|
6797
|
-
if (desc)
|
|
6798
|
-
desc = substituteVars(desc, resolved);
|
|
6799
|
-
const task = createTask({
|
|
6800
|
-
title,
|
|
6801
|
-
description: desc ?? undefined,
|
|
6802
|
-
priority: tt.priority,
|
|
6803
|
-
tags: tt.tags,
|
|
6804
|
-
task_type: tt.task_type ?? undefined,
|
|
6805
|
-
project_id: projectId,
|
|
6806
|
-
task_list_id: taskListId,
|
|
6807
|
-
metadata: tt.metadata
|
|
6808
|
-
}, d);
|
|
6809
|
-
createdTasks.push(task);
|
|
6810
|
-
positionToId.set(tt.position, task.id);
|
|
7006
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
7007
|
+
var init_task_lifecycle = __esm(() => {
|
|
7008
|
+
init_types();
|
|
7009
|
+
init_database();
|
|
7010
|
+
init_completion_guard();
|
|
7011
|
+
init_event_emission_safety();
|
|
7012
|
+
init_event_hooks();
|
|
7013
|
+
init_shared_events();
|
|
7014
|
+
init_audit();
|
|
7015
|
+
init_recurrence();
|
|
7016
|
+
init_webhooks();
|
|
7017
|
+
init_templates();
|
|
7018
|
+
init_task_crud();
|
|
7019
|
+
init_task_graph();
|
|
7020
|
+
});
|
|
7021
|
+
|
|
7022
|
+
// src/db/task-crud.ts
|
|
7023
|
+
function rowToTask(row) {
|
|
7024
|
+
return {
|
|
7025
|
+
...row,
|
|
7026
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
7027
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
7028
|
+
status: row.status,
|
|
7029
|
+
priority: row.priority,
|
|
7030
|
+
requires_approval: !!row.requires_approval
|
|
7031
|
+
};
|
|
7032
|
+
}
|
|
7033
|
+
function insertTaskTags(taskId, tags, db) {
|
|
7034
|
+
if (tags.length === 0)
|
|
7035
|
+
return;
|
|
7036
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
7037
|
+
for (const tag of tags) {
|
|
7038
|
+
if (tag)
|
|
7039
|
+
stmt.run(taskId, tag);
|
|
6811
7040
|
}
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
if (taskId && depId) {
|
|
6824
|
-
addDependency(taskId, depId, d);
|
|
6825
|
-
}
|
|
7041
|
+
}
|
|
7042
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
7043
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7044
|
+
insertTaskTags(taskId, tags, db);
|
|
7045
|
+
}
|
|
7046
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
7047
|
+
if (!metadata)
|
|
7048
|
+
return;
|
|
7049
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
7050
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
7051
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
6826
7052
|
}
|
|
7053
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
7054
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
6827
7055
|
}
|
|
6828
|
-
return createdTasks;
|
|
6829
7056
|
}
|
|
6830
|
-
function
|
|
7057
|
+
function createTask(input, db) {
|
|
6831
7058
|
const d = db || getDatabase();
|
|
6832
|
-
const
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
const
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7059
|
+
const timestamp = now();
|
|
7060
|
+
const tags = input.tags || [];
|
|
7061
|
+
const machineId = currentStorageMachineId(d);
|
|
7062
|
+
const assignedBy = input.assigned_by || input.agent_id;
|
|
7063
|
+
const assignedFromProject = input.assigned_from_project || null;
|
|
7064
|
+
let id = uuid();
|
|
7065
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
7066
|
+
try {
|
|
7067
|
+
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)
|
|
7068
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7069
|
+
id,
|
|
7070
|
+
null,
|
|
7071
|
+
input.project_id || null,
|
|
7072
|
+
input.parent_id || null,
|
|
7073
|
+
input.plan_id || null,
|
|
7074
|
+
input.task_list_id || null,
|
|
7075
|
+
input.cycle_id || null,
|
|
7076
|
+
input.title,
|
|
7077
|
+
input.description || null,
|
|
7078
|
+
input.status || "pending",
|
|
7079
|
+
input.priority || "medium",
|
|
7080
|
+
input.agent_id || null,
|
|
7081
|
+
input.assigned_to || null,
|
|
7082
|
+
input.session_id || null,
|
|
7083
|
+
input.working_dir || null,
|
|
7084
|
+
JSON.stringify(tags),
|
|
7085
|
+
JSON.stringify(input.metadata || {}),
|
|
7086
|
+
timestamp,
|
|
7087
|
+
timestamp,
|
|
7088
|
+
input.due_at || null,
|
|
7089
|
+
input.estimated_minutes || null,
|
|
7090
|
+
input.sla_minutes ?? null,
|
|
7091
|
+
input.confidence ?? null,
|
|
7092
|
+
input.retry_count ?? 0,
|
|
7093
|
+
input.max_retries ?? 3,
|
|
7094
|
+
input.retry_after ?? null,
|
|
7095
|
+
input.requires_approval ? 1 : 0,
|
|
7096
|
+
null,
|
|
7097
|
+
null,
|
|
7098
|
+
input.recurrence_rule || null,
|
|
7099
|
+
input.recurrence_parent_id || null,
|
|
7100
|
+
input.spawns_template_id || null,
|
|
7101
|
+
input.reason || null,
|
|
7102
|
+
input.spawned_from_session || null,
|
|
7103
|
+
assignedBy || null,
|
|
7104
|
+
assignedFromProject || null,
|
|
7105
|
+
input.task_type || null,
|
|
7106
|
+
machineId
|
|
7107
|
+
]);
|
|
7108
|
+
break;
|
|
7109
|
+
} catch (e) {
|
|
7110
|
+
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
7111
|
+
id = uuid();
|
|
6850
7112
|
continue;
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
title: substituteVars(tt.title_pattern, resolved),
|
|
6854
|
-
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
6855
|
-
priority: tt.priority,
|
|
6856
|
-
tags: tt.tags,
|
|
6857
|
-
task_type: tt.task_type,
|
|
6858
|
-
depends_on_positions: tt.depends_on_positions
|
|
6859
|
-
});
|
|
7113
|
+
}
|
|
7114
|
+
throw e;
|
|
6860
7115
|
}
|
|
6861
7116
|
}
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
template_name: template.name,
|
|
6865
|
-
description: template.description,
|
|
6866
|
-
variables: template.variables,
|
|
6867
|
-
resolved_variables: resolved,
|
|
6868
|
-
tasks
|
|
6869
|
-
};
|
|
6870
|
-
}
|
|
6871
|
-
var init_templates = __esm(() => {
|
|
6872
|
-
init_database();
|
|
6873
|
-
init_tasks();
|
|
6874
|
-
init_storage_tombstones();
|
|
6875
|
-
});
|
|
6876
|
-
|
|
6877
|
-
// src/db/task-graph.ts
|
|
6878
|
-
function addDependency(taskId, dependsOn, db) {
|
|
6879
|
-
const d = db || getDatabase();
|
|
6880
|
-
if (!getTask(taskId, d))
|
|
6881
|
-
throw new TaskNotFoundError(taskId);
|
|
6882
|
-
if (!getTask(dependsOn, d))
|
|
6883
|
-
throw new TaskNotFoundError(dependsOn);
|
|
6884
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
6885
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
7117
|
+
if (tags.length > 0) {
|
|
7118
|
+
insertTaskTags(id, tags, d);
|
|
6886
7119
|
}
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
function getTaskDependencies(taskId, db) {
|
|
6895
|
-
const d = db || getDatabase();
|
|
6896
|
-
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
7120
|
+
const task = getTask(id, d);
|
|
7121
|
+
const payload = taskEventData(task);
|
|
7122
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7123
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
7124
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
7125
|
+
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
7126
|
+
return task;
|
|
6897
7127
|
}
|
|
6898
|
-
function
|
|
7128
|
+
function getTask(id, db) {
|
|
6899
7129
|
const d = db || getDatabase();
|
|
6900
|
-
|
|
7130
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
7131
|
+
if (!row)
|
|
7132
|
+
return null;
|
|
7133
|
+
return rowToTask(row);
|
|
6901
7134
|
}
|
|
6902
|
-
function
|
|
7135
|
+
function getTaskWithRelations(id, db) {
|
|
6903
7136
|
const d = db || getDatabase();
|
|
6904
|
-
const
|
|
6905
|
-
if (!
|
|
6906
|
-
|
|
6907
|
-
const
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
7137
|
+
const task = getTask(id, d);
|
|
7138
|
+
if (!task)
|
|
7139
|
+
return null;
|
|
7140
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
7141
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
7142
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
7143
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
7144
|
+
WHERE td.task_id = ?`).all(id);
|
|
7145
|
+
const dependencies = depRows.map(rowToTask);
|
|
7146
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
7147
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
7148
|
+
WHERE td.depends_on = ?`).all(id);
|
|
7149
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
7150
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
7151
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
7152
|
+
const checklist = getChecklist(id, d);
|
|
7153
|
+
return {
|
|
7154
|
+
...task,
|
|
7155
|
+
subtasks,
|
|
7156
|
+
dependencies,
|
|
7157
|
+
blocked_by,
|
|
7158
|
+
comments,
|
|
7159
|
+
parent,
|
|
7160
|
+
checklist
|
|
6922
7161
|
};
|
|
6923
|
-
return createTask(input, d);
|
|
6924
7162
|
}
|
|
6925
|
-
function
|
|
7163
|
+
function listTasks(filter = {}, db) {
|
|
6926
7164
|
const d = db || getDatabase();
|
|
6927
|
-
const
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
return depTask && depTask.status !== "completed";
|
|
6935
|
-
});
|
|
6936
|
-
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
7165
|
+
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
7166
|
+
clearExpiredLocks2(d);
|
|
7167
|
+
const conditions = [];
|
|
7168
|
+
const params = [];
|
|
7169
|
+
if (filter.project_id) {
|
|
7170
|
+
conditions.push("project_id = ?");
|
|
7171
|
+
params.push(filter.project_id);
|
|
6937
7172
|
}
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
visited.add(id);
|
|
6942
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
6943
|
-
return deps.map((dep) => {
|
|
6944
|
-
const depTask = getTask(dep.depends_on, d);
|
|
6945
|
-
if (!depTask)
|
|
6946
|
-
return null;
|
|
6947
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
6948
|
-
}).filter(Boolean);
|
|
7173
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
7174
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
7175
|
+
params.push(...filter.ids);
|
|
6949
7176
|
}
|
|
6950
|
-
|
|
6951
|
-
if (
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
if (!depTask)
|
|
6958
|
-
return null;
|
|
6959
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
6960
|
-
}).filter(Boolean);
|
|
7177
|
+
if (filter.parent_id !== undefined) {
|
|
7178
|
+
if (filter.parent_id === null) {
|
|
7179
|
+
conditions.push("parent_id IS NULL");
|
|
7180
|
+
} else {
|
|
7181
|
+
conditions.push("parent_id = ?");
|
|
7182
|
+
params.push(filter.parent_id);
|
|
7183
|
+
}
|
|
6961
7184
|
}
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
}
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
if (!task)
|
|
6971
|
-
throw new TaskNotFoundError(taskId);
|
|
6972
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
6973
|
-
const params = [now()];
|
|
6974
|
-
if (target.task_list_id !== undefined) {
|
|
6975
|
-
sets.push("task_list_id = ?");
|
|
6976
|
-
params.push(target.task_list_id);
|
|
7185
|
+
if (filter.status) {
|
|
7186
|
+
if (Array.isArray(filter.status)) {
|
|
7187
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
7188
|
+
params.push(...filter.status);
|
|
7189
|
+
} else {
|
|
7190
|
+
conditions.push("status = ?");
|
|
7191
|
+
params.push(filter.status);
|
|
7192
|
+
}
|
|
6977
7193
|
}
|
|
6978
|
-
if (
|
|
6979
|
-
|
|
6980
|
-
|
|
7194
|
+
if (filter.priority) {
|
|
7195
|
+
if (Array.isArray(filter.priority)) {
|
|
7196
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
7197
|
+
params.push(...filter.priority);
|
|
7198
|
+
} else {
|
|
7199
|
+
conditions.push("priority = ?");
|
|
7200
|
+
params.push(filter.priority);
|
|
7201
|
+
}
|
|
6981
7202
|
}
|
|
6982
|
-
if (
|
|
6983
|
-
|
|
6984
|
-
params.push(
|
|
7203
|
+
if (filter.assigned_to) {
|
|
7204
|
+
conditions.push("assigned_to = ?");
|
|
7205
|
+
params.push(filter.assigned_to);
|
|
6985
7206
|
}
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
}
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7207
|
+
if (filter.agent_id) {
|
|
7208
|
+
conditions.push("agent_id = ?");
|
|
7209
|
+
params.push(filter.agent_id);
|
|
7210
|
+
}
|
|
7211
|
+
if (filter.session_id) {
|
|
7212
|
+
conditions.push("session_id = ?");
|
|
7213
|
+
params.push(filter.session_id);
|
|
7214
|
+
}
|
|
7215
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
7216
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
7217
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
7218
|
+
params.push(...filter.tags);
|
|
7219
|
+
}
|
|
7220
|
+
if (filter.plan_id) {
|
|
7221
|
+
conditions.push("plan_id = ?");
|
|
7222
|
+
params.push(filter.plan_id);
|
|
7223
|
+
}
|
|
7224
|
+
if (filter.task_list_id) {
|
|
7225
|
+
conditions.push("task_list_id = ?");
|
|
7226
|
+
params.push(filter.task_list_id);
|
|
7227
|
+
}
|
|
7228
|
+
if (filter.has_recurrence === true) {
|
|
7229
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
7230
|
+
} else if (filter.has_recurrence === false) {
|
|
7231
|
+
conditions.push("recurrence_rule IS NULL");
|
|
7232
|
+
}
|
|
7233
|
+
if (filter.task_type) {
|
|
7234
|
+
if (Array.isArray(filter.task_type)) {
|
|
7235
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
7236
|
+
params.push(...filter.task_type);
|
|
7237
|
+
} else {
|
|
7238
|
+
conditions.push("task_type = ?");
|
|
7239
|
+
params.push(filter.task_type);
|
|
7003
7240
|
}
|
|
7004
7241
|
}
|
|
7005
|
-
|
|
7242
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
7243
|
+
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
7244
|
+
if (filter.cursor) {
|
|
7245
|
+
try {
|
|
7246
|
+
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
7247
|
+
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
7248
|
+
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
7249
|
+
} catch {}
|
|
7250
|
+
}
|
|
7251
|
+
if (!filter.include_archived) {
|
|
7252
|
+
conditions.push("archived_at IS NULL");
|
|
7253
|
+
}
|
|
7254
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7255
|
+
let limitClause = "";
|
|
7256
|
+
if (filter.limit) {
|
|
7257
|
+
limitClause = " LIMIT ?";
|
|
7258
|
+
params.push(filter.limit);
|
|
7259
|
+
if (!filter.cursor && filter.offset) {
|
|
7260
|
+
limitClause += " OFFSET ?";
|
|
7261
|
+
params.push(filter.offset);
|
|
7262
|
+
}
|
|
7263
|
+
}
|
|
7264
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
|
|
7265
|
+
return rows.map(rowToTask);
|
|
7006
7266
|
}
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
init_task_crud();
|
|
7011
|
-
});
|
|
7012
|
-
|
|
7013
|
-
// src/db/task-lifecycle.ts
|
|
7014
|
-
function lockExpiresAt(lockedAt) {
|
|
7015
|
-
if (!lockedAt)
|
|
7016
|
-
return null;
|
|
7017
|
-
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
7267
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
7268
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
7269
|
+
return tasks[0] ?? null;
|
|
7018
7270
|
}
|
|
7019
|
-
function
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7271
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
7272
|
+
return {
|
|
7273
|
+
...current,
|
|
7274
|
+
...next ?? {},
|
|
7275
|
+
fingerprint
|
|
7276
|
+
};
|
|
7025
7277
|
}
|
|
7026
|
-
function
|
|
7278
|
+
function upsertTaskByFingerprint(input, db) {
|
|
7027
7279
|
const d = db || getDatabase();
|
|
7028
|
-
const
|
|
7029
|
-
if (
|
|
7030
|
-
|
|
7031
|
-
const
|
|
7032
|
-
|
|
7033
|
-
const
|
|
7034
|
-
if (
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7280
|
+
const fingerprint = input.fingerprint.trim();
|
|
7281
|
+
if (!fingerprint)
|
|
7282
|
+
throw new Error("fingerprint is required");
|
|
7283
|
+
const tx = d.transaction(() => {
|
|
7284
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
7285
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
7286
|
+
if (!existing) {
|
|
7287
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
7288
|
+
return { task: task2, created: true };
|
|
7289
|
+
}
|
|
7290
|
+
const task = updateTask(existing.id, {
|
|
7291
|
+
version: existing.version,
|
|
7292
|
+
title: input.title,
|
|
7293
|
+
description: input.description,
|
|
7294
|
+
status: input.status,
|
|
7295
|
+
priority: input.priority,
|
|
7296
|
+
project_id: input.project_id,
|
|
7297
|
+
assigned_to: input.assigned_to,
|
|
7298
|
+
working_dir: input.working_dir,
|
|
7299
|
+
plan_id: input.plan_id,
|
|
7300
|
+
task_list_id: input.task_list_id,
|
|
7301
|
+
tags: input.tags,
|
|
7302
|
+
metadata,
|
|
7303
|
+
due_at: input.due_at,
|
|
7304
|
+
estimated_minutes: input.estimated_minutes,
|
|
7305
|
+
sla_minutes: input.sla_minutes,
|
|
7306
|
+
confidence: input.confidence,
|
|
7307
|
+
retry_count: input.retry_count,
|
|
7308
|
+
max_retries: input.max_retries,
|
|
7309
|
+
retry_after: input.retry_after,
|
|
7310
|
+
requires_approval: input.requires_approval,
|
|
7311
|
+
recurrence_rule: input.recurrence_rule,
|
|
7312
|
+
task_type: input.task_type
|
|
7313
|
+
}, d);
|
|
7314
|
+
return { task, created: false };
|
|
7315
|
+
});
|
|
7316
|
+
return tx();
|
|
7038
7317
|
}
|
|
7039
|
-
function
|
|
7318
|
+
function countTasks(filter = {}, db) {
|
|
7040
7319
|
const d = db || getDatabase();
|
|
7041
|
-
const
|
|
7042
|
-
const
|
|
7043
|
-
if (
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
const blocking = getBlockingDeps(id, d);
|
|
7047
|
-
if (blocking.length > 0) {
|
|
7048
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
7049
|
-
emitLocalEventHooksQuiet({
|
|
7050
|
-
type: "task.blocked",
|
|
7051
|
-
payload: {
|
|
7052
|
-
id,
|
|
7053
|
-
agent_id: agentId,
|
|
7054
|
-
title: task.title,
|
|
7055
|
-
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
7056
|
-
},
|
|
7057
|
-
databasePath
|
|
7058
|
-
});
|
|
7059
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
7060
|
-
}
|
|
7061
|
-
const cutoff = lockExpiryCutoff();
|
|
7062
|
-
const timestamp = now();
|
|
7063
|
-
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 = ?
|
|
7064
|
-
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]);
|
|
7065
|
-
if (result.changes === 0) {
|
|
7066
|
-
const current = getTask(id, d);
|
|
7067
|
-
if (!current)
|
|
7068
|
-
throw new TaskNotFoundError(id);
|
|
7069
|
-
assertStartable(current, agentId);
|
|
7070
|
-
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
7071
|
-
throw new LockError(id, current.locked_by);
|
|
7072
|
-
}
|
|
7073
|
-
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
7320
|
+
const conditions = [];
|
|
7321
|
+
const params = [];
|
|
7322
|
+
if (filter.project_id) {
|
|
7323
|
+
conditions.push("project_id = ?");
|
|
7324
|
+
params.push(filter.project_id);
|
|
7074
7325
|
}
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
7079
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
7080
|
-
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
7081
|
-
return startedTask;
|
|
7082
|
-
}
|
|
7083
|
-
function completeTask(id, agentId, db, options) {
|
|
7084
|
-
const d = db || getDatabase();
|
|
7085
|
-
const databasePath = databasePathFromDatabase(d);
|
|
7086
|
-
const task = getTask(id, d);
|
|
7087
|
-
if (!task)
|
|
7088
|
-
throw new TaskNotFoundError(id);
|
|
7089
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
7090
|
-
throw new LockError(id, task.locked_by);
|
|
7326
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
7327
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
7328
|
+
params.push(...filter.ids);
|
|
7091
7329
|
}
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
completionMeta._completion = { confidence: options.confidence };
|
|
7330
|
+
if (filter.parent_id !== undefined) {
|
|
7331
|
+
if (filter.parent_id === null) {
|
|
7332
|
+
conditions.push("parent_id IS NULL");
|
|
7333
|
+
} else {
|
|
7334
|
+
conditions.push("parent_id = ?");
|
|
7335
|
+
params.push(filter.parent_id);
|
|
7336
|
+
}
|
|
7100
7337
|
}
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
if (metaResult.changes === 0) {
|
|
7109
|
-
const current = getTask(id, d);
|
|
7110
|
-
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
7111
|
-
}
|
|
7338
|
+
if (filter.status) {
|
|
7339
|
+
if (Array.isArray(filter.status)) {
|
|
7340
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
7341
|
+
params.push(...filter.status);
|
|
7342
|
+
} else {
|
|
7343
|
+
conditions.push("status = ?");
|
|
7344
|
+
params.push(filter.status);
|
|
7112
7345
|
}
|
|
7113
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
7114
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
7115
|
-
});
|
|
7116
|
-
tx();
|
|
7117
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
7118
|
-
const completedTaskForEvent = {
|
|
7119
|
-
...task,
|
|
7120
|
-
status: "completed",
|
|
7121
|
-
locked_by: null,
|
|
7122
|
-
locked_at: null,
|
|
7123
|
-
completed_at: timestamp,
|
|
7124
|
-
confidence,
|
|
7125
|
-
version: task.version + 1,
|
|
7126
|
-
updated_at: timestamp,
|
|
7127
|
-
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
7128
|
-
};
|
|
7129
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
7130
|
-
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7131
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
7132
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
7133
|
-
let spawnedTask = null;
|
|
7134
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7135
|
-
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
7136
7346
|
}
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
7347
|
+
if (filter.priority) {
|
|
7348
|
+
if (Array.isArray(filter.priority)) {
|
|
7349
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
7350
|
+
params.push(...filter.priority);
|
|
7142
7351
|
} else {
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
project_id: task.project_id ?? undefined,
|
|
7146
|
-
plan_id: task.plan_id ?? undefined,
|
|
7147
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
7148
|
-
assigned_to: task.assigned_to ?? undefined
|
|
7149
|
-
}, d);
|
|
7150
|
-
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
7151
|
-
spawnedFromTemplate = createTask(input, d);
|
|
7152
|
-
} catch {}
|
|
7352
|
+
conditions.push("priority = ?");
|
|
7353
|
+
params.push(filter.priority);
|
|
7153
7354
|
}
|
|
7154
7355
|
}
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7356
|
+
if (filter.assigned_to) {
|
|
7357
|
+
conditions.push("assigned_to = ?");
|
|
7358
|
+
params.push(filter.assigned_to);
|
|
7158
7359
|
}
|
|
7159
|
-
if (
|
|
7160
|
-
|
|
7360
|
+
if (filter.agent_id) {
|
|
7361
|
+
conditions.push("agent_id = ?");
|
|
7362
|
+
params.push(filter.agent_id);
|
|
7161
7363
|
}
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7364
|
+
if (filter.session_id) {
|
|
7365
|
+
conditions.push("session_id = ?");
|
|
7366
|
+
params.push(filter.session_id);
|
|
7367
|
+
}
|
|
7368
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
7369
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
7370
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
7371
|
+
params.push(...filter.tags);
|
|
7372
|
+
}
|
|
7373
|
+
if (filter.plan_id) {
|
|
7374
|
+
conditions.push("plan_id = ?");
|
|
7375
|
+
params.push(filter.plan_id);
|
|
7376
|
+
}
|
|
7377
|
+
if (filter.task_list_id) {
|
|
7378
|
+
conditions.push("task_list_id = ?");
|
|
7379
|
+
params.push(filter.task_list_id);
|
|
7380
|
+
}
|
|
7381
|
+
if (filter.has_recurrence === true) {
|
|
7382
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
7383
|
+
} else if (filter.has_recurrence === false) {
|
|
7384
|
+
conditions.push("recurrence_rule IS NULL");
|
|
7385
|
+
}
|
|
7386
|
+
if (filter.task_type) {
|
|
7387
|
+
if (Array.isArray(filter.task_type)) {
|
|
7388
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
7389
|
+
params.push(...filter.task_type);
|
|
7390
|
+
} else {
|
|
7391
|
+
conditions.push("task_type = ?");
|
|
7392
|
+
params.push(filter.task_type);
|
|
7179
7393
|
}
|
|
7180
7394
|
}
|
|
7181
|
-
|
|
7395
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
7396
|
+
if (!filter.include_archived) {
|
|
7397
|
+
conditions.push("archived_at IS NULL");
|
|
7398
|
+
}
|
|
7399
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7400
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
7401
|
+
return row.count;
|
|
7182
7402
|
}
|
|
7183
|
-
function
|
|
7403
|
+
function updateTask(id, input, db) {
|
|
7184
7404
|
const d = db || getDatabase();
|
|
7185
7405
|
const task = getTask(id, d);
|
|
7186
7406
|
if (!task)
|
|
7187
7407
|
throw new TaskNotFoundError(id);
|
|
7188
|
-
if (task.
|
|
7189
|
-
|
|
7190
|
-
success: false,
|
|
7191
|
-
error: `Task is ${task.status} and cannot be locked`
|
|
7192
|
-
};
|
|
7193
|
-
}
|
|
7194
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
7195
|
-
const timestamp2 = now();
|
|
7196
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
7197
|
-
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
7198
|
-
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
7408
|
+
if (task.version !== input.version) {
|
|
7409
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
7199
7410
|
}
|
|
7200
|
-
const cutoff = lockExpiryCutoff();
|
|
7201
7411
|
const timestamp = now();
|
|
7202
|
-
const
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7412
|
+
const completionTimestamp = input.completed_at ?? timestamp;
|
|
7413
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
7414
|
+
const params = [timestamp];
|
|
7415
|
+
if (input.title !== undefined) {
|
|
7416
|
+
sets.push("title = ?");
|
|
7417
|
+
params.push(input.title);
|
|
7418
|
+
}
|
|
7419
|
+
if (input.description !== undefined) {
|
|
7420
|
+
sets.push("description = ?");
|
|
7421
|
+
params.push(input.description);
|
|
7422
|
+
}
|
|
7423
|
+
if (input.status !== undefined) {
|
|
7424
|
+
if (input.status === "completed") {
|
|
7425
|
+
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
7213
7426
|
}
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7427
|
+
sets.push("status = ?");
|
|
7428
|
+
params.push(input.status);
|
|
7429
|
+
if (input.status === "completed") {
|
|
7430
|
+
sets.push("completed_at = ?");
|
|
7431
|
+
params.push(completionTimestamp);
|
|
7432
|
+
sets.push("locked_by = NULL");
|
|
7433
|
+
sets.push("locked_at = NULL");
|
|
7434
|
+
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
7435
|
+
sets.push("completed_at = NULL");
|
|
7221
7436
|
}
|
|
7222
|
-
return {
|
|
7223
|
-
success: false,
|
|
7224
|
-
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
7225
|
-
};
|
|
7226
7437
|
}
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
function unlockTask(id, agentId, db) {
|
|
7231
|
-
const d = db || getDatabase();
|
|
7232
|
-
const task = getTask(id, d);
|
|
7233
|
-
if (!task)
|
|
7234
|
-
throw new TaskNotFoundError(id);
|
|
7235
|
-
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
7236
|
-
throw new LockError(id, task.locked_by);
|
|
7438
|
+
if (input.priority !== undefined) {
|
|
7439
|
+
sets.push("priority = ?");
|
|
7440
|
+
params.push(input.priority);
|
|
7237
7441
|
}
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
return true;
|
|
7242
|
-
}
|
|
7243
|
-
function getTaskLockStatus(id, db) {
|
|
7244
|
-
const d = db || getDatabase();
|
|
7245
|
-
const task = getTask(id, d);
|
|
7246
|
-
if (!task)
|
|
7247
|
-
throw new TaskNotFoundError(id);
|
|
7248
|
-
const expired = isLockExpired(task.locked_at);
|
|
7249
|
-
return {
|
|
7250
|
-
task_id: id,
|
|
7251
|
-
locked: !!task.locked_by && !expired,
|
|
7252
|
-
locked_by: task.locked_by,
|
|
7253
|
-
locked_at: task.locked_at,
|
|
7254
|
-
expires_at: lockExpiresAt(task.locked_at),
|
|
7255
|
-
expired
|
|
7256
|
-
};
|
|
7257
|
-
}
|
|
7258
|
-
function claimNextTask(agentId, filters, db) {
|
|
7259
|
-
const d = db || getDatabase();
|
|
7260
|
-
const tx = d.transaction(() => {
|
|
7261
|
-
const task = getNextTask(agentId, filters, d);
|
|
7262
|
-
if (!task)
|
|
7263
|
-
return null;
|
|
7264
|
-
return startTask(task.id, agentId, d);
|
|
7265
|
-
});
|
|
7266
|
-
return tx();
|
|
7267
|
-
}
|
|
7268
|
-
function getNextTask(agentId, filters, db) {
|
|
7269
|
-
const d = db || getDatabase();
|
|
7270
|
-
clearExpiredLocks(d);
|
|
7271
|
-
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
7272
|
-
const params = [lockExpiryCutoff()];
|
|
7273
|
-
if (filters?.project_id) {
|
|
7274
|
-
conditions.push("project_id = ?");
|
|
7275
|
-
params.push(filters.project_id);
|
|
7442
|
+
if (input.project_id !== undefined) {
|
|
7443
|
+
sets.push("project_id = ?");
|
|
7444
|
+
params.push(input.project_id);
|
|
7276
7445
|
}
|
|
7277
|
-
if (
|
|
7278
|
-
|
|
7279
|
-
params.push(
|
|
7446
|
+
if (input.assigned_to !== undefined) {
|
|
7447
|
+
sets.push("assigned_to = ?");
|
|
7448
|
+
params.push(input.assigned_to);
|
|
7280
7449
|
}
|
|
7281
|
-
if (
|
|
7282
|
-
|
|
7283
|
-
params.push(
|
|
7450
|
+
if (input.working_dir !== undefined) {
|
|
7451
|
+
sets.push("working_dir = ?");
|
|
7452
|
+
params.push(input.working_dir);
|
|
7284
7453
|
}
|
|
7285
|
-
if (
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
params.push(...filters.tags);
|
|
7454
|
+
if (input.tags !== undefined) {
|
|
7455
|
+
sets.push("tags = ?");
|
|
7456
|
+
params.push(JSON.stringify(input.tags));
|
|
7289
7457
|
}
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
if (agentId) {
|
|
7294
|
-
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);
|
|
7295
|
-
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
7458
|
+
if (input.metadata !== undefined) {
|
|
7459
|
+
sets.push("metadata = ?");
|
|
7460
|
+
params.push(JSON.stringify(input.metadata));
|
|
7296
7461
|
}
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
params.push(agentId);
|
|
7462
|
+
if (input.plan_id !== undefined) {
|
|
7463
|
+
sets.push("plan_id = ?");
|
|
7464
|
+
params.push(input.plan_id);
|
|
7301
7465
|
}
|
|
7302
|
-
if (
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
params.push(...recentProjectIds);
|
|
7466
|
+
if (input.task_list_id !== undefined) {
|
|
7467
|
+
sets.push("task_list_id = ?");
|
|
7468
|
+
params.push(input.task_list_id);
|
|
7306
7469
|
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
}
|
|
7311
|
-
function getActiveWork(filters, db) {
|
|
7312
|
-
const d = db || getDatabase();
|
|
7313
|
-
clearExpiredLocks(d);
|
|
7314
|
-
const conditions = ["status = 'in_progress'"];
|
|
7315
|
-
const params = [];
|
|
7316
|
-
if (filters?.project_id) {
|
|
7317
|
-
conditions.push("project_id = ?");
|
|
7318
|
-
params.push(filters.project_id);
|
|
7470
|
+
if (input.due_at !== undefined) {
|
|
7471
|
+
sets.push("due_at = ?");
|
|
7472
|
+
params.push(input.due_at);
|
|
7319
7473
|
}
|
|
7320
|
-
if (
|
|
7321
|
-
|
|
7322
|
-
params.push(
|
|
7474
|
+
if (input.estimated_minutes !== undefined) {
|
|
7475
|
+
sets.push("estimated_minutes = ?");
|
|
7476
|
+
params.push(input.estimated_minutes);
|
|
7323
7477
|
}
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
updated_at DESC`).all(...params);
|
|
7328
|
-
return rows;
|
|
7329
|
-
}
|
|
7330
|
-
function getTasksChangedSince(since, filters, db) {
|
|
7331
|
-
const d = db || getDatabase();
|
|
7332
|
-
const conditions = ["updated_at > ?"];
|
|
7333
|
-
const params = [since];
|
|
7334
|
-
if (filters?.project_id) {
|
|
7335
|
-
conditions.push("project_id = ?");
|
|
7336
|
-
params.push(filters.project_id);
|
|
7478
|
+
if (input.sla_minutes !== undefined) {
|
|
7479
|
+
sets.push("sla_minutes = ?");
|
|
7480
|
+
params.push(input.sla_minutes);
|
|
7337
7481
|
}
|
|
7338
|
-
if (
|
|
7339
|
-
|
|
7340
|
-
params.push(
|
|
7482
|
+
if (input.actual_minutes !== undefined) {
|
|
7483
|
+
sets.push("actual_minutes = ?");
|
|
7484
|
+
params.push(input.actual_minutes);
|
|
7341
7485
|
}
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
}
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
if (
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7486
|
+
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
7487
|
+
sets.push("completed_at = ?");
|
|
7488
|
+
params.push(input.completed_at);
|
|
7489
|
+
}
|
|
7490
|
+
if (input.confidence !== undefined) {
|
|
7491
|
+
sets.push("confidence = ?");
|
|
7492
|
+
params.push(input.confidence);
|
|
7493
|
+
}
|
|
7494
|
+
if (input.retry_count !== undefined) {
|
|
7495
|
+
sets.push("retry_count = ?");
|
|
7496
|
+
params.push(input.retry_count);
|
|
7497
|
+
}
|
|
7498
|
+
if (input.max_retries !== undefined) {
|
|
7499
|
+
sets.push("max_retries = ?");
|
|
7500
|
+
params.push(input.max_retries);
|
|
7501
|
+
}
|
|
7502
|
+
if (input.retry_after !== undefined) {
|
|
7503
|
+
sets.push("retry_after = ?");
|
|
7504
|
+
params.push(input.retry_after);
|
|
7505
|
+
}
|
|
7506
|
+
if (input.requires_approval !== undefined) {
|
|
7507
|
+
sets.push("requires_approval = ?");
|
|
7508
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
7509
|
+
}
|
|
7510
|
+
if (input.approved_by !== undefined) {
|
|
7511
|
+
sets.push("approved_by = ?");
|
|
7512
|
+
params.push(input.approved_by);
|
|
7513
|
+
sets.push("approved_at = ?");
|
|
7514
|
+
params.push(now());
|
|
7515
|
+
}
|
|
7516
|
+
if (input.recurrence_rule !== undefined) {
|
|
7517
|
+
sets.push("recurrence_rule = ?");
|
|
7518
|
+
params.push(input.recurrence_rule);
|
|
7519
|
+
}
|
|
7520
|
+
if (input.task_type !== undefined) {
|
|
7521
|
+
sets.push("task_type = ?");
|
|
7522
|
+
params.push(input.task_type ?? null);
|
|
7523
|
+
}
|
|
7524
|
+
params.push(id, input.version);
|
|
7525
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
7526
|
+
if (result.changes === 0) {
|
|
7527
|
+
const current = getTask(id, d);
|
|
7528
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
7529
|
+
}
|
|
7530
|
+
if (input.tags !== undefined) {
|
|
7531
|
+
replaceTaskTags(id, input.tags, d);
|
|
7532
|
+
}
|
|
7533
|
+
const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
|
|
7534
|
+
if (transitionedToCompleted && task.recurrence_rule) {
|
|
7535
|
+
try {
|
|
7536
|
+
const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
|
|
7537
|
+
spawnNextRecurrence2(task, d, completionTimestamp);
|
|
7538
|
+
} catch (e) {
|
|
7539
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7360
7540
|
}
|
|
7361
|
-
}
|
|
7362
|
-
const
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7541
|
+
}
|
|
7542
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
7543
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
7544
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
7545
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
7546
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
7547
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
7548
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
7549
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
7550
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
7551
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
7552
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
7553
|
+
if (input.approved_by !== undefined)
|
|
7554
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
7555
|
+
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
7556
|
+
const completedNow = input.status === "completed";
|
|
7557
|
+
const updatedTask = {
|
|
7366
7558
|
...task,
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
metadata: meta,
|
|
7559
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
7560
|
+
tags: input.tags ?? task.tags,
|
|
7561
|
+
metadata: input.metadata ?? task.metadata,
|
|
7371
7562
|
version: task.version + 1,
|
|
7372
|
-
updated_at: timestamp
|
|
7563
|
+
updated_at: timestamp,
|
|
7564
|
+
locked_by: completedNow ? null : task.locked_by,
|
|
7565
|
+
locked_at: completedNow ? null : task.locked_at,
|
|
7566
|
+
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
7567
|
+
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
7568
|
+
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
7569
|
+
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
7570
|
+
retry_count: input.retry_count ?? task.retry_count,
|
|
7571
|
+
max_retries: input.max_retries ?? task.max_retries,
|
|
7572
|
+
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
7573
|
+
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
7574
|
+
approved_by: input.approved_by ?? task.approved_by,
|
|
7575
|
+
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
7373
7576
|
};
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
if (options?.retry) {
|
|
7381
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
7382
|
-
const maxRetries = task.max_retries || 3;
|
|
7383
|
-
if (retryCount > maxRetries) {
|
|
7384
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
7385
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
7386
|
-
id
|
|
7387
|
-
]);
|
|
7388
|
-
} else {
|
|
7389
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
7390
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
7391
|
-
let title = task.title;
|
|
7392
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
7393
|
-
title = title.slice(task.short_id.length + 2);
|
|
7394
|
-
}
|
|
7395
|
-
retryTask = createTask({
|
|
7396
|
-
title,
|
|
7397
|
-
description: task.description ?? undefined,
|
|
7398
|
-
priority: task.priority,
|
|
7399
|
-
project_id: task.project_id ?? undefined,
|
|
7400
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
7401
|
-
plan_id: task.plan_id ?? undefined,
|
|
7402
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
7403
|
-
tags: task.tags,
|
|
7404
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
7405
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
7406
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
7407
|
-
due_at: retryAfter
|
|
7408
|
-
}, d);
|
|
7409
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
7410
|
-
}
|
|
7577
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7578
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
7579
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
7580
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7581
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
7582
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
7411
7583
|
}
|
|
7412
|
-
|
|
7413
|
-
}
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
7418
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7419
|
-
const conditions = [
|
|
7420
|
-
"status = 'in_progress'",
|
|
7421
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
7422
|
-
];
|
|
7423
|
-
const params = [cutoff, cutoff];
|
|
7424
|
-
if (effectiveFilters?.project_id) {
|
|
7425
|
-
conditions.push("project_id = ?");
|
|
7426
|
-
params.push(effectiveFilters.project_id);
|
|
7584
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
7585
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
7586
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
7587
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
7588
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
7427
7589
|
}
|
|
7428
|
-
if (
|
|
7429
|
-
|
|
7430
|
-
params.push(effectiveFilters.task_list_id);
|
|
7590
|
+
if (input.approved_by !== undefined) {
|
|
7591
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
7431
7592
|
}
|
|
7432
|
-
const
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
}
|
|
7436
|
-
|
|
7437
|
-
const d = db || getDatabase();
|
|
7438
|
-
const databasePath = databasePathFromDatabase(d);
|
|
7439
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
7440
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
7441
|
-
if (staleTasks.length === 0)
|
|
7442
|
-
return null;
|
|
7443
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7444
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
7445
|
-
const target = staleTasks[0];
|
|
7446
|
-
const timestamp = now();
|
|
7447
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7448
|
-
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
7449
|
-
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]);
|
|
7450
|
-
if (result.changes === 0)
|
|
7451
|
-
return null;
|
|
7452
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
7453
|
-
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
7454
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
7455
|
-
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7456
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7457
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
7458
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
7459
|
-
return stolenTask;
|
|
7593
|
+
const updatePayload = taskEventData(updatedTask);
|
|
7594
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
7595
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
7596
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
7597
|
+
return updatedTask;
|
|
7460
7598
|
}
|
|
7461
|
-
function
|
|
7599
|
+
function deleteTask(id, db) {
|
|
7462
7600
|
const d = db || getDatabase();
|
|
7463
|
-
const
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
return tx();
|
|
7475
|
-
}
|
|
7476
|
-
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
7477
|
-
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
7478
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
7479
|
-
let title = completedTask.title;
|
|
7480
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
7481
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
7482
|
-
}
|
|
7483
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
7484
|
-
return createTask({
|
|
7485
|
-
title,
|
|
7486
|
-
description: completedTask.description ?? undefined,
|
|
7487
|
-
priority: completedTask.priority,
|
|
7488
|
-
project_id: completedTask.project_id ?? undefined,
|
|
7489
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
7490
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
7491
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
7492
|
-
tags: completedTask.tags,
|
|
7493
|
-
metadata: completedTask.metadata,
|
|
7494
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
7495
|
-
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
7496
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
7497
|
-
recurrence_parent_id: recurrenceParentId,
|
|
7498
|
-
due_at: dueAt
|
|
7499
|
-
}, db);
|
|
7601
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
7602
|
+
if (!row)
|
|
7603
|
+
return false;
|
|
7604
|
+
recordStorageTombstone({
|
|
7605
|
+
object_type: "tasks",
|
|
7606
|
+
object_id: id,
|
|
7607
|
+
payload: rowToTask(row),
|
|
7608
|
+
version: row.version
|
|
7609
|
+
}, d);
|
|
7610
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
7611
|
+
return result.changes > 0;
|
|
7500
7612
|
}
|
|
7501
|
-
var
|
|
7502
|
-
var init_task_lifecycle = __esm(() => {
|
|
7613
|
+
var init_task_crud = __esm(() => {
|
|
7503
7614
|
init_types();
|
|
7504
7615
|
init_database();
|
|
7505
7616
|
init_completion_guard();
|
|
@@ -7507,11 +7618,9 @@ var init_task_lifecycle = __esm(() => {
|
|
|
7507
7618
|
init_event_hooks();
|
|
7508
7619
|
init_shared_events();
|
|
7509
7620
|
init_audit();
|
|
7510
|
-
init_recurrence();
|
|
7511
7621
|
init_webhooks();
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
init_task_graph();
|
|
7622
|
+
init_checklists();
|
|
7623
|
+
init_storage_tombstones();
|
|
7515
7624
|
});
|
|
7516
7625
|
|
|
7517
7626
|
// src/db/task-status.ts
|
|
@@ -7601,6 +7710,15 @@ function setTaskStatus(id, status, _agentId, db) {
|
|
|
7601
7710
|
throw new TaskNotFoundError(id);
|
|
7602
7711
|
if (task.status === status)
|
|
7603
7712
|
return task;
|
|
7713
|
+
if (status === "completed") {
|
|
7714
|
+
try {
|
|
7715
|
+
return completeTask(id, _agentId, d);
|
|
7716
|
+
} catch (e) {
|
|
7717
|
+
if (e instanceof VersionConflictError && attempt < 2)
|
|
7718
|
+
continue;
|
|
7719
|
+
throw e;
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7604
7722
|
try {
|
|
7605
7723
|
return updateTask(id, { status, version: task.version }, d);
|
|
7606
7724
|
} catch (e) {
|
|
@@ -11557,6 +11675,37 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
11557
11675
|
return fallback;
|
|
11558
11676
|
return Math.min(parsed, max);
|
|
11559
11677
|
}
|
|
11678
|
+
function mapTaskError(e, json2) {
|
|
11679
|
+
if (e instanceof VersionConflictError) {
|
|
11680
|
+
return json2({
|
|
11681
|
+
error: e.message,
|
|
11682
|
+
code: VersionConflictError.code,
|
|
11683
|
+
expected_version: e.expectedVersion,
|
|
11684
|
+
current_version: e.actualVersion
|
|
11685
|
+
}, 409);
|
|
11686
|
+
}
|
|
11687
|
+
if (e instanceof TaskNotFoundError) {
|
|
11688
|
+
return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
|
|
11689
|
+
}
|
|
11690
|
+
if (e instanceof LockError) {
|
|
11691
|
+
return json2({ error: e.message, code: LockError.code }, 409);
|
|
11692
|
+
}
|
|
11693
|
+
if (e instanceof CompletionGuardError) {
|
|
11694
|
+
return json2({
|
|
11695
|
+
error: e.message,
|
|
11696
|
+
code: CompletionGuardError.code,
|
|
11697
|
+
retry_after: e.retryAfterSeconds ?? null
|
|
11698
|
+
}, 409);
|
|
11699
|
+
}
|
|
11700
|
+
if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
|
|
11701
|
+
return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
|
|
11702
|
+
}
|
|
11703
|
+
return null;
|
|
11704
|
+
}
|
|
11705
|
+
function countRecurringTasks() {
|
|
11706
|
+
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();
|
|
11707
|
+
return row?.count ?? 0;
|
|
11708
|
+
}
|
|
11560
11709
|
function handleSseEvents(_req, url, ctx) {
|
|
11561
11710
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
11562
11711
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
@@ -11635,35 +11784,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
11635
11784
|
});
|
|
11636
11785
|
}
|
|
11637
11786
|
function handleHealth(_ctx, json2) {
|
|
11638
|
-
const
|
|
11639
|
-
const
|
|
11640
|
-
const
|
|
11641
|
-
return json2({
|
|
11787
|
+
const stats = getTaskStats();
|
|
11788
|
+
const staleCount = getStaleTasks(30).length;
|
|
11789
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
11790
|
+
return json2({
|
|
11791
|
+
status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
|
|
11792
|
+
tasks: stats.total,
|
|
11793
|
+
stale: staleCount,
|
|
11794
|
+
overdue_recurring: overdueRecurring,
|
|
11795
|
+
timestamp: new Date().toISOString()
|
|
11796
|
+
});
|
|
11642
11797
|
}
|
|
11643
11798
|
function handleHeadlessBoundary(_ctx, json2) {
|
|
11644
11799
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
11645
11800
|
return json2(getHeadlessBoundaryManifest2());
|
|
11646
11801
|
}
|
|
11647
11802
|
function handleStats(_ctx, json2) {
|
|
11648
|
-
const
|
|
11803
|
+
const stats = getTaskStats();
|
|
11804
|
+
const byStatus = stats.by_status;
|
|
11649
11805
|
const projects = listProjects();
|
|
11650
11806
|
const agents = listAgents();
|
|
11651
|
-
const
|
|
11652
|
-
const
|
|
11653
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
11654
|
-
const recurringTasks = all.filter((t) => t.recurrence_rule).length;
|
|
11807
|
+
const staleCount = getStaleTasks(30).length;
|
|
11808
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
11655
11809
|
return json2({
|
|
11656
|
-
total_tasks:
|
|
11657
|
-
pending:
|
|
11658
|
-
in_progress:
|
|
11659
|
-
completed:
|
|
11660
|
-
failed:
|
|
11661
|
-
cancelled:
|
|
11810
|
+
total_tasks: stats.total,
|
|
11811
|
+
pending: byStatus["pending"] ?? 0,
|
|
11812
|
+
in_progress: byStatus["in_progress"] ?? 0,
|
|
11813
|
+
completed: byStatus["completed"] ?? 0,
|
|
11814
|
+
failed: byStatus["failed"] ?? 0,
|
|
11815
|
+
cancelled: byStatus["cancelled"] ?? 0,
|
|
11662
11816
|
projects: projects.length,
|
|
11663
11817
|
agents: agents.length,
|
|
11664
|
-
stale_count:
|
|
11818
|
+
stale_count: staleCount,
|
|
11665
11819
|
overdue_recurring: overdueRecurring,
|
|
11666
|
-
recurring_tasks:
|
|
11820
|
+
recurring_tasks: countRecurringTasks()
|
|
11667
11821
|
});
|
|
11668
11822
|
}
|
|
11669
11823
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
@@ -11742,27 +11896,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
11742
11896
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
11743
11897
|
if (format === "csv") {
|
|
11744
11898
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
11745
|
-
const
|
|
11746
|
-
const val = t[h];
|
|
11899
|
+
const csvCell = (val) => {
|
|
11747
11900
|
if (val === null || val === undefined)
|
|
11748
11901
|
return "";
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11902
|
+
let str = String(val);
|
|
11903
|
+
if (/^[=+\-@\t\r]/.test(str))
|
|
11904
|
+
str = `'${str}`;
|
|
11905
|
+
if (str.includes(",") || str.includes('"') || str.includes(`
|
|
11906
|
+
`) || str.includes("\r")) {
|
|
11907
|
+
str = `"${str.replace(/"/g, '""')}"`;
|
|
11908
|
+
}
|
|
11909
|
+
return str;
|
|
11910
|
+
};
|
|
11911
|
+
const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
|
|
11753
11912
|
const csv = [headers.join(","), ...rows].join(`
|
|
11754
11913
|
`);
|
|
11755
11914
|
return new Response(csv, {
|
|
11756
11915
|
headers: {
|
|
11757
11916
|
"Content-Type": "text/csv",
|
|
11758
|
-
"Content-Disposition": "attachment; filename=tasks.csv"
|
|
11917
|
+
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
11918
|
+
...SECURITY_HEADERS
|
|
11759
11919
|
}
|
|
11760
11920
|
});
|
|
11761
11921
|
}
|
|
11762
11922
|
return new Response(JSON.stringify(summaries, null, 2), {
|
|
11763
11923
|
headers: {
|
|
11764
11924
|
"Content-Type": "application/json",
|
|
11765
|
-
"Content-Disposition": "attachment; filename=tasks.json"
|
|
11925
|
+
"Content-Disposition": "attachment; filename=tasks.json",
|
|
11926
|
+
...SECURITY_HEADERS
|
|
11766
11927
|
}
|
|
11767
11928
|
});
|
|
11768
11929
|
}
|
|
@@ -11936,12 +12097,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
11936
12097
|
if (ALLOWED.has(key))
|
|
11937
12098
|
safeBody[key] = value;
|
|
11938
12099
|
}
|
|
12100
|
+
const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
|
|
11939
12101
|
const updated = updateTask(id, {
|
|
11940
12102
|
...safeBody,
|
|
11941
|
-
version:
|
|
12103
|
+
version: clientVersion
|
|
11942
12104
|
});
|
|
11943
12105
|
return json2(taskToSummary2(updated));
|
|
11944
12106
|
} catch (e) {
|
|
12107
|
+
const mapped = mapTaskError(e, json2);
|
|
12108
|
+
if (mapped)
|
|
12109
|
+
return mapped;
|
|
11945
12110
|
return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
11946
12111
|
}
|
|
11947
12112
|
}
|
|
@@ -11957,6 +12122,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
|
|
|
11957
12122
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
|
|
11958
12123
|
return json2(taskToSummary2(task));
|
|
11959
12124
|
} catch (e) {
|
|
12125
|
+
const mapped = mapTaskError(e, json2);
|
|
12126
|
+
if (mapped)
|
|
12127
|
+
return mapped;
|
|
11960
12128
|
return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
11961
12129
|
}
|
|
11962
12130
|
}
|
|
@@ -11976,6 +12144,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
|
|
|
11976
12144
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
|
|
11977
12145
|
return json2(taskToSummary2(task));
|
|
11978
12146
|
} catch (e) {
|
|
12147
|
+
const mapped = mapTaskError(e, json2);
|
|
12148
|
+
if (mapped)
|
|
12149
|
+
return mapped;
|
|
11979
12150
|
return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
11980
12151
|
}
|
|
11981
12152
|
}
|
|
@@ -12300,6 +12471,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
12300
12471
|
}
|
|
12301
12472
|
var init_routes = __esm(() => {
|
|
12302
12473
|
init_tasks();
|
|
12474
|
+
init_database();
|
|
12475
|
+
init_types();
|
|
12303
12476
|
init_projects();
|
|
12304
12477
|
init_agents();
|
|
12305
12478
|
init_plans();
|
|
@@ -33318,10 +33491,8 @@ var init_token_utils = __esm(() => {
|
|
|
33318
33491
|
"cancel_task",
|
|
33319
33492
|
"check_task_done_contract",
|
|
33320
33493
|
"claim_task",
|
|
33321
|
-
"clone_task",
|
|
33322
33494
|
"delete_task",
|
|
33323
33495
|
"extend_task",
|
|
33324
|
-
"get_active_work",
|
|
33325
33496
|
"get_archived_tasks",
|
|
33326
33497
|
"get_blocked_tasks",
|
|
33327
33498
|
"get_blocking_tasks",
|
|
@@ -33357,7 +33528,8 @@ var init_token_utils = __esm(() => {
|
|
|
33357
33528
|
"task_context",
|
|
33358
33529
|
"unlock_task",
|
|
33359
33530
|
"unarchive_task",
|
|
33360
|
-
"update_task"
|
|
33531
|
+
"update_task",
|
|
33532
|
+
"upsert_task"
|
|
33361
33533
|
],
|
|
33362
33534
|
projects: [
|
|
33363
33535
|
"bootstrap_project",
|
|
@@ -33616,12 +33788,8 @@ var init_token_utils = __esm(() => {
|
|
|
33616
33788
|
"delete_tag",
|
|
33617
33789
|
"get_label",
|
|
33618
33790
|
"get_activity_timeline",
|
|
33619
|
-
"get_recent_activity",
|
|
33620
33791
|
"get_tag",
|
|
33621
33792
|
"get_task_fields",
|
|
33622
|
-
"get_task_graph",
|
|
33623
|
-
"get_task_history",
|
|
33624
|
-
"get_task_stats",
|
|
33625
33793
|
"list_workflow_states",
|
|
33626
33794
|
"list_labels",
|
|
33627
33795
|
"list_tags",
|
|
@@ -33632,6 +33800,10 @@ var init_token_utils = __esm(() => {
|
|
|
33632
33800
|
"describe_tools",
|
|
33633
33801
|
"set_task_workflow_state",
|
|
33634
33802
|
"set_task_fields",
|
|
33803
|
+
"assign_label_to_task",
|
|
33804
|
+
"create_custom_field",
|
|
33805
|
+
"set_task_custom_field",
|
|
33806
|
+
"set_task_priority_meta",
|
|
33635
33807
|
"update_label",
|
|
33636
33808
|
"update_tag"
|
|
33637
33809
|
],
|
|
@@ -33657,7 +33829,6 @@ var init_token_utils = __esm(() => {
|
|
|
33657
33829
|
"update_template",
|
|
33658
33830
|
"write_template_library"
|
|
33659
33831
|
],
|
|
33660
|
-
webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
|
|
33661
33832
|
machines: [
|
|
33662
33833
|
"machines_archive",
|
|
33663
33834
|
"machines_delete",
|
|
@@ -79511,14 +79682,16 @@ function printHelp() {
|
|
|
79511
79682
|
Start the @hasna/todos MCP server.
|
|
79512
79683
|
|
|
79513
79684
|
Options:
|
|
79514
|
-
--stdio Use stdio transport
|
|
79515
|
-
--
|
|
79685
|
+
--stdio Use stdio transport (default)
|
|
79686
|
+
--http Use Streamable HTTP transport
|
|
79687
|
+
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
79516
79688
|
-V, --version output the version number
|
|
79517
79689
|
-h, --help display help for command
|
|
79518
79690
|
|
|
79519
79691
|
Environment:
|
|
79520
|
-
|
|
79521
|
-
|
|
79692
|
+
MCP_STDIO=1 Force stdio transport
|
|
79693
|
+
MCP_HTTP=1 Use Streamable HTTP transport
|
|
79694
|
+
MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
|
|
79522
79695
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
79523
79696
|
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
|
|
79524
79697
|
}
|
|
@@ -79607,8 +79780,22 @@ function formatError2(error2) {
|
|
|
79607
79780
|
function resolveId(partialId, table = "tasks") {
|
|
79608
79781
|
const db = getDatabase();
|
|
79609
79782
|
const id = resolvePartialId(db, table, partialId);
|
|
79610
|
-
if (!id)
|
|
79611
|
-
|
|
79783
|
+
if (!id) {
|
|
79784
|
+
switch (table) {
|
|
79785
|
+
case "tasks":
|
|
79786
|
+
throw new TaskNotFoundError(partialId);
|
|
79787
|
+
case "projects":
|
|
79788
|
+
throw new ProjectNotFoundError(partialId);
|
|
79789
|
+
case "plans":
|
|
79790
|
+
throw new PlanNotFoundError(partialId);
|
|
79791
|
+
case "task_lists":
|
|
79792
|
+
throw new TaskListNotFoundError(partialId);
|
|
79793
|
+
case "agents":
|
|
79794
|
+
throw new AgentNotFoundError(partialId);
|
|
79795
|
+
default:
|
|
79796
|
+
throw new TaskNotFoundError(partialId);
|
|
79797
|
+
}
|
|
79798
|
+
}
|
|
79612
79799
|
return id;
|
|
79613
79800
|
}
|
|
79614
79801
|
function formatTask(task2) {
|
|
@@ -79697,8 +79884,9 @@ function buildServer() {
|
|
|
79697
79884
|
return server;
|
|
79698
79885
|
}
|
|
79699
79886
|
async function main() {
|
|
79700
|
-
const {
|
|
79701
|
-
|
|
79887
|
+
const { isHttpMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
79888
|
+
const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
|
|
79889
|
+
if (!isHttpMode() && !portRequested) {
|
|
79702
79890
|
const server = buildServer();
|
|
79703
79891
|
const transport = new StdioServerTransport;
|
|
79704
79892
|
await server.connect(transport);
|
|
@@ -79876,7 +80064,7 @@ function checkAuth(req, apiKey) {
|
|
|
79876
80064
|
if (!apiKey && !generatedKeysEnabled)
|
|
79877
80065
|
return null;
|
|
79878
80066
|
const provided = getProvidedApiKey(req);
|
|
79879
|
-
const matchesEnvKey = Boolean(apiKey && provided && provided
|
|
80067
|
+
const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
|
|
79880
80068
|
const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
|
|
79881
80069
|
if (!matchesEnvKey && !matchesGeneratedKey) {
|
|
79882
80070
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
@@ -79886,6 +80074,15 @@ function checkAuth(req, apiKey) {
|
|
|
79886
80074
|
}
|
|
79887
80075
|
return null;
|
|
79888
80076
|
}
|
|
80077
|
+
function resolveClientIp(req, server) {
|
|
80078
|
+
const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
|
|
80079
|
+
if (trustProxy) {
|
|
80080
|
+
const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
|
|
80081
|
+
if (forwarded)
|
|
80082
|
+
return forwarded;
|
|
80083
|
+
}
|
|
80084
|
+
return server.requestIP(req)?.address || "unknown";
|
|
80085
|
+
}
|
|
79889
80086
|
function checkRateLimit(ip) {
|
|
79890
80087
|
const now4 = Date.now();
|
|
79891
80088
|
const entry = rateLimitMap.get(ip);
|
|
@@ -80013,7 +80210,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80013
80210
|
const server = Bun.serve({
|
|
80014
80211
|
port,
|
|
80015
80212
|
hostname: hostname4,
|
|
80016
|
-
async fetch(req) {
|
|
80213
|
+
async fetch(req, server2) {
|
|
80017
80214
|
const url = new URL(req.url);
|
|
80018
80215
|
const path = url.pathname;
|
|
80019
80216
|
const method = req.method;
|
|
@@ -80025,15 +80222,6 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80025
80222
|
Vary: "Origin"
|
|
80026
80223
|
} : undefined;
|
|
80027
80224
|
const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
|
|
80028
|
-
if (path === "/health" && method === "GET") {
|
|
80029
|
-
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80030
|
-
return healthResponse2("todos");
|
|
80031
|
-
}
|
|
80032
|
-
if (path === "/mcp") {
|
|
80033
|
-
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80034
|
-
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
|
|
80035
|
-
return handleMcpHttpRequest2(req, buildServer2);
|
|
80036
|
-
}
|
|
80037
80225
|
if (method === "OPTIONS") {
|
|
80038
80226
|
return new Response(null, {
|
|
80039
80227
|
headers: corsHeaders || {
|
|
@@ -80041,7 +80229,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80041
80229
|
}
|
|
80042
80230
|
});
|
|
80043
80231
|
}
|
|
80044
|
-
const ip = req
|
|
80232
|
+
const ip = resolveClientIp(req, server2);
|
|
80045
80233
|
const rl = checkRateLimit(ip);
|
|
80046
80234
|
if (!rl.allowed) {
|
|
80047
80235
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
@@ -80049,6 +80237,18 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80049
80237
|
headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
|
|
80050
80238
|
});
|
|
80051
80239
|
}
|
|
80240
|
+
if (path === "/health" && method === "GET") {
|
|
80241
|
+
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80242
|
+
return healthResponse2("todos");
|
|
80243
|
+
}
|
|
80244
|
+
if (path === "/mcp") {
|
|
80245
|
+
const authError = checkAuth(req, apiKey);
|
|
80246
|
+
if (authError)
|
|
80247
|
+
return authError;
|
|
80248
|
+
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80249
|
+
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
|
|
80250
|
+
return handleMcpHttpRequest2(req, buildServer2);
|
|
80251
|
+
}
|
|
80052
80252
|
if (path.startsWith("/api/")) {
|
|
80053
80253
|
const authError = checkAuth(req, apiKey);
|
|
80054
80254
|
if (authError)
|