@hasna/todos 0.11.71 → 0.11.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2162 -1450
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +6526 -6258
- package/dist/db/api-keys.d.ts +9 -0
- package/dist/db/api-keys.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +1 -0
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-status.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10121 -9282
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/lib/task-route-contract.d.ts +2 -1
- package/dist/lib/task-route-contract.d.ts.map +1 -1
- package/dist/lib/task-route-sources.d.ts +68 -0
- package/dist/lib/task-route-sources.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1869 -1650
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp.js +6 -8
- package/dist/registry.js +6526 -6258
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +6 -4
- package/dist/server/index.js +1871 -1652
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +3324 -3029
- package/package.json +1 -1
package/dist/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());
|
|
@@ -5060,8 +5080,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
5060
5080
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
5061
5081
|
if (explicit !== undefined)
|
|
5062
5082
|
return explicit;
|
|
5063
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
5064
|
-
return true;
|
|
5065
5083
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
5066
5084
|
if (taskListDefault !== undefined)
|
|
5067
5085
|
return taskListDefault;
|
|
@@ -5080,8 +5098,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
5080
5098
|
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
5081
5099
|
};
|
|
5082
5100
|
}
|
|
5083
|
-
function
|
|
5084
|
-
|
|
5101
|
+
function metadataStringField(record, keys) {
|
|
5102
|
+
if (!record)
|
|
5103
|
+
return;
|
|
5104
|
+
for (const key of keys) {
|
|
5105
|
+
const value = record[key];
|
|
5106
|
+
if (typeof value === "string" && value.trim())
|
|
5107
|
+
return value.trim();
|
|
5108
|
+
}
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
function projectKindFromMetadata(...records) {
|
|
5112
|
+
for (const record of records) {
|
|
5113
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
5114
|
+
if (value)
|
|
5115
|
+
return value;
|
|
5116
|
+
}
|
|
5117
|
+
return null;
|
|
5118
|
+
}
|
|
5119
|
+
function classifyProjectKind(_path, metadata) {
|
|
5120
|
+
return projectKindFromMetadata(metadata);
|
|
5085
5121
|
}
|
|
5086
5122
|
function isWorktreePath(path) {
|
|
5087
5123
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -5154,7 +5190,6 @@ function taskEventMetadata(task) {
|
|
|
5154
5190
|
metadata.project_canonical_path = projectPath;
|
|
5155
5191
|
}
|
|
5156
5192
|
if (projectPath) {
|
|
5157
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
5158
5193
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
5159
5194
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
5160
5195
|
}
|
|
@@ -5166,6 +5201,10 @@ function taskEventMetadata(task) {
|
|
|
5166
5201
|
metadata.task_list_project_id = taskList.project_id;
|
|
5167
5202
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
5168
5203
|
}
|
|
5204
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
5205
|
+
if (projectKind) {
|
|
5206
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
5207
|
+
}
|
|
5169
5208
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
5170
5209
|
if (routeEnabled !== undefined) {
|
|
5171
5210
|
metadata.route_enabled = routeEnabled;
|
|
@@ -5720,1767 +5759,1858 @@ var init_checklists = __esm(() => {
|
|
|
5720
5759
|
init_database();
|
|
5721
5760
|
});
|
|
5722
5761
|
|
|
5723
|
-
// src/
|
|
5724
|
-
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) {
|
|
5725
5882
|
return {
|
|
5726
5883
|
...row,
|
|
5727
5884
|
tags: JSON.parse(row.tags || "[]"),
|
|
5885
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
5728
5886
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
requires_approval: !!row.requires_approval
|
|
5887
|
+
priority: row.priority || "medium",
|
|
5888
|
+
version: row.version ?? 1
|
|
5732
5889
|
};
|
|
5733
5890
|
}
|
|
5734
|
-
function
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5745
|
-
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
|
+
};
|
|
5746
5901
|
}
|
|
5747
|
-
function
|
|
5748
|
-
|
|
5749
|
-
return;
|
|
5750
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
5751
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5752
|
-
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5753
|
-
}
|
|
5754
|
-
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5755
|
-
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5756
|
-
}
|
|
5902
|
+
function resolveTemplateId(id, d) {
|
|
5903
|
+
return resolvePartialId(d, "task_templates", id);
|
|
5757
5904
|
}
|
|
5758
|
-
function
|
|
5905
|
+
function createTemplate(input, db) {
|
|
5759
5906
|
const d = db || getDatabase();
|
|
5760
|
-
const
|
|
5761
|
-
const tags = input.tags || [];
|
|
5907
|
+
const id = uuid();
|
|
5762
5908
|
const machineId = currentStorageMachineId(d);
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
input.priority || "medium",
|
|
5781
|
-
input.agent_id || null,
|
|
5782
|
-
input.assigned_to || null,
|
|
5783
|
-
input.session_id || null,
|
|
5784
|
-
input.working_dir || null,
|
|
5785
|
-
JSON.stringify(tags),
|
|
5786
|
-
JSON.stringify(input.metadata || {}),
|
|
5787
|
-
timestamp,
|
|
5788
|
-
timestamp,
|
|
5789
|
-
input.due_at || null,
|
|
5790
|
-
input.estimated_minutes || null,
|
|
5791
|
-
input.sla_minutes ?? null,
|
|
5792
|
-
input.confidence ?? null,
|
|
5793
|
-
input.retry_count ?? 0,
|
|
5794
|
-
input.max_retries ?? 3,
|
|
5795
|
-
input.retry_after ?? null,
|
|
5796
|
-
input.requires_approval ? 1 : 0,
|
|
5797
|
-
null,
|
|
5798
|
-
null,
|
|
5799
|
-
input.recurrence_rule || null,
|
|
5800
|
-
input.recurrence_parent_id || null,
|
|
5801
|
-
input.spawns_template_id || null,
|
|
5802
|
-
input.reason || null,
|
|
5803
|
-
input.spawned_from_session || null,
|
|
5804
|
-
assignedBy || null,
|
|
5805
|
-
assignedFromProject || null,
|
|
5806
|
-
input.task_type || null,
|
|
5807
|
-
machineId
|
|
5808
|
-
]);
|
|
5809
|
-
break;
|
|
5810
|
-
} catch (e) {
|
|
5811
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
5812
|
-
id = uuid();
|
|
5813
|
-
continue;
|
|
5814
|
-
}
|
|
5815
|
-
throw e;
|
|
5816
|
-
}
|
|
5817
|
-
}
|
|
5818
|
-
if (tags.length > 0) {
|
|
5819
|
-
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);
|
|
5820
5926
|
}
|
|
5821
|
-
|
|
5822
|
-
const payload = taskEventData(task);
|
|
5823
|
-
const databasePath = databasePathFromDatabase(d);
|
|
5824
|
-
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5825
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
5826
|
-
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
5827
|
-
return task;
|
|
5927
|
+
return getTemplate(id, d);
|
|
5828
5928
|
}
|
|
5829
|
-
function
|
|
5929
|
+
function getTemplate(id, db) {
|
|
5830
5930
|
const d = db || getDatabase();
|
|
5831
|
-
const
|
|
5832
|
-
if (!
|
|
5931
|
+
const resolved = resolveTemplateId(id, d);
|
|
5932
|
+
if (!resolved)
|
|
5833
5933
|
return null;
|
|
5834
|
-
|
|
5934
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
5935
|
+
return row ? rowToTemplate(row) : null;
|
|
5835
5936
|
}
|
|
5836
|
-
function
|
|
5937
|
+
function listTemplates(db) {
|
|
5837
5938
|
const d = db || getDatabase();
|
|
5838
|
-
|
|
5839
|
-
if (!task)
|
|
5840
|
-
return null;
|
|
5841
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
5842
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
5843
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
5844
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
5845
|
-
WHERE td.task_id = ?`).all(id);
|
|
5846
|
-
const dependencies = depRows.map(rowToTask);
|
|
5847
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
5848
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
5849
|
-
WHERE td.depends_on = ?`).all(id);
|
|
5850
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
5851
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
5852
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
5853
|
-
const checklist = getChecklist(id, d);
|
|
5854
|
-
return {
|
|
5855
|
-
...task,
|
|
5856
|
-
subtasks,
|
|
5857
|
-
dependencies,
|
|
5858
|
-
blocked_by,
|
|
5859
|
-
comments,
|
|
5860
|
-
parent,
|
|
5861
|
-
checklist
|
|
5862
|
-
};
|
|
5939
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
5863
5940
|
}
|
|
5864
|
-
function
|
|
5941
|
+
function deleteTemplate(id, db) {
|
|
5865
5942
|
const d = db || getDatabase();
|
|
5866
|
-
const
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
const
|
|
5870
|
-
if (
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
}
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
if (
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
conditions.push("priority = ?");
|
|
5901
|
-
params.push(filter.priority);
|
|
5902
|
-
}
|
|
5903
|
-
}
|
|
5904
|
-
if (filter.assigned_to) {
|
|
5905
|
-
conditions.push("assigned_to = ?");
|
|
5906
|
-
params.push(filter.assigned_to);
|
|
5907
|
-
}
|
|
5908
|
-
if (filter.agent_id) {
|
|
5909
|
-
conditions.push("agent_id = ?");
|
|
5910
|
-
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()]);
|
|
5911
5977
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5978
|
+
const sets = ["version = version + 1"];
|
|
5979
|
+
const values = [];
|
|
5980
|
+
if (updates.name !== undefined) {
|
|
5981
|
+
sets.push("name = ?");
|
|
5982
|
+
values.push(updates.name);
|
|
5915
5983
|
}
|
|
5916
|
-
if (
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
params.push(...filter.tags);
|
|
5984
|
+
if (updates.title_pattern !== undefined) {
|
|
5985
|
+
sets.push("title_pattern = ?");
|
|
5986
|
+
values.push(updates.title_pattern);
|
|
5920
5987
|
}
|
|
5921
|
-
if (
|
|
5922
|
-
|
|
5923
|
-
|
|
5988
|
+
if (updates.description !== undefined) {
|
|
5989
|
+
sets.push("description = ?");
|
|
5990
|
+
values.push(updates.description);
|
|
5924
5991
|
}
|
|
5925
|
-
if (
|
|
5926
|
-
|
|
5927
|
-
|
|
5992
|
+
if (updates.priority !== undefined) {
|
|
5993
|
+
sets.push("priority = ?");
|
|
5994
|
+
values.push(updates.priority);
|
|
5928
5995
|
}
|
|
5929
|
-
if (
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
conditions.push("recurrence_rule IS NULL");
|
|
5996
|
+
if (updates.tags !== undefined) {
|
|
5997
|
+
sets.push("tags = ?");
|
|
5998
|
+
values.push(JSON.stringify(updates.tags));
|
|
5933
5999
|
}
|
|
5934
|
-
if (
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
params.push(...filter.task_type);
|
|
5938
|
-
} else {
|
|
5939
|
-
conditions.push("task_type = ?");
|
|
5940
|
-
params.push(filter.task_type);
|
|
5941
|
-
}
|
|
6000
|
+
if (updates.variables !== undefined) {
|
|
6001
|
+
sets.push("variables = ?");
|
|
6002
|
+
values.push(JSON.stringify(updates.variables));
|
|
5942
6003
|
}
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
try {
|
|
5947
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
5948
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
5949
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
5950
|
-
} catch {}
|
|
6004
|
+
if (updates.project_id !== undefined) {
|
|
6005
|
+
sets.push("project_id = ?");
|
|
6006
|
+
values.push(updates.project_id);
|
|
5951
6007
|
}
|
|
5952
|
-
if (
|
|
5953
|
-
|
|
6008
|
+
if (updates.plan_id !== undefined) {
|
|
6009
|
+
sets.push("plan_id = ?");
|
|
6010
|
+
values.push(updates.plan_id);
|
|
5954
6011
|
}
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
limitClause = " LIMIT ?";
|
|
5959
|
-
params.push(filter.limit);
|
|
5960
|
-
if (!filter.cursor && filter.offset) {
|
|
5961
|
-
limitClause += " OFFSET ?";
|
|
5962
|
-
params.push(filter.offset);
|
|
5963
|
-
}
|
|
6012
|
+
if (updates.metadata !== undefined) {
|
|
6013
|
+
sets.push("metadata = ?");
|
|
6014
|
+
values.push(JSON.stringify(updates.metadata));
|
|
5964
6015
|
}
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
function getTaskByFingerprint(fingerprint, db) {
|
|
5969
|
-
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5970
|
-
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);
|
|
5971
6019
|
}
|
|
5972
|
-
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));
|
|
5973
6025
|
return {
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
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
|
|
5977
6034
|
};
|
|
5978
6035
|
}
|
|
5979
|
-
function
|
|
5980
|
-
const d = db || getDatabase();
|
|
5981
|
-
const fingerprint = input.fingerprint.trim();
|
|
5982
|
-
if (!fingerprint)
|
|
5983
|
-
throw new Error("fingerprint is required");
|
|
5984
|
-
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5985
|
-
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5986
|
-
if (!existing) {
|
|
5987
|
-
const task2 = createTask({ ...input, metadata }, d);
|
|
5988
|
-
return { task: task2, created: true };
|
|
5989
|
-
}
|
|
5990
|
-
const task = updateTask(existing.id, {
|
|
5991
|
-
version: existing.version,
|
|
5992
|
-
title: input.title,
|
|
5993
|
-
description: input.description,
|
|
5994
|
-
status: input.status,
|
|
5995
|
-
priority: input.priority,
|
|
5996
|
-
project_id: input.project_id,
|
|
5997
|
-
assigned_to: input.assigned_to,
|
|
5998
|
-
working_dir: input.working_dir,
|
|
5999
|
-
plan_id: input.plan_id,
|
|
6000
|
-
task_list_id: input.task_list_id,
|
|
6001
|
-
tags: input.tags,
|
|
6002
|
-
metadata,
|
|
6003
|
-
due_at: input.due_at,
|
|
6004
|
-
estimated_minutes: input.estimated_minutes,
|
|
6005
|
-
sla_minutes: input.sla_minutes,
|
|
6006
|
-
confidence: input.confidence,
|
|
6007
|
-
retry_count: input.retry_count,
|
|
6008
|
-
max_retries: input.max_retries,
|
|
6009
|
-
retry_after: input.retry_after,
|
|
6010
|
-
requires_approval: input.requires_approval,
|
|
6011
|
-
recurrence_rule: input.recurrence_rule,
|
|
6012
|
-
task_type: input.task_type
|
|
6013
|
-
}, d);
|
|
6014
|
-
return { task, created: false };
|
|
6015
|
-
}
|
|
6016
|
-
function countTasks(filter = {}, db) {
|
|
6036
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
6017
6037
|
const d = db || getDatabase();
|
|
6018
|
-
const
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
if (filter.priority) {
|
|
6046
|
-
if (Array.isArray(filter.priority)) {
|
|
6047
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
6048
|
-
params.push(...filter.priority);
|
|
6049
|
-
} else {
|
|
6050
|
-
conditions.push("priority = ?");
|
|
6051
|
-
params.push(filter.priority);
|
|
6052
|
-
}
|
|
6053
|
-
}
|
|
6054
|
-
if (filter.assigned_to) {
|
|
6055
|
-
conditions.push("assigned_to = ?");
|
|
6056
|
-
params.push(filter.assigned_to);
|
|
6057
|
-
}
|
|
6058
|
-
if (filter.agent_id) {
|
|
6059
|
-
conditions.push("agent_id = ?");
|
|
6060
|
-
params.push(filter.agent_id);
|
|
6061
|
-
}
|
|
6062
|
-
if (filter.session_id) {
|
|
6063
|
-
conditions.push("session_id = ?");
|
|
6064
|
-
params.push(filter.session_id);
|
|
6065
|
-
}
|
|
6066
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
6067
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
6068
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
6069
|
-
params.push(...filter.tags);
|
|
6070
|
-
}
|
|
6071
|
-
if (filter.plan_id) {
|
|
6072
|
-
conditions.push("plan_id = ?");
|
|
6073
|
-
params.push(filter.plan_id);
|
|
6074
|
-
}
|
|
6075
|
-
if (filter.task_list_id) {
|
|
6076
|
-
conditions.push("task_list_id = ?");
|
|
6077
|
-
params.push(filter.task_list_id);
|
|
6078
|
-
}
|
|
6079
|
-
addMetadataConditions(filter.metadata, conditions, params);
|
|
6080
|
-
if (!filter.include_archived) {
|
|
6081
|
-
conditions.push("archived_at IS NULL");
|
|
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));
|
|
6082
6065
|
}
|
|
6083
|
-
|
|
6084
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
6085
|
-
return row.count;
|
|
6066
|
+
return results;
|
|
6086
6067
|
}
|
|
6087
|
-
function
|
|
6068
|
+
function getTemplateWithTasks(id, db) {
|
|
6088
6069
|
const d = db || getDatabase();
|
|
6089
|
-
const
|
|
6090
|
-
if (!
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
}
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
const
|
|
6098
|
-
const
|
|
6099
|
-
if (
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
if (input.status === "completed") {
|
|
6114
|
-
sets.push("completed_at = ?");
|
|
6115
|
-
params.push(completionTimestamp);
|
|
6116
|
-
}
|
|
6117
|
-
}
|
|
6118
|
-
if (input.priority !== undefined) {
|
|
6119
|
-
sets.push("priority = ?");
|
|
6120
|
-
params.push(input.priority);
|
|
6121
|
-
}
|
|
6122
|
-
if (input.project_id !== undefined) {
|
|
6123
|
-
sets.push("project_id = ?");
|
|
6124
|
-
params.push(input.project_id);
|
|
6125
|
-
}
|
|
6126
|
-
if (input.assigned_to !== undefined) {
|
|
6127
|
-
sets.push("assigned_to = ?");
|
|
6128
|
-
params.push(input.assigned_to);
|
|
6129
|
-
}
|
|
6130
|
-
if (input.working_dir !== undefined) {
|
|
6131
|
-
sets.push("working_dir = ?");
|
|
6132
|
-
params.push(input.working_dir);
|
|
6133
|
-
}
|
|
6134
|
-
if (input.tags !== undefined) {
|
|
6135
|
-
sets.push("tags = ?");
|
|
6136
|
-
params.push(JSON.stringify(input.tags));
|
|
6137
|
-
}
|
|
6138
|
-
if (input.metadata !== undefined) {
|
|
6139
|
-
sets.push("metadata = ?");
|
|
6140
|
-
params.push(JSON.stringify(input.metadata));
|
|
6141
|
-
}
|
|
6142
|
-
if (input.plan_id !== undefined) {
|
|
6143
|
-
sets.push("plan_id = ?");
|
|
6144
|
-
params.push(input.plan_id);
|
|
6145
|
-
}
|
|
6146
|
-
if (input.task_list_id !== undefined) {
|
|
6147
|
-
sets.push("task_list_id = ?");
|
|
6148
|
-
params.push(input.task_list_id);
|
|
6149
|
-
}
|
|
6150
|
-
if (input.due_at !== undefined) {
|
|
6151
|
-
sets.push("due_at = ?");
|
|
6152
|
-
params.push(input.due_at);
|
|
6153
|
-
}
|
|
6154
|
-
if (input.estimated_minutes !== undefined) {
|
|
6155
|
-
sets.push("estimated_minutes = ?");
|
|
6156
|
-
params.push(input.estimated_minutes);
|
|
6157
|
-
}
|
|
6158
|
-
if (input.sla_minutes !== undefined) {
|
|
6159
|
-
sets.push("sla_minutes = ?");
|
|
6160
|
-
params.push(input.sla_minutes);
|
|
6161
|
-
}
|
|
6162
|
-
if (input.actual_minutes !== undefined) {
|
|
6163
|
-
sets.push("actual_minutes = ?");
|
|
6164
|
-
params.push(input.actual_minutes);
|
|
6165
|
-
}
|
|
6166
|
-
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
6167
|
-
sets.push("completed_at = ?");
|
|
6168
|
-
params.push(input.completed_at);
|
|
6169
|
-
}
|
|
6170
|
-
if (input.confidence !== undefined) {
|
|
6171
|
-
sets.push("confidence = ?");
|
|
6172
|
-
params.push(input.confidence);
|
|
6173
|
-
}
|
|
6174
|
-
if (input.retry_count !== undefined) {
|
|
6175
|
-
sets.push("retry_count = ?");
|
|
6176
|
-
params.push(input.retry_count);
|
|
6177
|
-
}
|
|
6178
|
-
if (input.max_retries !== undefined) {
|
|
6179
|
-
sets.push("max_retries = ?");
|
|
6180
|
-
params.push(input.max_retries);
|
|
6181
|
-
}
|
|
6182
|
-
if (input.retry_after !== undefined) {
|
|
6183
|
-
sets.push("retry_after = ?");
|
|
6184
|
-
params.push(input.retry_after);
|
|
6185
|
-
}
|
|
6186
|
-
if (input.requires_approval !== undefined) {
|
|
6187
|
-
sets.push("requires_approval = ?");
|
|
6188
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
6189
|
-
}
|
|
6190
|
-
if (input.approved_by !== undefined) {
|
|
6191
|
-
sets.push("approved_by = ?");
|
|
6192
|
-
params.push(input.approved_by);
|
|
6193
|
-
sets.push("approved_at = ?");
|
|
6194
|
-
params.push(now());
|
|
6195
|
-
}
|
|
6196
|
-
if (input.recurrence_rule !== undefined) {
|
|
6197
|
-
sets.push("recurrence_rule = ?");
|
|
6198
|
-
params.push(input.recurrence_rule);
|
|
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;
|
|
6199
6094
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
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;
|
|
6203
6100
|
}
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
const
|
|
6208
|
-
|
|
6101
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
6102
|
+
if (falsyMatch) {
|
|
6103
|
+
const varName = falsyMatch[1];
|
|
6104
|
+
const val = variables[varName];
|
|
6105
|
+
return !val || val === "" || val === "false";
|
|
6209
6106
|
}
|
|
6210
|
-
|
|
6211
|
-
|
|
6107
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
6108
|
+
if (truthyMatch) {
|
|
6109
|
+
const varName = truthyMatch[1];
|
|
6110
|
+
const val = variables[varName];
|
|
6111
|
+
return !!val && val !== "" && val !== "false";
|
|
6212
6112
|
}
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
if (
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
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
|
+
}))
|
|
6243
6142
|
};
|
|
6244
|
-
const databasePath = databasePathFromDatabase(d);
|
|
6245
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
6246
|
-
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
6247
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6248
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
6249
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
6250
|
-
}
|
|
6251
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
6252
|
-
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
6253
|
-
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
6254
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
6255
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
6256
|
-
}
|
|
6257
|
-
if (input.approved_by !== undefined) {
|
|
6258
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
6259
|
-
}
|
|
6260
|
-
const updatePayload = taskEventData(updatedTask);
|
|
6261
|
-
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
6262
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
6263
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
6264
|
-
return updatedTask;
|
|
6265
6143
|
}
|
|
6266
|
-
function
|
|
6144
|
+
function importTemplate(json, db) {
|
|
6267
6145
|
const d = db || getDatabase();
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
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
|
|
6276
6168
|
}, d);
|
|
6277
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
6278
|
-
return result.changes > 0;
|
|
6279
6169
|
}
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
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;
|
|
6190
|
+
}
|
|
6298
6191
|
}
|
|
6299
|
-
|
|
6300
|
-
|
|
6192
|
+
const missing = [];
|
|
6193
|
+
for (const v of templateVars) {
|
|
6194
|
+
if (v.required && merged[v.name] === undefined) {
|
|
6195
|
+
missing.push(v.name);
|
|
6196
|
+
}
|
|
6301
6197
|
}
|
|
6302
|
-
if (
|
|
6303
|
-
|
|
6198
|
+
if (missing.length > 0) {
|
|
6199
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
6304
6200
|
}
|
|
6305
|
-
|
|
6306
|
-
|
|
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);
|
|
6307
6207
|
}
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
};
|
|
6208
|
+
return result;
|
|
6209
|
+
}
|
|
6210
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
6211
|
+
const d = db || getDatabase();
|
|
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}`);
|
|
6315
6218
|
}
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
const
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
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];
|
|
6225
|
+
}
|
|
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);
|
|
6324
6237
|
}
|
|
6238
|
+
continue;
|
|
6325
6239
|
}
|
|
6326
|
-
if (
|
|
6327
|
-
|
|
6240
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
6241
|
+
skippedPositions.add(tt.position);
|
|
6242
|
+
continue;
|
|
6328
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);
|
|
6329
6261
|
}
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
const next = new Date(base);
|
|
6345
|
-
if (parsed.unit === "day") {
|
|
6346
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
6347
|
-
} else if (parsed.unit === "week") {
|
|
6348
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
6349
|
-
} else if (parsed.unit === "month") {
|
|
6350
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
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
|
+
}
|
|
6351
6276
|
}
|
|
6352
|
-
return next.toISOString();
|
|
6353
6277
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
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
|
+
});
|
|
6364
6310
|
}
|
|
6365
|
-
const next = new Date(base);
|
|
6366
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
6367
|
-
return next.toISOString();
|
|
6368
6311
|
}
|
|
6369
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
6370
|
-
}
|
|
6371
|
-
var DAY_NAMES;
|
|
6372
|
-
var init_recurrence = __esm(() => {
|
|
6373
|
-
DAY_NAMES = {
|
|
6374
|
-
sunday: 0,
|
|
6375
|
-
sun: 0,
|
|
6376
|
-
monday: 1,
|
|
6377
|
-
mon: 1,
|
|
6378
|
-
tuesday: 2,
|
|
6379
|
-
tue: 2,
|
|
6380
|
-
wednesday: 3,
|
|
6381
|
-
wed: 3,
|
|
6382
|
-
thursday: 4,
|
|
6383
|
-
thu: 4,
|
|
6384
|
-
friday: 5,
|
|
6385
|
-
fri: 5,
|
|
6386
|
-
saturday: 6,
|
|
6387
|
-
sat: 6
|
|
6388
|
-
};
|
|
6389
|
-
});
|
|
6390
|
-
|
|
6391
|
-
// src/db/templates.ts
|
|
6392
|
-
var exports_templates = {};
|
|
6393
|
-
__export(exports_templates, {
|
|
6394
|
-
updateTemplate: () => updateTemplate,
|
|
6395
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
6396
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
6397
|
-
resolveVariables: () => resolveVariables,
|
|
6398
|
-
previewTemplate: () => previewTemplate,
|
|
6399
|
-
listTemplates: () => listTemplates,
|
|
6400
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
6401
|
-
importTemplate: () => importTemplate,
|
|
6402
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
6403
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
6404
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
6405
|
-
getTemplate: () => getTemplate,
|
|
6406
|
-
exportTemplate: () => exportTemplate,
|
|
6407
|
-
evaluateCondition: () => evaluateCondition,
|
|
6408
|
-
deleteTemplate: () => deleteTemplate,
|
|
6409
|
-
createTemplate: () => createTemplate,
|
|
6410
|
-
addTemplateTasks: () => addTemplateTasks
|
|
6411
|
-
});
|
|
6412
|
-
function rowToTemplate(row) {
|
|
6413
|
-
return {
|
|
6414
|
-
...row,
|
|
6415
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
6416
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
6417
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
6418
|
-
priority: row.priority || "medium",
|
|
6419
|
-
version: row.version ?? 1
|
|
6420
|
-
};
|
|
6421
|
-
}
|
|
6422
|
-
function rowToTemplateTask(row) {
|
|
6423
6312
|
return {
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
include_template_id: row.include_template_id ?? null
|
|
6313
|
+
template_id: template.id,
|
|
6314
|
+
template_name: template.name,
|
|
6315
|
+
description: template.description,
|
|
6316
|
+
variables: template.variables,
|
|
6317
|
+
resolved_variables: resolved,
|
|
6318
|
+
tasks
|
|
6431
6319
|
};
|
|
6432
6320
|
}
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
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) {
|
|
6437
6329
|
const d = db || getDatabase();
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
input.title_pattern,
|
|
6445
|
-
input.description || null,
|
|
6446
|
-
input.priority || "medium",
|
|
6447
|
-
JSON.stringify(input.tags || []),
|
|
6448
|
-
JSON.stringify(input.variables || []),
|
|
6449
|
-
input.project_id || null,
|
|
6450
|
-
input.plan_id || null,
|
|
6451
|
-
JSON.stringify(input.metadata || {}),
|
|
6452
|
-
now(),
|
|
6453
|
-
machineId
|
|
6454
|
-
]);
|
|
6455
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
6456
|
-
addTemplateTasks(id, input.tasks, d);
|
|
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);
|
|
6457
6336
|
}
|
|
6458
|
-
|
|
6337
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
6459
6338
|
}
|
|
6460
|
-
function
|
|
6339
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
6461
6340
|
const d = db || getDatabase();
|
|
6462
|
-
const
|
|
6463
|
-
|
|
6464
|
-
return null;
|
|
6465
|
-
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
6466
|
-
return row ? rowToTemplate(row) : null;
|
|
6341
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
6342
|
+
return result.changes > 0;
|
|
6467
6343
|
}
|
|
6468
|
-
function
|
|
6344
|
+
function getTaskDependencies(taskId, db) {
|
|
6469
6345
|
const d = db || getDatabase();
|
|
6470
|
-
return d.query("SELECT * FROM
|
|
6346
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
6471
6347
|
}
|
|
6472
|
-
function
|
|
6348
|
+
function getTaskDependents(taskId, db) {
|
|
6473
6349
|
const d = db || getDatabase();
|
|
6474
|
-
|
|
6475
|
-
if (!resolved)
|
|
6476
|
-
return false;
|
|
6477
|
-
const template = getTemplate(resolved, d);
|
|
6478
|
-
if (!template)
|
|
6479
|
-
return false;
|
|
6480
|
-
recordStorageTombstone({
|
|
6481
|
-
object_type: "templates",
|
|
6482
|
-
object_id: resolved,
|
|
6483
|
-
payload: template,
|
|
6484
|
-
version: template.version
|
|
6485
|
-
}, d);
|
|
6486
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
6350
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
6487
6351
|
}
|
|
6488
|
-
function
|
|
6352
|
+
function cloneTask(taskId, overrides, db) {
|
|
6489
6353
|
const d = db || getDatabase();
|
|
6490
|
-
const
|
|
6491
|
-
if (!
|
|
6492
|
-
|
|
6493
|
-
const
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
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";
|
|
6506
6385
|
});
|
|
6507
|
-
|
|
6508
|
-
}
|
|
6509
|
-
const sets = ["version = version + 1"];
|
|
6510
|
-
const values = [];
|
|
6511
|
-
if (updates.name !== undefined) {
|
|
6512
|
-
sets.push("name = ?");
|
|
6513
|
-
values.push(updates.name);
|
|
6514
|
-
}
|
|
6515
|
-
if (updates.title_pattern !== undefined) {
|
|
6516
|
-
sets.push("title_pattern = ?");
|
|
6517
|
-
values.push(updates.title_pattern);
|
|
6518
|
-
}
|
|
6519
|
-
if (updates.description !== undefined) {
|
|
6520
|
-
sets.push("description = ?");
|
|
6521
|
-
values.push(updates.description);
|
|
6386
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
6522
6387
|
}
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
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);
|
|
6526
6399
|
}
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
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);
|
|
6530
6411
|
}
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
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) {
|
|
6425
|
+
sets.push("task_list_id = ?");
|
|
6426
|
+
params.push(target.task_list_id);
|
|
6534
6427
|
}
|
|
6535
|
-
if (
|
|
6428
|
+
if (target.project_id !== undefined) {
|
|
6536
6429
|
sets.push("project_id = ?");
|
|
6537
|
-
|
|
6430
|
+
params.push(target.project_id);
|
|
6538
6431
|
}
|
|
6539
|
-
if (
|
|
6432
|
+
if (target.plan_id !== undefined) {
|
|
6540
6433
|
sets.push("plan_id = ?");
|
|
6541
|
-
|
|
6434
|
+
params.push(target.plan_id);
|
|
6542
6435
|
}
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
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
|
+
}
|
|
6546
6454
|
}
|
|
6547
|
-
|
|
6548
|
-
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
6549
|
-
return getTemplate(resolved, d);
|
|
6455
|
+
return false;
|
|
6550
6456
|
}
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
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();
|
|
6566
6486
|
}
|
|
6567
|
-
function
|
|
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) {
|
|
6568
6495
|
const d = db || getDatabase();
|
|
6569
|
-
const
|
|
6570
|
-
if (
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
const
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
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)
|
|
6578
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6579
|
-
id,
|
|
6580
|
-
templateId,
|
|
6581
|
-
i,
|
|
6582
|
-
task.title_pattern,
|
|
6583
|
-
task.description || null,
|
|
6584
|
-
task.priority || "medium",
|
|
6585
|
-
JSON.stringify(task.tags || []),
|
|
6586
|
-
task.task_type || null,
|
|
6587
|
-
task.condition || null,
|
|
6588
|
-
task.include_template_id || null,
|
|
6589
|
-
JSON.stringify(task.depends_on || []),
|
|
6590
|
-
JSON.stringify(task.metadata || {}),
|
|
6591
|
-
now()
|
|
6592
|
-
]);
|
|
6593
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
6594
|
-
if (row)
|
|
6595
|
-
results.push(rowToTemplateTask(row));
|
|
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);
|
|
6596
6504
|
}
|
|
6597
|
-
return
|
|
6505
|
+
return blocking;
|
|
6598
6506
|
}
|
|
6599
|
-
function
|
|
6507
|
+
function startTask(id, agentId, db) {
|
|
6600
6508
|
const d = db || getDatabase();
|
|
6601
|
-
const
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
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}`);
|
|
6528
|
+
}
|
|
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]);
|
|
6533
|
+
if (result.changes === 0) {
|
|
6534
|
+
const current = getTask(id, d);
|
|
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`);
|
|
6542
|
+
}
|
|
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;
|
|
6607
6550
|
}
|
|
6608
|
-
function
|
|
6551
|
+
function completeTask(id, agentId, db, options) {
|
|
6609
6552
|
const d = db || getDatabase();
|
|
6610
|
-
const
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6553
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6554
|
+
const task = getTask(id, d);
|
|
6555
|
+
if (!task)
|
|
6556
|
+
throw new TaskNotFoundError(id);
|
|
6557
|
+
if (task.status === "completed") {
|
|
6558
|
+
return task;
|
|
6559
|
+
}
|
|
6560
|
+
if (task.status === "cancelled") {
|
|
6561
|
+
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
6562
|
+
}
|
|
6563
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
6564
|
+
throw new LockError(id, task.locked_by);
|
|
6565
|
+
}
|
|
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 };
|
|
6574
|
+
}
|
|
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
|
+
}
|
|
6621
|
+
}
|
|
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
|
+
}
|
|
6639
|
+
}
|
|
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 };
|
|
6643
|
+
}
|
|
6644
|
+
if (spawnedFromTemplate) {
|
|
6645
|
+
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
6646
|
+
}
|
|
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 });
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
|
|
6615
6667
|
}
|
|
6616
|
-
function
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
if (
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
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
|
+
};
|
|
6625
6678
|
}
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
return
|
|
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) };
|
|
6631
6684
|
}
|
|
6632
|
-
const
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
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
|
+
};
|
|
6698
|
+
}
|
|
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
|
+
};
|
|
6637
6711
|
}
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
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) };
|
|
6714
|
+
}
|
|
6715
|
+
function unlockTask(id, agentId, db) {
|
|
6716
|
+
const d = db || getDatabase();
|
|
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);
|
|
6643
6722
|
}
|
|
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]);
|
|
6644
6726
|
return true;
|
|
6645
6727
|
}
|
|
6646
|
-
function
|
|
6728
|
+
function getTaskLockStatus(id, db) {
|
|
6647
6729
|
const d = db || getDatabase();
|
|
6648
|
-
const
|
|
6649
|
-
if (!
|
|
6650
|
-
throw new
|
|
6730
|
+
const task = getTask(id, d);
|
|
6731
|
+
if (!task)
|
|
6732
|
+
throw new TaskNotFoundError(id);
|
|
6733
|
+
const expired = isLockExpired(task.locked_at);
|
|
6651
6734
|
return {
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
project_id: template.project_id,
|
|
6659
|
-
plan_id: template.plan_id,
|
|
6660
|
-
metadata: template.metadata,
|
|
6661
|
-
tasks: template.tasks.map((t) => ({
|
|
6662
|
-
position: t.position,
|
|
6663
|
-
title_pattern: t.title_pattern,
|
|
6664
|
-
description: t.description,
|
|
6665
|
-
priority: t.priority,
|
|
6666
|
-
tags: t.tags,
|
|
6667
|
-
task_type: t.task_type,
|
|
6668
|
-
condition: t.condition,
|
|
6669
|
-
include_template_id: t.include_template_id,
|
|
6670
|
-
depends_on_positions: t.depends_on_positions,
|
|
6671
|
-
metadata: t.metadata
|
|
6672
|
-
}))
|
|
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
|
|
6673
6741
|
};
|
|
6674
6742
|
}
|
|
6675
|
-
function
|
|
6676
|
-
const d = db || getDatabase();
|
|
6677
|
-
const taskInputs = (json.tasks || []).map((t) => ({
|
|
6678
|
-
title_pattern: t.title_pattern,
|
|
6679
|
-
description: t.description ?? undefined,
|
|
6680
|
-
priority: t.priority,
|
|
6681
|
-
tags: t.tags,
|
|
6682
|
-
task_type: t.task_type ?? undefined,
|
|
6683
|
-
condition: t.condition ?? undefined,
|
|
6684
|
-
include_template_id: t.include_template_id ?? undefined,
|
|
6685
|
-
depends_on: t.depends_on_positions,
|
|
6686
|
-
metadata: t.metadata
|
|
6687
|
-
}));
|
|
6688
|
-
return createTemplate({
|
|
6689
|
-
name: json.name,
|
|
6690
|
-
title_pattern: json.title_pattern,
|
|
6691
|
-
description: json.description ?? undefined,
|
|
6692
|
-
priority: json.priority,
|
|
6693
|
-
tags: json.tags,
|
|
6694
|
-
variables: json.variables,
|
|
6695
|
-
project_id: json.project_id ?? undefined,
|
|
6696
|
-
plan_id: json.plan_id ?? undefined,
|
|
6697
|
-
metadata: json.metadata,
|
|
6698
|
-
tasks: taskInputs
|
|
6699
|
-
}, d);
|
|
6700
|
-
}
|
|
6701
|
-
function getTemplateVersion(id, version, db) {
|
|
6743
|
+
function claimNextTask(agentId, filters, db) {
|
|
6702
6744
|
const d = db || getDatabase();
|
|
6703
|
-
const
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
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;
|
|
6708
6765
|
}
|
|
6709
|
-
function
|
|
6766
|
+
function getNextTask(agentId, filters, db) {
|
|
6710
6767
|
const d = db || getDatabase();
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
const merged = { ...provided };
|
|
6718
|
-
for (const v of templateVars) {
|
|
6719
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
6720
|
-
merged[v.name] = v.default;
|
|
6721
|
-
}
|
|
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);
|
|
6722
6774
|
}
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
missing.push(v.name);
|
|
6727
|
-
}
|
|
6775
|
+
if (filters?.task_list_id) {
|
|
6776
|
+
conditions.push("task_list_id = ?");
|
|
6777
|
+
params.push(filters.task_list_id);
|
|
6728
6778
|
}
|
|
6729
|
-
if (
|
|
6730
|
-
|
|
6779
|
+
if (filters?.plan_id) {
|
|
6780
|
+
conditions.push("plan_id = ?");
|
|
6781
|
+
params.push(filters.plan_id);
|
|
6731
6782
|
}
|
|
6732
|
-
|
|
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);
|
|
6787
|
+
}
|
|
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);
|
|
6794
|
+
}
|
|
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);
|
|
6799
|
+
}
|
|
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);
|
|
6804
|
+
}
|
|
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;
|
|
6733
6808
|
}
|
|
6734
|
-
function
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6809
|
+
function getActiveWork(filters, db) {
|
|
6810
|
+
const d = db || getDatabase();
|
|
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);
|
|
6738
6817
|
}
|
|
6739
|
-
|
|
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;
|
|
6740
6827
|
}
|
|
6741
|
-
function
|
|
6828
|
+
function getTasksChangedSince(since, filters, db) {
|
|
6742
6829
|
const d = db || getDatabase();
|
|
6743
|
-
const
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
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);
|
|
6749
6835
|
}
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
6754
|
-
const task = createTask(input, d);
|
|
6755
|
-
return [task];
|
|
6836
|
+
if (filters?.task_list_id) {
|
|
6837
|
+
conditions.push("task_list_id = ?");
|
|
6838
|
+
params.push(filters.task_list_id);
|
|
6756
6839
|
}
|
|
6757
|
-
const
|
|
6758
|
-
const
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
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);
|
|
6843
|
+
}
|
|
6844
|
+
function failTask(id, agentId, reason, options, db) {
|
|
6845
|
+
const d = db || getDatabase();
|
|
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
|
|
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);
|
|
6768
6899
|
}
|
|
6769
|
-
|
|
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]);
|
|
6770
6915
|
}
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6916
|
+
}
|
|
6917
|
+
return { task: failedTask, retryTask };
|
|
6918
|
+
}
|
|
6919
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
6920
|
+
const d = db || getDatabase();
|
|
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);
|
|
6940
|
+
}
|
|
6941
|
+
function stealTask(agentId, opts, db) {
|
|
6942
|
+
const d = db || getDatabase();
|
|
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)
|
|
6947
|
+
return null;
|
|
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;
|
|
6965
|
+
}
|
|
6966
|
+
function claimOrSteal(agentId, filters, db) {
|
|
6967
|
+
const d = db || getDatabase();
|
|
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 };
|
|
6774
6973
|
}
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
metadata: tt.metadata
|
|
6789
|
-
}, d);
|
|
6790
|
-
createdTasks.push(task);
|
|
6791
|
-
positionToId.set(tt.position, task.id);
|
|
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();
|
|
6980
|
+
}
|
|
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);
|
|
6792
6987
|
}
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
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);
|
|
7005
|
+
}
|
|
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);
|
|
7040
|
+
}
|
|
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}`);
|
|
6807
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));
|
|
6808
7055
|
}
|
|
6809
|
-
return createdTasks;
|
|
6810
7056
|
}
|
|
6811
|
-
function
|
|
7057
|
+
function createTask(input, db) {
|
|
6812
7058
|
const d = db || getDatabase();
|
|
6813
|
-
const
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
const
|
|
6817
|
-
const
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
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();
|
|
7112
|
+
continue;
|
|
7113
|
+
}
|
|
7114
|
+
throw e;
|
|
6841
7115
|
}
|
|
6842
7116
|
}
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
template_name: template.name,
|
|
6846
|
-
description: template.description,
|
|
6847
|
-
variables: template.variables,
|
|
6848
|
-
resolved_variables: resolved,
|
|
6849
|
-
tasks
|
|
6850
|
-
};
|
|
6851
|
-
}
|
|
6852
|
-
var init_templates = __esm(() => {
|
|
6853
|
-
init_database();
|
|
6854
|
-
init_tasks();
|
|
6855
|
-
init_storage_tombstones();
|
|
6856
|
-
});
|
|
6857
|
-
|
|
6858
|
-
// src/db/task-graph.ts
|
|
6859
|
-
function addDependency(taskId, dependsOn, db) {
|
|
6860
|
-
const d = db || getDatabase();
|
|
6861
|
-
if (!getTask(taskId, d))
|
|
6862
|
-
throw new TaskNotFoundError(taskId);
|
|
6863
|
-
if (!getTask(dependsOn, d))
|
|
6864
|
-
throw new TaskNotFoundError(dependsOn);
|
|
6865
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
6866
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
7117
|
+
if (tags.length > 0) {
|
|
7118
|
+
insertTaskTags(id, tags, d);
|
|
6867
7119
|
}
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
function getTaskDependencies(taskId, db) {
|
|
6876
|
-
const d = db || getDatabase();
|
|
6877
|
-
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;
|
|
6878
7127
|
}
|
|
6879
|
-
function
|
|
7128
|
+
function getTask(id, db) {
|
|
6880
7129
|
const d = db || getDatabase();
|
|
6881
|
-
|
|
7130
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
7131
|
+
if (!row)
|
|
7132
|
+
return null;
|
|
7133
|
+
return rowToTask(row);
|
|
6882
7134
|
}
|
|
6883
|
-
function
|
|
7135
|
+
function getTaskWithRelations(id, db) {
|
|
6884
7136
|
const d = db || getDatabase();
|
|
6885
|
-
const
|
|
6886
|
-
if (!
|
|
6887
|
-
|
|
6888
|
-
const
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
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
|
|
6903
7161
|
};
|
|
6904
|
-
return createTask(input, d);
|
|
6905
7162
|
}
|
|
6906
|
-
function
|
|
7163
|
+
function listTasks(filter = {}, db) {
|
|
6907
7164
|
const d = db || getDatabase();
|
|
6908
|
-
const
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
return depTask && depTask.status !== "completed";
|
|
6916
|
-
});
|
|
6917
|
-
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);
|
|
6918
7172
|
}
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
visited.add(id);
|
|
6923
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
6924
|
-
return deps.map((dep) => {
|
|
6925
|
-
const depTask = getTask(dep.depends_on, d);
|
|
6926
|
-
if (!depTask)
|
|
6927
|
-
return null;
|
|
6928
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
6929
|
-
}).filter(Boolean);
|
|
7173
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
7174
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
7175
|
+
params.push(...filter.ids);
|
|
6930
7176
|
}
|
|
6931
|
-
|
|
6932
|
-
if (
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
if (!depTask)
|
|
6939
|
-
return null;
|
|
6940
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
6941
|
-
}).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
|
+
}
|
|
6942
7184
|
}
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
}
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
if (!task)
|
|
6952
|
-
throw new TaskNotFoundError(taskId);
|
|
6953
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
6954
|
-
const params = [now()];
|
|
6955
|
-
if (target.task_list_id !== undefined) {
|
|
6956
|
-
sets.push("task_list_id = ?");
|
|
6957
|
-
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
|
+
}
|
|
6958
7193
|
}
|
|
6959
|
-
if (
|
|
6960
|
-
|
|
6961
|
-
|
|
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
|
+
}
|
|
6962
7202
|
}
|
|
6963
|
-
if (
|
|
6964
|
-
|
|
6965
|
-
params.push(
|
|
7203
|
+
if (filter.assigned_to) {
|
|
7204
|
+
conditions.push("assigned_to = ?");
|
|
7205
|
+
params.push(filter.assigned_to);
|
|
6966
7206
|
}
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
}
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
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);
|
|
6984
7240
|
}
|
|
6985
7241
|
}
|
|
6986
|
-
|
|
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);
|
|
6987
7266
|
}
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
init_task_crud();
|
|
6992
|
-
});
|
|
6993
|
-
|
|
6994
|
-
// src/db/task-lifecycle.ts
|
|
6995
|
-
function lockExpiresAt(lockedAt) {
|
|
6996
|
-
if (!lockedAt)
|
|
6997
|
-
return null;
|
|
6998
|
-
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;
|
|
6999
7270
|
}
|
|
7000
|
-
function
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7271
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
7272
|
+
return {
|
|
7273
|
+
...current,
|
|
7274
|
+
...next ?? {},
|
|
7275
|
+
fingerprint
|
|
7276
|
+
};
|
|
7006
7277
|
}
|
|
7007
|
-
function
|
|
7278
|
+
function upsertTaskByFingerprint(input, db) {
|
|
7008
7279
|
const d = db || getDatabase();
|
|
7009
|
-
const
|
|
7010
|
-
if (
|
|
7011
|
-
|
|
7012
|
-
const
|
|
7013
|
-
|
|
7014
|
-
const
|
|
7015
|
-
if (
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
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();
|
|
7019
7317
|
}
|
|
7020
|
-
function
|
|
7318
|
+
function countTasks(filter = {}, db) {
|
|
7021
7319
|
const d = db || getDatabase();
|
|
7022
|
-
const
|
|
7023
|
-
const
|
|
7024
|
-
if (
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
const blocking = getBlockingDeps(id, d);
|
|
7028
|
-
if (blocking.length > 0) {
|
|
7029
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
7030
|
-
emitLocalEventHooksQuiet({
|
|
7031
|
-
type: "task.blocked",
|
|
7032
|
-
payload: {
|
|
7033
|
-
id,
|
|
7034
|
-
agent_id: agentId,
|
|
7035
|
-
title: task.title,
|
|
7036
|
-
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
7037
|
-
},
|
|
7038
|
-
databasePath
|
|
7039
|
-
});
|
|
7040
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
7041
|
-
}
|
|
7042
|
-
const cutoff = lockExpiryCutoff();
|
|
7043
|
-
const timestamp = now();
|
|
7044
|
-
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 = ?
|
|
7045
|
-
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]);
|
|
7046
|
-
if (result.changes === 0) {
|
|
7047
|
-
const current = getTask(id, d);
|
|
7048
|
-
if (!current)
|
|
7049
|
-
throw new TaskNotFoundError(id);
|
|
7050
|
-
assertStartable(current, agentId);
|
|
7051
|
-
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
7052
|
-
throw new LockError(id, current.locked_by);
|
|
7053
|
-
}
|
|
7054
|
-
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);
|
|
7055
7325
|
}
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
7060
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
7061
|
-
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
7062
|
-
return startedTask;
|
|
7063
|
-
}
|
|
7064
|
-
function completeTask(id, agentId, db, options) {
|
|
7065
|
-
const d = db || getDatabase();
|
|
7066
|
-
const databasePath = databasePathFromDatabase(d);
|
|
7067
|
-
const task = getTask(id, d);
|
|
7068
|
-
if (!task)
|
|
7069
|
-
throw new TaskNotFoundError(id);
|
|
7070
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
7071
|
-
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);
|
|
7072
7329
|
}
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
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
|
+
}
|
|
7081
7337
|
}
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
if (metaResult.changes === 0) {
|
|
7090
|
-
const current = getTask(id, d);
|
|
7091
|
-
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
7092
|
-
}
|
|
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);
|
|
7093
7345
|
}
|
|
7094
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
7095
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
7096
|
-
});
|
|
7097
|
-
tx();
|
|
7098
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
7099
|
-
const completedTaskForEvent = {
|
|
7100
|
-
...task,
|
|
7101
|
-
status: "completed",
|
|
7102
|
-
locked_by: null,
|
|
7103
|
-
locked_at: null,
|
|
7104
|
-
completed_at: timestamp,
|
|
7105
|
-
confidence,
|
|
7106
|
-
version: task.version + 1,
|
|
7107
|
-
updated_at: timestamp,
|
|
7108
|
-
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
7109
|
-
};
|
|
7110
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
7111
|
-
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7112
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
7113
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
7114
|
-
let spawnedTask = null;
|
|
7115
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7116
|
-
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
7117
7346
|
}
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
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);
|
|
7123
7351
|
} else {
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
project_id: task.project_id ?? undefined,
|
|
7127
|
-
plan_id: task.plan_id ?? undefined,
|
|
7128
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
7129
|
-
assigned_to: task.assigned_to ?? undefined
|
|
7130
|
-
}, d);
|
|
7131
|
-
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
7132
|
-
spawnedFromTemplate = createTask(input, d);
|
|
7133
|
-
} catch {}
|
|
7352
|
+
conditions.push("priority = ?");
|
|
7353
|
+
params.push(filter.priority);
|
|
7134
7354
|
}
|
|
7135
7355
|
}
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7356
|
+
if (filter.assigned_to) {
|
|
7357
|
+
conditions.push("assigned_to = ?");
|
|
7358
|
+
params.push(filter.assigned_to);
|
|
7139
7359
|
}
|
|
7140
|
-
if (
|
|
7141
|
-
|
|
7360
|
+
if (filter.agent_id) {
|
|
7361
|
+
conditions.push("agent_id = ?");
|
|
7362
|
+
params.push(filter.agent_id);
|
|
7142
7363
|
}
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
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);
|
|
7160
7393
|
}
|
|
7161
7394
|
}
|
|
7162
|
-
|
|
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;
|
|
7163
7402
|
}
|
|
7164
|
-
function
|
|
7403
|
+
function updateTask(id, input, db) {
|
|
7165
7404
|
const d = db || getDatabase();
|
|
7166
7405
|
const task = getTask(id, d);
|
|
7167
7406
|
if (!task)
|
|
7168
7407
|
throw new TaskNotFoundError(id);
|
|
7169
|
-
if (task.
|
|
7170
|
-
|
|
7171
|
-
success: false,
|
|
7172
|
-
error: `Task is ${task.status} and cannot be locked`
|
|
7173
|
-
};
|
|
7174
|
-
}
|
|
7175
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
7176
|
-
const timestamp2 = now();
|
|
7177
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
7178
|
-
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
7179
|
-
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);
|
|
7180
7410
|
}
|
|
7181
|
-
const cutoff = lockExpiryCutoff();
|
|
7182
7411
|
const timestamp = now();
|
|
7183
|
-
const
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
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);
|
|
7194
7426
|
}
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
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");
|
|
7202
7436
|
}
|
|
7203
|
-
return {
|
|
7204
|
-
success: false,
|
|
7205
|
-
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
7206
|
-
};
|
|
7207
7437
|
}
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
function unlockTask(id, agentId, db) {
|
|
7212
|
-
const d = db || getDatabase();
|
|
7213
|
-
const task = getTask(id, d);
|
|
7214
|
-
if (!task)
|
|
7215
|
-
throw new TaskNotFoundError(id);
|
|
7216
|
-
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
7217
|
-
throw new LockError(id, task.locked_by);
|
|
7438
|
+
if (input.priority !== undefined) {
|
|
7439
|
+
sets.push("priority = ?");
|
|
7440
|
+
params.push(input.priority);
|
|
7218
7441
|
}
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
return true;
|
|
7223
|
-
}
|
|
7224
|
-
function getTaskLockStatus(id, db) {
|
|
7225
|
-
const d = db || getDatabase();
|
|
7226
|
-
const task = getTask(id, d);
|
|
7227
|
-
if (!task)
|
|
7228
|
-
throw new TaskNotFoundError(id);
|
|
7229
|
-
const expired = isLockExpired(task.locked_at);
|
|
7230
|
-
return {
|
|
7231
|
-
task_id: id,
|
|
7232
|
-
locked: !!task.locked_by && !expired,
|
|
7233
|
-
locked_by: task.locked_by,
|
|
7234
|
-
locked_at: task.locked_at,
|
|
7235
|
-
expires_at: lockExpiresAt(task.locked_at),
|
|
7236
|
-
expired
|
|
7237
|
-
};
|
|
7238
|
-
}
|
|
7239
|
-
function claimNextTask(agentId, filters, db) {
|
|
7240
|
-
const d = db || getDatabase();
|
|
7241
|
-
const tx = d.transaction(() => {
|
|
7242
|
-
const task = getNextTask(agentId, filters, d);
|
|
7243
|
-
if (!task)
|
|
7244
|
-
return null;
|
|
7245
|
-
return startTask(task.id, agentId, d);
|
|
7246
|
-
});
|
|
7247
|
-
return tx();
|
|
7248
|
-
}
|
|
7249
|
-
function getNextTask(agentId, filters, db) {
|
|
7250
|
-
const d = db || getDatabase();
|
|
7251
|
-
clearExpiredLocks(d);
|
|
7252
|
-
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
7253
|
-
const params = [lockExpiryCutoff()];
|
|
7254
|
-
if (filters?.project_id) {
|
|
7255
|
-
conditions.push("project_id = ?");
|
|
7256
|
-
params.push(filters.project_id);
|
|
7442
|
+
if (input.project_id !== undefined) {
|
|
7443
|
+
sets.push("project_id = ?");
|
|
7444
|
+
params.push(input.project_id);
|
|
7257
7445
|
}
|
|
7258
|
-
if (
|
|
7259
|
-
|
|
7260
|
-
params.push(
|
|
7446
|
+
if (input.assigned_to !== undefined) {
|
|
7447
|
+
sets.push("assigned_to = ?");
|
|
7448
|
+
params.push(input.assigned_to);
|
|
7261
7449
|
}
|
|
7262
|
-
if (
|
|
7263
|
-
|
|
7264
|
-
params.push(
|
|
7450
|
+
if (input.working_dir !== undefined) {
|
|
7451
|
+
sets.push("working_dir = ?");
|
|
7452
|
+
params.push(input.working_dir);
|
|
7265
7453
|
}
|
|
7266
|
-
if (
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
params.push(...filters.tags);
|
|
7454
|
+
if (input.tags !== undefined) {
|
|
7455
|
+
sets.push("tags = ?");
|
|
7456
|
+
params.push(JSON.stringify(input.tags));
|
|
7270
7457
|
}
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
if (agentId) {
|
|
7275
|
-
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);
|
|
7276
|
-
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
7458
|
+
if (input.metadata !== undefined) {
|
|
7459
|
+
sets.push("metadata = ?");
|
|
7460
|
+
params.push(JSON.stringify(input.metadata));
|
|
7277
7461
|
}
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
params.push(agentId);
|
|
7462
|
+
if (input.plan_id !== undefined) {
|
|
7463
|
+
sets.push("plan_id = ?");
|
|
7464
|
+
params.push(input.plan_id);
|
|
7282
7465
|
}
|
|
7283
|
-
if (
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
params.push(...recentProjectIds);
|
|
7466
|
+
if (input.task_list_id !== undefined) {
|
|
7467
|
+
sets.push("task_list_id = ?");
|
|
7468
|
+
params.push(input.task_list_id);
|
|
7287
7469
|
}
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
}
|
|
7292
|
-
function getActiveWork(filters, db) {
|
|
7293
|
-
const d = db || getDatabase();
|
|
7294
|
-
clearExpiredLocks(d);
|
|
7295
|
-
const conditions = ["status = 'in_progress'"];
|
|
7296
|
-
const params = [];
|
|
7297
|
-
if (filters?.project_id) {
|
|
7298
|
-
conditions.push("project_id = ?");
|
|
7299
|
-
params.push(filters.project_id);
|
|
7470
|
+
if (input.due_at !== undefined) {
|
|
7471
|
+
sets.push("due_at = ?");
|
|
7472
|
+
params.push(input.due_at);
|
|
7300
7473
|
}
|
|
7301
|
-
if (
|
|
7302
|
-
|
|
7303
|
-
params.push(
|
|
7474
|
+
if (input.estimated_minutes !== undefined) {
|
|
7475
|
+
sets.push("estimated_minutes = ?");
|
|
7476
|
+
params.push(input.estimated_minutes);
|
|
7304
7477
|
}
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
updated_at DESC`).all(...params);
|
|
7309
|
-
return rows;
|
|
7310
|
-
}
|
|
7311
|
-
function getTasksChangedSince(since, filters, db) {
|
|
7312
|
-
const d = db || getDatabase();
|
|
7313
|
-
const conditions = ["updated_at > ?"];
|
|
7314
|
-
const params = [since];
|
|
7315
|
-
if (filters?.project_id) {
|
|
7316
|
-
conditions.push("project_id = ?");
|
|
7317
|
-
params.push(filters.project_id);
|
|
7478
|
+
if (input.sla_minutes !== undefined) {
|
|
7479
|
+
sets.push("sla_minutes = ?");
|
|
7480
|
+
params.push(input.sla_minutes);
|
|
7318
7481
|
}
|
|
7319
|
-
if (
|
|
7320
|
-
|
|
7321
|
-
params.push(
|
|
7482
|
+
if (input.actual_minutes !== undefined) {
|
|
7483
|
+
sets.push("actual_minutes = ?");
|
|
7484
|
+
params.push(input.actual_minutes);
|
|
7322
7485
|
}
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
}
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
if (
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
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)}`);
|
|
7341
7540
|
}
|
|
7342
|
-
}
|
|
7343
|
-
const
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
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 = {
|
|
7347
7558
|
...task,
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
metadata: meta,
|
|
7559
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
7560
|
+
tags: input.tags ?? task.tags,
|
|
7561
|
+
metadata: input.metadata ?? task.metadata,
|
|
7352
7562
|
version: task.version + 1,
|
|
7353
|
-
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
|
|
7354
7576
|
};
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
if (options?.retry) {
|
|
7362
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
7363
|
-
const maxRetries = task.max_retries || 3;
|
|
7364
|
-
if (retryCount > maxRetries) {
|
|
7365
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
7366
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
7367
|
-
id
|
|
7368
|
-
]);
|
|
7369
|
-
} else {
|
|
7370
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
7371
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
7372
|
-
let title = task.title;
|
|
7373
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
7374
|
-
title = title.slice(task.short_id.length + 2);
|
|
7375
|
-
}
|
|
7376
|
-
retryTask = createTask({
|
|
7377
|
-
title,
|
|
7378
|
-
description: task.description ?? undefined,
|
|
7379
|
-
priority: task.priority,
|
|
7380
|
-
project_id: task.project_id ?? undefined,
|
|
7381
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
7382
|
-
plan_id: task.plan_id ?? undefined,
|
|
7383
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
7384
|
-
tags: task.tags,
|
|
7385
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
7386
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
7387
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
7388
|
-
due_at: retryAfter
|
|
7389
|
-
}, d);
|
|
7390
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
7391
|
-
}
|
|
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 });
|
|
7392
7583
|
}
|
|
7393
|
-
|
|
7394
|
-
}
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
7399
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7400
|
-
const conditions = [
|
|
7401
|
-
"status = 'in_progress'",
|
|
7402
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
7403
|
-
];
|
|
7404
|
-
const params = [cutoff, cutoff];
|
|
7405
|
-
if (effectiveFilters?.project_id) {
|
|
7406
|
-
conditions.push("project_id = ?");
|
|
7407
|
-
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 });
|
|
7408
7589
|
}
|
|
7409
|
-
if (
|
|
7410
|
-
|
|
7411
|
-
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 });
|
|
7412
7592
|
}
|
|
7413
|
-
const
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
}
|
|
7417
|
-
|
|
7418
|
-
const d = db || getDatabase();
|
|
7419
|
-
const databasePath = databasePathFromDatabase(d);
|
|
7420
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
7421
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
7422
|
-
if (staleTasks.length === 0)
|
|
7423
|
-
return null;
|
|
7424
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7425
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
7426
|
-
const target = staleTasks[0];
|
|
7427
|
-
const timestamp = now();
|
|
7428
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7429
|
-
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
7430
|
-
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]);
|
|
7431
|
-
if (result.changes === 0)
|
|
7432
|
-
return null;
|
|
7433
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
7434
|
-
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
7435
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
7436
|
-
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7437
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7438
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
7439
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
7440
|
-
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;
|
|
7441
7598
|
}
|
|
7442
|
-
function
|
|
7599
|
+
function deleteTask(id, db) {
|
|
7443
7600
|
const d = db || getDatabase();
|
|
7444
|
-
const
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
return tx();
|
|
7456
|
-
}
|
|
7457
|
-
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
7458
|
-
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
7459
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
7460
|
-
let title = completedTask.title;
|
|
7461
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
7462
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
7463
|
-
}
|
|
7464
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
7465
|
-
return createTask({
|
|
7466
|
-
title,
|
|
7467
|
-
description: completedTask.description ?? undefined,
|
|
7468
|
-
priority: completedTask.priority,
|
|
7469
|
-
project_id: completedTask.project_id ?? undefined,
|
|
7470
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
7471
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
7472
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
7473
|
-
tags: completedTask.tags,
|
|
7474
|
-
metadata: completedTask.metadata,
|
|
7475
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
7476
|
-
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
7477
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
7478
|
-
recurrence_parent_id: recurrenceParentId,
|
|
7479
|
-
due_at: dueAt
|
|
7480
|
-
}, 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;
|
|
7481
7612
|
}
|
|
7482
|
-
var
|
|
7483
|
-
var init_task_lifecycle = __esm(() => {
|
|
7613
|
+
var init_task_crud = __esm(() => {
|
|
7484
7614
|
init_types();
|
|
7485
7615
|
init_database();
|
|
7486
7616
|
init_completion_guard();
|
|
@@ -7488,11 +7618,9 @@ var init_task_lifecycle = __esm(() => {
|
|
|
7488
7618
|
init_event_hooks();
|
|
7489
7619
|
init_shared_events();
|
|
7490
7620
|
init_audit();
|
|
7491
|
-
init_recurrence();
|
|
7492
7621
|
init_webhooks();
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
init_task_graph();
|
|
7622
|
+
init_checklists();
|
|
7623
|
+
init_storage_tombstones();
|
|
7496
7624
|
});
|
|
7497
7625
|
|
|
7498
7626
|
// src/db/task-status.ts
|
|
@@ -7582,6 +7710,15 @@ function setTaskStatus(id, status, _agentId, db) {
|
|
|
7582
7710
|
throw new TaskNotFoundError(id);
|
|
7583
7711
|
if (task.status === status)
|
|
7584
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
|
+
}
|
|
7585
7722
|
try {
|
|
7586
7723
|
return updateTask(id, { status, version: task.version }, d);
|
|
7587
7724
|
} catch (e) {
|
|
@@ -11538,6 +11675,37 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
11538
11675
|
return fallback;
|
|
11539
11676
|
return Math.min(parsed, max);
|
|
11540
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
|
+
}
|
|
11541
11709
|
function handleSseEvents(_req, url, ctx) {
|
|
11542
11710
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
11543
11711
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
@@ -11616,35 +11784,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
11616
11784
|
});
|
|
11617
11785
|
}
|
|
11618
11786
|
function handleHealth(_ctx, json2) {
|
|
11619
|
-
const
|
|
11620
|
-
const
|
|
11621
|
-
const
|
|
11622
|
-
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
|
+
});
|
|
11623
11797
|
}
|
|
11624
11798
|
function handleHeadlessBoundary(_ctx, json2) {
|
|
11625
11799
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
11626
11800
|
return json2(getHeadlessBoundaryManifest2());
|
|
11627
11801
|
}
|
|
11628
11802
|
function handleStats(_ctx, json2) {
|
|
11629
|
-
const
|
|
11803
|
+
const stats = getTaskStats();
|
|
11804
|
+
const byStatus = stats.by_status;
|
|
11630
11805
|
const projects = listProjects();
|
|
11631
11806
|
const agents = listAgents();
|
|
11632
|
-
const
|
|
11633
|
-
const
|
|
11634
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
11635
|
-
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;
|
|
11636
11809
|
return json2({
|
|
11637
|
-
total_tasks:
|
|
11638
|
-
pending:
|
|
11639
|
-
in_progress:
|
|
11640
|
-
completed:
|
|
11641
|
-
failed:
|
|
11642
|
-
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,
|
|
11643
11816
|
projects: projects.length,
|
|
11644
11817
|
agents: agents.length,
|
|
11645
|
-
stale_count:
|
|
11818
|
+
stale_count: staleCount,
|
|
11646
11819
|
overdue_recurring: overdueRecurring,
|
|
11647
|
-
recurring_tasks:
|
|
11820
|
+
recurring_tasks: countRecurringTasks()
|
|
11648
11821
|
});
|
|
11649
11822
|
}
|
|
11650
11823
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
@@ -11723,27 +11896,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
11723
11896
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
11724
11897
|
if (format === "csv") {
|
|
11725
11898
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
11726
|
-
const
|
|
11727
|
-
const val = t[h];
|
|
11899
|
+
const csvCell = (val) => {
|
|
11728
11900
|
if (val === null || val === undefined)
|
|
11729
11901
|
return "";
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
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(","));
|
|
11734
11912
|
const csv = [headers.join(","), ...rows].join(`
|
|
11735
11913
|
`);
|
|
11736
11914
|
return new Response(csv, {
|
|
11737
11915
|
headers: {
|
|
11738
11916
|
"Content-Type": "text/csv",
|
|
11739
|
-
"Content-Disposition": "attachment; filename=tasks.csv"
|
|
11917
|
+
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
11918
|
+
...SECURITY_HEADERS
|
|
11740
11919
|
}
|
|
11741
11920
|
});
|
|
11742
11921
|
}
|
|
11743
11922
|
return new Response(JSON.stringify(summaries, null, 2), {
|
|
11744
11923
|
headers: {
|
|
11745
11924
|
"Content-Type": "application/json",
|
|
11746
|
-
"Content-Disposition": "attachment; filename=tasks.json"
|
|
11925
|
+
"Content-Disposition": "attachment; filename=tasks.json",
|
|
11926
|
+
...SECURITY_HEADERS
|
|
11747
11927
|
}
|
|
11748
11928
|
});
|
|
11749
11929
|
}
|
|
@@ -11917,12 +12097,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
11917
12097
|
if (ALLOWED.has(key))
|
|
11918
12098
|
safeBody[key] = value;
|
|
11919
12099
|
}
|
|
12100
|
+
const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
|
|
11920
12101
|
const updated = updateTask(id, {
|
|
11921
12102
|
...safeBody,
|
|
11922
|
-
version:
|
|
12103
|
+
version: clientVersion
|
|
11923
12104
|
});
|
|
11924
12105
|
return json2(taskToSummary2(updated));
|
|
11925
12106
|
} catch (e) {
|
|
12107
|
+
const mapped = mapTaskError(e, json2);
|
|
12108
|
+
if (mapped)
|
|
12109
|
+
return mapped;
|
|
11926
12110
|
return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
11927
12111
|
}
|
|
11928
12112
|
}
|
|
@@ -11938,6 +12122,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
|
|
|
11938
12122
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
|
|
11939
12123
|
return json2(taskToSummary2(task));
|
|
11940
12124
|
} catch (e) {
|
|
12125
|
+
const mapped = mapTaskError(e, json2);
|
|
12126
|
+
if (mapped)
|
|
12127
|
+
return mapped;
|
|
11941
12128
|
return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
11942
12129
|
}
|
|
11943
12130
|
}
|
|
@@ -11957,6 +12144,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
|
|
|
11957
12144
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
|
|
11958
12145
|
return json2(taskToSummary2(task));
|
|
11959
12146
|
} catch (e) {
|
|
12147
|
+
const mapped = mapTaskError(e, json2);
|
|
12148
|
+
if (mapped)
|
|
12149
|
+
return mapped;
|
|
11960
12150
|
return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
11961
12151
|
}
|
|
11962
12152
|
}
|
|
@@ -12281,6 +12471,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
12281
12471
|
}
|
|
12282
12472
|
var init_routes = __esm(() => {
|
|
12283
12473
|
init_tasks();
|
|
12474
|
+
init_database();
|
|
12475
|
+
init_types();
|
|
12284
12476
|
init_projects();
|
|
12285
12477
|
init_agents();
|
|
12286
12478
|
init_plans();
|
|
@@ -15665,7 +15857,7 @@ class JSONSchemaGenerator {
|
|
|
15665
15857
|
if (val === undefined) {
|
|
15666
15858
|
if (this.unrepresentable === "throw") {
|
|
15667
15859
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
15668
|
-
}
|
|
15860
|
+
} else {}
|
|
15669
15861
|
} else if (typeof val === "bigint") {
|
|
15670
15862
|
if (this.unrepresentable === "throw") {
|
|
15671
15863
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -33299,10 +33491,8 @@ var init_token_utils = __esm(() => {
|
|
|
33299
33491
|
"cancel_task",
|
|
33300
33492
|
"check_task_done_contract",
|
|
33301
33493
|
"claim_task",
|
|
33302
|
-
"clone_task",
|
|
33303
33494
|
"delete_task",
|
|
33304
33495
|
"extend_task",
|
|
33305
|
-
"get_active_work",
|
|
33306
33496
|
"get_archived_tasks",
|
|
33307
33497
|
"get_blocked_tasks",
|
|
33308
33498
|
"get_blocking_tasks",
|
|
@@ -33338,7 +33528,8 @@ var init_token_utils = __esm(() => {
|
|
|
33338
33528
|
"task_context",
|
|
33339
33529
|
"unlock_task",
|
|
33340
33530
|
"unarchive_task",
|
|
33341
|
-
"update_task"
|
|
33531
|
+
"update_task",
|
|
33532
|
+
"upsert_task"
|
|
33342
33533
|
],
|
|
33343
33534
|
projects: [
|
|
33344
33535
|
"bootstrap_project",
|
|
@@ -33597,12 +33788,8 @@ var init_token_utils = __esm(() => {
|
|
|
33597
33788
|
"delete_tag",
|
|
33598
33789
|
"get_label",
|
|
33599
33790
|
"get_activity_timeline",
|
|
33600
|
-
"get_recent_activity",
|
|
33601
33791
|
"get_tag",
|
|
33602
33792
|
"get_task_fields",
|
|
33603
|
-
"get_task_graph",
|
|
33604
|
-
"get_task_history",
|
|
33605
|
-
"get_task_stats",
|
|
33606
33793
|
"list_workflow_states",
|
|
33607
33794
|
"list_labels",
|
|
33608
33795
|
"list_tags",
|
|
@@ -33613,6 +33800,10 @@ var init_token_utils = __esm(() => {
|
|
|
33613
33800
|
"describe_tools",
|
|
33614
33801
|
"set_task_workflow_state",
|
|
33615
33802
|
"set_task_fields",
|
|
33803
|
+
"assign_label_to_task",
|
|
33804
|
+
"create_custom_field",
|
|
33805
|
+
"set_task_custom_field",
|
|
33806
|
+
"set_task_priority_meta",
|
|
33616
33807
|
"update_label",
|
|
33617
33808
|
"update_tag"
|
|
33618
33809
|
],
|
|
@@ -33638,7 +33829,6 @@ var init_token_utils = __esm(() => {
|
|
|
33638
33829
|
"update_template",
|
|
33639
33830
|
"write_template_library"
|
|
33640
33831
|
],
|
|
33641
|
-
webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
|
|
33642
33832
|
machines: [
|
|
33643
33833
|
"machines_archive",
|
|
33644
33834
|
"machines_delete",
|
|
@@ -58753,7 +58943,7 @@ var require_to_json_schema = __commonJS((exports) => {
|
|
|
58753
58943
|
if (val === undefined) {
|
|
58754
58944
|
if (this.unrepresentable === "throw") {
|
|
58755
58945
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
58756
|
-
}
|
|
58946
|
+
} else {}
|
|
58757
58947
|
} else if (typeof val === "bigint") {
|
|
58758
58948
|
if (this.unrepresentable === "throw") {
|
|
58759
58949
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -79492,14 +79682,16 @@ function printHelp() {
|
|
|
79492
79682
|
Start the @hasna/todos MCP server.
|
|
79493
79683
|
|
|
79494
79684
|
Options:
|
|
79495
|
-
--stdio Use stdio transport
|
|
79496
|
-
--
|
|
79685
|
+
--stdio Use stdio transport (default)
|
|
79686
|
+
--http Use Streamable HTTP transport
|
|
79687
|
+
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
79497
79688
|
-V, --version output the version number
|
|
79498
79689
|
-h, --help display help for command
|
|
79499
79690
|
|
|
79500
79691
|
Environment:
|
|
79501
|
-
|
|
79502
|
-
|
|
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
|
|
79503
79695
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
79504
79696
|
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
|
|
79505
79697
|
}
|
|
@@ -79588,8 +79780,22 @@ function formatError2(error2) {
|
|
|
79588
79780
|
function resolveId(partialId, table = "tasks") {
|
|
79589
79781
|
const db = getDatabase();
|
|
79590
79782
|
const id = resolvePartialId(db, table, partialId);
|
|
79591
|
-
if (!id)
|
|
79592
|
-
|
|
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
|
+
}
|
|
79593
79799
|
return id;
|
|
79594
79800
|
}
|
|
79595
79801
|
function formatTask(task2) {
|
|
@@ -79678,8 +79884,9 @@ function buildServer() {
|
|
|
79678
79884
|
return server;
|
|
79679
79885
|
}
|
|
79680
79886
|
async function main() {
|
|
79681
|
-
const {
|
|
79682
|
-
|
|
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) {
|
|
79683
79890
|
const server = buildServer();
|
|
79684
79891
|
const transport = new StdioServerTransport;
|
|
79685
79892
|
await server.connect(transport);
|
|
@@ -79857,7 +80064,7 @@ function checkAuth(req, apiKey) {
|
|
|
79857
80064
|
if (!apiKey && !generatedKeysEnabled)
|
|
79858
80065
|
return null;
|
|
79859
80066
|
const provided = getProvidedApiKey(req);
|
|
79860
|
-
const matchesEnvKey = Boolean(apiKey && provided && provided
|
|
80067
|
+
const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
|
|
79861
80068
|
const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
|
|
79862
80069
|
if (!matchesEnvKey && !matchesGeneratedKey) {
|
|
79863
80070
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
@@ -79867,6 +80074,15 @@ function checkAuth(req, apiKey) {
|
|
|
79867
80074
|
}
|
|
79868
80075
|
return null;
|
|
79869
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
|
+
}
|
|
79870
80086
|
function checkRateLimit(ip) {
|
|
79871
80087
|
const now4 = Date.now();
|
|
79872
80088
|
const entry = rateLimitMap.get(ip);
|
|
@@ -79994,7 +80210,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
79994
80210
|
const server = Bun.serve({
|
|
79995
80211
|
port,
|
|
79996
80212
|
hostname: hostname4,
|
|
79997
|
-
async fetch(req) {
|
|
80213
|
+
async fetch(req, server2) {
|
|
79998
80214
|
const url = new URL(req.url);
|
|
79999
80215
|
const path = url.pathname;
|
|
80000
80216
|
const method = req.method;
|
|
@@ -80006,15 +80222,6 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80006
80222
|
Vary: "Origin"
|
|
80007
80223
|
} : undefined;
|
|
80008
80224
|
const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
|
|
80009
|
-
if (path === "/health" && method === "GET") {
|
|
80010
|
-
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80011
|
-
return healthResponse2("todos");
|
|
80012
|
-
}
|
|
80013
|
-
if (path === "/mcp") {
|
|
80014
|
-
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
80015
|
-
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
|
|
80016
|
-
return handleMcpHttpRequest2(req, buildServer2);
|
|
80017
|
-
}
|
|
80018
80225
|
if (method === "OPTIONS") {
|
|
80019
80226
|
return new Response(null, {
|
|
80020
80227
|
headers: corsHeaders || {
|
|
@@ -80022,7 +80229,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80022
80229
|
}
|
|
80023
80230
|
});
|
|
80024
80231
|
}
|
|
80025
|
-
const ip = req
|
|
80232
|
+
const ip = resolveClientIp(req, server2);
|
|
80026
80233
|
const rl = checkRateLimit(ip);
|
|
80027
80234
|
if (!rl.allowed) {
|
|
80028
80235
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
@@ -80030,6 +80237,18 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
80030
80237
|
headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
|
|
80031
80238
|
});
|
|
80032
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
|
+
}
|
|
80033
80252
|
if (path.startsWith("/api/")) {
|
|
80034
80253
|
const authError = checkAuth(req, apiKey);
|
|
80035
80254
|
if (authError)
|