@hasna/todos 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -14
- package/dist/cli/cloud-router.d.ts +100 -7
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/doctor-integrity.d.ts +106 -0
- package/dist/cli/doctor-integrity.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +2 -2
- package/dist/cli/index.js +1509 -786
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- package/dist/db/agent-names.d.ts +2 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/findings.d.ts +1 -0
- package/dist/db/findings.d.ts.map +1 -1
- package/dist/db/integrity.d.ts +20 -0
- package/dist/db/integrity.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +584 -140
- package/dist/lib/agent-name-normalize.d.ts +17 -0
- package/dist/lib/agent-name-normalize.d.ts.map +1 -0
- package/dist/lib/context-packs.d.ts +1 -0
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/dependency-graph.d.ts +1 -1
- package/dist/lib/doctor.d.ts +20 -0
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/integrity.d.ts +215 -0
- package/dist/lib/integrity.d.ts.map +1 -0
- package/dist/lib/native-storage-status.d.ts +2 -2
- package/dist/lib/native-storage-status.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +15 -0
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +554 -92
- package/dist/mcp.js +7 -3
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +125 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/types.d.ts +42 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +1 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +560 -98
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +23 -5
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/factory.d.ts +9 -3
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/hybrid.d.ts +7 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +29 -8
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.d.ts +3 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +623 -258
- package/dist/testing.d.ts +79 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +107 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/storage.js
CHANGED
|
@@ -43,15 +43,18 @@ var __require = import.meta.require;
|
|
|
43
43
|
var exports_config = {};
|
|
44
44
|
__export(exports_config, {
|
|
45
45
|
parseStorageMode: () => parseStorageMode,
|
|
46
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
46
47
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
47
48
|
loadStorageConfig: () => loadStorageConfig,
|
|
48
49
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
49
50
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
51
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
50
52
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
51
53
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
52
54
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
53
55
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
54
56
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
57
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
55
58
|
getStorageMode: () => getStorageMode,
|
|
56
59
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
57
60
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -76,7 +79,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
function loadTodosStorageConfig(env = process.env) {
|
|
79
|
-
const mode =
|
|
82
|
+
const mode = getTodosStorageBackend(env);
|
|
80
83
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
81
84
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
82
85
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -113,29 +116,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
113
116
|
function loadStorageConfig(env = process.env) {
|
|
114
117
|
return loadTodosStorageConfig(env);
|
|
115
118
|
}
|
|
119
|
+
function isTodosPostgresBackend(config) {
|
|
120
|
+
return config.mode === "postgres";
|
|
121
|
+
}
|
|
116
122
|
function isTodosRemoteStorageEnabled(config) {
|
|
117
|
-
return config
|
|
123
|
+
return isTodosPostgresBackend(config);
|
|
118
124
|
}
|
|
119
125
|
function assertTodosRemoteStorageConfig(config) {
|
|
120
|
-
if (!
|
|
126
|
+
if (!isTodosPostgresBackend(config))
|
|
121
127
|
return;
|
|
122
128
|
if (!config.database?.url) {
|
|
123
129
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
|
-
function
|
|
132
|
+
function parseStorageBackend(value) {
|
|
127
133
|
const normalized = clean(value)?.toLowerCase();
|
|
128
134
|
if (!normalized)
|
|
129
|
-
return "
|
|
130
|
-
if (normalized === "
|
|
135
|
+
return "sqlite";
|
|
136
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
131
137
|
return normalized;
|
|
132
|
-
|
|
138
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
139
|
+
if (legacy)
|
|
140
|
+
return legacy;
|
|
141
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
142
|
+
}
|
|
143
|
+
function parseStorageMode(value) {
|
|
144
|
+
return parseStorageBackend(value);
|
|
145
|
+
}
|
|
146
|
+
function getTodosStorageBackend(env = process.env) {
|
|
147
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
133
148
|
}
|
|
134
149
|
function getTodosStorageMode(env = process.env) {
|
|
135
|
-
return
|
|
150
|
+
return getTodosStorageBackend(env);
|
|
136
151
|
}
|
|
137
152
|
function getStorageMode(env = process.env) {
|
|
138
|
-
return
|
|
153
|
+
return getTodosStorageBackend(env);
|
|
139
154
|
}
|
|
140
155
|
function isTodosShadowEnabled(env = process.env) {
|
|
141
156
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -146,8 +161,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
146
161
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
147
162
|
if (!isTodosShadowEnabled(env))
|
|
148
163
|
return;
|
|
149
|
-
if (config.mode !== "
|
|
150
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
164
|
+
if (config.mode !== "sqlite") {
|
|
165
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
151
166
|
}
|
|
152
167
|
if (!config.database?.url) {
|
|
153
168
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -203,7 +218,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
203
218
|
}
|
|
204
219
|
return parsed;
|
|
205
220
|
}
|
|
206
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
221
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos", LEGACY_BACKEND_TOKENS;
|
|
207
222
|
var init_config = __esm(() => {
|
|
208
223
|
TODOS_STORAGE_TABLES = [
|
|
209
224
|
"todos_sync_records",
|
|
@@ -244,6 +259,187 @@ var init_config = __esm(() => {
|
|
|
244
259
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
245
260
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
246
261
|
};
|
|
262
|
+
LEGACY_BACKEND_TOKENS = {
|
|
263
|
+
local: "sqlite",
|
|
264
|
+
remote: "postgres",
|
|
265
|
+
postgresql: "postgres",
|
|
266
|
+
hybrid: "postgres",
|
|
267
|
+
self_hosted: "postgres",
|
|
268
|
+
cloud: "postgres"
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// src/types/index.ts
|
|
273
|
+
function isBlockingDependencyStatus(status) {
|
|
274
|
+
return status !== "completed" && status !== "cancelled";
|
|
275
|
+
}
|
|
276
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
277
|
+
var init_types = __esm(() => {
|
|
278
|
+
TASK_STATUSES = [
|
|
279
|
+
"pending",
|
|
280
|
+
"in_progress",
|
|
281
|
+
"completed",
|
|
282
|
+
"failed",
|
|
283
|
+
"cancelled"
|
|
284
|
+
];
|
|
285
|
+
TASK_PRIORITIES = [
|
|
286
|
+
"low",
|
|
287
|
+
"medium",
|
|
288
|
+
"high",
|
|
289
|
+
"critical"
|
|
290
|
+
];
|
|
291
|
+
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
292
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
293
|
+
taskId;
|
|
294
|
+
expectedVersion;
|
|
295
|
+
actualVersion;
|
|
296
|
+
static code = "VERSION_CONFLICT";
|
|
297
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
298
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
299
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
300
|
+
this.taskId = taskId;
|
|
301
|
+
this.expectedVersion = expectedVersion;
|
|
302
|
+
this.actualVersion = actualVersion;
|
|
303
|
+
this.name = "VersionConflictError";
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
307
|
+
taskId;
|
|
308
|
+
static code = "TASK_NOT_FOUND";
|
|
309
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
310
|
+
constructor(taskId) {
|
|
311
|
+
super(`Task not found: ${taskId}`);
|
|
312
|
+
this.taskId = taskId;
|
|
313
|
+
this.name = "TaskNotFoundError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
317
|
+
reference;
|
|
318
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
319
|
+
candidateTaskIds;
|
|
320
|
+
candidateProjectIds;
|
|
321
|
+
constructor(reference, candidates) {
|
|
322
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
323
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
324
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
325
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
326
|
+
this.reference = reference;
|
|
327
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
328
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
329
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
333
|
+
projectId;
|
|
334
|
+
static code = "PROJECT_NOT_FOUND";
|
|
335
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
336
|
+
constructor(projectId) {
|
|
337
|
+
super(`Project not found: ${projectId}`);
|
|
338
|
+
this.projectId = projectId;
|
|
339
|
+
this.name = "ProjectNotFoundError";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
343
|
+
code;
|
|
344
|
+
constructor(code, message) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.code = code;
|
|
347
|
+
this.name = "ResourceConflictError";
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
351
|
+
planId;
|
|
352
|
+
static code = "PLAN_NOT_FOUND";
|
|
353
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
354
|
+
constructor(planId) {
|
|
355
|
+
super(`Plan not found: ${planId}`);
|
|
356
|
+
this.planId = planId;
|
|
357
|
+
this.name = "PlanNotFoundError";
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
LockError = class LockError extends Error {
|
|
361
|
+
taskId;
|
|
362
|
+
lockedBy;
|
|
363
|
+
static code = "LOCK_ERROR";
|
|
364
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
365
|
+
constructor(taskId, lockedBy) {
|
|
366
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
367
|
+
this.taskId = taskId;
|
|
368
|
+
this.lockedBy = lockedBy;
|
|
369
|
+
this.name = "LockError";
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
373
|
+
agentId;
|
|
374
|
+
static code = "AGENT_NOT_FOUND";
|
|
375
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
376
|
+
constructor(agentId) {
|
|
377
|
+
super(`Agent not found: ${agentId}`);
|
|
378
|
+
this.agentId = agentId;
|
|
379
|
+
this.name = "AgentNotFoundError";
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
383
|
+
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
384
|
+
candidates;
|
|
385
|
+
constructor(subject, candidates = []) {
|
|
386
|
+
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
387
|
+
this.name = "IdentityAliasAmbiguousError";
|
|
388
|
+
this.candidates = [...new Set(candidates)].sort();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
392
|
+
code = "IDENTITY_ID_IMMUTABLE";
|
|
393
|
+
constructor(agentId) {
|
|
394
|
+
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
395
|
+
this.name = "IdentityIdImmutableError";
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
399
|
+
taskListId;
|
|
400
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
401
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
402
|
+
constructor(taskListId) {
|
|
403
|
+
super(`Task list not found: ${taskListId}`);
|
|
404
|
+
this.taskListId = taskListId;
|
|
405
|
+
this.name = "TaskListNotFoundError";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
409
|
+
taskId;
|
|
410
|
+
dependsOn;
|
|
411
|
+
static code = "DEPENDENCY_CYCLE";
|
|
412
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
413
|
+
constructor(taskId, dependsOn) {
|
|
414
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
415
|
+
this.taskId = taskId;
|
|
416
|
+
this.dependsOn = dependsOn;
|
|
417
|
+
this.name = "DependencyCycleError";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
CompletionGuardError = class CompletionGuardError extends Error {
|
|
421
|
+
reason;
|
|
422
|
+
retryAfterSeconds;
|
|
423
|
+
static code = "COMPLETION_BLOCKED";
|
|
424
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
425
|
+
constructor(reason, retryAfterSeconds) {
|
|
426
|
+
super(reason);
|
|
427
|
+
this.reason = reason;
|
|
428
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
429
|
+
this.name = "CompletionGuardError";
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
433
|
+
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
434
|
+
dispatchId;
|
|
435
|
+
static code = "DISPATCH_NOT_FOUND";
|
|
436
|
+
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
437
|
+
constructor(dispatchId) {
|
|
438
|
+
super(`Dispatch not found: ${dispatchId}`);
|
|
439
|
+
this.dispatchId = dispatchId;
|
|
440
|
+
this.name = "DispatchNotFoundError";
|
|
441
|
+
}
|
|
442
|
+
};
|
|
247
443
|
});
|
|
248
444
|
|
|
249
445
|
// src/db/migrations.ts
|
|
@@ -3630,163 +3826,6 @@ var init_machines = __esm(() => {
|
|
|
3630
3826
|
];
|
|
3631
3827
|
});
|
|
3632
3828
|
|
|
3633
|
-
// src/types/index.ts
|
|
3634
|
-
function isBlockingDependencyStatus(status) {
|
|
3635
|
-
return status !== "completed" && status !== "cancelled";
|
|
3636
|
-
}
|
|
3637
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3638
|
-
var init_types = __esm(() => {
|
|
3639
|
-
TASK_STATUSES = [
|
|
3640
|
-
"pending",
|
|
3641
|
-
"in_progress",
|
|
3642
|
-
"completed",
|
|
3643
|
-
"failed",
|
|
3644
|
-
"cancelled"
|
|
3645
|
-
];
|
|
3646
|
-
TASK_PRIORITIES = [
|
|
3647
|
-
"low",
|
|
3648
|
-
"medium",
|
|
3649
|
-
"high",
|
|
3650
|
-
"critical"
|
|
3651
|
-
];
|
|
3652
|
-
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
3653
|
-
VersionConflictError = class VersionConflictError extends Error {
|
|
3654
|
-
taskId;
|
|
3655
|
-
expectedVersion;
|
|
3656
|
-
actualVersion;
|
|
3657
|
-
static code = "VERSION_CONFLICT";
|
|
3658
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
3659
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
3660
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
3661
|
-
this.taskId = taskId;
|
|
3662
|
-
this.expectedVersion = expectedVersion;
|
|
3663
|
-
this.actualVersion = actualVersion;
|
|
3664
|
-
this.name = "VersionConflictError";
|
|
3665
|
-
}
|
|
3666
|
-
};
|
|
3667
|
-
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
3668
|
-
taskId;
|
|
3669
|
-
static code = "TASK_NOT_FOUND";
|
|
3670
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
3671
|
-
constructor(taskId) {
|
|
3672
|
-
super(`Task not found: ${taskId}`);
|
|
3673
|
-
this.taskId = taskId;
|
|
3674
|
-
this.name = "TaskNotFoundError";
|
|
3675
|
-
}
|
|
3676
|
-
};
|
|
3677
|
-
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
3678
|
-
projectId;
|
|
3679
|
-
static code = "PROJECT_NOT_FOUND";
|
|
3680
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
3681
|
-
constructor(projectId) {
|
|
3682
|
-
super(`Project not found: ${projectId}`);
|
|
3683
|
-
this.projectId = projectId;
|
|
3684
|
-
this.name = "ProjectNotFoundError";
|
|
3685
|
-
}
|
|
3686
|
-
};
|
|
3687
|
-
ResourceConflictError = class ResourceConflictError extends Error {
|
|
3688
|
-
code;
|
|
3689
|
-
constructor(code, message) {
|
|
3690
|
-
super(message);
|
|
3691
|
-
this.code = code;
|
|
3692
|
-
this.name = "ResourceConflictError";
|
|
3693
|
-
}
|
|
3694
|
-
};
|
|
3695
|
-
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3696
|
-
planId;
|
|
3697
|
-
static code = "PLAN_NOT_FOUND";
|
|
3698
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
3699
|
-
constructor(planId) {
|
|
3700
|
-
super(`Plan not found: ${planId}`);
|
|
3701
|
-
this.planId = planId;
|
|
3702
|
-
this.name = "PlanNotFoundError";
|
|
3703
|
-
}
|
|
3704
|
-
};
|
|
3705
|
-
LockError = class LockError extends Error {
|
|
3706
|
-
taskId;
|
|
3707
|
-
lockedBy;
|
|
3708
|
-
static code = "LOCK_ERROR";
|
|
3709
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
3710
|
-
constructor(taskId, lockedBy) {
|
|
3711
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
3712
|
-
this.taskId = taskId;
|
|
3713
|
-
this.lockedBy = lockedBy;
|
|
3714
|
-
this.name = "LockError";
|
|
3715
|
-
}
|
|
3716
|
-
};
|
|
3717
|
-
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
3718
|
-
agentId;
|
|
3719
|
-
static code = "AGENT_NOT_FOUND";
|
|
3720
|
-
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
3721
|
-
constructor(agentId) {
|
|
3722
|
-
super(`Agent not found: ${agentId}`);
|
|
3723
|
-
this.agentId = agentId;
|
|
3724
|
-
this.name = "AgentNotFoundError";
|
|
3725
|
-
}
|
|
3726
|
-
};
|
|
3727
|
-
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
3728
|
-
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
3729
|
-
candidates;
|
|
3730
|
-
constructor(subject, candidates = []) {
|
|
3731
|
-
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
3732
|
-
this.name = "IdentityAliasAmbiguousError";
|
|
3733
|
-
this.candidates = [...new Set(candidates)].sort();
|
|
3734
|
-
}
|
|
3735
|
-
};
|
|
3736
|
-
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
3737
|
-
code = "IDENTITY_ID_IMMUTABLE";
|
|
3738
|
-
constructor(agentId) {
|
|
3739
|
-
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
3740
|
-
this.name = "IdentityIdImmutableError";
|
|
3741
|
-
}
|
|
3742
|
-
};
|
|
3743
|
-
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
3744
|
-
taskListId;
|
|
3745
|
-
static code = "TASK_LIST_NOT_FOUND";
|
|
3746
|
-
static suggestion = "Use list_task_lists to see available lists.";
|
|
3747
|
-
constructor(taskListId) {
|
|
3748
|
-
super(`Task list not found: ${taskListId}`);
|
|
3749
|
-
this.taskListId = taskListId;
|
|
3750
|
-
this.name = "TaskListNotFoundError";
|
|
3751
|
-
}
|
|
3752
|
-
};
|
|
3753
|
-
DependencyCycleError = class DependencyCycleError extends Error {
|
|
3754
|
-
taskId;
|
|
3755
|
-
dependsOn;
|
|
3756
|
-
static code = "DEPENDENCY_CYCLE";
|
|
3757
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
3758
|
-
constructor(taskId, dependsOn) {
|
|
3759
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
3760
|
-
this.taskId = taskId;
|
|
3761
|
-
this.dependsOn = dependsOn;
|
|
3762
|
-
this.name = "DependencyCycleError";
|
|
3763
|
-
}
|
|
3764
|
-
};
|
|
3765
|
-
CompletionGuardError = class CompletionGuardError extends Error {
|
|
3766
|
-
reason;
|
|
3767
|
-
retryAfterSeconds;
|
|
3768
|
-
static code = "COMPLETION_BLOCKED";
|
|
3769
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
3770
|
-
constructor(reason, retryAfterSeconds) {
|
|
3771
|
-
super(reason);
|
|
3772
|
-
this.reason = reason;
|
|
3773
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
3774
|
-
this.name = "CompletionGuardError";
|
|
3775
|
-
}
|
|
3776
|
-
};
|
|
3777
|
-
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
3778
|
-
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
3779
|
-
dispatchId;
|
|
3780
|
-
static code = "DISPATCH_NOT_FOUND";
|
|
3781
|
-
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
3782
|
-
constructor(dispatchId) {
|
|
3783
|
-
super(`Dispatch not found: ${dispatchId}`);
|
|
3784
|
-
this.dispatchId = dispatchId;
|
|
3785
|
-
this.name = "DispatchNotFoundError";
|
|
3786
|
-
}
|
|
3787
|
-
};
|
|
3788
|
-
});
|
|
3789
|
-
|
|
3790
3829
|
// src/db/identity-mapping.ts
|
|
3791
3830
|
function timestamp() {
|
|
3792
3831
|
return new Date().toISOString();
|
|
@@ -4092,6 +4131,30 @@ function findGitRoot(startDir) {
|
|
|
4092
4131
|
}
|
|
4093
4132
|
return null;
|
|
4094
4133
|
}
|
|
4134
|
+
function getGlobalDbPath() {
|
|
4135
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4136
|
+
return join(home, ".hasna", "todos", "todos.db");
|
|
4137
|
+
}
|
|
4138
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4139
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
4140
|
+
}
|
|
4141
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
4142
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
4143
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
4144
|
+
const arg = args[index];
|
|
4145
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
4146
|
+
index += 1;
|
|
4147
|
+
continue;
|
|
4148
|
+
}
|
|
4149
|
+
if (arg.startsWith("-"))
|
|
4150
|
+
continue;
|
|
4151
|
+
return arg;
|
|
4152
|
+
}
|
|
4153
|
+
return null;
|
|
4154
|
+
}
|
|
4155
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
4156
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
4157
|
+
}
|
|
4095
4158
|
function getDbPath() {
|
|
4096
4159
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4097
4160
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4099,18 +4162,19 @@ function getDbPath() {
|
|
|
4099
4162
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4100
4163
|
return process.env["TODOS_DB_PATH"];
|
|
4101
4164
|
}
|
|
4165
|
+
if (hasExplicitProjectArg())
|
|
4166
|
+
return getGlobalDbPath();
|
|
4102
4167
|
const cwd = process.cwd();
|
|
4103
4168
|
const nearest = findNearestProjectDb(cwd);
|
|
4104
4169
|
if (nearest)
|
|
4105
4170
|
return nearest;
|
|
4106
4171
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4107
4172
|
const gitRoot = findGitRoot(cwd);
|
|
4108
|
-
if (gitRoot) {
|
|
4173
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4109
4174
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4110
4175
|
}
|
|
4111
4176
|
}
|
|
4112
|
-
|
|
4113
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4177
|
+
return getGlobalDbPath();
|
|
4114
4178
|
}
|
|
4115
4179
|
function getDatabasePath() {
|
|
4116
4180
|
return getDbPath();
|
|
@@ -4213,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4213
4277
|
return null;
|
|
4214
4278
|
}
|
|
4215
4279
|
if (table === "tasks") {
|
|
4216
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
4280
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
4217
4281
|
if (shortIdRows.length === 1) {
|
|
4218
4282
|
return shortIdRows[0].id;
|
|
4219
4283
|
}
|
|
4284
|
+
if (shortIdRows.length > 1) {
|
|
4285
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4286
|
+
}
|
|
4220
4287
|
}
|
|
4221
4288
|
if (table === "task_lists") {
|
|
4222
4289
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -11834,6 +11901,9 @@ var init_tasks = __esm(() => {
|
|
|
11834
11901
|
// src/storage/index.ts
|
|
11835
11902
|
init_config();
|
|
11836
11903
|
|
|
11904
|
+
// src/storage/local-sqlite.ts
|
|
11905
|
+
init_types();
|
|
11906
|
+
|
|
11837
11907
|
// src/lib/search.ts
|
|
11838
11908
|
init_database();
|
|
11839
11909
|
function rowToTask(row) {
|
|
@@ -11999,6 +12069,11 @@ init_database();
|
|
|
11999
12069
|
init_identity_mapping();
|
|
12000
12070
|
init_storage_tombstones();
|
|
12001
12071
|
|
|
12072
|
+
// src/lib/agent-name-normalize.ts
|
|
12073
|
+
function normalizeAgentNameInput(name) {
|
|
12074
|
+
return name.trim().toLowerCase();
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12002
12077
|
// src/db/agent-names.ts
|
|
12003
12078
|
class InvalidAgentNameError extends Error {
|
|
12004
12079
|
suggestions;
|
|
@@ -12143,9 +12218,6 @@ var RESERVED_GENERIC_NAMES = new Set([
|
|
|
12143
12218
|
]);
|
|
12144
12219
|
var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
|
|
12145
12220
|
var ONE_WORD_NAME_RE = /^[a-z]+$/;
|
|
12146
|
-
function normalizeAgentNameInput(name) {
|
|
12147
|
-
return name.trim().toLowerCase();
|
|
12148
|
-
}
|
|
12149
12221
|
function hasGeneratedNumericSuffix(name) {
|
|
12150
12222
|
return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
|
|
12151
12223
|
}
|
|
@@ -12650,6 +12722,267 @@ init_audit();
|
|
|
12650
12722
|
init_comments();
|
|
12651
12723
|
init_database();
|
|
12652
12724
|
|
|
12725
|
+
// src/db/integrity.ts
|
|
12726
|
+
init_database();
|
|
12727
|
+
|
|
12728
|
+
// src/lib/integrity.ts
|
|
12729
|
+
init_types();
|
|
12730
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
|
|
12731
|
+
var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
12732
|
+
var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
12733
|
+
var INTEGRITY_CONDITIONS = [
|
|
12734
|
+
{
|
|
12735
|
+
id: "tasks_without_project",
|
|
12736
|
+
entity: "task",
|
|
12737
|
+
field: "project_id",
|
|
12738
|
+
target: "project",
|
|
12739
|
+
kind: "missing",
|
|
12740
|
+
base_severity: "warn",
|
|
12741
|
+
escalate_when_open: true,
|
|
12742
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
12743
|
+
},
|
|
12744
|
+
{
|
|
12745
|
+
id: "tasks_without_task_list",
|
|
12746
|
+
entity: "task",
|
|
12747
|
+
field: "task_list_id",
|
|
12748
|
+
target: "task_list",
|
|
12749
|
+
kind: "missing",
|
|
12750
|
+
base_severity: "warn",
|
|
12751
|
+
escalate_when_open: true,
|
|
12752
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
12753
|
+
},
|
|
12754
|
+
{
|
|
12755
|
+
id: "tasks_with_unregistered_project",
|
|
12756
|
+
entity: "task",
|
|
12757
|
+
field: "project_id",
|
|
12758
|
+
target: "project",
|
|
12759
|
+
kind: "dangling",
|
|
12760
|
+
base_severity: "error",
|
|
12761
|
+
escalate_when_open: false,
|
|
12762
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
12763
|
+
},
|
|
12764
|
+
{
|
|
12765
|
+
id: "tasks_with_unregistered_task_list",
|
|
12766
|
+
entity: "task",
|
|
12767
|
+
field: "task_list_id",
|
|
12768
|
+
target: "task_list",
|
|
12769
|
+
kind: "dangling",
|
|
12770
|
+
base_severity: "error",
|
|
12771
|
+
escalate_when_open: false,
|
|
12772
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
12773
|
+
},
|
|
12774
|
+
{
|
|
12775
|
+
id: "task_lists_without_project",
|
|
12776
|
+
entity: "task_list",
|
|
12777
|
+
field: "project_id",
|
|
12778
|
+
target: "project",
|
|
12779
|
+
kind: "missing",
|
|
12780
|
+
base_severity: "warn",
|
|
12781
|
+
escalate_when_open: false,
|
|
12782
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
12783
|
+
},
|
|
12784
|
+
{
|
|
12785
|
+
id: "task_lists_with_unregistered_project",
|
|
12786
|
+
entity: "task_list",
|
|
12787
|
+
field: "project_id",
|
|
12788
|
+
target: "project",
|
|
12789
|
+
kind: "dangling",
|
|
12790
|
+
base_severity: "error",
|
|
12791
|
+
escalate_when_open: false,
|
|
12792
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
12793
|
+
}
|
|
12794
|
+
];
|
|
12795
|
+
function integrityCondition(id) {
|
|
12796
|
+
const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
|
|
12797
|
+
if (!spec)
|
|
12798
|
+
throw new Error(`unknown integrity condition: ${id}`);
|
|
12799
|
+
return spec;
|
|
12800
|
+
}
|
|
12801
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
12802
|
+
if (spec.kind === "dangling")
|
|
12803
|
+
return "error";
|
|
12804
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
12805
|
+
return "error";
|
|
12806
|
+
return spec.base_severity;
|
|
12807
|
+
}
|
|
12808
|
+
var ENTITY_LABEL = {
|
|
12809
|
+
task: ["task", "tasks"],
|
|
12810
|
+
task_list: ["task list", "task lists"]
|
|
12811
|
+
};
|
|
12812
|
+
function plural(entity, count) {
|
|
12813
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
12814
|
+
return count === 1 ? one : many;
|
|
12815
|
+
}
|
|
12816
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
12817
|
+
const noun = plural(spec.entity, measurement.count);
|
|
12818
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
12819
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
12820
|
+
return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
|
|
12821
|
+
}
|
|
12822
|
+
function measuredCondition(spec, measurement, source) {
|
|
12823
|
+
return {
|
|
12824
|
+
id: spec.id,
|
|
12825
|
+
entity: spec.entity,
|
|
12826
|
+
field: spec.field,
|
|
12827
|
+
kind: spec.kind,
|
|
12828
|
+
count: measurement.count,
|
|
12829
|
+
open_count: measurement.open_count,
|
|
12830
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
12831
|
+
verified: true,
|
|
12832
|
+
source,
|
|
12833
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
12834
|
+
impact: spec.impact
|
|
12835
|
+
};
|
|
12836
|
+
}
|
|
12837
|
+
function unverifiedCondition(spec, reason) {
|
|
12838
|
+
return {
|
|
12839
|
+
id: spec.id,
|
|
12840
|
+
entity: spec.entity,
|
|
12841
|
+
field: spec.field,
|
|
12842
|
+
kind: spec.kind,
|
|
12843
|
+
count: null,
|
|
12844
|
+
open_count: null,
|
|
12845
|
+
severity: null,
|
|
12846
|
+
verified: false,
|
|
12847
|
+
source: "unverified",
|
|
12848
|
+
unverified_reason: reason,
|
|
12849
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
12850
|
+
impact: spec.impact
|
|
12851
|
+
};
|
|
12852
|
+
}
|
|
12853
|
+
function summarizeIntegrity(conditions) {
|
|
12854
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
12855
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
12856
|
+
const unverified = conditions.length - measured.length;
|
|
12857
|
+
return {
|
|
12858
|
+
ok: unverified === 0 && findings.length === 0,
|
|
12859
|
+
findings: findings.length,
|
|
12860
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
12861
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
12862
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
12863
|
+
unverified,
|
|
12864
|
+
complete: unverified === 0
|
|
12865
|
+
};
|
|
12866
|
+
}
|
|
12867
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
12868
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
12869
|
+
return {
|
|
12870
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
12871
|
+
generated_at: generatedAt,
|
|
12872
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
12873
|
+
conditions,
|
|
12874
|
+
summary: summarizeIntegrity(conditions)
|
|
12875
|
+
};
|
|
12876
|
+
}
|
|
12877
|
+
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
12878
|
+
const received = new Map;
|
|
12879
|
+
if (Array.isArray(raw.conditions)) {
|
|
12880
|
+
for (const entry of raw.conditions) {
|
|
12881
|
+
if (entry && typeof entry["id"] === "string")
|
|
12882
|
+
received.set(entry["id"], entry);
|
|
12883
|
+
}
|
|
12884
|
+
}
|
|
12885
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12886
|
+
const entry = received.get(spec.id);
|
|
12887
|
+
const count = entry?.["count"];
|
|
12888
|
+
const verified = entry?.["verified"];
|
|
12889
|
+
if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
|
|
12890
|
+
return unverifiedCondition(spec, entry ? typeof entry["unverified_reason"] === "string" ? String(entry["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
|
|
12891
|
+
}
|
|
12892
|
+
const openRaw = entry["open_count"];
|
|
12893
|
+
return measuredCondition(spec, {
|
|
12894
|
+
count,
|
|
12895
|
+
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
12896
|
+
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
12897
|
+
});
|
|
12898
|
+
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
12899
|
+
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
12900
|
+
}
|
|
12901
|
+
var SQLITE_TABLE = {
|
|
12902
|
+
task: "tasks",
|
|
12903
|
+
task_list: "task_lists",
|
|
12904
|
+
project: "projects"
|
|
12905
|
+
};
|
|
12906
|
+
function sqliteOpenStatusList() {
|
|
12907
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
12908
|
+
}
|
|
12909
|
+
function buildSqliteIntegritySql(spec) {
|
|
12910
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
12911
|
+
const target = SQLITE_TABLE[spec.target];
|
|
12912
|
+
const column = `t."${spec.field}"`;
|
|
12913
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
|
|
12914
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
12915
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
12916
|
+
}
|
|
12917
|
+
var RECORD_TYPE = {
|
|
12918
|
+
task: "tasks",
|
|
12919
|
+
task_list: "task_lists",
|
|
12920
|
+
project: "projects"
|
|
12921
|
+
};
|
|
12922
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
12923
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
12924
|
+
const p = (value) => {
|
|
12925
|
+
params.push(value);
|
|
12926
|
+
return `$${params.length}`;
|
|
12927
|
+
};
|
|
12928
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
12929
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
|
|
12930
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
12931
|
+
const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
|
|
12932
|
+
return { sql, params };
|
|
12933
|
+
}
|
|
12934
|
+
function referenceOf(row, field) {
|
|
12935
|
+
const raw = row[field];
|
|
12936
|
+
if (typeof raw !== "string")
|
|
12937
|
+
return null;
|
|
12938
|
+
const trimmed = raw.trim();
|
|
12939
|
+
return trimmed === "" ? null : trimmed;
|
|
12940
|
+
}
|
|
12941
|
+
function measureIntegrityRows(spec, sets) {
|
|
12942
|
+
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
12943
|
+
if (!rows)
|
|
12944
|
+
return null;
|
|
12945
|
+
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
12946
|
+
if (spec.kind === "dangling" && !registered)
|
|
12947
|
+
return null;
|
|
12948
|
+
let count = 0;
|
|
12949
|
+
let open = 0;
|
|
12950
|
+
for (const row of rows) {
|
|
12951
|
+
const reference = referenceOf(row, spec.field);
|
|
12952
|
+
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
12953
|
+
if (!matches)
|
|
12954
|
+
continue;
|
|
12955
|
+
count++;
|
|
12956
|
+
if (spec.entity === "task") {
|
|
12957
|
+
const status = row.status;
|
|
12958
|
+
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
12959
|
+
open++;
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12962
|
+
return { count, open_count: spec.entity === "task" ? open : null };
|
|
12963
|
+
}
|
|
12964
|
+
|
|
12965
|
+
// src/db/integrity.ts
|
|
12966
|
+
var REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
12967
|
+
function tableExists(db, table) {
|
|
12968
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
12969
|
+
}
|
|
12970
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
12971
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
12972
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12973
|
+
if (missing.length > 0) {
|
|
12974
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
12975
|
+
}
|
|
12976
|
+
try {
|
|
12977
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
12978
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
12979
|
+
} catch (error) {
|
|
12980
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
12981
|
+
}
|
|
12982
|
+
});
|
|
12983
|
+
return buildIntegrityReport(conditions, now());
|
|
12984
|
+
}
|
|
12985
|
+
|
|
12653
12986
|
// src/storage/sqlite-snapshot.ts
|
|
12654
12987
|
init_database();
|
|
12655
12988
|
init_audit();
|
|
@@ -13041,15 +13374,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
13041
13374
|
return null;
|
|
13042
13375
|
if (TASK_UUID_RE.test(raw))
|
|
13043
13376
|
return getTask(raw, db);
|
|
13044
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
13377
|
+
const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
13045
13378
|
if (prefixRows.length > 1) {
|
|
13046
|
-
throw new
|
|
13379
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13047
13380
|
}
|
|
13048
13381
|
if (prefixRows.length === 1)
|
|
13049
13382
|
return getTask(prefixRows[0].id, db);
|
|
13050
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
13383
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
13051
13384
|
if (shortIdRows.length > 1) {
|
|
13052
|
-
throw new
|
|
13385
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13053
13386
|
}
|
|
13054
13387
|
if (shortIdRows.length === 1)
|
|
13055
13388
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -13198,6 +13531,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13198
13531
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
13199
13532
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
13200
13533
|
},
|
|
13534
|
+
integrity: {
|
|
13535
|
+
report: () => scanSqliteIntegrity(database())
|
|
13536
|
+
},
|
|
13201
13537
|
transaction: (fn) => {
|
|
13202
13538
|
const tx = database().transaction(() => fn(adapter));
|
|
13203
13539
|
return tx();
|
|
@@ -13206,6 +13542,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13206
13542
|
return adapter;
|
|
13207
13543
|
}
|
|
13208
13544
|
|
|
13545
|
+
// src/storage/postgres-adapter.ts
|
|
13546
|
+
init_types();
|
|
13547
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13548
|
+
|
|
13209
13549
|
// src/storage/postgres-sync.ts
|
|
13210
13550
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
13211
13551
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -13595,54 +13935,7 @@ function assertSafeIdentifier(value) {
|
|
|
13595
13935
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
13596
13936
|
}
|
|
13597
13937
|
|
|
13598
|
-
// src/storage/hybrid.ts
|
|
13599
|
-
function createHybridTodosStorageAdapter(options) {
|
|
13600
|
-
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
13601
|
-
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
13602
|
-
if (!syncStore)
|
|
13603
|
-
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
13604
|
-
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
13605
|
-
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
13606
|
-
}
|
|
13607
|
-
const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
|
|
13608
|
-
const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
13609
|
-
const remote = {
|
|
13610
|
-
ensureSchema: () => syncStore.ensureSchema(),
|
|
13611
|
-
async pushSnapshot(context) {
|
|
13612
|
-
const snapshot = await exportSnapshot(context);
|
|
13613
|
-
return syncStore.pushSnapshot(snapshot, context);
|
|
13614
|
-
},
|
|
13615
|
-
async pullSnapshot(options2, context) {
|
|
13616
|
-
const snapshot = await syncStore.pullSnapshot(options2);
|
|
13617
|
-
return importSnapshot(snapshot, context);
|
|
13618
|
-
},
|
|
13619
|
-
async syncOnce(options2, context) {
|
|
13620
|
-
const pulled = await remote.pullSnapshot(options2, context);
|
|
13621
|
-
const pushed = await remote.pushSnapshot(context);
|
|
13622
|
-
return { pulled, pushed };
|
|
13623
|
-
}
|
|
13624
|
-
};
|
|
13625
|
-
return {
|
|
13626
|
-
...local,
|
|
13627
|
-
kind: "hybrid",
|
|
13628
|
-
capabilities: {
|
|
13629
|
-
...local.capabilities,
|
|
13630
|
-
localPersistence: true,
|
|
13631
|
-
remotePersistence: true,
|
|
13632
|
-
sync: true
|
|
13633
|
-
},
|
|
13634
|
-
sync: {
|
|
13635
|
-
...local.sync,
|
|
13636
|
-
exportSnapshot,
|
|
13637
|
-
importSnapshot
|
|
13638
|
-
},
|
|
13639
|
-
remote
|
|
13640
|
-
};
|
|
13641
|
-
}
|
|
13642
|
-
|
|
13643
13938
|
// src/storage/postgres-adapter.ts
|
|
13644
|
-
init_types();
|
|
13645
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
13646
13939
|
init_redaction();
|
|
13647
13940
|
function createPostgresTodosStorageAdapter(options) {
|
|
13648
13941
|
const store = new PostgresJsonRecordStore(options);
|
|
@@ -13713,7 +14006,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13713
14006
|
agents: {
|
|
13714
14007
|
register: (input, context) => registerAgent2(input, store, context),
|
|
13715
14008
|
get: (id) => store.get("agents", id),
|
|
13716
|
-
getByName: async (name) => (await store.list("agents")
|
|
14009
|
+
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
13717
14010
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
13718
14011
|
update: (id, input) => updateAgent2(id, input, store),
|
|
13719
14012
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
@@ -13770,6 +14063,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13770
14063
|
exportSnapshot: () => exportSnapshot(store),
|
|
13771
14064
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
13772
14065
|
},
|
|
14066
|
+
integrity: {
|
|
14067
|
+
report: () => store.integrityReport()
|
|
14068
|
+
},
|
|
13773
14069
|
transaction: (fn) => fn(adapter)
|
|
13774
14070
|
};
|
|
13775
14071
|
return adapter;
|
|
@@ -13876,8 +14172,9 @@ class PostgresJsonRecordStore {
|
|
|
13876
14172
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
13877
14173
|
if (filter.session_id !== undefined)
|
|
13878
14174
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
13879
|
-
if (filter.tags?.length)
|
|
13880
|
-
conds.push(`payload->'tags'
|
|
14175
|
+
if (filter.tags?.length) {
|
|
14176
|
+
conds.push(`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(payload->'tags', '[]'::jsonb)) AS task_tags(tag) ` + `WHERE ${inClause("task_tags.tag", filter.tags)})`);
|
|
14177
|
+
}
|
|
13881
14178
|
if (filter.has_recurrence !== undefined) {
|
|
13882
14179
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
13883
14180
|
}
|
|
@@ -13938,7 +14235,8 @@ class PostgresJsonRecordStore {
|
|
|
13938
14235
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
13939
14236
|
LIMIT 2`, [this.service, raw, upper]);
|
|
13940
14237
|
if (prefixResult.rows.length > 1) {
|
|
13941
|
-
|
|
14238
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14239
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13942
14240
|
}
|
|
13943
14241
|
if (prefixResult.rows.length === 1) {
|
|
13944
14242
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -13949,7 +14247,8 @@ class PostgresJsonRecordStore {
|
|
|
13949
14247
|
AND LOWER(payload->>'short_id') = $2
|
|
13950
14248
|
LIMIT 2`, [this.service, raw]);
|
|
13951
14249
|
if (shortIdResult.rows.length > 1) {
|
|
13952
|
-
|
|
14250
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14251
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13953
14252
|
}
|
|
13954
14253
|
if (shortIdResult.rows.length === 1) {
|
|
13955
14254
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -13963,6 +14262,24 @@ class PostgresJsonRecordStore {
|
|
|
13963
14262
|
const result = await this.options.client.query(sql, params);
|
|
13964
14263
|
return Number(result.rows[0]?.count ?? 0);
|
|
13965
14264
|
}
|
|
14265
|
+
async integrityReport() {
|
|
14266
|
+
await this.ensureSchema();
|
|
14267
|
+
const conditions = [];
|
|
14268
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
14269
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
14270
|
+
try {
|
|
14271
|
+
const result = await this.options.client.query(sql, params);
|
|
14272
|
+
const row = result.rows[0];
|
|
14273
|
+
conditions.push(measuredCondition(spec, {
|
|
14274
|
+
count: Number(row?.count ?? 0),
|
|
14275
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
14276
|
+
}, "postgres"));
|
|
14277
|
+
} catch (error) {
|
|
14278
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
14279
|
+
}
|
|
14280
|
+
}
|
|
14281
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
14282
|
+
}
|
|
13966
14283
|
async listTombstones() {
|
|
13967
14284
|
await this.ensureSchema();
|
|
13968
14285
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -14438,7 +14755,7 @@ async function startTask2(id, agentId, store) {
|
|
|
14438
14755
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
14439
14756
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
14440
14757
|
}
|
|
14441
|
-
|
|
14758
|
+
const started = await patchTask(task, {
|
|
14442
14759
|
status: "in_progress",
|
|
14443
14760
|
assigned_to: task.assigned_to ?? agentId,
|
|
14444
14761
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -14446,6 +14763,8 @@ async function startTask2(id, agentId, store) {
|
|
|
14446
14763
|
locked_at: new Date().toISOString(),
|
|
14447
14764
|
started_at: task.started_at ?? new Date().toISOString()
|
|
14448
14765
|
}, store);
|
|
14766
|
+
await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
14767
|
+
return started;
|
|
14449
14768
|
}
|
|
14450
14769
|
async function completeTask2(id, agentId, options, store) {
|
|
14451
14770
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -14751,15 +15070,27 @@ async function updatePlan2(id, input, store) {
|
|
|
14751
15070
|
}
|
|
14752
15071
|
return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
|
|
14753
15072
|
}
|
|
15073
|
+
function matchAgentByName(agents, name, options) {
|
|
15074
|
+
const target = normalizeAgentNameInput(name);
|
|
15075
|
+
if (!target)
|
|
15076
|
+
return null;
|
|
15077
|
+
const matches = agents.filter((agent) => normalizeAgentNameInput(agent.name) === target && (options?.includeArchived !== false || agent.status !== "archived"));
|
|
15078
|
+
if (matches.length === 0)
|
|
15079
|
+
return null;
|
|
15080
|
+
return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
|
|
15081
|
+
}
|
|
14754
15082
|
async function registerAgent2(input, store, context) {
|
|
14755
|
-
const
|
|
15083
|
+
const canonicalName = normalizeAgentNameInput(input.name);
|
|
15084
|
+
const existing = matchAgentByName(await store.list("agents"), canonicalName, {
|
|
15085
|
+
includeArchived: false
|
|
15086
|
+
});
|
|
14756
15087
|
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
14757
|
-
return { conflict: true, message: `Agent name '${
|
|
15088
|
+
return { conflict: true, message: `Agent name '${canonicalName}' is already active` };
|
|
14758
15089
|
}
|
|
14759
15090
|
const timestamp2 = new Date().toISOString();
|
|
14760
15091
|
const agent = {
|
|
14761
15092
|
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
14762
|
-
name:
|
|
15093
|
+
name: canonicalName,
|
|
14763
15094
|
description: input.description ?? existing?.description ?? null,
|
|
14764
15095
|
role: input.role ?? existing?.role ?? null,
|
|
14765
15096
|
title: input.title ?? existing?.title ?? null,
|
|
@@ -14797,7 +15128,7 @@ async function resolveAgent(idOrName, store) {
|
|
|
14797
15128
|
const byId = await store.get("agents", idOrName);
|
|
14798
15129
|
if (byId)
|
|
14799
15130
|
return byId;
|
|
14800
|
-
return (await store.list("agents")
|
|
15131
|
+
return matchAgentByName(await store.list("agents"), idOrName);
|
|
14801
15132
|
}
|
|
14802
15133
|
async function heartbeatAgent(idOrName, store, context) {
|
|
14803
15134
|
const agent = await resolveAgent(idOrName, store);
|
|
@@ -15554,17 +15885,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
|
15554
15885
|
init_config();
|
|
15555
15886
|
function createTodosStorageAdapter(options = {}) {
|
|
15556
15887
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
15557
|
-
if (config.mode === "
|
|
15888
|
+
if (config.mode === "sqlite") {
|
|
15558
15889
|
if (isTodosShadowEnabled(options.env))
|
|
15559
15890
|
return createShadowAdapter(options, config);
|
|
15560
15891
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
15561
15892
|
}
|
|
15562
15893
|
assertTodosRemoteStorageConfig(config);
|
|
15563
|
-
const adapter =
|
|
15894
|
+
const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
15564
15895
|
if (!adapter) {
|
|
15565
|
-
throw new Error(
|
|
15896
|
+
throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
|
|
15566
15897
|
}
|
|
15567
|
-
|
|
15898
|
+
assertPostgresAdapterCapabilities(adapter);
|
|
15568
15899
|
return adapter;
|
|
15569
15900
|
}
|
|
15570
15901
|
function createShadowAdapter(options, config) {
|
|
@@ -15578,7 +15909,7 @@ function createShadowAdapter(options, config) {
|
|
|
15578
15909
|
...options.local ? { local: options.local } : {},
|
|
15579
15910
|
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
15580
15911
|
...postgresClient ? { postgresClient } : {},
|
|
15581
|
-
...options.
|
|
15912
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15582
15913
|
});
|
|
15583
15914
|
}
|
|
15584
15915
|
function createImplicitPostgresAdapter(options) {
|
|
@@ -15586,25 +15917,12 @@ function createImplicitPostgresAdapter(options) {
|
|
|
15586
15917
|
return null;
|
|
15587
15918
|
return createPostgresTodosStorageAdapter({
|
|
15588
15919
|
client: options.postgresClient,
|
|
15589
|
-
...options.
|
|
15590
|
-
});
|
|
15591
|
-
}
|
|
15592
|
-
function createImplicitHybridAdapter(options) {
|
|
15593
|
-
if (!options.postgresClient && !options.postgresSyncStore)
|
|
15594
|
-
return null;
|
|
15595
|
-
return createHybridTodosStorageAdapter({
|
|
15596
|
-
...options.hybrid ?? {},
|
|
15597
|
-
local: options.local,
|
|
15598
|
-
postgresClient: options.postgresClient,
|
|
15599
|
-
syncStore: options.postgresSyncStore
|
|
15920
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15600
15921
|
});
|
|
15601
15922
|
}
|
|
15602
|
-
function
|
|
15923
|
+
function assertPostgresAdapterCapabilities(adapter) {
|
|
15603
15924
|
if (!adapter.capabilities.remotePersistence) {
|
|
15604
|
-
throw new Error(
|
|
15605
|
-
}
|
|
15606
|
-
if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
|
|
15607
|
-
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
15925
|
+
throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
|
|
15608
15926
|
}
|
|
15609
15927
|
}
|
|
15610
15928
|
// src/storage/shadow-outbox.ts
|
|
@@ -15931,6 +16249,50 @@ async function closeRuntimeShadowCloud() {
|
|
|
15931
16249
|
await cloud.close().catch(() => {});
|
|
15932
16250
|
}
|
|
15933
16251
|
}
|
|
16252
|
+
// src/storage/hybrid.ts
|
|
16253
|
+
function createHybridTodosStorageAdapter(options) {
|
|
16254
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
16255
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
16256
|
+
if (!syncStore)
|
|
16257
|
+
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
16258
|
+
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
16259
|
+
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
16260
|
+
}
|
|
16261
|
+
const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
|
|
16262
|
+
const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
16263
|
+
const remote = {
|
|
16264
|
+
ensureSchema: () => syncStore.ensureSchema(),
|
|
16265
|
+
async pushSnapshot(context) {
|
|
16266
|
+
const snapshot = await exportSnapshot2(context);
|
|
16267
|
+
return syncStore.pushSnapshot(snapshot, context);
|
|
16268
|
+
},
|
|
16269
|
+
async pullSnapshot(options2, context) {
|
|
16270
|
+
const snapshot = await syncStore.pullSnapshot(options2);
|
|
16271
|
+
return importSnapshot2(snapshot, context);
|
|
16272
|
+
},
|
|
16273
|
+
async syncOnce(options2, context) {
|
|
16274
|
+
const pulled = await remote.pullSnapshot(options2, context);
|
|
16275
|
+
const pushed = await remote.pushSnapshot(context);
|
|
16276
|
+
return { pulled, pushed };
|
|
16277
|
+
}
|
|
16278
|
+
};
|
|
16279
|
+
return {
|
|
16280
|
+
...local,
|
|
16281
|
+
kind: "hybrid",
|
|
16282
|
+
capabilities: {
|
|
16283
|
+
...local.capabilities,
|
|
16284
|
+
localPersistence: true,
|
|
16285
|
+
remotePersistence: true,
|
|
16286
|
+
sync: true
|
|
16287
|
+
},
|
|
16288
|
+
sync: {
|
|
16289
|
+
...local.sync,
|
|
16290
|
+
exportSnapshot: exportSnapshot2,
|
|
16291
|
+
importSnapshot: importSnapshot2
|
|
16292
|
+
},
|
|
16293
|
+
remote
|
|
16294
|
+
};
|
|
16295
|
+
}
|
|
15934
16296
|
// src/storage/comment-redaction-backfill.ts
|
|
15935
16297
|
init_redaction();
|
|
15936
16298
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
@@ -16465,11 +16827,13 @@ export {
|
|
|
16465
16827
|
postgresTodosCommentCursorIndexSql,
|
|
16466
16828
|
planRunArtifactsS3Sync,
|
|
16467
16829
|
parseStorageMode,
|
|
16830
|
+
parseStorageBackend,
|
|
16468
16831
|
maybeInstallShadowCapture2 as maybeInstallShadowCapture,
|
|
16469
16832
|
loadTodosStorageConfig,
|
|
16470
16833
|
loadStorageConfig,
|
|
16471
16834
|
isTodosShadowEnabled,
|
|
16472
16835
|
isTodosRemoteStorageEnabled,
|
|
16836
|
+
isTodosPostgresBackend,
|
|
16473
16837
|
isCommentRedactionBackfillComplete,
|
|
16474
16838
|
installShadowOutboxSchema,
|
|
16475
16839
|
importSqliteTodosStorageSnapshot,
|
|
@@ -16478,6 +16842,7 @@ export {
|
|
|
16478
16842
|
getTodosStorageEnvName,
|
|
16479
16843
|
getTodosStorageDatabaseUrl,
|
|
16480
16844
|
getTodosStorageDatabaseEnv,
|
|
16845
|
+
getTodosStorageBackend,
|
|
16481
16846
|
getStorageMode,
|
|
16482
16847
|
getStorageDatabaseUrl,
|
|
16483
16848
|
getStorageDatabaseEnv,
|