@hasna/todos 0.11.66 → 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 +2 -2
- package/dashboard/dist/assets/{index-aJefI7kh.js → index-DVotjwab.js} +1 -1
- package/dashboard/dist/index.html +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/index.js +667 -169
- package/dist/contracts.js +301 -71
- 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.js +628 -160
- 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/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/mcp/index.js +343 -104
- package/dist/registry.js +301 -71
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +366 -127
- 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 +569 -94
- 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) {
|
|
@@ -5382,6 +5563,8 @@ function readMachineLocalPath(project) {
|
|
|
5382
5563
|
}
|
|
5383
5564
|
}
|
|
5384
5565
|
async function emitSharedTaskEvent(input) {
|
|
5566
|
+
if (!shouldEmitSharedTaskEvents(input.databasePath))
|
|
5567
|
+
return;
|
|
5385
5568
|
const data = taskEventData(input.task, input.data);
|
|
5386
5569
|
await new EventsClient().emit({
|
|
5387
5570
|
source: SOURCE,
|
|
@@ -5406,8 +5589,9 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
|
5406
5589
|
const d = db || getDatabase();
|
|
5407
5590
|
const id = uuid();
|
|
5408
5591
|
const timestamp = now();
|
|
5409
|
-
|
|
5410
|
-
|
|
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]);
|
|
5411
5595
|
try {
|
|
5412
5596
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
5413
5597
|
logActivity2({
|
|
@@ -5420,7 +5604,7 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
|
5420
5604
|
actor_id: agentId ?? undefined
|
|
5421
5605
|
}, d);
|
|
5422
5606
|
} catch {}
|
|
5423
|
-
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 };
|
|
5424
5608
|
}
|
|
5425
5609
|
function getTaskHistory(taskId, db) {
|
|
5426
5610
|
const d = db || getDatabase();
|
|
@@ -5769,13 +5953,14 @@ function createTask(input, db) {
|
|
|
5769
5953
|
const d = db || getDatabase();
|
|
5770
5954
|
const timestamp = now();
|
|
5771
5955
|
const tags = input.tags || [];
|
|
5956
|
+
const machineId = currentStorageMachineId(d);
|
|
5772
5957
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
5773
5958
|
const assignedFromProject = input.assigned_from_project || null;
|
|
5774
5959
|
let id = uuid();
|
|
5775
5960
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
5776
5961
|
try {
|
|
5777
|
-
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)
|
|
5778
|
-
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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5779
5964
|
id,
|
|
5780
5965
|
null,
|
|
5781
5966
|
input.project_id || null,
|
|
@@ -5812,7 +5997,8 @@ function createTask(input, db) {
|
|
|
5812
5997
|
input.spawned_from_session || null,
|
|
5813
5998
|
assignedBy || null,
|
|
5814
5999
|
assignedFromProject || null,
|
|
5815
|
-
input.task_type || null
|
|
6000
|
+
input.task_type || null,
|
|
6001
|
+
machineId
|
|
5816
6002
|
]);
|
|
5817
6003
|
break;
|
|
5818
6004
|
} catch (e) {
|
|
@@ -5828,9 +6014,10 @@ function createTask(input, db) {
|
|
|
5828
6014
|
}
|
|
5829
6015
|
const task = getTask(id, d);
|
|
5830
6016
|
const payload = taskEventData(task);
|
|
6017
|
+
const databasePath = databasePathFromDatabase(d);
|
|
5831
6018
|
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5832
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
5833
|
-
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
6019
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
6020
|
+
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
5834
6021
|
return task;
|
|
5835
6022
|
}
|
|
5836
6023
|
function getTask(id, db) {
|
|
@@ -6248,29 +6435,39 @@ function updateTask(id, input, db) {
|
|
|
6248
6435
|
approved_by: input.approved_by ?? task.approved_by,
|
|
6249
6436
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
6250
6437
|
};
|
|
6438
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6251
6439
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
6252
6440
|
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
6253
6441
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6254
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
6255
|
-
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 });
|
|
6256
6444
|
}
|
|
6257
6445
|
if (input.status !== undefined && input.status !== task.status) {
|
|
6258
6446
|
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
6259
6447
|
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
6260
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
6261
|
-
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 });
|
|
6262
6450
|
}
|
|
6263
6451
|
if (input.approved_by !== undefined) {
|
|
6264
|
-
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 });
|
|
6265
6453
|
}
|
|
6266
6454
|
const updatePayload = taskEventData(updatedTask);
|
|
6267
6455
|
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
6268
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
6269
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
6456
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
6457
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
6270
6458
|
return updatedTask;
|
|
6271
6459
|
}
|
|
6272
6460
|
function deleteTask(id, db) {
|
|
6273
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);
|
|
6274
6471
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
6275
6472
|
return result.changes > 0;
|
|
6276
6473
|
}
|
|
@@ -6402,8 +6599,9 @@ function resolveTemplateId(id, d) {
|
|
|
6402
6599
|
function createTemplate(input, db) {
|
|
6403
6600
|
const d = db || getDatabase();
|
|
6404
6601
|
const id = uuid();
|
|
6405
|
-
|
|
6406
|
-
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6407
6605
|
id,
|
|
6408
6606
|
input.name,
|
|
6409
6607
|
input.title_pattern,
|
|
@@ -6414,7 +6612,8 @@ function createTemplate(input, db) {
|
|
|
6414
6612
|
input.project_id || null,
|
|
6415
6613
|
input.plan_id || null,
|
|
6416
6614
|
JSON.stringify(input.metadata || {}),
|
|
6417
|
-
now()
|
|
6615
|
+
now(),
|
|
6616
|
+
machineId
|
|
6418
6617
|
]);
|
|
6419
6618
|
if (input.tasks && input.tasks.length > 0) {
|
|
6420
6619
|
addTemplateTasks(id, input.tasks, d);
|
|
@@ -6438,6 +6637,15 @@ function deleteTemplate(id, db) {
|
|
|
6438
6637
|
const resolved = resolveTemplateId(id, d);
|
|
6439
6638
|
if (!resolved)
|
|
6440
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);
|
|
6441
6649
|
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
6442
6650
|
}
|
|
6443
6651
|
function updateTemplate(id, updates, db) {
|
|
@@ -6967,6 +7175,7 @@ function getBlockingDeps(id, db) {
|
|
|
6967
7175
|
}
|
|
6968
7176
|
function startTask(id, agentId, db) {
|
|
6969
7177
|
const d = db || getDatabase();
|
|
7178
|
+
const databasePath = databasePathFromDatabase(d);
|
|
6970
7179
|
const task = getTask(id, d);
|
|
6971
7180
|
if (!task)
|
|
6972
7181
|
throw new TaskNotFoundError(id);
|
|
@@ -6981,7 +7190,8 @@ function startTask(id, agentId, db) {
|
|
|
6981
7190
|
agent_id: agentId,
|
|
6982
7191
|
title: task.title,
|
|
6983
7192
|
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
6984
|
-
}
|
|
7193
|
+
},
|
|
7194
|
+
databasePath
|
|
6985
7195
|
});
|
|
6986
7196
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
6987
7197
|
}
|
|
@@ -7003,12 +7213,13 @@ function startTask(id, agentId, db) {
|
|
|
7003
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 };
|
|
7004
7214
|
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
7005
7215
|
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
7006
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
7007
|
-
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 });
|
|
7008
7218
|
return startedTask;
|
|
7009
7219
|
}
|
|
7010
7220
|
function completeTask(id, agentId, db, options) {
|
|
7011
7221
|
const d = db || getDatabase();
|
|
7222
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7012
7223
|
const task = getTask(id, d);
|
|
7013
7224
|
if (!task)
|
|
7014
7225
|
throw new TaskNotFoundError(id);
|
|
@@ -7054,8 +7265,8 @@ function completeTask(id, agentId, db, options) {
|
|
|
7054
7265
|
};
|
|
7055
7266
|
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
7056
7267
|
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7057
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
7058
|
-
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 });
|
|
7059
7270
|
let spawnedTask = null;
|
|
7060
7271
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7061
7272
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -7099,9 +7310,9 @@ function completeTask(id, agentId, db, options) {
|
|
|
7099
7310
|
const depTask = getTask(dep.id, d);
|
|
7100
7311
|
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
7101
7312
|
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
7102
|
-
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
7313
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
7103
7314
|
if (depTask)
|
|
7104
|
-
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
7315
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
7105
7316
|
}
|
|
7106
7317
|
}
|
|
7107
7318
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -7271,6 +7482,7 @@ function getTasksChangedSince(since, filters, db) {
|
|
|
7271
7482
|
}
|
|
7272
7483
|
function failTask(id, agentId, reason, options, db) {
|
|
7273
7484
|
const d = db || getDatabase();
|
|
7485
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7274
7486
|
const task = getTask(id, d);
|
|
7275
7487
|
if (!task)
|
|
7276
7488
|
throw new TaskNotFoundError(id);
|
|
@@ -7299,8 +7511,8 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7299
7511
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
7300
7512
|
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
7301
7513
|
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
7302
|
-
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
7303
|
-
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 });
|
|
7304
7516
|
let retryTask;
|
|
7305
7517
|
if (options?.retry) {
|
|
7306
7518
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -7360,6 +7572,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
|
|
|
7360
7572
|
}
|
|
7361
7573
|
function stealTask(agentId, opts, db) {
|
|
7362
7574
|
const d = db || getDatabase();
|
|
7575
|
+
const databasePath = databasePathFromDatabase(d);
|
|
7363
7576
|
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
7364
7577
|
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
7365
7578
|
if (staleTasks.length === 0)
|
|
@@ -7378,8 +7591,8 @@ function stealTask(agentId, opts, db) {
|
|
|
7378
7591
|
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
7379
7592
|
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7380
7593
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7381
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
7382
|
-
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 });
|
|
7383
7596
|
return stolenTask;
|
|
7384
7597
|
}
|
|
7385
7598
|
function claimOrSteal(agentId, filters, db) {
|
|
@@ -7970,8 +8183,9 @@ function createPlan(input, db) {
|
|
|
7970
8183
|
const d = db || getDatabase();
|
|
7971
8184
|
const id = uuid();
|
|
7972
8185
|
const timestamp = now();
|
|
7973
|
-
|
|
7974
|
-
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7975
8189
|
id,
|
|
7976
8190
|
input.project_id || null,
|
|
7977
8191
|
input.task_list_id || null,
|
|
@@ -7980,7 +8194,8 @@ function createPlan(input, db) {
|
|
|
7980
8194
|
input.description || null,
|
|
7981
8195
|
input.status || "active",
|
|
7982
8196
|
timestamp,
|
|
7983
|
-
timestamp
|
|
8197
|
+
timestamp,
|
|
8198
|
+
machineId
|
|
7984
8199
|
]);
|
|
7985
8200
|
return getPlan(id, d);
|
|
7986
8201
|
}
|
|
@@ -8028,12 +8243,21 @@ function updatePlan(id, input, db) {
|
|
|
8028
8243
|
const updated = getPlan(id, d);
|
|
8029
8244
|
emitLocalEventHooksQuiet({
|
|
8030
8245
|
type: "plan.updated",
|
|
8031
|
-
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)
|
|
8032
8248
|
});
|
|
8033
8249
|
return updated;
|
|
8034
8250
|
}
|
|
8035
8251
|
function deletePlan(id, db) {
|
|
8036
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);
|
|
8037
8261
|
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
8038
8262
|
return result.changes > 0;
|
|
8039
8263
|
}
|
|
@@ -8440,20 +8664,20 @@ init_database();
|
|
|
8440
8664
|
init_redaction();
|
|
8441
8665
|
import { createHash as createHash2 } from "crypto";
|
|
8442
8666
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
8443
|
-
import { basename, dirname as dirname4, join as join5, resolve as
|
|
8444
|
-
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";
|
|
8445
8669
|
function isInMemoryDb2(path) {
|
|
8446
8670
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
8447
8671
|
}
|
|
8448
8672
|
function artifactStoreRoot() {
|
|
8449
8673
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
8450
|
-
return
|
|
8674
|
+
return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
8451
8675
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
8452
|
-
return
|
|
8676
|
+
return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
8453
8677
|
const dbPath = getDatabasePath();
|
|
8454
8678
|
if (isInMemoryDb2(dbPath))
|
|
8455
|
-
return join5(
|
|
8456
|
-
return join5(dirname4(
|
|
8679
|
+
return join5(tmpdir2(), "hasna-todos-artifacts");
|
|
8680
|
+
return join5(dirname4(resolve7(dbPath)), "artifacts");
|
|
8457
8681
|
}
|
|
8458
8682
|
function artifactStorePath(relativePath) {
|
|
8459
8683
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
@@ -8500,7 +8724,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
8500
8724
|
return "application/octet-stream";
|
|
8501
8725
|
}
|
|
8502
8726
|
function storeArtifactContent(input) {
|
|
8503
|
-
const sourcePath =
|
|
8727
|
+
const sourcePath = resolve7(input.path);
|
|
8504
8728
|
if (!existsSync7(sourcePath))
|
|
8505
8729
|
return null;
|
|
8506
8730
|
const sourceStat = statSync2(sourcePath);
|
|
@@ -8656,19 +8880,19 @@ function importStoredArtifactContent(content) {
|
|
|
8656
8880
|
var DEFAULT_DELETED_RETENTION_DAYS = 30;
|
|
8657
8881
|
function getArtifactStoreRoot(dbPath) {
|
|
8658
8882
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
8659
|
-
return
|
|
8883
|
+
return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
8660
8884
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
8661
|
-
return
|
|
8885
|
+
return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
8662
8886
|
const path = dbPath ?? getDatabasePath();
|
|
8663
8887
|
if (isInMemoryDb2(path))
|
|
8664
|
-
return join5(
|
|
8665
|
-
return join5(dirname4(
|
|
8888
|
+
return join5(tmpdir2(), "hasna-todos-artifacts");
|
|
8889
|
+
return join5(dirname4(resolve7(path)), "artifacts");
|
|
8666
8890
|
}
|
|
8667
8891
|
function computeContentHash(path) {
|
|
8668
|
-
return sha256(readFileSync3(
|
|
8892
|
+
return sha256(readFileSync3(resolve7(path)));
|
|
8669
8893
|
}
|
|
8670
8894
|
function storeArtifactFile(input) {
|
|
8671
|
-
const sourcePath =
|
|
8895
|
+
const sourcePath = resolve7(input.sourcePath);
|
|
8672
8896
|
if (!existsSync7(sourcePath)) {
|
|
8673
8897
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
8674
8898
|
}
|
|
@@ -8717,7 +8941,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
|
|
|
8717
8941
|
};
|
|
8718
8942
|
}
|
|
8719
8943
|
function writeArtifactExportManifest(manifest, outputPath) {
|
|
8720
|
-
const destination =
|
|
8944
|
+
const destination = resolve7(outputPath);
|
|
8721
8945
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
8722
8946
|
writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
|
|
8723
8947
|
`);
|
|
@@ -9104,7 +9328,11 @@ function startTaskRun(input, db) {
|
|
|
9104
9328
|
}, d);
|
|
9105
9329
|
}
|
|
9106
9330
|
const run = getTaskRun(id, d);
|
|
9107
|
-
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
|
+
});
|
|
9108
9336
|
return run;
|
|
9109
9337
|
}
|
|
9110
9338
|
function beginTaskRunTransaction(input, db) {
|
|
@@ -9373,7 +9601,8 @@ function finishTaskRun(input, db) {
|
|
|
9373
9601
|
const updated = getTaskRun(run.id, d);
|
|
9374
9602
|
emitLocalEventHooksQuiet({
|
|
9375
9603
|
type: `run.${input.status}`,
|
|
9376
|
-
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)
|
|
9377
9606
|
});
|
|
9378
9607
|
return updated;
|
|
9379
9608
|
}
|
|
@@ -10205,6 +10434,7 @@ function rowToAgent(row) {
|
|
|
10205
10434
|
}
|
|
10206
10435
|
function registerAgent(input, db) {
|
|
10207
10436
|
const d = db || getDatabase();
|
|
10437
|
+
const machineId = currentStorageMachineId(d);
|
|
10208
10438
|
const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
|
|
10209
10439
|
const normalizedName = validateAgentName(input.name, existingNames);
|
|
10210
10440
|
const existing = getAgentByName(normalizedName, d);
|
|
@@ -10245,14 +10475,18 @@ function registerAgent(input, db) {
|
|
|
10245
10475
|
updates.push("active_project_id = ?");
|
|
10246
10476
|
params.push(input.project_id);
|
|
10247
10477
|
}
|
|
10478
|
+
if (!existing.machine_id && machineId) {
|
|
10479
|
+
updates.push("machine_id = ?");
|
|
10480
|
+
params.push(machineId);
|
|
10481
|
+
}
|
|
10248
10482
|
params.push(existing.id);
|
|
10249
10483
|
d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
10250
10484
|
return getAgent(existing.id, d);
|
|
10251
10485
|
}
|
|
10252
10486
|
const id = shortUuid();
|
|
10253
10487
|
const timestamp = now();
|
|
10254
|
-
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)
|
|
10255
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
10256
10490
|
id,
|
|
10257
10491
|
normalizedName,
|
|
10258
10492
|
input.description || null,
|
|
@@ -10268,7 +10502,8 @@ function registerAgent(input, db) {
|
|
|
10268
10502
|
timestamp,
|
|
10269
10503
|
input.session_id || null,
|
|
10270
10504
|
input.working_dir || null,
|
|
10271
|
-
input.project_id && input.session_id ? input.project_id : null
|
|
10505
|
+
input.project_id && input.session_id ? input.project_id : null,
|
|
10506
|
+
machineId
|
|
10272
10507
|
]);
|
|
10273
10508
|
return getAgent(id, d);
|
|
10274
10509
|
}
|
|
@@ -10468,6 +10703,14 @@ var PROJECT_COLUMNS = [
|
|
|
10468
10703
|
"machine_id",
|
|
10469
10704
|
"synced_at"
|
|
10470
10705
|
];
|
|
10706
|
+
var PROJECT_MACHINE_PATH_COLUMNS = [
|
|
10707
|
+
"id",
|
|
10708
|
+
"project_id",
|
|
10709
|
+
"machine_id",
|
|
10710
|
+
"path",
|
|
10711
|
+
"created_at",
|
|
10712
|
+
"updated_at"
|
|
10713
|
+
];
|
|
10471
10714
|
var TASK_LIST_COLUMNS = [
|
|
10472
10715
|
"id",
|
|
10473
10716
|
"project_id",
|
|
@@ -10607,11 +10850,13 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
10607
10850
|
source: "sqlite",
|
|
10608
10851
|
tasks: listTasks({ include_archived: true }, d),
|
|
10609
10852
|
projects: listProjects(d),
|
|
10853
|
+
projectMachinePaths: listProjectMachinePaths(d),
|
|
10610
10854
|
plans: listPlans(undefined, d),
|
|
10611
10855
|
agents: listAgents({ include_archived: true }, d),
|
|
10612
10856
|
taskLists: listTaskLists(undefined, d),
|
|
10613
10857
|
templates: listTemplates(d),
|
|
10614
|
-
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
|
|
10858
|
+
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
10859
|
+
tombstones: listStorageTombstones(d)
|
|
10615
10860
|
};
|
|
10616
10861
|
}
|
|
10617
10862
|
function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
@@ -10619,13 +10864,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
10619
10864
|
const result = {
|
|
10620
10865
|
inserted: 0,
|
|
10621
10866
|
updated: 0,
|
|
10867
|
+
deleted: 0,
|
|
10622
10868
|
skipped: 0,
|
|
10623
10869
|
errors: []
|
|
10624
10870
|
};
|
|
10625
|
-
const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
10871
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
10626
10872
|
for (const row of rows) {
|
|
10627
10873
|
try {
|
|
10628
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
|
+
}
|
|
10629
10880
|
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
10630
10881
|
if (state === "inserted")
|
|
10631
10882
|
result.inserted += 1;
|
|
@@ -10639,17 +10890,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
10639
10890
|
}
|
|
10640
10891
|
}
|
|
10641
10892
|
};
|
|
10642
|
-
applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
10643
|
-
applyRows("
|
|
10644
|
-
applyRows("
|
|
10645
|
-
applyRows("
|
|
10646
|
-
applyRows("
|
|
10647
|
-
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) => {
|
|
10648
10900
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
10649
10901
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
10650
10902
|
}
|
|
10651
10903
|
});
|
|
10652
|
-
applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
10904
|
+
applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
10905
|
+
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
10653
10906
|
return result;
|
|
10654
10907
|
}
|
|
10655
10908
|
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
@@ -10704,6 +10957,85 @@ function sortedTasks(tasks) {
|
|
|
10704
10957
|
visit(task);
|
|
10705
10958
|
return result;
|
|
10706
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
|
+
}
|
|
10707
11039
|
|
|
10708
11040
|
// src/storage/local-sqlite.ts
|
|
10709
11041
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
@@ -10870,7 +11202,7 @@ class PostgresTodosSyncStore {
|
|
|
10870
11202
|
}
|
|
10871
11203
|
async pullSnapshot(options = {}) {
|
|
10872
11204
|
const params = [this.service];
|
|
10873
|
-
const filters = ["service = $1"
|
|
11205
|
+
const filters = ["service = $1"];
|
|
10874
11206
|
if (options.since) {
|
|
10875
11207
|
params.push(options.since);
|
|
10876
11208
|
filters.push(`updated_at > $${params.length}::timestamptz`);
|
|
@@ -10879,7 +11211,7 @@ class PostgresTodosSyncStore {
|
|
|
10879
11211
|
params.push(options.objectTypes);
|
|
10880
11212
|
filters.push(`object_type = ANY($${params.length}::text[])`);
|
|
10881
11213
|
}
|
|
10882
|
-
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}
|
|
10883
11215
|
WHERE ${filters.join(" AND ")}
|
|
10884
11216
|
ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
|
|
10885
11217
|
return rowsToSnapshot(response.rows);
|
|
@@ -10903,11 +11235,20 @@ function snapshotEntries(snapshot) {
|
|
|
10903
11235
|
return [
|
|
10904
11236
|
...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
|
|
10905
11237
|
...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
|
|
11238
|
+
...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
|
|
10906
11239
|
...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
|
|
10907
11240
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
10908
11241
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
10909
11242
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
10910
|
-
...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
|
+
}))
|
|
10911
11252
|
];
|
|
10912
11253
|
}
|
|
10913
11254
|
function entry(type, payload, fallbackUpdatedAt) {
|
|
@@ -10929,19 +11270,38 @@ function rowsToSnapshot(rows) {
|
|
|
10929
11270
|
source: "postgres",
|
|
10930
11271
|
tasks: [],
|
|
10931
11272
|
projects: [],
|
|
11273
|
+
projectMachinePaths: [],
|
|
10932
11274
|
plans: [],
|
|
10933
11275
|
agents: [],
|
|
10934
11276
|
taskLists: [],
|
|
10935
11277
|
templates: [],
|
|
10936
|
-
auditHistory: []
|
|
11278
|
+
auditHistory: [],
|
|
11279
|
+
tombstones: []
|
|
10937
11280
|
};
|
|
10938
11281
|
for (const row of rows) {
|
|
10939
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
|
+
}
|
|
10940
11297
|
if (row.object_type === "tasks")
|
|
10941
11298
|
snapshot.tasks.push(payload);
|
|
10942
11299
|
else if (row.object_type === "projects")
|
|
10943
11300
|
snapshot.projects.push(payload);
|
|
10944
|
-
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")
|
|
10945
11305
|
snapshot.plans.push(payload);
|
|
10946
11306
|
else if (row.object_type === "agents")
|
|
10947
11307
|
snapshot.agents.push(payload);
|
|
@@ -10962,6 +11322,8 @@ function payloadRecord(value) {
|
|
|
10962
11322
|
throw new Error("Postgres sync payload must be a JSON object");
|
|
10963
11323
|
}
|
|
10964
11324
|
function stringValue(value) {
|
|
11325
|
+
if (value instanceof Date)
|
|
11326
|
+
return value.toISOString();
|
|
10965
11327
|
return typeof value === "string" && value ? value : null;
|
|
10966
11328
|
}
|
|
10967
11329
|
function numberValue(value) {
|
|
@@ -11116,6 +11478,9 @@ class PostgresJsonRecordStore {
|
|
|
11116
11478
|
this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
|
|
11117
11479
|
this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
|
|
11118
11480
|
}
|
|
11481
|
+
machineId(context) {
|
|
11482
|
+
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
11483
|
+
}
|
|
11119
11484
|
async ensureSchema() {
|
|
11120
11485
|
this.schemaReady ??= (async () => {
|
|
11121
11486
|
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
@@ -11147,6 +11512,25 @@ class PostgresJsonRecordStore {
|
|
|
11147
11512
|
updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
|
|
11148
11513
|
}));
|
|
11149
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
|
+
}
|
|
11150
11534
|
async upsert(type, value, context = {}) {
|
|
11151
11535
|
await this.ensureSchema();
|
|
11152
11536
|
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
@@ -11159,7 +11543,8 @@ class PostgresJsonRecordStore {
|
|
|
11159
11543
|
updated_at = EXCLUDED.updated_at,
|
|
11160
11544
|
deleted_at = NULL,
|
|
11161
11545
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
11162
|
-
version = EXCLUDED.version
|
|
11546
|
+
version = EXCLUDED.version
|
|
11547
|
+
WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
|
|
11163
11548
|
this.service,
|
|
11164
11549
|
type,
|
|
11165
11550
|
value.id,
|
|
@@ -11176,13 +11561,58 @@ class PostgresJsonRecordStore {
|
|
|
11176
11561
|
if (!existing)
|
|
11177
11562
|
return false;
|
|
11178
11563
|
const timestamp = new Date().toISOString();
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
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
|
+
]);
|
|
11184
11600
|
return true;
|
|
11185
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
|
+
}
|
|
11186
11616
|
async getCursor(name) {
|
|
11187
11617
|
await this.ensureSchema();
|
|
11188
11618
|
const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
|
|
@@ -11251,7 +11681,10 @@ async function createTask2(input, store, context) {
|
|
|
11251
11681
|
runner_started_at: null,
|
|
11252
11682
|
runner_completed_at: null,
|
|
11253
11683
|
current_step: null,
|
|
11254
|
-
total_steps: null
|
|
11684
|
+
total_steps: null,
|
|
11685
|
+
machine_id: store.machineId(context),
|
|
11686
|
+
synced_at: null,
|
|
11687
|
+
archived_at: null
|
|
11255
11688
|
};
|
|
11256
11689
|
await store.upsert("tasks", task, context);
|
|
11257
11690
|
await logTaskChange2(task.id, "created", "status", null, task.status, task.assigned_by ?? task.agent_id, store, context);
|
|
@@ -11376,7 +11809,9 @@ async function createProject2(input, store, context) {
|
|
|
11376
11809
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
11377
11810
|
task_counter: 0,
|
|
11378
11811
|
created_at: timestamp,
|
|
11379
|
-
updated_at: timestamp
|
|
11812
|
+
updated_at: timestamp,
|
|
11813
|
+
machine_id: store.machineId(context),
|
|
11814
|
+
synced_at: null
|
|
11380
11815
|
};
|
|
11381
11816
|
return store.upsert("projects", project, context);
|
|
11382
11817
|
}
|
|
@@ -11396,7 +11831,9 @@ async function createPlan2(input, store, context) {
|
|
|
11396
11831
|
description: input.description ?? null,
|
|
11397
11832
|
status: input.status ?? "active",
|
|
11398
11833
|
created_at: timestamp,
|
|
11399
|
-
updated_at: timestamp
|
|
11834
|
+
updated_at: timestamp,
|
|
11835
|
+
machine_id: store.machineId(context),
|
|
11836
|
+
synced_at: null
|
|
11400
11837
|
}, context);
|
|
11401
11838
|
}
|
|
11402
11839
|
async function updatePlan2(id, input, store) {
|
|
@@ -11426,7 +11863,9 @@ async function registerAgent2(input, store, context) {
|
|
|
11426
11863
|
last_seen_at: timestamp,
|
|
11427
11864
|
session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
|
|
11428
11865
|
working_dir: input.working_dir ?? existing?.working_dir ?? null,
|
|
11429
|
-
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
|
|
11430
11869
|
};
|
|
11431
11870
|
return store.upsert("agents", agent, context);
|
|
11432
11871
|
}
|
|
@@ -11453,7 +11892,9 @@ async function createTaskList2(input, store, context) {
|
|
|
11453
11892
|
description: input.description ?? null,
|
|
11454
11893
|
metadata: input.metadata ?? {},
|
|
11455
11894
|
created_at: timestamp,
|
|
11456
|
-
updated_at: timestamp
|
|
11895
|
+
updated_at: timestamp,
|
|
11896
|
+
machine_id: store.machineId(context),
|
|
11897
|
+
synced_at: null
|
|
11457
11898
|
}, context);
|
|
11458
11899
|
}
|
|
11459
11900
|
async function updateTaskList2(id, input, store) {
|
|
@@ -11479,7 +11920,9 @@ async function createTemplate2(input, store, context) {
|
|
|
11479
11920
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
11480
11921
|
plan_id: input.plan_id ?? null,
|
|
11481
11922
|
metadata: input.metadata ?? {},
|
|
11482
|
-
created_at: timestamp
|
|
11923
|
+
created_at: timestamp,
|
|
11924
|
+
machine_id: store.machineId(context),
|
|
11925
|
+
synced_at: null
|
|
11483
11926
|
}, context);
|
|
11484
11927
|
}
|
|
11485
11928
|
async function updateTemplate2(id, input, store) {
|
|
@@ -11504,7 +11947,8 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
11504
11947
|
old_value: oldValue ?? null,
|
|
11505
11948
|
new_value: newValue ?? null,
|
|
11506
11949
|
agent_id: agentId ?? context?.agentId ?? null,
|
|
11507
|
-
created_at: new Date().toISOString()
|
|
11950
|
+
created_at: new Date().toISOString(),
|
|
11951
|
+
machine_id: store.machineId(context)
|
|
11508
11952
|
};
|
|
11509
11953
|
return store.upsert("audit_history", entry2, context);
|
|
11510
11954
|
}
|
|
@@ -11527,18 +11971,21 @@ async function exportSnapshot(store) {
|
|
|
11527
11971
|
source: "postgres",
|
|
11528
11972
|
tasks: await store.list("tasks"),
|
|
11529
11973
|
projects: await store.list("projects"),
|
|
11974
|
+
projectMachinePaths: await store.list("project_machine_paths"),
|
|
11530
11975
|
plans: await store.list("plans"),
|
|
11531
11976
|
agents: await store.list("agents"),
|
|
11532
11977
|
taskLists: await store.list("task_lists"),
|
|
11533
11978
|
templates: await store.list("templates"),
|
|
11534
|
-
auditHistory: await store.list("audit_history")
|
|
11979
|
+
auditHistory: await store.list("audit_history"),
|
|
11980
|
+
tombstones: await store.listTombstones()
|
|
11535
11981
|
};
|
|
11536
11982
|
}
|
|
11537
11983
|
async function importSnapshot(snapshot, store, context) {
|
|
11538
|
-
const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
|
|
11984
|
+
const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
|
|
11539
11985
|
const entries = [
|
|
11540
11986
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
11541
11987
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
11988
|
+
...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
|
|
11542
11989
|
...snapshot.plans.map((row) => ["plans", row]),
|
|
11543
11990
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
11544
11991
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
@@ -11557,6 +12004,25 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
11557
12004
|
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
11558
12005
|
}
|
|
11559
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
|
+
}
|
|
11560
12026
|
return result;
|
|
11561
12027
|
}
|
|
11562
12028
|
async function requireRecord(type, id, store) {
|
|
@@ -11639,8 +12105,17 @@ function payloadRecord2(value) {
|
|
|
11639
12105
|
throw new Error("Postgres storage payload must be a JSON object");
|
|
11640
12106
|
}
|
|
11641
12107
|
function stringValue2(value) {
|
|
12108
|
+
if (value instanceof Date)
|
|
12109
|
+
return value.toISOString();
|
|
11642
12110
|
return typeof value === "string" && value ? value : null;
|
|
11643
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
|
+
}
|
|
11644
12119
|
function numberValue2(value) {
|
|
11645
12120
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
11646
12121
|
}
|