@hasna/todos 0.11.65 → 0.11.67
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 +78 -49
- package/dashboard/dist/assets/{index-aJefI7kh.js → index-DVotjwab.js} +1 -1
- package/dashboard/dist/index.html +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/storage-commands.d.ts +5 -0
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +1034 -231
- package/dist/contracts.js +409 -129
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/storage-tombstones.d.ts +26 -0
- package/dist/db/storage-tombstones.d.ts.map +1 -0
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/db/templates.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +928 -219
- package/dist/lib/approval-gates.d.ts.map +1 -1
- package/dist/lib/event-emission-safety.d.ts +9 -0
- package/dist/lib/event-emission-safety.d.ts.map +1 -0
- package/dist/lib/event-hooks.d.ts +1 -0
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/feature-manifest.d.ts.map +1 -1
- package/dist/lib/project-bootstrap.d.ts +1 -0
- package/dist/lib/project-bootstrap.d.ts.map +1 -1
- package/dist/lib/review-queues.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +1 -0
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/lib/task-route-contract.d.ts +33 -0
- package/dist/lib/task-route-contract.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts +55 -0
- package/dist/lib/task-routing.d.ts.map +1 -0
- package/dist/mcp/index.js +459 -162
- package/dist/registry.js +409 -129
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +482 -185
- package/dist/storage/interfaces.d.ts +20 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +6 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +677 -152
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/storage.js
CHANGED
|
@@ -1312,6 +1312,22 @@ var init_migrations = __esm(() => {
|
|
|
1312
1312
|
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1313
1313
|
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1314
1314
|
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1315
|
+
`,
|
|
1316
|
+
`
|
|
1317
|
+
CREATE TABLE IF NOT EXISTS storage_tombstones (
|
|
1318
|
+
id TEXT PRIMARY KEY,
|
|
1319
|
+
object_type TEXT NOT NULL,
|
|
1320
|
+
object_id TEXT NOT NULL,
|
|
1321
|
+
deleted_at TEXT NOT NULL,
|
|
1322
|
+
updated_at TEXT NOT NULL,
|
|
1323
|
+
source_machine_id TEXT,
|
|
1324
|
+
payload TEXT,
|
|
1325
|
+
version INTEGER,
|
|
1326
|
+
UNIQUE(object_type, object_id)
|
|
1327
|
+
);
|
|
1328
|
+
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
|
|
1329
|
+
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
|
|
1330
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (63);
|
|
1315
1331
|
`
|
|
1316
1332
|
];
|
|
1317
1333
|
});
|
|
@@ -1833,6 +1849,20 @@ function ensureSchema(db) {
|
|
|
1833
1849
|
)`);
|
|
1834
1850
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
|
|
1835
1851
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
|
|
1852
|
+
ensureTable("storage_tombstones", `
|
|
1853
|
+
CREATE TABLE storage_tombstones (
|
|
1854
|
+
id TEXT PRIMARY KEY,
|
|
1855
|
+
object_type TEXT NOT NULL,
|
|
1856
|
+
object_id TEXT NOT NULL,
|
|
1857
|
+
deleted_at TEXT NOT NULL,
|
|
1858
|
+
updated_at TEXT NOT NULL,
|
|
1859
|
+
source_machine_id TEXT,
|
|
1860
|
+
payload TEXT,
|
|
1861
|
+
version INTEGER,
|
|
1862
|
+
UNIQUE(object_type, object_id)
|
|
1863
|
+
)`);
|
|
1864
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id)");
|
|
1865
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at)");
|
|
1836
1866
|
ensureTable("machines", `
|
|
1837
1867
|
CREATE TABLE machines (
|
|
1838
1868
|
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
|
|
@@ -3616,6 +3646,80 @@ init_config();
|
|
|
3616
3646
|
init_types();
|
|
3617
3647
|
init_database();
|
|
3618
3648
|
init_machines();
|
|
3649
|
+
|
|
3650
|
+
// src/db/storage-tombstones.ts
|
|
3651
|
+
init_database();
|
|
3652
|
+
init_machines();
|
|
3653
|
+
function recordStorageTombstone(input, db) {
|
|
3654
|
+
const d = db ?? getDatabase();
|
|
3655
|
+
const deletedAt = input.deleted_at ?? now();
|
|
3656
|
+
const machineId = input.source_machine_id ?? currentStorageMachineId(d);
|
|
3657
|
+
d.run(`INSERT INTO storage_tombstones (
|
|
3658
|
+
id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
|
|
3659
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3660
|
+
ON CONFLICT(object_type, object_id) DO UPDATE SET
|
|
3661
|
+
deleted_at = excluded.deleted_at,
|
|
3662
|
+
updated_at = excluded.updated_at,
|
|
3663
|
+
source_machine_id = excluded.source_machine_id,
|
|
3664
|
+
payload = excluded.payload,
|
|
3665
|
+
version = excluded.version
|
|
3666
|
+
WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
|
|
3667
|
+
uuid(),
|
|
3668
|
+
input.object_type,
|
|
3669
|
+
input.object_id,
|
|
3670
|
+
deletedAt,
|
|
3671
|
+
deletedAt,
|
|
3672
|
+
machineId,
|
|
3673
|
+
input.payload ? JSON.stringify(input.payload) : null,
|
|
3674
|
+
input.version ?? null
|
|
3675
|
+
]);
|
|
3676
|
+
return getStorageTombstone(input.object_type, input.object_id, d);
|
|
3677
|
+
}
|
|
3678
|
+
function getStorageTombstone(objectType, objectId, db) {
|
|
3679
|
+
const d = db ?? getDatabase();
|
|
3680
|
+
const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
|
|
3681
|
+
return row ? rowToStorageTombstone(row) : null;
|
|
3682
|
+
}
|
|
3683
|
+
function listStorageTombstones(db) {
|
|
3684
|
+
const d = db ?? getDatabase();
|
|
3685
|
+
return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
|
|
3686
|
+
}
|
|
3687
|
+
function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
|
|
3688
|
+
const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
|
|
3689
|
+
if (!existingUpdatedAt)
|
|
3690
|
+
return true;
|
|
3691
|
+
const existingClock = Date.parse(existingUpdatedAt);
|
|
3692
|
+
if (Number.isNaN(tombstoneClock))
|
|
3693
|
+
return true;
|
|
3694
|
+
if (Number.isNaN(existingClock))
|
|
3695
|
+
return true;
|
|
3696
|
+
return tombstoneClock >= existingClock;
|
|
3697
|
+
}
|
|
3698
|
+
function rowToStorageTombstone(row) {
|
|
3699
|
+
return {
|
|
3700
|
+
...row,
|
|
3701
|
+
payload: parsePayload(row.payload)
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
function parsePayload(value) {
|
|
3705
|
+
if (!value)
|
|
3706
|
+
return null;
|
|
3707
|
+
try {
|
|
3708
|
+
const parsed = JSON.parse(value);
|
|
3709
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
3710
|
+
} catch {
|
|
3711
|
+
return null;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
function currentStorageMachineId(db) {
|
|
3715
|
+
try {
|
|
3716
|
+
return getMachineId(db);
|
|
3717
|
+
} catch {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// src/db/projects.ts
|
|
3619
3723
|
function slugify(name) {
|
|
3620
3724
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3621
3725
|
}
|
|
@@ -3645,8 +3749,9 @@ function createProject(input, db) {
|
|
|
3645
3749
|
const timestamp = now();
|
|
3646
3750
|
const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
|
|
3647
3751
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
3648
|
-
|
|
3649
|
-
|
|
3752
|
+
const machineId = currentStorageMachineId(d);
|
|
3753
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
3754
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
|
|
3650
3755
|
return getProject(id, d);
|
|
3651
3756
|
}
|
|
3652
3757
|
function getProject(id, db) {
|
|
@@ -3725,6 +3830,14 @@ function renameProject(id, input, db) {
|
|
|
3725
3830
|
}
|
|
3726
3831
|
function deleteProject(id, db) {
|
|
3727
3832
|
const d = db || getDatabase();
|
|
3833
|
+
const project = getProject(id, d);
|
|
3834
|
+
if (!project)
|
|
3835
|
+
return false;
|
|
3836
|
+
recordStorageTombstone({
|
|
3837
|
+
object_type: "projects",
|
|
3838
|
+
object_id: id,
|
|
3839
|
+
payload: project
|
|
3840
|
+
}, d);
|
|
3728
3841
|
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
3729
3842
|
return result.changes > 0;
|
|
3730
3843
|
}
|
|
@@ -3829,6 +3942,14 @@ function listMachineLocalPaths(projectId, db) {
|
|
|
3829
3942
|
function removeMachineLocalPath(projectId, machineId, db) {
|
|
3830
3943
|
const d = db || getDatabase();
|
|
3831
3944
|
const mid = machineId ?? getMachineId(d);
|
|
3945
|
+
const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, mid);
|
|
3946
|
+
if (!existing)
|
|
3947
|
+
return false;
|
|
3948
|
+
recordStorageTombstone({
|
|
3949
|
+
object_type: "project_machine_paths",
|
|
3950
|
+
object_id: existing.id,
|
|
3951
|
+
payload: existing
|
|
3952
|
+
}, d);
|
|
3832
3953
|
const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
|
|
3833
3954
|
return result.changes > 0;
|
|
3834
3955
|
}
|
|
@@ -3877,20 +3998,69 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
3877
3998
|
}
|
|
3878
3999
|
}
|
|
3879
4000
|
|
|
4001
|
+
// src/lib/event-emission-safety.ts
|
|
4002
|
+
init_database();
|
|
4003
|
+
init_sync_utils();
|
|
4004
|
+
import { tmpdir } from "os";
|
|
4005
|
+
import { resolve as resolve3, sep } from "path";
|
|
4006
|
+
function envFlag(name) {
|
|
4007
|
+
const value = process.env[name];
|
|
4008
|
+
return value === "1" || value === "true" || value === "yes";
|
|
4009
|
+
}
|
|
4010
|
+
function isUnder(parent, child) {
|
|
4011
|
+
const normalizedParent = resolve3(parent);
|
|
4012
|
+
const normalizedChild = resolve3(child);
|
|
4013
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`);
|
|
4014
|
+
}
|
|
4015
|
+
function databasePathFromDatabase(db) {
|
|
4016
|
+
const filename = db?.filename;
|
|
4017
|
+
return typeof filename === "string" && filename.trim() ? filename : undefined;
|
|
4018
|
+
}
|
|
4019
|
+
function isEphemeralTodosDatabase(dbPath) {
|
|
4020
|
+
const resolvedPath = dbPath ?? getDatabasePath();
|
|
4021
|
+
if (resolvedPath === ":memory:" || resolvedPath.startsWith("file::memory:"))
|
|
4022
|
+
return true;
|
|
4023
|
+
return isUnder(tmpdir(), resolvedPath);
|
|
4024
|
+
}
|
|
4025
|
+
function hasExplicitSharedEventsStore() {
|
|
4026
|
+
return Boolean(process.env["HASNA_EVENTS_DIR"] || process.env["HASNA_EVENTS_HOME"]);
|
|
4027
|
+
}
|
|
4028
|
+
function usesIsolatedTodosHome() {
|
|
4029
|
+
return isUnder(tmpdir(), getTodosGlobalDir());
|
|
4030
|
+
}
|
|
4031
|
+
function shouldEmitSharedTaskEvents(dbPath) {
|
|
4032
|
+
if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
|
|
4033
|
+
return false;
|
|
4034
|
+
if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
|
|
4035
|
+
return true;
|
|
4036
|
+
if (!isEphemeralTodosDatabase(dbPath))
|
|
4037
|
+
return true;
|
|
4038
|
+
return hasExplicitSharedEventsStore();
|
|
4039
|
+
}
|
|
4040
|
+
function shouldDeliverLocalLifecycleHooks(dbPath) {
|
|
4041
|
+
if (envFlag("TODOS_DISABLE_LOCAL_EVENT_HOOKS"))
|
|
4042
|
+
return false;
|
|
4043
|
+
if (envFlag("TODOS_ALLOW_EPHEMERAL_LOCAL_EVENT_HOOKS"))
|
|
4044
|
+
return true;
|
|
4045
|
+
if (!isEphemeralTodosDatabase(dbPath))
|
|
4046
|
+
return true;
|
|
4047
|
+
return usesIsolatedTodosHome();
|
|
4048
|
+
}
|
|
4049
|
+
|
|
3880
4050
|
// src/lib/event-hooks.ts
|
|
3881
4051
|
init_redaction();
|
|
3882
4052
|
import { createHash, randomUUID } from "crypto";
|
|
3883
4053
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
3884
|
-
import { dirname as dirname3, resolve as
|
|
4054
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
3885
4055
|
import { createConnection } from "net";
|
|
3886
4056
|
|
|
3887
4057
|
// src/lib/runner-sandbox.ts
|
|
3888
4058
|
init_config();
|
|
3889
|
-
import { relative as relative2, resolve as
|
|
4059
|
+
import { relative as relative2, resolve as resolve5 } from "path";
|
|
3890
4060
|
|
|
3891
4061
|
// src/lib/workspace-trust.ts
|
|
3892
4062
|
init_config();
|
|
3893
|
-
import { relative, resolve as
|
|
4063
|
+
import { relative, resolve as resolve4 } from "path";
|
|
3894
4064
|
var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
3895
4065
|
var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
3896
4066
|
var PRESET_DEFAULTS = {
|
|
@@ -3936,7 +4106,7 @@ var PRESET_DEFAULTS = {
|
|
|
3936
4106
|
}
|
|
3937
4107
|
};
|
|
3938
4108
|
function normalizePath(path) {
|
|
3939
|
-
return
|
|
4109
|
+
return resolve4(path);
|
|
3940
4110
|
}
|
|
3941
4111
|
function unique2(values) {
|
|
3942
4112
|
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
@@ -4073,7 +4243,7 @@ function checkWorkspacePermission(input = {}) {
|
|
|
4073
4243
|
var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
4074
4244
|
var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
4075
4245
|
function normalizePath2(path) {
|
|
4076
|
-
return
|
|
4246
|
+
return resolve5(path);
|
|
4077
4247
|
}
|
|
4078
4248
|
function unique3(values) {
|
|
4079
4249
|
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
@@ -4419,7 +4589,7 @@ async function deliverHook(hook, envelope) {
|
|
|
4419
4589
|
if (hook.target === "stdout") {
|
|
4420
4590
|
output = line.trim();
|
|
4421
4591
|
} else if (hook.target === "file") {
|
|
4422
|
-
const filePath =
|
|
4592
|
+
const filePath = resolve6(hook.file_path);
|
|
4423
4593
|
mkdirSync3(dirname3(filePath), { recursive: true });
|
|
4424
4594
|
appendFileSync(filePath, line);
|
|
4425
4595
|
} else if (hook.target === "socket") {
|
|
@@ -4494,6 +4664,8 @@ async function emitLocalEventHooks(input) {
|
|
|
4494
4664
|
return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
|
|
4495
4665
|
}
|
|
4496
4666
|
function emitLocalEventHooksQuiet(input) {
|
|
4667
|
+
if (!shouldDeliverLocalLifecycleHooks(input.databasePath))
|
|
4668
|
+
return;
|
|
4497
4669
|
emitLocalEventHooks(input).catch(() => {});
|
|
4498
4670
|
}
|
|
4499
4671
|
async function testLocalEventHook(name, input) {
|
|
@@ -4810,7 +4982,7 @@ async function dispatchCommand(event, channel) {
|
|
|
4810
4982
|
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
4811
4983
|
HASNA_EVENT_JSON: eventJson
|
|
4812
4984
|
};
|
|
4813
|
-
return new Promise((
|
|
4985
|
+
return new Promise((resolve7) => {
|
|
4814
4986
|
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
4815
4987
|
cwd: channel.command.cwd,
|
|
4816
4988
|
env,
|
|
@@ -4828,7 +5000,7 @@ async function dispatchCommand(event, channel) {
|
|
|
4828
5000
|
});
|
|
4829
5001
|
child.on("error", (error) => {
|
|
4830
5002
|
clearTimeout(timeout);
|
|
4831
|
-
|
|
5003
|
+
resolve7({
|
|
4832
5004
|
attempt: 1,
|
|
4833
5005
|
status: "failed",
|
|
4834
5006
|
startedAt,
|
|
@@ -4841,7 +5013,7 @@ async function dispatchCommand(event, channel) {
|
|
|
4841
5013
|
child.on("close", (code, signal) => {
|
|
4842
5014
|
clearTimeout(timeout);
|
|
4843
5015
|
const success = code === 0;
|
|
4844
|
-
|
|
5016
|
+
resolve7({
|
|
4845
5017
|
attempt: 1,
|
|
4846
5018
|
status: success ? "success" : "failed",
|
|
4847
5019
|
startedAt,
|
|
@@ -5109,14 +5281,15 @@ function createTaskList(input, db) {
|
|
|
5109
5281
|
const id = uuid();
|
|
5110
5282
|
const timestamp = now();
|
|
5111
5283
|
const slug = input.slug || slugify(input.name);
|
|
5284
|
+
const machineId = currentStorageMachineId(d);
|
|
5112
5285
|
if (!input.project_id) {
|
|
5113
5286
|
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
5114
5287
|
if (existing) {
|
|
5115
5288
|
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
5116
5289
|
}
|
|
5117
5290
|
}
|
|
5118
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
5119
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
5291
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
|
|
5292
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
|
|
5120
5293
|
return getTaskList(id, d);
|
|
5121
5294
|
}
|
|
5122
5295
|
function getTaskList(id, db) {
|
|
@@ -5166,6 +5339,14 @@ function updateTaskList(id, input, db) {
|
|
|
5166
5339
|
}
|
|
5167
5340
|
function deleteTaskList(id, db) {
|
|
5168
5341
|
const d = db || getDatabase();
|
|
5342
|
+
const list = getTaskList(id, d);
|
|
5343
|
+
if (!list)
|
|
5344
|
+
return false;
|
|
5345
|
+
recordStorageTombstone({
|
|
5346
|
+
object_type: "task_lists",
|
|
5347
|
+
object_id: id,
|
|
5348
|
+
payload: list
|
|
5349
|
+
}, d);
|
|
5169
5350
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
5170
5351
|
}
|
|
5171
5352
|
function ensureTaskList(name, slug, projectId, db) {
|
|
@@ -5176,39 +5357,9 @@ function ensureTaskList(name, slug, projectId, db) {
|
|
|
5176
5357
|
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
5177
5358
|
}
|
|
5178
5359
|
|
|
5179
|
-
// src/lib/
|
|
5180
|
-
var
|
|
5181
|
-
|
|
5182
|
-
return {
|
|
5183
|
-
id: task.id,
|
|
5184
|
-
task_id: task.id,
|
|
5185
|
-
short_id: task.short_id,
|
|
5186
|
-
title: task.title,
|
|
5187
|
-
description: task.description,
|
|
5188
|
-
status: task.status,
|
|
5189
|
-
priority: task.priority,
|
|
5190
|
-
project_id: task.project_id,
|
|
5191
|
-
parent_id: task.parent_id,
|
|
5192
|
-
plan_id: task.plan_id,
|
|
5193
|
-
task_list_id: task.task_list_id,
|
|
5194
|
-
agent_id: task.agent_id,
|
|
5195
|
-
assigned_to: task.assigned_to,
|
|
5196
|
-
session_id: task.session_id,
|
|
5197
|
-
working_dir: task.working_dir,
|
|
5198
|
-
tags: task.tags,
|
|
5199
|
-
metadata: task.metadata,
|
|
5200
|
-
version: task.version,
|
|
5201
|
-
created_at: task.created_at,
|
|
5202
|
-
updated_at: task.updated_at,
|
|
5203
|
-
started_at: task.started_at,
|
|
5204
|
-
completed_at: task.completed_at,
|
|
5205
|
-
due_at: task.due_at,
|
|
5206
|
-
requires_approval: task.requires_approval,
|
|
5207
|
-
approved_by: task.approved_by,
|
|
5208
|
-
approved_at: task.approved_at,
|
|
5209
|
-
...extra
|
|
5210
|
-
};
|
|
5211
|
-
}
|
|
5360
|
+
// src/lib/task-route-contract.ts
|
|
5361
|
+
var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1";
|
|
5362
|
+
var TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
|
|
5212
5363
|
function booleanField(value) {
|
|
5213
5364
|
if (typeof value === "boolean")
|
|
5214
5365
|
return value;
|
|
@@ -5230,21 +5381,31 @@ function booleanField(value) {
|
|
|
5230
5381
|
function objectField(value) {
|
|
5231
5382
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
5232
5383
|
}
|
|
5233
|
-
function
|
|
5384
|
+
function collectBooleans(records, keys) {
|
|
5385
|
+
const values = [];
|
|
5234
5386
|
for (const record of records) {
|
|
5387
|
+
if (!record)
|
|
5388
|
+
continue;
|
|
5235
5389
|
for (const key of keys) {
|
|
5236
5390
|
const value = booleanField(record[key]);
|
|
5237
5391
|
if (value !== undefined)
|
|
5238
|
-
|
|
5392
|
+
values.push(value);
|
|
5239
5393
|
}
|
|
5240
5394
|
}
|
|
5241
|
-
return;
|
|
5395
|
+
return values;
|
|
5242
5396
|
}
|
|
5243
|
-
function
|
|
5244
|
-
const
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5397
|
+
function mergedBoolean(records, keys, trueWins) {
|
|
5398
|
+
const values = collectBooleans(records, keys);
|
|
5399
|
+
if (values.length === 0)
|
|
5400
|
+
return;
|
|
5401
|
+
if (trueWins)
|
|
5402
|
+
return values.some(Boolean);
|
|
5403
|
+
return values.includes(false) ? false : true;
|
|
5404
|
+
}
|
|
5405
|
+
function routingAutomationMetadata(task, taskList) {
|
|
5406
|
+
const taskAutomation = objectField(task.metadata.automation);
|
|
5407
|
+
const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
|
|
5408
|
+
const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
|
|
5248
5409
|
const result = {};
|
|
5249
5410
|
const aliases = [
|
|
5250
5411
|
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
@@ -5255,7 +5416,7 @@ function routingAutomationMetadata(task) {
|
|
|
5255
5416
|
["approval_required", ["approval_required", "approvalRequired"]]
|
|
5256
5417
|
];
|
|
5257
5418
|
for (const [canonical, keys] of aliases) {
|
|
5258
|
-
const value =
|
|
5419
|
+
const value = mergedBoolean(records, keys, canonical !== "allowed");
|
|
5259
5420
|
if (value !== undefined)
|
|
5260
5421
|
result[canonical] = value;
|
|
5261
5422
|
}
|
|
@@ -5263,23 +5424,91 @@ function routingAutomationMetadata(task) {
|
|
|
5263
5424
|
result.requires_approval = true;
|
|
5264
5425
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
5265
5426
|
}
|
|
5427
|
+
function routeEnabledForTask(task, taskList) {
|
|
5428
|
+
const explicit = booleanField(task.metadata.route_enabled);
|
|
5429
|
+
if (explicit !== undefined)
|
|
5430
|
+
return explicit;
|
|
5431
|
+
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
5432
|
+
return true;
|
|
5433
|
+
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
5434
|
+
if (taskListDefault !== undefined)
|
|
5435
|
+
return taskListDefault;
|
|
5436
|
+
return;
|
|
5437
|
+
}
|
|
5438
|
+
function stringField(value) {
|
|
5439
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
5440
|
+
}
|
|
5441
|
+
function workflowPointersFromMetadata(metadata) {
|
|
5442
|
+
const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
|
|
5443
|
+
return {
|
|
5444
|
+
current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
|
|
5445
|
+
current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
|
|
5446
|
+
latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
|
|
5447
|
+
latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
|
|
5448
|
+
workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5451
|
+
function compactWorkflowPointers(pointers) {
|
|
5452
|
+
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
5453
|
+
}
|
|
5454
|
+
function classifyProjectKind(path) {
|
|
5455
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
5456
|
+
}
|
|
5457
|
+
function isWorktreePath(path) {
|
|
5458
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
5459
|
+
}
|
|
5460
|
+
function inferRootProjectId(project) {
|
|
5461
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
// src/lib/shared-events.ts
|
|
5465
|
+
var SOURCE = "todos";
|
|
5466
|
+
function taskEventData(task, extra = {}) {
|
|
5467
|
+
return {
|
|
5468
|
+
id: task.id,
|
|
5469
|
+
task_id: task.id,
|
|
5470
|
+
short_id: task.short_id,
|
|
5471
|
+
title: task.title,
|
|
5472
|
+
description: task.description,
|
|
5473
|
+
status: task.status,
|
|
5474
|
+
priority: task.priority,
|
|
5475
|
+
project_id: task.project_id,
|
|
5476
|
+
parent_id: task.parent_id,
|
|
5477
|
+
plan_id: task.plan_id,
|
|
5478
|
+
task_list_id: task.task_list_id,
|
|
5479
|
+
agent_id: task.agent_id,
|
|
5480
|
+
assigned_to: task.assigned_to,
|
|
5481
|
+
session_id: task.session_id,
|
|
5482
|
+
working_dir: task.working_dir,
|
|
5483
|
+
tags: task.tags,
|
|
5484
|
+
metadata: task.metadata,
|
|
5485
|
+
version: task.version,
|
|
5486
|
+
created_at: task.created_at,
|
|
5487
|
+
updated_at: task.updated_at,
|
|
5488
|
+
started_at: task.started_at,
|
|
5489
|
+
completed_at: task.completed_at,
|
|
5490
|
+
due_at: task.due_at,
|
|
5491
|
+
requires_approval: task.requires_approval,
|
|
5492
|
+
approved_by: task.approved_by,
|
|
5493
|
+
approved_at: task.approved_at,
|
|
5494
|
+
...extra
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
5266
5497
|
function taskEventMetadata(task) {
|
|
5267
5498
|
const metadata = {
|
|
5268
5499
|
package: "@hasna/todos",
|
|
5269
5500
|
todos_event_schema_version: 1,
|
|
5501
|
+
route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
|
|
5270
5502
|
task_id: task.id,
|
|
5271
5503
|
task_short_id: task.short_id,
|
|
5272
5504
|
project_id: task.project_id,
|
|
5273
5505
|
task_list_id: task.task_list_id,
|
|
5274
5506
|
working_dir: task.working_dir
|
|
5275
5507
|
};
|
|
5276
|
-
const
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
const automation = routingAutomationMetadata(task);
|
|
5281
|
-
if (automation) {
|
|
5282
|
-
metadata.automation = automation;
|
|
5508
|
+
const pointers = workflowPointersFromMetadata(task.metadata);
|
|
5509
|
+
for (const [key, value] of Object.entries(pointers)) {
|
|
5510
|
+
if (value)
|
|
5511
|
+
metadata[key] = value;
|
|
5283
5512
|
}
|
|
5284
5513
|
try {
|
|
5285
5514
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
@@ -5308,18 +5537,20 @@ function taskEventMetadata(task) {
|
|
|
5308
5537
|
metadata.task_list_project_id = taskList.project_id;
|
|
5309
5538
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
5310
5539
|
}
|
|
5540
|
+
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
5541
|
+
if (routeEnabled !== undefined) {
|
|
5542
|
+
metadata.route_enabled = routeEnabled;
|
|
5543
|
+
}
|
|
5544
|
+
const automation = routingAutomationMetadata(task, taskList);
|
|
5545
|
+
if (automation) {
|
|
5546
|
+
metadata.automation = automation;
|
|
5547
|
+
metadata.route_blocked_by_no_auto = automation.no_auto === true;
|
|
5548
|
+
metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
|
|
5549
|
+
metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
|
|
5550
|
+
}
|
|
5311
5551
|
} catch {}
|
|
5312
5552
|
return metadata;
|
|
5313
5553
|
}
|
|
5314
|
-
function classifyProjectKind(path) {
|
|
5315
|
-
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
5316
|
-
}
|
|
5317
|
-
function isWorktreePath(path) {
|
|
5318
|
-
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
5319
|
-
}
|
|
5320
|
-
function inferRootProjectId(project) {
|
|
5321
|
-
return isWorktreePath(project.path) ? null : project.id;
|
|
5322
|
-
}
|
|
5323
5554
|
function readMachineLocalPath(project) {
|
|
5324
5555
|
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
5325
5556
|
if (!machineId)
|
|
@@ -5332,6 +5563,8 @@ function readMachineLocalPath(project) {
|
|
|
5332
5563
|
}
|
|
5333
5564
|
}
|
|
5334
5565
|
async function emitSharedTaskEvent(input) {
|
|
5566
|
+
if (!shouldEmitSharedTaskEvents(input.databasePath))
|
|
5567
|
+
return;
|
|
5335
5568
|
const data = taskEventData(input.task, input.data);
|
|
5336
5569
|
await new EventsClient().emit({
|
|
5337
5570
|
source: SOURCE,
|
|
@@ -5356,8 +5589,9 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
|
5356
5589
|
const d = db || getDatabase();
|
|
5357
5590
|
const id = uuid();
|
|
5358
5591
|
const timestamp = now();
|
|
5359
|
-
|
|
5360
|
-
|
|
5592
|
+
const machineId = currentStorageMachineId(d);
|
|
5593
|
+
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
5594
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
|
|
5361
5595
|
try {
|
|
5362
5596
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
5363
5597
|
logActivity2({
|
|
@@ -5370,7 +5604,7 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
|
5370
5604
|
actor_id: agentId ?? undefined
|
|
5371
5605
|
}, d);
|
|
5372
5606
|
} catch {}
|
|
5373
|
-
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
5607
|
+
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
|
|
5374
5608
|
}
|
|
5375
5609
|
function getTaskHistory(taskId, db) {
|
|
5376
5610
|
const d = db || getDatabase();
|
|
@@ -5719,13 +5953,14 @@ function createTask(input, db) {
|
|
|
5719
5953
|
const d = db || getDatabase();
|
|
5720
5954
|
const timestamp = now();
|
|
5721
5955
|
const tags = input.tags || [];
|
|
5956
|
+
const machineId = currentStorageMachineId(d);
|
|
5722
5957
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
5723
5958
|
const assignedFromProject = input.assigned_from_project || null;
|
|
5724
5959
|
let id = uuid();
|
|
5725
5960
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
5726
5961
|
try {
|
|
5727
|
-
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)
|
|
5728
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5962
|
+
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)
|
|
5963
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5729
5964
|
id,
|
|
5730
5965
|
null,
|
|
5731
5966
|
input.project_id || null,
|
|
@@ -5762,7 +5997,8 @@ function createTask(input, db) {
|
|
|
5762
5997
|
input.spawned_from_session || null,
|
|
5763
5998
|
assignedBy || null,
|
|
5764
5999
|
assignedFromProject || null,
|
|
5765
|
-
input.task_type || null
|
|
6000
|
+
input.task_type || null,
|
|
6001
|
+
machineId
|
|
5766
6002
|
]);
|
|
5767
6003
|
break;
|
|
5768
6004
|
} catch (e) {
|
|
@@ -5778,9 +6014,10 @@ function createTask(input, db) {
|
|
|
5778
6014
|
}
|
|
5779
6015
|
const task = getTask(id, d);
|
|
5780
6016
|
const payload = taskEventData(task);
|
|
6017
|
+
const databasePath = databasePathFromDatabase(d);
|
|
5781
6018
|
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5782
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
5783
|
-
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
6019
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
6020
|
+
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
5784
6021
|
return task;
|
|
5785
6022
|
}
|
|
5786
6023
|
function getTask(id, db) {
|
|
@@ -6198,29 +6435,39 @@ function updateTask(id, input, db) {
|
|
|
6198
6435
|
approved_by: input.approved_by ?? task.approved_by,
|
|
6199
6436
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
6200
6437
|
};
|
|
6438
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6201
6439
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
6202
6440
|
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
6203
6441
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6204
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
6205
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
6442
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
6443
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
6206
6444
|
}
|
|
6207
6445
|
if (input.status !== undefined && input.status !== task.status) {
|
|
6208
6446
|
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
6209
6447
|
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
6210
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
6211
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
6448
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
6449
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
6212
6450
|
}
|
|
6213
6451
|
if (input.approved_by !== undefined) {
|
|
6214
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
6452
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
6215
6453
|
}
|
|
6216
6454
|
const updatePayload = taskEventData(updatedTask);
|
|
6217
6455
|
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
6218
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
6219
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
6456
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
6457
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
6220
6458
|
return updatedTask;
|
|
6221
6459
|
}
|
|
6222
6460
|
function deleteTask(id, db) {
|
|
6223
6461
|
const d = db || getDatabase();
|
|
6462
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
6463
|
+
if (!row)
|
|
6464
|
+
return false;
|
|
6465
|
+
recordStorageTombstone({
|
|
6466
|
+
object_type: "tasks",
|
|
6467
|
+
object_id: id,
|
|
6468
|
+
payload: rowToTask(row),
|
|
6469
|
+
version: row.version
|
|
6470
|
+
}, d);
|
|
6224
6471
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
6225
6472
|
return result.changes > 0;
|
|
6226
6473
|
}
|
|
@@ -6352,8 +6599,9 @@ function resolveTemplateId(id, d) {
|
|
|
6352
6599
|
function createTemplate(input, db) {
|
|
6353
6600
|
const d = db || getDatabase();
|
|
6354
6601
|
const id = uuid();
|
|
6355
|
-
|
|
6356
|
-
|
|
6602
|
+
const machineId = currentStorageMachineId(d);
|
|
6603
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
|
|
6604
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6357
6605
|
id,
|
|
6358
6606
|
input.name,
|
|
6359
6607
|
input.title_pattern,
|
|
@@ -6364,7 +6612,8 @@ function createTemplate(input, db) {
|
|
|
6364
6612
|
input.project_id || null,
|
|
6365
6613
|
input.plan_id || null,
|
|
6366
6614
|
JSON.stringify(input.metadata || {}),
|
|
6367
|
-
now()
|
|
6615
|
+
now(),
|
|
6616
|
+
machineId
|
|
6368
6617
|
]);
|
|
6369
6618
|
if (input.tasks && input.tasks.length > 0) {
|
|
6370
6619
|
addTemplateTasks(id, input.tasks, d);
|
|
@@ -6388,6 +6637,15 @@ function deleteTemplate(id, db) {
|
|
|
6388
6637
|
const resolved = resolveTemplateId(id, d);
|
|
6389
6638
|
if (!resolved)
|
|
6390
6639
|
return false;
|
|
6640
|
+
const template = getTemplate(resolved, d);
|
|
6641
|
+
if (!template)
|
|
6642
|
+
return false;
|
|
6643
|
+
recordStorageTombstone({
|
|
6644
|
+
object_type: "templates",
|
|
6645
|
+
object_id: resolved,
|
|
6646
|
+
payload: template,
|
|
6647
|
+
version: template.version
|
|
6648
|
+
}, d);
|
|
6391
6649
|
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
6392
6650
|
}
|
|
6393
6651
|
function updateTemplate(id, updates, db) {
|
|
@@ -6917,6 +7175,7 @@ function getBlockingDeps(id, db) {
|
|
|
6917
7175
|
}
|
|
6918
7176
|
function startTask(id, agentId, db) {
|
|
6919
7177
|
const d = db || getDatabase();
|
|
7178
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6920
7179
|
const task = getTask(id, d);
|
|
6921
7180
|
if (!task)
|
|
6922
7181
|
throw new TaskNotFoundError(id);
|
|
@@ -6931,7 +7190,8 @@ function startTask(id, agentId, db) {
|
|
|
6931
7190
|
agent_id: agentId,
|
|
6932
7191
|
title: task.title,
|
|
6933
7192
|
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
6934
|
-
}
|
|
7193
|
+
},
|
|
7194
|
+
databasePath
|
|
6935
7195
|
});
|
|
6936
7196
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
6937
7197
|
}
|
|
@@ -6953,12 +7213,13 @@ function startTask(id, agentId, db) {
|
|
|
6953
7213
|
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 };
|
|
6954
7214
|
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
6955
7215
|
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
6956
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
6957
|
-
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
7216
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
7217
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
6958
7218
|
return startedTask;
|
|
6959
7219
|
}
|
|
6960
7220
|
function completeTask(id, agentId, db, options) {
|
|
6961
7221
|
const d = db || getDatabase();
|
|
7222
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6962
7223
|
const task = getTask(id, d);
|
|
6963
7224
|
if (!task)
|
|
6964
7225
|
throw new TaskNotFoundError(id);
|
|
@@ -7004,8 +7265,8 @@ function completeTask(id, agentId, db, options) {
|
|
|
7004
7265
|
};
|
|
7005
7266
|
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
7006
7267
|
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7007
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
7008
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
7268
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
7269
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
7009
7270
|
let spawnedTask = null;
|
|
7010
7271
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7011
7272
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -7049,9 +7310,9 @@ function completeTask(id, agentId, db, options) {
|
|
|
7049
7310
|
const depTask = getTask(dep.id, d);
|
|
7050
7311
|
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
7051
7312
|
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
7052
|
-
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
7313
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
7053
7314
|
if (depTask)
|
|
7054
|
-
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
7315
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
7055
7316
|
}
|
|
7056
7317
|
}
|
|
7057
7318
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -7221,6 +7482,7 @@ function getTasksChangedSince(since, filters, db) {
|
|
|
7221
7482
|
}
|
|
7222
7483
|
function failTask(id, agentId, reason, options, db) {
|
|
7223
7484
|
const d = db || getDatabase();
|
|
7485
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7224
7486
|
const task = getTask(id, d);
|
|
7225
7487
|
if (!task)
|
|
7226
7488
|
throw new TaskNotFoundError(id);
|
|
@@ -7249,8 +7511,8 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7249
7511
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
7250
7512
|
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
7251
7513
|
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
7252
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
7253
|
-
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
7514
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
7515
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
|
|
7254
7516
|
let retryTask;
|
|
7255
7517
|
if (options?.retry) {
|
|
7256
7518
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -7310,6 +7572,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
|
|
|
7310
7572
|
}
|
|
7311
7573
|
function stealTask(agentId, opts, db) {
|
|
7312
7574
|
const d = db || getDatabase();
|
|
7575
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7313
7576
|
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
7314
7577
|
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
7315
7578
|
if (staleTasks.length === 0)
|
|
@@ -7328,8 +7591,8 @@ function stealTask(agentId, opts, db) {
|
|
|
7328
7591
|
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
7329
7592
|
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7330
7593
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7331
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
7332
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
7594
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
7595
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
7333
7596
|
return stolenTask;
|
|
7334
7597
|
}
|
|
7335
7598
|
function claimOrSteal(agentId, filters, db) {
|
|
@@ -7920,8 +8183,9 @@ function createPlan(input, db) {
|
|
|
7920
8183
|
const d = db || getDatabase();
|
|
7921
8184
|
const id = uuid();
|
|
7922
8185
|
const timestamp = now();
|
|
7923
|
-
|
|
7924
|
-
|
|
8186
|
+
const machineId = currentStorageMachineId(d);
|
|
8187
|
+
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
8188
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7925
8189
|
id,
|
|
7926
8190
|
input.project_id || null,
|
|
7927
8191
|
input.task_list_id || null,
|
|
@@ -7930,7 +8194,8 @@ function createPlan(input, db) {
|
|
|
7930
8194
|
input.description || null,
|
|
7931
8195
|
input.status || "active",
|
|
7932
8196
|
timestamp,
|
|
7933
|
-
timestamp
|
|
8197
|
+
timestamp,
|
|
8198
|
+
machineId
|
|
7934
8199
|
]);
|
|
7935
8200
|
return getPlan(id, d);
|
|
7936
8201
|
}
|
|
@@ -7978,12 +8243,21 @@ function updatePlan(id, input, db) {
|
|
|
7978
8243
|
const updated = getPlan(id, d);
|
|
7979
8244
|
emitLocalEventHooksQuiet({
|
|
7980
8245
|
type: "plan.updated",
|
|
7981
|
-
payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
|
|
8246
|
+
payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id },
|
|
8247
|
+
databasePath: databasePathFromDatabase(d)
|
|
7982
8248
|
});
|
|
7983
8249
|
return updated;
|
|
7984
8250
|
}
|
|
7985
8251
|
function deletePlan(id, db) {
|
|
7986
8252
|
const d = db || getDatabase();
|
|
8253
|
+
const plan = getPlan(id, d);
|
|
8254
|
+
if (!plan)
|
|
8255
|
+
return false;
|
|
8256
|
+
recordStorageTombstone({
|
|
8257
|
+
object_type: "plans",
|
|
8258
|
+
object_id: id,
|
|
8259
|
+
payload: plan
|
|
8260
|
+
}, d);
|
|
7987
8261
|
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
7988
8262
|
return result.changes > 0;
|
|
7989
8263
|
}
|
|
@@ -8390,20 +8664,20 @@ init_database();
|
|
|
8390
8664
|
init_redaction();
|
|
8391
8665
|
import { createHash as createHash2 } from "crypto";
|
|
8392
8666
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
8393
|
-
import { basename, dirname as dirname4, join as join5, resolve as
|
|
8394
|
-
import { tmpdir } from "os";
|
|
8667
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve7 } from "path";
|
|
8668
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
8395
8669
|
function isInMemoryDb2(path) {
|
|
8396
8670
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
8397
8671
|
}
|
|
8398
8672
|
function artifactStoreRoot() {
|
|
8399
8673
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
8400
|
-
return
|
|
8674
|
+
return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
8401
8675
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
8402
|
-
return
|
|
8676
|
+
return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
8403
8677
|
const dbPath = getDatabasePath();
|
|
8404
8678
|
if (isInMemoryDb2(dbPath))
|
|
8405
|
-
return join5(
|
|
8406
|
-
return join5(dirname4(
|
|
8679
|
+
return join5(tmpdir2(), "hasna-todos-artifacts");
|
|
8680
|
+
return join5(dirname4(resolve7(dbPath)), "artifacts");
|
|
8407
8681
|
}
|
|
8408
8682
|
function artifactStorePath(relativePath) {
|
|
8409
8683
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
@@ -8450,7 +8724,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
8450
8724
|
return "application/octet-stream";
|
|
8451
8725
|
}
|
|
8452
8726
|
function storeArtifactContent(input) {
|
|
8453
|
-
const sourcePath =
|
|
8727
|
+
const sourcePath = resolve7(input.path);
|
|
8454
8728
|
if (!existsSync7(sourcePath))
|
|
8455
8729
|
return null;
|
|
8456
8730
|
const sourceStat = statSync2(sourcePath);
|
|
@@ -8606,19 +8880,19 @@ function importStoredArtifactContent(content) {
|
|
|
8606
8880
|
var DEFAULT_DELETED_RETENTION_DAYS = 30;
|
|
8607
8881
|
function getArtifactStoreRoot(dbPath) {
|
|
8608
8882
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
8609
|
-
return
|
|
8883
|
+
return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
8610
8884
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
8611
|
-
return
|
|
8885
|
+
return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
8612
8886
|
const path = dbPath ?? getDatabasePath();
|
|
8613
8887
|
if (isInMemoryDb2(path))
|
|
8614
|
-
return join5(
|
|
8615
|
-
return join5(dirname4(
|
|
8888
|
+
return join5(tmpdir2(), "hasna-todos-artifacts");
|
|
8889
|
+
return join5(dirname4(resolve7(path)), "artifacts");
|
|
8616
8890
|
}
|
|
8617
8891
|
function computeContentHash(path) {
|
|
8618
|
-
return sha256(readFileSync3(
|
|
8892
|
+
return sha256(readFileSync3(resolve7(path)));
|
|
8619
8893
|
}
|
|
8620
8894
|
function storeArtifactFile(input) {
|
|
8621
|
-
const sourcePath =
|
|
8895
|
+
const sourcePath = resolve7(input.sourcePath);
|
|
8622
8896
|
if (!existsSync7(sourcePath)) {
|
|
8623
8897
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
8624
8898
|
}
|
|
@@ -8667,7 +8941,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
|
|
|
8667
8941
|
};
|
|
8668
8942
|
}
|
|
8669
8943
|
function writeArtifactExportManifest(manifest, outputPath) {
|
|
8670
|
-
const destination =
|
|
8944
|
+
const destination = resolve7(outputPath);
|
|
8671
8945
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
8672
8946
|
writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
|
|
8673
8947
|
`);
|
|
@@ -9054,7 +9328,11 @@ function startTaskRun(input, db) {
|
|
|
9054
9328
|
}, d);
|
|
9055
9329
|
}
|
|
9056
9330
|
const run = getTaskRun(id, d);
|
|
9057
|
-
emitLocalEventHooksQuiet({
|
|
9331
|
+
emitLocalEventHooksQuiet({
|
|
9332
|
+
type: "run.started",
|
|
9333
|
+
payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title },
|
|
9334
|
+
databasePath: databasePathFromDatabase(d)
|
|
9335
|
+
});
|
|
9058
9336
|
return run;
|
|
9059
9337
|
}
|
|
9060
9338
|
function beginTaskRunTransaction(input, db) {
|
|
@@ -9323,7 +9601,8 @@ function finishTaskRun(input, db) {
|
|
|
9323
9601
|
const updated = getTaskRun(run.id, d);
|
|
9324
9602
|
emitLocalEventHooksQuiet({
|
|
9325
9603
|
type: `run.${input.status}`,
|
|
9326
|
-
payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
|
|
9604
|
+
payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
|
|
9605
|
+
databasePath: databasePathFromDatabase(d)
|
|
9327
9606
|
});
|
|
9328
9607
|
return updated;
|
|
9329
9608
|
}
|
|
@@ -10155,6 +10434,7 @@ function rowToAgent(row) {
|
|
|
10155
10434
|
}
|
|
10156
10435
|
function registerAgent(input, db) {
|
|
10157
10436
|
const d = db || getDatabase();
|
|
10437
|
+
const machineId = currentStorageMachineId(d);
|
|
10158
10438
|
const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
|
|
10159
10439
|
const normalizedName = validateAgentName(input.name, existingNames);
|
|
10160
10440
|
const existing = getAgentByName(normalizedName, d);
|
|
@@ -10195,14 +10475,18 @@ function registerAgent(input, db) {
|
|
|
10195
10475
|
updates.push("active_project_id = ?");
|
|
10196
10476
|
params.push(input.project_id);
|
|
10197
10477
|
}
|
|
10478
|
+
if (!existing.machine_id && machineId) {
|
|
10479
|
+
updates.push("machine_id = ?");
|
|
10480
|
+
params.push(machineId);
|
|
10481
|
+
}
|
|
10198
10482
|
params.push(existing.id);
|
|
10199
10483
|
d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
10200
10484
|
return getAgent(existing.id, d);
|
|
10201
10485
|
}
|
|
10202
10486
|
const id = shortUuid();
|
|
10203
10487
|
const timestamp = now();
|
|
10204
|
-
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id)
|
|
10205
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10488
|
+
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id, machine_id)
|
|
10489
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10206
10490
|
id,
|
|
10207
10491
|
normalizedName,
|
|
10208
10492
|
input.description || null,
|
|
@@ -10218,7 +10502,8 @@ function registerAgent(input, db) {
|
|
|
10218
10502
|
timestamp,
|
|
10219
10503
|
input.session_id || null,
|
|
10220
10504
|
input.working_dir || null,
|
|
10221
|
-
input.project_id && input.session_id ? input.project_id : null
|
|
10505
|
+
input.project_id && input.session_id ? input.project_id : null,
|
|
10506
|
+
machineId
|
|
10222
10507
|
]);
|
|
10223
10508
|
return getAgent(id, d);
|
|
10224
10509
|
}
|
|
@@ -10418,6 +10703,14 @@ var PROJECT_COLUMNS = [
|
|
|
10418
10703
|
"machine_id",
|
|
10419
10704
|
"synced_at"
|
|
10420
10705
|
];
|
|
10706
|
+
var PROJECT_MACHINE_PATH_COLUMNS = [
|
|
10707
|
+
"id",
|
|
10708
|
+
"project_id",
|
|
10709
|
+
"machine_id",
|
|
10710
|
+
"path",
|
|
10711
|
+
"created_at",
|
|
10712
|
+
"updated_at"
|
|
10713
|
+
];
|
|
10421
10714
|
var TASK_LIST_COLUMNS = [
|
|
10422
10715
|
"id",
|
|
10423
10716
|
"project_id",
|
|
@@ -10557,11 +10850,13 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
10557
10850
|
source: "sqlite",
|
|
10558
10851
|
tasks: listTasks({ include_archived: true }, d),
|
|
10559
10852
|
projects: listProjects(d),
|
|
10853
|
+
projectMachinePaths: listProjectMachinePaths(d),
|
|
10560
10854
|
plans: listPlans(undefined, d),
|
|
10561
10855
|
agents: listAgents({ include_archived: true }, d),
|
|
10562
10856
|
taskLists: listTaskLists(undefined, d),
|
|
10563
10857
|
templates: listTemplates(d),
|
|
10564
|
-
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
|
|
10858
|
+
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
10859
|
+
tombstones: listStorageTombstones(d)
|
|
10565
10860
|
};
|
|
10566
10861
|
}
|
|
10567
10862
|
function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
@@ -10569,13 +10864,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
10569
10864
|
const result = {
|
|
10570
10865
|
inserted: 0,
|
|
10571
10866
|
updated: 0,
|
|
10867
|
+
deleted: 0,
|
|
10572
10868
|
skipped: 0,
|
|
10573
10869
|
errors: []
|
|
10574
10870
|
};
|
|
10575
|
-
const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
10871
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
10576
10872
|
for (const row of rows) {
|
|
10577
10873
|
try {
|
|
10578
10874
|
const record = asRecord(row);
|
|
10875
|
+
const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
|
|
10876
|
+
if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
|
|
10877
|
+
result.skipped += 1;
|
|
10878
|
+
continue;
|
|
10879
|
+
}
|
|
10579
10880
|
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
10580
10881
|
if (state === "inserted")
|
|
10581
10882
|
result.inserted += 1;
|
|
@@ -10589,17 +10890,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
10589
10890
|
}
|
|
10590
10891
|
}
|
|
10591
10892
|
};
|
|
10592
|
-
applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
10593
|
-
applyRows("
|
|
10594
|
-
applyRows("
|
|
10595
|
-
applyRows("
|
|
10596
|
-
applyRows("
|
|
10597
|
-
applyRows("
|
|
10893
|
+
applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
10894
|
+
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
10895
|
+
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
10896
|
+
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
10897
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
10898
|
+
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
10899
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
10598
10900
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
10599
10901
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
10600
10902
|
}
|
|
10601
10903
|
});
|
|
10602
|
-
applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
10904
|
+
applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
10905
|
+
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
10603
10906
|
return result;
|
|
10604
10907
|
}
|
|
10605
10908
|
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
@@ -10654,6 +10957,85 @@ function sortedTasks(tasks) {
|
|
|
10654
10957
|
visit(task);
|
|
10655
10958
|
return result;
|
|
10656
10959
|
}
|
|
10960
|
+
function applyTombstones(db, tombstones, result) {
|
|
10961
|
+
for (const tombstone of tombstones) {
|
|
10962
|
+
try {
|
|
10963
|
+
recordStorageTombstone({
|
|
10964
|
+
object_type: tombstone.object_type,
|
|
10965
|
+
object_id: tombstone.object_id,
|
|
10966
|
+
deleted_at: tombstone.deleted_at,
|
|
10967
|
+
source_machine_id: tombstone.source_machine_id ?? null,
|
|
10968
|
+
payload: tombstone.payload ?? null,
|
|
10969
|
+
version: tombstone.version ?? null
|
|
10970
|
+
}, db);
|
|
10971
|
+
const table = tableForTombstone(tombstone.object_type);
|
|
10972
|
+
const existing = existingClock(db, table, tombstone.object_id);
|
|
10973
|
+
if (!shouldApplyStorageTombstone(tombstone, existing)) {
|
|
10974
|
+
result.skipped += 1;
|
|
10975
|
+
continue;
|
|
10976
|
+
}
|
|
10977
|
+
const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
|
|
10978
|
+
const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
|
|
10979
|
+
if (deleted > 0 || deletedTags > 0)
|
|
10980
|
+
result.deleted = (result.deleted ?? 0) + 1;
|
|
10981
|
+
else
|
|
10982
|
+
result.skipped += 1;
|
|
10983
|
+
} catch (error) {
|
|
10984
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
}
|
|
10988
|
+
function tableForTombstone(objectType) {
|
|
10989
|
+
if (objectType === "tasks")
|
|
10990
|
+
return "tasks";
|
|
10991
|
+
if (objectType === "projects")
|
|
10992
|
+
return "projects";
|
|
10993
|
+
if (objectType === "project_machine_paths")
|
|
10994
|
+
return "project_machine_paths";
|
|
10995
|
+
if (objectType === "plans")
|
|
10996
|
+
return "plans";
|
|
10997
|
+
if (objectType === "agents")
|
|
10998
|
+
return "agents";
|
|
10999
|
+
if (objectType === "task_lists")
|
|
11000
|
+
return "task_lists";
|
|
11001
|
+
if (objectType === "templates")
|
|
11002
|
+
return "task_templates";
|
|
11003
|
+
return "task_history";
|
|
11004
|
+
}
|
|
11005
|
+
function listRows(db, table, columns) {
|
|
11006
|
+
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
11007
|
+
}
|
|
11008
|
+
function listProjectMachinePaths(db) {
|
|
11009
|
+
return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
|
|
11010
|
+
id: String(row.id),
|
|
11011
|
+
project_id: String(row.project_id),
|
|
11012
|
+
machine_id: String(row.machine_id),
|
|
11013
|
+
path: String(row.path),
|
|
11014
|
+
created_at: String(row.created_at),
|
|
11015
|
+
updated_at: String(row.updated_at)
|
|
11016
|
+
}));
|
|
11017
|
+
}
|
|
11018
|
+
function existingClock(db, table, id) {
|
|
11019
|
+
const clockColumns = clockColumnsForTable(table);
|
|
11020
|
+
const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
|
|
11021
|
+
return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
|
|
11022
|
+
}
|
|
11023
|
+
function rowClock(row, updateClockColumn) {
|
|
11024
|
+
const value = updateClockColumn ? row[updateClockColumn] : null;
|
|
11025
|
+
return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
|
|
11026
|
+
}
|
|
11027
|
+
function stringClock(value) {
|
|
11028
|
+
return typeof value === "string" && value ? value : null;
|
|
11029
|
+
}
|
|
11030
|
+
function clockColumnsForTable(table) {
|
|
11031
|
+
if (table === "agents")
|
|
11032
|
+
return ["last_seen_at", "created_at"];
|
|
11033
|
+
if (table === "task_templates")
|
|
11034
|
+
return ["created_at"];
|
|
11035
|
+
if (table === "task_history")
|
|
11036
|
+
return ["created_at"];
|
|
11037
|
+
return ["updated_at", "created_at"];
|
|
11038
|
+
}
|
|
10657
11039
|
|
|
10658
11040
|
// src/storage/local-sqlite.ts
|
|
10659
11041
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
@@ -10820,7 +11202,7 @@ class PostgresTodosSyncStore {
|
|
|
10820
11202
|
}
|
|
10821
11203
|
async pullSnapshot(options = {}) {
|
|
10822
11204
|
const params = [this.service];
|
|
10823
|
-
const filters = ["service = $1"
|
|
11205
|
+
const filters = ["service = $1"];
|
|
10824
11206
|
if (options.since) {
|
|
10825
11207
|
params.push(options.since);
|
|
10826
11208
|
filters.push(`updated_at > $${params.length}::timestamptz`);
|
|
@@ -10829,7 +11211,7 @@ class PostgresTodosSyncStore {
|
|
|
10829
11211
|
params.push(options.objectTypes);
|
|
10830
11212
|
filters.push(`object_type = ANY($${params.length}::text[])`);
|
|
10831
11213
|
}
|
|
10832
|
-
const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
|
|
11214
|
+
const response = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version FROM ${this.tableName}
|
|
10833
11215
|
WHERE ${filters.join(" AND ")}
|
|
10834
11216
|
ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
|
|
10835
11217
|
return rowsToSnapshot(response.rows);
|
|
@@ -10853,11 +11235,20 @@ function snapshotEntries(snapshot) {
|
|
|
10853
11235
|
return [
|
|
10854
11236
|
...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
|
|
10855
11237
|
...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
|
|
11238
|
+
...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
|
|
10856
11239
|
...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
|
|
10857
11240
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
10858
11241
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
10859
11242
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
10860
|
-
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
|
|
11243
|
+
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
|
|
11244
|
+
...(snapshot.tombstones ?? []).map((tombstone) => ({
|
|
11245
|
+
type: tombstone.object_type,
|
|
11246
|
+
id: tombstone.object_id,
|
|
11247
|
+
payload: tombstone.payload ?? { id: tombstone.object_id, deleted_at: tombstone.deleted_at },
|
|
11248
|
+
updatedAt: tombstone.updated_at || tombstone.deleted_at,
|
|
11249
|
+
deletedAt: tombstone.deleted_at,
|
|
11250
|
+
version: tombstone.version ?? null
|
|
11251
|
+
}))
|
|
10861
11252
|
];
|
|
10862
11253
|
}
|
|
10863
11254
|
function entry(type, payload, fallbackUpdatedAt) {
|
|
@@ -10879,19 +11270,38 @@ function rowsToSnapshot(rows) {
|
|
|
10879
11270
|
source: "postgres",
|
|
10880
11271
|
tasks: [],
|
|
10881
11272
|
projects: [],
|
|
11273
|
+
projectMachinePaths: [],
|
|
10882
11274
|
plans: [],
|
|
10883
11275
|
agents: [],
|
|
10884
11276
|
taskLists: [],
|
|
10885
11277
|
templates: [],
|
|
10886
|
-
auditHistory: []
|
|
11278
|
+
auditHistory: [],
|
|
11279
|
+
tombstones: []
|
|
10887
11280
|
};
|
|
10888
11281
|
for (const row of rows) {
|
|
10889
11282
|
const payload = payloadRecord(row.payload);
|
|
11283
|
+
const deletedAt = stringValue(row.deleted_at);
|
|
11284
|
+
if (deletedAt) {
|
|
11285
|
+
snapshot.tombstones ??= [];
|
|
11286
|
+
snapshot.tombstones.push({
|
|
11287
|
+
object_type: row.object_type,
|
|
11288
|
+
object_id: stringValue(row.object_id) ?? stringValue(payload["id"]) ?? "",
|
|
11289
|
+
deleted_at: deletedAt,
|
|
11290
|
+
updated_at: stringValue(row.updated_at) ?? deletedAt,
|
|
11291
|
+
source_machine_id: stringValue(row.source_machine_id),
|
|
11292
|
+
payload,
|
|
11293
|
+
version: numberValue(row.version)
|
|
11294
|
+
});
|
|
11295
|
+
continue;
|
|
11296
|
+
}
|
|
10890
11297
|
if (row.object_type === "tasks")
|
|
10891
11298
|
snapshot.tasks.push(payload);
|
|
10892
11299
|
else if (row.object_type === "projects")
|
|
10893
11300
|
snapshot.projects.push(payload);
|
|
10894
|
-
else if (row.object_type === "
|
|
11301
|
+
else if (row.object_type === "project_machine_paths") {
|
|
11302
|
+
snapshot.projectMachinePaths ??= [];
|
|
11303
|
+
snapshot.projectMachinePaths.push(payload);
|
|
11304
|
+
} else if (row.object_type === "plans")
|
|
10895
11305
|
snapshot.plans.push(payload);
|
|
10896
11306
|
else if (row.object_type === "agents")
|
|
10897
11307
|
snapshot.agents.push(payload);
|
|
@@ -10912,6 +11322,8 @@ function payloadRecord(value) {
|
|
|
10912
11322
|
throw new Error("Postgres sync payload must be a JSON object");
|
|
10913
11323
|
}
|
|
10914
11324
|
function stringValue(value) {
|
|
11325
|
+
if (value instanceof Date)
|
|
11326
|
+
return value.toISOString();
|
|
10915
11327
|
return typeof value === "string" && value ? value : null;
|
|
10916
11328
|
}
|
|
10917
11329
|
function numberValue(value) {
|
|
@@ -11066,6 +11478,9 @@ class PostgresJsonRecordStore {
|
|
|
11066
11478
|
this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
|
|
11067
11479
|
this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
|
|
11068
11480
|
}
|
|
11481
|
+
machineId(context) {
|
|
11482
|
+
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
11483
|
+
}
|
|
11069
11484
|
async ensureSchema() {
|
|
11070
11485
|
this.schemaReady ??= (async () => {
|
|
11071
11486
|
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
@@ -11097,6 +11512,25 @@ class PostgresJsonRecordStore {
|
|
|
11097
11512
|
updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
|
|
11098
11513
|
}));
|
|
11099
11514
|
}
|
|
11515
|
+
async listTombstones() {
|
|
11516
|
+
await this.ensureSchema();
|
|
11517
|
+
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
11518
|
+
FROM ${this.tableName}
|
|
11519
|
+
WHERE service = $1 AND deleted_at IS NOT NULL
|
|
11520
|
+
ORDER BY updated_at ASC, object_type ASC, object_id ASC`, [this.service]);
|
|
11521
|
+
return result.rows.map((row) => {
|
|
11522
|
+
const deletedAt = stringValue2(row.deleted_at) ?? stringValue2(row.updated_at) ?? new Date().toISOString();
|
|
11523
|
+
return {
|
|
11524
|
+
object_type: row.object_type,
|
|
11525
|
+
object_id: row.object_id,
|
|
11526
|
+
deleted_at: deletedAt,
|
|
11527
|
+
updated_at: stringValue2(row.updated_at) ?? deletedAt,
|
|
11528
|
+
source_machine_id: stringValue2(row.source_machine_id),
|
|
11529
|
+
payload: payloadRecord2(row.payload),
|
|
11530
|
+
version: numberValue2(row.version)
|
|
11531
|
+
};
|
|
11532
|
+
});
|
|
11533
|
+
}
|
|
11100
11534
|
async upsert(type, value, context = {}) {
|
|
11101
11535
|
await this.ensureSchema();
|
|
11102
11536
|
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
@@ -11109,7 +11543,8 @@ class PostgresJsonRecordStore {
|
|
|
11109
11543
|
updated_at = EXCLUDED.updated_at,
|
|
11110
11544
|
deleted_at = NULL,
|
|
11111
11545
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
11112
|
-
version = EXCLUDED.version
|
|
11546
|
+
version = EXCLUDED.version
|
|
11547
|
+
WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
|
|
11113
11548
|
this.service,
|
|
11114
11549
|
type,
|
|
11115
11550
|
value.id,
|
|
@@ -11126,13 +11561,58 @@ class PostgresJsonRecordStore {
|
|
|
11126
11561
|
if (!existing)
|
|
11127
11562
|
return false;
|
|
11128
11563
|
const timestamp = new Date().toISOString();
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11564
|
+
return this.tombstone({
|
|
11565
|
+
object_type: type,
|
|
11566
|
+
object_id: id,
|
|
11567
|
+
deleted_at: timestamp,
|
|
11568
|
+
updated_at: timestamp,
|
|
11569
|
+
payload: existing,
|
|
11570
|
+
version: numberValue2(existing["version"])
|
|
11571
|
+
}, context);
|
|
11572
|
+
}
|
|
11573
|
+
async tombstone(tombstone, context = {}) {
|
|
11574
|
+
await this.ensureSchema();
|
|
11575
|
+
const deletedAt = stringValue2(tombstone.deleted_at) ?? new Date().toISOString();
|
|
11576
|
+
const updatedAt = stringValue2(tombstone.updated_at) ?? deletedAt;
|
|
11577
|
+
const existing = await this.clock(tombstone.object_type, tombstone.object_id);
|
|
11578
|
+
if (existing && compareClock(existing.updatedAt, updatedAt) > 0)
|
|
11579
|
+
return false;
|
|
11580
|
+
await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
11581
|
+
service, object_type, object_id, payload, updated_at,
|
|
11582
|
+
deleted_at, source_machine_id, version
|
|
11583
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
11584
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
11585
|
+
payload = EXCLUDED.payload,
|
|
11586
|
+
updated_at = EXCLUDED.updated_at,
|
|
11587
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
11588
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
11589
|
+
version = EXCLUDED.version
|
|
11590
|
+
WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
|
|
11591
|
+
this.service,
|
|
11592
|
+
tombstone.object_type,
|
|
11593
|
+
tombstone.object_id,
|
|
11594
|
+
JSON.stringify(tombstone.payload ?? { id: tombstone.object_id, deleted_at: deletedAt }),
|
|
11595
|
+
updatedAt,
|
|
11596
|
+
deletedAt,
|
|
11597
|
+
tombstone.source_machine_id ?? context.requestId ?? this.sourceMachineId ?? null,
|
|
11598
|
+
tombstone.version ?? null
|
|
11599
|
+
]);
|
|
11134
11600
|
return true;
|
|
11135
11601
|
}
|
|
11602
|
+
async clock(type, id) {
|
|
11603
|
+
await this.ensureSchema();
|
|
11604
|
+
const result = await this.options.client.query(`SELECT object_type, object_id, updated_at, deleted_at
|
|
11605
|
+
FROM ${this.tableName}
|
|
11606
|
+
WHERE service = $1 AND object_type = $2 AND object_id = $3
|
|
11607
|
+
LIMIT 1`, [this.service, type, id]);
|
|
11608
|
+
const row = result.rows[0];
|
|
11609
|
+
if (!row)
|
|
11610
|
+
return null;
|
|
11611
|
+
return {
|
|
11612
|
+
updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString(),
|
|
11613
|
+
deletedAt: stringValue2(row.deleted_at)
|
|
11614
|
+
};
|
|
11615
|
+
}
|
|
11136
11616
|
async getCursor(name) {
|
|
11137
11617
|
await this.ensureSchema();
|
|
11138
11618
|
const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
|
|
@@ -11201,7 +11681,10 @@ async function createTask2(input, store, context) {
|
|
|
11201
11681
|
runner_started_at: null,
|
|
11202
11682
|
runner_completed_at: null,
|
|
11203
11683
|
current_step: null,
|
|
11204
|
-
total_steps: null
|
|
11684
|
+
total_steps: null,
|
|
11685
|
+
machine_id: store.machineId(context),
|
|
11686
|
+
synced_at: null,
|
|
11687
|
+
archived_at: null
|
|
11205
11688
|
};
|
|
11206
11689
|
await store.upsert("tasks", task, context);
|
|
11207
11690
|
await logTaskChange2(task.id, "created", "status", null, task.status, task.assigned_by ?? task.agent_id, store, context);
|
|
@@ -11326,7 +11809,9 @@ async function createProject2(input, store, context) {
|
|
|
11326
11809
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
11327
11810
|
task_counter: 0,
|
|
11328
11811
|
created_at: timestamp,
|
|
11329
|
-
updated_at: timestamp
|
|
11812
|
+
updated_at: timestamp,
|
|
11813
|
+
machine_id: store.machineId(context),
|
|
11814
|
+
synced_at: null
|
|
11330
11815
|
};
|
|
11331
11816
|
return store.upsert("projects", project, context);
|
|
11332
11817
|
}
|
|
@@ -11346,7 +11831,9 @@ async function createPlan2(input, store, context) {
|
|
|
11346
11831
|
description: input.description ?? null,
|
|
11347
11832
|
status: input.status ?? "active",
|
|
11348
11833
|
created_at: timestamp,
|
|
11349
|
-
updated_at: timestamp
|
|
11834
|
+
updated_at: timestamp,
|
|
11835
|
+
machine_id: store.machineId(context),
|
|
11836
|
+
synced_at: null
|
|
11350
11837
|
}, context);
|
|
11351
11838
|
}
|
|
11352
11839
|
async function updatePlan2(id, input, store) {
|
|
@@ -11376,7 +11863,9 @@ async function registerAgent2(input, store, context) {
|
|
|
11376
11863
|
last_seen_at: timestamp,
|
|
11377
11864
|
session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
|
|
11378
11865
|
working_dir: input.working_dir ?? existing?.working_dir ?? null,
|
|
11379
|
-
active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
|
|
11866
|
+
active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
|
|
11867
|
+
machine_id: existing?.machine_id ?? store.machineId(context),
|
|
11868
|
+
synced_at: existing?.synced_at ?? null
|
|
11380
11869
|
};
|
|
11381
11870
|
return store.upsert("agents", agent, context);
|
|
11382
11871
|
}
|
|
@@ -11403,7 +11892,9 @@ async function createTaskList2(input, store, context) {
|
|
|
11403
11892
|
description: input.description ?? null,
|
|
11404
11893
|
metadata: input.metadata ?? {},
|
|
11405
11894
|
created_at: timestamp,
|
|
11406
|
-
updated_at: timestamp
|
|
11895
|
+
updated_at: timestamp,
|
|
11896
|
+
machine_id: store.machineId(context),
|
|
11897
|
+
synced_at: null
|
|
11407
11898
|
}, context);
|
|
11408
11899
|
}
|
|
11409
11900
|
async function updateTaskList2(id, input, store) {
|
|
@@ -11429,7 +11920,9 @@ async function createTemplate2(input, store, context) {
|
|
|
11429
11920
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
11430
11921
|
plan_id: input.plan_id ?? null,
|
|
11431
11922
|
metadata: input.metadata ?? {},
|
|
11432
|
-
created_at: timestamp
|
|
11923
|
+
created_at: timestamp,
|
|
11924
|
+
machine_id: store.machineId(context),
|
|
11925
|
+
synced_at: null
|
|
11433
11926
|
}, context);
|
|
11434
11927
|
}
|
|
11435
11928
|
async function updateTemplate2(id, input, store) {
|
|
@@ -11454,7 +11947,8 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
11454
11947
|
old_value: oldValue ?? null,
|
|
11455
11948
|
new_value: newValue ?? null,
|
|
11456
11949
|
agent_id: agentId ?? context?.agentId ?? null,
|
|
11457
|
-
created_at: new Date().toISOString()
|
|
11950
|
+
created_at: new Date().toISOString(),
|
|
11951
|
+
machine_id: store.machineId(context)
|
|
11458
11952
|
};
|
|
11459
11953
|
return store.upsert("audit_history", entry2, context);
|
|
11460
11954
|
}
|
|
@@ -11477,18 +11971,21 @@ async function exportSnapshot(store) {
|
|
|
11477
11971
|
source: "postgres",
|
|
11478
11972
|
tasks: await store.list("tasks"),
|
|
11479
11973
|
projects: await store.list("projects"),
|
|
11974
|
+
projectMachinePaths: await store.list("project_machine_paths"),
|
|
11480
11975
|
plans: await store.list("plans"),
|
|
11481
11976
|
agents: await store.list("agents"),
|
|
11482
11977
|
taskLists: await store.list("task_lists"),
|
|
11483
11978
|
templates: await store.list("templates"),
|
|
11484
|
-
auditHistory: await store.list("audit_history")
|
|
11979
|
+
auditHistory: await store.list("audit_history"),
|
|
11980
|
+
tombstones: await store.listTombstones()
|
|
11485
11981
|
};
|
|
11486
11982
|
}
|
|
11487
11983
|
async function importSnapshot(snapshot, store, context) {
|
|
11488
|
-
const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
|
|
11984
|
+
const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
|
|
11489
11985
|
const entries = [
|
|
11490
11986
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
11491
11987
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
11988
|
+
...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
|
|
11492
11989
|
...snapshot.plans.map((row) => ["plans", row]),
|
|
11493
11990
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
11494
11991
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
@@ -11507,6 +12004,25 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
11507
12004
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
11508
12005
|
}
|
|
11509
12006
|
}
|
|
12007
|
+
for (const tombstone of snapshot.tombstones ?? []) {
|
|
12008
|
+
try {
|
|
12009
|
+
const deleted = await store.tombstone({
|
|
12010
|
+
object_type: tombstone.object_type,
|
|
12011
|
+
object_id: tombstone.object_id,
|
|
12012
|
+
deleted_at: tombstone.deleted_at,
|
|
12013
|
+
updated_at: tombstone.updated_at,
|
|
12014
|
+
source_machine_id: tombstone.source_machine_id ?? null,
|
|
12015
|
+
payload: tombstone.payload ?? null,
|
|
12016
|
+
version: tombstone.version ?? null
|
|
12017
|
+
}, context);
|
|
12018
|
+
if (deleted)
|
|
12019
|
+
result.deleted = (result.deleted ?? 0) + 1;
|
|
12020
|
+
else
|
|
12021
|
+
result.skipped += 1;
|
|
12022
|
+
} catch (error) {
|
|
12023
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
12024
|
+
}
|
|
12025
|
+
}
|
|
11510
12026
|
return result;
|
|
11511
12027
|
}
|
|
11512
12028
|
async function requireRecord(type, id, store) {
|
|
@@ -11589,8 +12105,17 @@ function payloadRecord2(value) {
|
|
|
11589
12105
|
throw new Error("Postgres storage payload must be a JSON object");
|
|
11590
12106
|
}
|
|
11591
12107
|
function stringValue2(value) {
|
|
12108
|
+
if (value instanceof Date)
|
|
12109
|
+
return value.toISOString();
|
|
11592
12110
|
return typeof value === "string" && value ? value : null;
|
|
11593
12111
|
}
|
|
12112
|
+
function compareClock(left, right) {
|
|
12113
|
+
const leftClock = Date.parse(left);
|
|
12114
|
+
const rightClock = Date.parse(right);
|
|
12115
|
+
if (Number.isNaN(leftClock) || Number.isNaN(rightClock))
|
|
12116
|
+
return left.localeCompare(right);
|
|
12117
|
+
return leftClock - rightClock;
|
|
12118
|
+
}
|
|
11594
12119
|
function numberValue2(value) {
|
|
11595
12120
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
11596
12121
|
}
|