@hasna/todos 0.15.46 → 0.15.49
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/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
- package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/components/Dashboard.d.ts +1 -1
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/ProjectList.d.ts +1 -1
- package/dist/cli/components/ProjectList.d.ts.map +1 -1
- package/dist/cli/components/SearchView.d.ts +1 -1
- package/dist/cli/components/SearchView.d.ts.map +1 -1
- package/dist/cli/components/TaskDetail.d.ts +1 -1
- package/dist/cli/components/TaskDetail.d.ts.map +1 -1
- package/dist/cli/components/TaskForm.d.ts +1 -1
- package/dist/cli/components/TaskForm.d.ts.map +1 -1
- package/dist/cli/components/TaskList.d.ts +1 -1
- package/dist/cli/components/TaskList.d.ts.map +1 -1
- package/dist/cli/index.js +671 -155
- package/dist/contracts.js +396 -82
- package/dist/db/task-crud.d.ts +6 -0
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +561 -143
- package/dist/lib/dedupe-projection.d.ts +48 -0
- package/dist/lib/dedupe-projection.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +5 -9
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.js +621 -154
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +6 -4
- package/dist/project-registration.js +502 -141
- package/dist/registry.js +396 -82
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1278 -342
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-errors.d.ts +15 -0
- package/dist/storage/postgres-errors.d.ts.map +1 -0
- package/dist/storage/postgres-sync.d.ts +7 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage.js +510 -137
- package/dist/task-manifest.js +118 -47
- package/dist/task-subtree-transfer.js +326 -44
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -4
- package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
- package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/task-manifest.js
CHANGED
|
@@ -615,13 +615,15 @@ function upsertSecretSafetyConfig(input) {
|
|
|
615
615
|
saveConfig({ ...config, secret_safety: next });
|
|
616
616
|
return next;
|
|
617
617
|
}
|
|
618
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
618
|
+
var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
619
619
|
var init_redaction = __esm(() => {
|
|
620
620
|
init_config();
|
|
621
|
+
XAI_PREFIX = ["x", "ai", "-"].join("");
|
|
621
622
|
DEFAULT_SECRET_PATTERNS = [
|
|
622
623
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
623
624
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
624
625
|
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
626
|
+
{ name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
625
627
|
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
626
628
|
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
627
629
|
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
@@ -6010,6 +6012,30 @@ class SqliteTodosTaskManifestBackend {
|
|
|
6010
6012
|
}
|
|
6011
6013
|
}
|
|
6012
6014
|
|
|
6015
|
+
// src/storage/postgres-sync.ts
|
|
6016
|
+
init_types();
|
|
6017
|
+
|
|
6018
|
+
// src/storage/postgres-errors.ts
|
|
6019
|
+
function isPostgresUniqueViolation(error) {
|
|
6020
|
+
if (typeof error !== "object" || error === null)
|
|
6021
|
+
return false;
|
|
6022
|
+
const candidate = error;
|
|
6023
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
6024
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
6025
|
+
const cause = candidate.cause;
|
|
6026
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
6027
|
+
}
|
|
6028
|
+
return states.some((state) => String(state) === "23505");
|
|
6029
|
+
}
|
|
6030
|
+
function postgresConstraintName(error) {
|
|
6031
|
+
if (typeof error !== "object" || error === null)
|
|
6032
|
+
return "";
|
|
6033
|
+
const candidate = error;
|
|
6034
|
+
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
6035
|
+
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
6036
|
+
return typeof constraint === "string" ? constraint : "";
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6013
6039
|
// src/storage/postgres-sync.ts
|
|
6014
6040
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
6015
6041
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -6212,53 +6238,98 @@ class PostgresTodosSyncStore {
|
|
|
6212
6238
|
if (routingErrors.length > 0) {
|
|
6213
6239
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
6214
6240
|
}
|
|
6215
|
-
const
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
const
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6241
|
+
const push = async (client) => {
|
|
6242
|
+
const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
6243
|
+
FROM ${this.tableName}
|
|
6244
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
6245
|
+
const existingProjects = [];
|
|
6246
|
+
const existingTaskLists = [];
|
|
6247
|
+
for (const row of existing.rows) {
|
|
6248
|
+
const payload = payloadRecord(row.payload);
|
|
6249
|
+
if (row.object_type === "projects")
|
|
6250
|
+
existingProjects.push(payload);
|
|
6251
|
+
if (row.object_type === "task_lists")
|
|
6252
|
+
existingTaskLists.push(payload);
|
|
6253
|
+
}
|
|
6254
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
6255
|
+
if (destinationErrors.length > 0) {
|
|
6256
|
+
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
6257
|
+
}
|
|
6258
|
+
const result = { records: 0, objectTypes: {} };
|
|
6259
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
6260
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
6261
|
+
if (entry.deletedAt === null)
|
|
6262
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
6263
|
+
try {
|
|
6264
|
+
await client.query(`INSERT INTO ${this.tableName} (
|
|
6265
|
+
service, object_type, object_id, payload, updated_at,
|
|
6266
|
+
deleted_at, source_machine_id, version
|
|
6267
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
6268
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
6269
|
+
payload = EXCLUDED.payload,
|
|
6270
|
+
updated_at = EXCLUDED.updated_at,
|
|
6271
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
6272
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
6273
|
+
version = EXCLUDED.version
|
|
6274
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
6275
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
6276
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
6277
|
+
this.service,
|
|
6278
|
+
entry.type,
|
|
6279
|
+
entry.id,
|
|
6280
|
+
entry.payload,
|
|
6281
|
+
entry.updatedAt,
|
|
6282
|
+
entry.deletedAt,
|
|
6283
|
+
sourceMachineId,
|
|
6284
|
+
entry.version
|
|
6285
|
+
]);
|
|
6286
|
+
} catch (error) {
|
|
6287
|
+
await this.classifySyncInsertConflict(error, entry);
|
|
6288
|
+
}
|
|
6289
|
+
result.records += 1;
|
|
6290
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
6291
|
+
}
|
|
6292
|
+
return result;
|
|
6293
|
+
};
|
|
6294
|
+
if (typeof this.client.transaction === "function") {
|
|
6295
|
+
return this.client.transaction((client) => push(client));
|
|
6296
|
+
}
|
|
6297
|
+
return push(this.client);
|
|
6298
|
+
}
|
|
6299
|
+
async classifySyncInsertConflict(error, entry) {
|
|
6300
|
+
if (!isPostgresUniqueViolation(error))
|
|
6301
|
+
throw error;
|
|
6302
|
+
const payload = entry.payload;
|
|
6303
|
+
const taskListSlug = String(payload["slug"] ?? "");
|
|
6304
|
+
const projectSlug = String(payload["task_list_id"] ?? "");
|
|
6305
|
+
const constraintName = postgresConstraintName(error);
|
|
6306
|
+
if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
|
|
6307
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
6308
|
+
}
|
|
6309
|
+
if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
|
|
6310
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
6311
|
+
}
|
|
6312
|
+
if (entry.type === "task_lists") {
|
|
6313
|
+
const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
|
|
6314
|
+
const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
|
|
6315
|
+
SELECT 1 FROM ${this.tableName}
|
|
6316
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
|
|
6317
|
+
AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
|
|
6318
|
+
) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
|
|
6319
|
+
if (conflict.rows[0]?.conflict) {
|
|
6320
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
6321
|
+
}
|
|
6322
|
+
} else if (entry.type === "projects") {
|
|
6323
|
+
const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
|
|
6324
|
+
SELECT 1 FROM ${this.tableName}
|
|
6325
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
6326
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
6327
|
+
) AS conflict`, [this.service, entry.id, projectSlug]);
|
|
6328
|
+
if (conflict.rows[0]?.conflict) {
|
|
6329
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
6330
|
+
}
|
|
6260
6331
|
}
|
|
6261
|
-
|
|
6332
|
+
throw error;
|
|
6262
6333
|
}
|
|
6263
6334
|
async pullSnapshot(options = {}) {
|
|
6264
6335
|
const params = [this.service];
|
|
@@ -39,6 +39,219 @@ var __export = (target, all) => {
|
|
|
39
39
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
40
|
var __require = import.meta.require;
|
|
41
41
|
|
|
42
|
+
// src/types/index.ts
|
|
43
|
+
function isBlockingDependencyStatus(status) {
|
|
44
|
+
return status !== "completed" && status !== "cancelled";
|
|
45
|
+
}
|
|
46
|
+
function isTerminalStatus(status) {
|
|
47
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
48
|
+
}
|
|
49
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
50
|
+
var init_types = __esm(() => {
|
|
51
|
+
TASK_STATUSES = [
|
|
52
|
+
"pending",
|
|
53
|
+
"in_progress",
|
|
54
|
+
"completed",
|
|
55
|
+
"failed",
|
|
56
|
+
"cancelled"
|
|
57
|
+
];
|
|
58
|
+
TASK_PRIORITIES = [
|
|
59
|
+
"low",
|
|
60
|
+
"medium",
|
|
61
|
+
"high",
|
|
62
|
+
"critical"
|
|
63
|
+
];
|
|
64
|
+
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
65
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
66
|
+
taskId;
|
|
67
|
+
expectedVersion;
|
|
68
|
+
actualVersion;
|
|
69
|
+
static code = "VERSION_CONFLICT";
|
|
70
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
71
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
72
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
73
|
+
this.taskId = taskId;
|
|
74
|
+
this.expectedVersion = expectedVersion;
|
|
75
|
+
this.actualVersion = actualVersion;
|
|
76
|
+
this.name = "VersionConflictError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
80
|
+
taskId;
|
|
81
|
+
static code = "TASK_NOT_FOUND";
|
|
82
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
83
|
+
constructor(taskId) {
|
|
84
|
+
super(`Task not found: ${taskId}`);
|
|
85
|
+
this.taskId = taskId;
|
|
86
|
+
this.name = "TaskNotFoundError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
TaskNotStartableError = class TaskNotStartableError extends Error {
|
|
90
|
+
taskId;
|
|
91
|
+
status;
|
|
92
|
+
agentId;
|
|
93
|
+
static code = "TASK_NOT_STARTABLE";
|
|
94
|
+
static suggestion = "Reset the task status to pending before starting it again.";
|
|
95
|
+
constructor(taskId, status, agentId) {
|
|
96
|
+
super(`Task ${taskId} is ${status} and cannot be started by ${agentId}; ` + "reset the task status to pending before starting it again");
|
|
97
|
+
this.taskId = taskId;
|
|
98
|
+
this.status = status;
|
|
99
|
+
this.agentId = agentId;
|
|
100
|
+
this.name = "TaskNotStartableError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
104
|
+
reference;
|
|
105
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
106
|
+
candidateTaskIds;
|
|
107
|
+
candidateProjectIds;
|
|
108
|
+
constructor(reference, candidates) {
|
|
109
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
110
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
111
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
112
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
113
|
+
this.reference = reference;
|
|
114
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
115
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
116
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
120
|
+
projectId;
|
|
121
|
+
static code = "PROJECT_NOT_FOUND";
|
|
122
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
123
|
+
constructor(projectId) {
|
|
124
|
+
super(`Project not found: ${projectId}`);
|
|
125
|
+
this.projectId = projectId;
|
|
126
|
+
this.name = "ProjectNotFoundError";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
130
|
+
code;
|
|
131
|
+
constructor(code, message) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.code = code;
|
|
134
|
+
this.name = "ResourceConflictError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
138
|
+
planId;
|
|
139
|
+
expectedUpdatedAt;
|
|
140
|
+
currentUpdatedAt;
|
|
141
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
142
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
143
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
144
|
+
this.planId = planId;
|
|
145
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
146
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
147
|
+
this.name = "PlanRevisionConflictError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
151
|
+
planId;
|
|
152
|
+
static code = "PLAN_NOT_FOUND";
|
|
153
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
154
|
+
constructor(planId) {
|
|
155
|
+
super(`Plan not found: ${planId}`);
|
|
156
|
+
this.planId = planId;
|
|
157
|
+
this.name = "PlanNotFoundError";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
LockError = class LockError extends Error {
|
|
161
|
+
taskId;
|
|
162
|
+
lockedBy;
|
|
163
|
+
static code = "LOCK_ERROR";
|
|
164
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
165
|
+
constructor(taskId, lockedBy) {
|
|
166
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
167
|
+
this.taskId = taskId;
|
|
168
|
+
this.lockedBy = lockedBy;
|
|
169
|
+
this.name = "LockError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
173
|
+
code;
|
|
174
|
+
details;
|
|
175
|
+
constructor(code, message, details = {}) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.code = code;
|
|
178
|
+
this.details = details;
|
|
179
|
+
this.name = "StaleLockHandoffError";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
183
|
+
agentId;
|
|
184
|
+
static code = "AGENT_NOT_FOUND";
|
|
185
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
186
|
+
constructor(agentId) {
|
|
187
|
+
super(`Agent not found: ${agentId}`);
|
|
188
|
+
this.agentId = agentId;
|
|
189
|
+
this.name = "AgentNotFoundError";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
193
|
+
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
194
|
+
candidates;
|
|
195
|
+
constructor(subject, candidates = []) {
|
|
196
|
+
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
197
|
+
this.name = "IdentityAliasAmbiguousError";
|
|
198
|
+
this.candidates = [...new Set(candidates)].sort();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
202
|
+
code = "IDENTITY_ID_IMMUTABLE";
|
|
203
|
+
constructor(agentId) {
|
|
204
|
+
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
205
|
+
this.name = "IdentityIdImmutableError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
209
|
+
taskListId;
|
|
210
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
211
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
212
|
+
constructor(taskListId) {
|
|
213
|
+
super(`Task list not found: ${taskListId}`);
|
|
214
|
+
this.taskListId = taskListId;
|
|
215
|
+
this.name = "TaskListNotFoundError";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
219
|
+
taskId;
|
|
220
|
+
dependsOn;
|
|
221
|
+
static code = "DEPENDENCY_CYCLE";
|
|
222
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
223
|
+
constructor(taskId, dependsOn) {
|
|
224
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
225
|
+
this.taskId = taskId;
|
|
226
|
+
this.dependsOn = dependsOn;
|
|
227
|
+
this.name = "DependencyCycleError";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
CompletionGuardError = class CompletionGuardError extends Error {
|
|
231
|
+
reason;
|
|
232
|
+
retryAfterSeconds;
|
|
233
|
+
static code = "COMPLETION_BLOCKED";
|
|
234
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
235
|
+
constructor(reason, retryAfterSeconds) {
|
|
236
|
+
super(reason);
|
|
237
|
+
this.reason = reason;
|
|
238
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
239
|
+
this.name = "CompletionGuardError";
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
243
|
+
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
244
|
+
dispatchId;
|
|
245
|
+
static code = "DISPATCH_NOT_FOUND";
|
|
246
|
+
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
247
|
+
constructor(dispatchId) {
|
|
248
|
+
super(`Dispatch not found: ${dispatchId}`);
|
|
249
|
+
this.dispatchId = dispatchId;
|
|
250
|
+
this.name = "DispatchNotFoundError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
42
255
|
// src/lib/slugs.ts
|
|
43
256
|
function normalizeSlug(value) {
|
|
44
257
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -165,6 +378,30 @@ function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
|
|
|
165
378
|
}).slice(0, 48)}`;
|
|
166
379
|
}
|
|
167
380
|
|
|
381
|
+
// src/storage/postgres-sync.ts
|
|
382
|
+
init_types();
|
|
383
|
+
|
|
384
|
+
// src/storage/postgres-errors.ts
|
|
385
|
+
function isPostgresUniqueViolation(error) {
|
|
386
|
+
if (typeof error !== "object" || error === null)
|
|
387
|
+
return false;
|
|
388
|
+
const candidate = error;
|
|
389
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
390
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
391
|
+
const cause = candidate.cause;
|
|
392
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
393
|
+
}
|
|
394
|
+
return states.some((state) => String(state) === "23505");
|
|
395
|
+
}
|
|
396
|
+
function postgresConstraintName(error) {
|
|
397
|
+
if (typeof error !== "object" || error === null)
|
|
398
|
+
return "";
|
|
399
|
+
const candidate = error;
|
|
400
|
+
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
401
|
+
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
402
|
+
return typeof constraint === "string" ? constraint : "";
|
|
403
|
+
}
|
|
404
|
+
|
|
168
405
|
// src/storage/postgres-sync.ts
|
|
169
406
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
170
407
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -367,53 +604,98 @@ class PostgresTodosSyncStore {
|
|
|
367
604
|
if (routingErrors.length > 0) {
|
|
368
605
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
369
606
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
607
|
+
const push = async (client) => {
|
|
608
|
+
const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
609
|
+
FROM ${this.tableName}
|
|
610
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
611
|
+
const existingProjects = [];
|
|
612
|
+
const existingTaskLists = [];
|
|
613
|
+
for (const row of existing.rows) {
|
|
614
|
+
const payload = payloadRecord(row.payload);
|
|
615
|
+
if (row.object_type === "projects")
|
|
616
|
+
existingProjects.push(payload);
|
|
617
|
+
if (row.object_type === "task_lists")
|
|
618
|
+
existingTaskLists.push(payload);
|
|
619
|
+
}
|
|
620
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
621
|
+
if (destinationErrors.length > 0) {
|
|
622
|
+
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
623
|
+
}
|
|
624
|
+
const result = { records: 0, objectTypes: {} };
|
|
625
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
626
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
627
|
+
if (entry.deletedAt === null)
|
|
628
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
629
|
+
try {
|
|
630
|
+
await client.query(`INSERT INTO ${this.tableName} (
|
|
631
|
+
service, object_type, object_id, payload, updated_at,
|
|
632
|
+
deleted_at, source_machine_id, version
|
|
633
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
634
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
635
|
+
payload = EXCLUDED.payload,
|
|
636
|
+
updated_at = EXCLUDED.updated_at,
|
|
637
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
638
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
639
|
+
version = EXCLUDED.version
|
|
640
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
641
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
642
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
643
|
+
this.service,
|
|
644
|
+
entry.type,
|
|
645
|
+
entry.id,
|
|
646
|
+
entry.payload,
|
|
647
|
+
entry.updatedAt,
|
|
648
|
+
entry.deletedAt,
|
|
649
|
+
sourceMachineId,
|
|
650
|
+
entry.version
|
|
651
|
+
]);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
await this.classifySyncInsertConflict(error, entry);
|
|
654
|
+
}
|
|
655
|
+
result.records += 1;
|
|
656
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
};
|
|
660
|
+
if (typeof this.client.transaction === "function") {
|
|
661
|
+
return this.client.transaction((client) => push(client));
|
|
662
|
+
}
|
|
663
|
+
return push(this.client);
|
|
664
|
+
}
|
|
665
|
+
async classifySyncInsertConflict(error, entry) {
|
|
666
|
+
if (!isPostgresUniqueViolation(error))
|
|
667
|
+
throw error;
|
|
668
|
+
const payload = entry.payload;
|
|
669
|
+
const taskListSlug = String(payload["slug"] ?? "");
|
|
670
|
+
const projectSlug = String(payload["task_list_id"] ?? "");
|
|
671
|
+
const constraintName = postgresConstraintName(error);
|
|
672
|
+
if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
|
|
673
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
381
674
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
675
|
+
if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
|
|
676
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
385
677
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
entry.type,
|
|
406
|
-
entry.id,
|
|
407
|
-
entry.payload,
|
|
408
|
-
entry.updatedAt,
|
|
409
|
-
entry.deletedAt,
|
|
410
|
-
sourceMachineId,
|
|
411
|
-
entry.version
|
|
412
|
-
]);
|
|
413
|
-
result.records += 1;
|
|
414
|
-
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
678
|
+
if (entry.type === "task_lists") {
|
|
679
|
+
const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
|
|
680
|
+
const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
|
|
681
|
+
SELECT 1 FROM ${this.tableName}
|
|
682
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
|
|
683
|
+
AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
|
|
684
|
+
) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
|
|
685
|
+
if (conflict.rows[0]?.conflict) {
|
|
686
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
687
|
+
}
|
|
688
|
+
} else if (entry.type === "projects") {
|
|
689
|
+
const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
|
|
690
|
+
SELECT 1 FROM ${this.tableName}
|
|
691
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
692
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
693
|
+
) AS conflict`, [this.service, entry.id, projectSlug]);
|
|
694
|
+
if (conflict.rows[0]?.conflict) {
|
|
695
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
696
|
+
}
|
|
415
697
|
}
|
|
416
|
-
|
|
698
|
+
throw error;
|
|
417
699
|
}
|
|
418
700
|
async pullSnapshot(options = {}) {
|
|
419
701
|
const params = [this.service];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1219,8 +1219,8 @@ export declare class ProjectNotFoundError extends Error {
|
|
|
1219
1219
|
constructor(projectId: string);
|
|
1220
1220
|
}
|
|
1221
1221
|
export declare class ResourceConflictError extends Error {
|
|
1222
|
-
readonly code: "PROJECT_SLUG_CONFLICT" | "TASK_LIST_SLUG_CONFLICT" | "PLAN_SLUG_CONFLICT" | "PLAN_PROJECT_LINK_CONFLICT" | "TASK_PARENT_CYCLE" | "PROJECT_PARENT_CYCLE";
|
|
1223
|
-
constructor(code: "PROJECT_SLUG_CONFLICT" | "TASK_LIST_SLUG_CONFLICT" | "PLAN_SLUG_CONFLICT" | "PLAN_PROJECT_LINK_CONFLICT" | "TASK_PARENT_CYCLE" | "PROJECT_PARENT_CYCLE", message: string);
|
|
1222
|
+
readonly code: "PROJECT_SLUG_CONFLICT" | "TASK_LIST_SLUG_CONFLICT" | "PLAN_SLUG_CONFLICT" | "PLAN_PROJECT_LINK_CONFLICT" | "TASK_PARENT_CYCLE" | "PROJECT_PARENT_CYCLE" | "SNAPSHOT_DESTINATION_CONFLICT";
|
|
1223
|
+
constructor(code: "PROJECT_SLUG_CONFLICT" | "TASK_LIST_SLUG_CONFLICT" | "PLAN_SLUG_CONFLICT" | "PLAN_PROJECT_LINK_CONFLICT" | "TASK_PARENT_CYCLE" | "PROJECT_PARENT_CYCLE" | "SNAPSHOT_DESTINATION_CONFLICT", message: string);
|
|
1224
1224
|
}
|
|
1225
1225
|
export declare class PlanRevisionConflictError extends Error {
|
|
1226
1226
|
readonly planId: string;
|