@hasna/todos 0.13.2 → 0.13.3
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 +57 -7
- package/dist/cli/cloud-router.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 +1461 -778
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- 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 +562 -132
- 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 +517 -85
- 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 +521 -89
- 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 +601 -250
- 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) {
|
|
@@ -12650,6 +12720,267 @@ init_audit();
|
|
|
12650
12720
|
init_comments();
|
|
12651
12721
|
init_database();
|
|
12652
12722
|
|
|
12723
|
+
// src/db/integrity.ts
|
|
12724
|
+
init_database();
|
|
12725
|
+
|
|
12726
|
+
// src/lib/integrity.ts
|
|
12727
|
+
init_types();
|
|
12728
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
|
|
12729
|
+
var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
12730
|
+
var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
12731
|
+
var INTEGRITY_CONDITIONS = [
|
|
12732
|
+
{
|
|
12733
|
+
id: "tasks_without_project",
|
|
12734
|
+
entity: "task",
|
|
12735
|
+
field: "project_id",
|
|
12736
|
+
target: "project",
|
|
12737
|
+
kind: "missing",
|
|
12738
|
+
base_severity: "warn",
|
|
12739
|
+
escalate_when_open: true,
|
|
12740
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
12741
|
+
},
|
|
12742
|
+
{
|
|
12743
|
+
id: "tasks_without_task_list",
|
|
12744
|
+
entity: "task",
|
|
12745
|
+
field: "task_list_id",
|
|
12746
|
+
target: "task_list",
|
|
12747
|
+
kind: "missing",
|
|
12748
|
+
base_severity: "warn",
|
|
12749
|
+
escalate_when_open: true,
|
|
12750
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
12751
|
+
},
|
|
12752
|
+
{
|
|
12753
|
+
id: "tasks_with_unregistered_project",
|
|
12754
|
+
entity: "task",
|
|
12755
|
+
field: "project_id",
|
|
12756
|
+
target: "project",
|
|
12757
|
+
kind: "dangling",
|
|
12758
|
+
base_severity: "error",
|
|
12759
|
+
escalate_when_open: false,
|
|
12760
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
12761
|
+
},
|
|
12762
|
+
{
|
|
12763
|
+
id: "tasks_with_unregistered_task_list",
|
|
12764
|
+
entity: "task",
|
|
12765
|
+
field: "task_list_id",
|
|
12766
|
+
target: "task_list",
|
|
12767
|
+
kind: "dangling",
|
|
12768
|
+
base_severity: "error",
|
|
12769
|
+
escalate_when_open: false,
|
|
12770
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
12771
|
+
},
|
|
12772
|
+
{
|
|
12773
|
+
id: "task_lists_without_project",
|
|
12774
|
+
entity: "task_list",
|
|
12775
|
+
field: "project_id",
|
|
12776
|
+
target: "project",
|
|
12777
|
+
kind: "missing",
|
|
12778
|
+
base_severity: "warn",
|
|
12779
|
+
escalate_when_open: false,
|
|
12780
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
12781
|
+
},
|
|
12782
|
+
{
|
|
12783
|
+
id: "task_lists_with_unregistered_project",
|
|
12784
|
+
entity: "task_list",
|
|
12785
|
+
field: "project_id",
|
|
12786
|
+
target: "project",
|
|
12787
|
+
kind: "dangling",
|
|
12788
|
+
base_severity: "error",
|
|
12789
|
+
escalate_when_open: false,
|
|
12790
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
12791
|
+
}
|
|
12792
|
+
];
|
|
12793
|
+
function integrityCondition(id) {
|
|
12794
|
+
const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
|
|
12795
|
+
if (!spec)
|
|
12796
|
+
throw new Error(`unknown integrity condition: ${id}`);
|
|
12797
|
+
return spec;
|
|
12798
|
+
}
|
|
12799
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
12800
|
+
if (spec.kind === "dangling")
|
|
12801
|
+
return "error";
|
|
12802
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
12803
|
+
return "error";
|
|
12804
|
+
return spec.base_severity;
|
|
12805
|
+
}
|
|
12806
|
+
var ENTITY_LABEL = {
|
|
12807
|
+
task: ["task", "tasks"],
|
|
12808
|
+
task_list: ["task list", "task lists"]
|
|
12809
|
+
};
|
|
12810
|
+
function plural(entity, count) {
|
|
12811
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
12812
|
+
return count === 1 ? one : many;
|
|
12813
|
+
}
|
|
12814
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
12815
|
+
const noun = plural(spec.entity, measurement.count);
|
|
12816
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
12817
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
12818
|
+
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}`;
|
|
12819
|
+
}
|
|
12820
|
+
function measuredCondition(spec, measurement, source) {
|
|
12821
|
+
return {
|
|
12822
|
+
id: spec.id,
|
|
12823
|
+
entity: spec.entity,
|
|
12824
|
+
field: spec.field,
|
|
12825
|
+
kind: spec.kind,
|
|
12826
|
+
count: measurement.count,
|
|
12827
|
+
open_count: measurement.open_count,
|
|
12828
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
12829
|
+
verified: true,
|
|
12830
|
+
source,
|
|
12831
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
12832
|
+
impact: spec.impact
|
|
12833
|
+
};
|
|
12834
|
+
}
|
|
12835
|
+
function unverifiedCondition(spec, reason) {
|
|
12836
|
+
return {
|
|
12837
|
+
id: spec.id,
|
|
12838
|
+
entity: spec.entity,
|
|
12839
|
+
field: spec.field,
|
|
12840
|
+
kind: spec.kind,
|
|
12841
|
+
count: null,
|
|
12842
|
+
open_count: null,
|
|
12843
|
+
severity: null,
|
|
12844
|
+
verified: false,
|
|
12845
|
+
source: "unverified",
|
|
12846
|
+
unverified_reason: reason,
|
|
12847
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
12848
|
+
impact: spec.impact
|
|
12849
|
+
};
|
|
12850
|
+
}
|
|
12851
|
+
function summarizeIntegrity(conditions) {
|
|
12852
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
12853
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
12854
|
+
const unverified = conditions.length - measured.length;
|
|
12855
|
+
return {
|
|
12856
|
+
ok: unverified === 0 && findings.length === 0,
|
|
12857
|
+
findings: findings.length,
|
|
12858
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
12859
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
12860
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
12861
|
+
unverified,
|
|
12862
|
+
complete: unverified === 0
|
|
12863
|
+
};
|
|
12864
|
+
}
|
|
12865
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
12866
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
12867
|
+
return {
|
|
12868
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
12869
|
+
generated_at: generatedAt,
|
|
12870
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
12871
|
+
conditions,
|
|
12872
|
+
summary: summarizeIntegrity(conditions)
|
|
12873
|
+
};
|
|
12874
|
+
}
|
|
12875
|
+
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
12876
|
+
const received = new Map;
|
|
12877
|
+
if (Array.isArray(raw.conditions)) {
|
|
12878
|
+
for (const entry of raw.conditions) {
|
|
12879
|
+
if (entry && typeof entry["id"] === "string")
|
|
12880
|
+
received.set(entry["id"], entry);
|
|
12881
|
+
}
|
|
12882
|
+
}
|
|
12883
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12884
|
+
const entry = received.get(spec.id);
|
|
12885
|
+
const count = entry?.["count"];
|
|
12886
|
+
const verified = entry?.["verified"];
|
|
12887
|
+
if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
|
|
12888
|
+
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");
|
|
12889
|
+
}
|
|
12890
|
+
const openRaw = entry["open_count"];
|
|
12891
|
+
return measuredCondition(spec, {
|
|
12892
|
+
count,
|
|
12893
|
+
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
12894
|
+
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
12895
|
+
});
|
|
12896
|
+
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
12897
|
+
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
12898
|
+
}
|
|
12899
|
+
var SQLITE_TABLE = {
|
|
12900
|
+
task: "tasks",
|
|
12901
|
+
task_list: "task_lists",
|
|
12902
|
+
project: "projects"
|
|
12903
|
+
};
|
|
12904
|
+
function sqliteOpenStatusList() {
|
|
12905
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
12906
|
+
}
|
|
12907
|
+
function buildSqliteIntegritySql(spec) {
|
|
12908
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
12909
|
+
const target = SQLITE_TABLE[spec.target];
|
|
12910
|
+
const column = `t."${spec.field}"`;
|
|
12911
|
+
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})`;
|
|
12912
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
12913
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
12914
|
+
}
|
|
12915
|
+
var RECORD_TYPE = {
|
|
12916
|
+
task: "tasks",
|
|
12917
|
+
task_list: "task_lists",
|
|
12918
|
+
project: "projects"
|
|
12919
|
+
};
|
|
12920
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
12921
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
12922
|
+
const p = (value) => {
|
|
12923
|
+
params.push(value);
|
|
12924
|
+
return `$${params.length}`;
|
|
12925
|
+
};
|
|
12926
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
12927
|
+
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})`;
|
|
12928
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
12929
|
+
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}`;
|
|
12930
|
+
return { sql, params };
|
|
12931
|
+
}
|
|
12932
|
+
function referenceOf(row, field) {
|
|
12933
|
+
const raw = row[field];
|
|
12934
|
+
if (typeof raw !== "string")
|
|
12935
|
+
return null;
|
|
12936
|
+
const trimmed = raw.trim();
|
|
12937
|
+
return trimmed === "" ? null : trimmed;
|
|
12938
|
+
}
|
|
12939
|
+
function measureIntegrityRows(spec, sets) {
|
|
12940
|
+
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
12941
|
+
if (!rows)
|
|
12942
|
+
return null;
|
|
12943
|
+
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
12944
|
+
if (spec.kind === "dangling" && !registered)
|
|
12945
|
+
return null;
|
|
12946
|
+
let count = 0;
|
|
12947
|
+
let open = 0;
|
|
12948
|
+
for (const row of rows) {
|
|
12949
|
+
const reference = referenceOf(row, spec.field);
|
|
12950
|
+
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
12951
|
+
if (!matches)
|
|
12952
|
+
continue;
|
|
12953
|
+
count++;
|
|
12954
|
+
if (spec.entity === "task") {
|
|
12955
|
+
const status = row.status;
|
|
12956
|
+
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
12957
|
+
open++;
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
return { count, open_count: spec.entity === "task" ? open : null };
|
|
12961
|
+
}
|
|
12962
|
+
|
|
12963
|
+
// src/db/integrity.ts
|
|
12964
|
+
var REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
12965
|
+
function tableExists(db, table) {
|
|
12966
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
12967
|
+
}
|
|
12968
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
12969
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
12970
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12971
|
+
if (missing.length > 0) {
|
|
12972
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
12973
|
+
}
|
|
12974
|
+
try {
|
|
12975
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
12976
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
12977
|
+
} catch (error) {
|
|
12978
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
12979
|
+
}
|
|
12980
|
+
});
|
|
12981
|
+
return buildIntegrityReport(conditions, now());
|
|
12982
|
+
}
|
|
12983
|
+
|
|
12653
12984
|
// src/storage/sqlite-snapshot.ts
|
|
12654
12985
|
init_database();
|
|
12655
12986
|
init_audit();
|
|
@@ -13041,15 +13372,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
13041
13372
|
return null;
|
|
13042
13373
|
if (TASK_UUID_RE.test(raw))
|
|
13043
13374
|
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}`)}%`);
|
|
13375
|
+
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
13376
|
if (prefixRows.length > 1) {
|
|
13046
|
-
throw new
|
|
13377
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13047
13378
|
}
|
|
13048
13379
|
if (prefixRows.length === 1)
|
|
13049
13380
|
return getTask(prefixRows[0].id, db);
|
|
13050
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
13381
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
13051
13382
|
if (shortIdRows.length > 1) {
|
|
13052
|
-
throw new
|
|
13383
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13053
13384
|
}
|
|
13054
13385
|
if (shortIdRows.length === 1)
|
|
13055
13386
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -13198,6 +13529,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13198
13529
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
13199
13530
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
13200
13531
|
},
|
|
13532
|
+
integrity: {
|
|
13533
|
+
report: () => scanSqliteIntegrity(database())
|
|
13534
|
+
},
|
|
13201
13535
|
transaction: (fn) => {
|
|
13202
13536
|
const tx = database().transaction(() => fn(adapter));
|
|
13203
13537
|
return tx();
|
|
@@ -13206,6 +13540,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13206
13540
|
return adapter;
|
|
13207
13541
|
}
|
|
13208
13542
|
|
|
13543
|
+
// src/storage/postgres-adapter.ts
|
|
13544
|
+
init_types();
|
|
13545
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13546
|
+
|
|
13209
13547
|
// src/storage/postgres-sync.ts
|
|
13210
13548
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
13211
13549
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -13595,54 +13933,7 @@ function assertSafeIdentifier(value) {
|
|
|
13595
13933
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
13596
13934
|
}
|
|
13597
13935
|
|
|
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
13936
|
// src/storage/postgres-adapter.ts
|
|
13644
|
-
init_types();
|
|
13645
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
13646
13937
|
init_redaction();
|
|
13647
13938
|
function createPostgresTodosStorageAdapter(options) {
|
|
13648
13939
|
const store = new PostgresJsonRecordStore(options);
|
|
@@ -13770,6 +14061,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13770
14061
|
exportSnapshot: () => exportSnapshot(store),
|
|
13771
14062
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
13772
14063
|
},
|
|
14064
|
+
integrity: {
|
|
14065
|
+
report: () => store.integrityReport()
|
|
14066
|
+
},
|
|
13773
14067
|
transaction: (fn) => fn(adapter)
|
|
13774
14068
|
};
|
|
13775
14069
|
return adapter;
|
|
@@ -13876,8 +14170,9 @@ class PostgresJsonRecordStore {
|
|
|
13876
14170
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
13877
14171
|
if (filter.session_id !== undefined)
|
|
13878
14172
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
13879
|
-
if (filter.tags?.length)
|
|
13880
|
-
conds.push(`payload->'tags'
|
|
14173
|
+
if (filter.tags?.length) {
|
|
14174
|
+
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)})`);
|
|
14175
|
+
}
|
|
13881
14176
|
if (filter.has_recurrence !== undefined) {
|
|
13882
14177
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
13883
14178
|
}
|
|
@@ -13938,7 +14233,8 @@ class PostgresJsonRecordStore {
|
|
|
13938
14233
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
13939
14234
|
LIMIT 2`, [this.service, raw, upper]);
|
|
13940
14235
|
if (prefixResult.rows.length > 1) {
|
|
13941
|
-
|
|
14236
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14237
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13942
14238
|
}
|
|
13943
14239
|
if (prefixResult.rows.length === 1) {
|
|
13944
14240
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -13949,7 +14245,8 @@ class PostgresJsonRecordStore {
|
|
|
13949
14245
|
AND LOWER(payload->>'short_id') = $2
|
|
13950
14246
|
LIMIT 2`, [this.service, raw]);
|
|
13951
14247
|
if (shortIdResult.rows.length > 1) {
|
|
13952
|
-
|
|
14248
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14249
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13953
14250
|
}
|
|
13954
14251
|
if (shortIdResult.rows.length === 1) {
|
|
13955
14252
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -13963,6 +14260,24 @@ class PostgresJsonRecordStore {
|
|
|
13963
14260
|
const result = await this.options.client.query(sql, params);
|
|
13964
14261
|
return Number(result.rows[0]?.count ?? 0);
|
|
13965
14262
|
}
|
|
14263
|
+
async integrityReport() {
|
|
14264
|
+
await this.ensureSchema();
|
|
14265
|
+
const conditions = [];
|
|
14266
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
14267
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
14268
|
+
try {
|
|
14269
|
+
const result = await this.options.client.query(sql, params);
|
|
14270
|
+
const row = result.rows[0];
|
|
14271
|
+
conditions.push(measuredCondition(spec, {
|
|
14272
|
+
count: Number(row?.count ?? 0),
|
|
14273
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
14274
|
+
}, "postgres"));
|
|
14275
|
+
} catch (error) {
|
|
14276
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
14277
|
+
}
|
|
14278
|
+
}
|
|
14279
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
14280
|
+
}
|
|
13966
14281
|
async listTombstones() {
|
|
13967
14282
|
await this.ensureSchema();
|
|
13968
14283
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -14438,7 +14753,7 @@ async function startTask2(id, agentId, store) {
|
|
|
14438
14753
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
14439
14754
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
14440
14755
|
}
|
|
14441
|
-
|
|
14756
|
+
const started = await patchTask(task, {
|
|
14442
14757
|
status: "in_progress",
|
|
14443
14758
|
assigned_to: task.assigned_to ?? agentId,
|
|
14444
14759
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -14446,6 +14761,8 @@ async function startTask2(id, agentId, store) {
|
|
|
14446
14761
|
locked_at: new Date().toISOString(),
|
|
14447
14762
|
started_at: task.started_at ?? new Date().toISOString()
|
|
14448
14763
|
}, store);
|
|
14764
|
+
await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
14765
|
+
return started;
|
|
14449
14766
|
}
|
|
14450
14767
|
async function completeTask2(id, agentId, options, store) {
|
|
14451
14768
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -15554,17 +15871,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
|
15554
15871
|
init_config();
|
|
15555
15872
|
function createTodosStorageAdapter(options = {}) {
|
|
15556
15873
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
15557
|
-
if (config.mode === "
|
|
15874
|
+
if (config.mode === "sqlite") {
|
|
15558
15875
|
if (isTodosShadowEnabled(options.env))
|
|
15559
15876
|
return createShadowAdapter(options, config);
|
|
15560
15877
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
15561
15878
|
}
|
|
15562
15879
|
assertTodosRemoteStorageConfig(config);
|
|
15563
|
-
const adapter =
|
|
15880
|
+
const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
15564
15881
|
if (!adapter) {
|
|
15565
|
-
throw new Error(
|
|
15882
|
+
throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
|
|
15566
15883
|
}
|
|
15567
|
-
|
|
15884
|
+
assertPostgresAdapterCapabilities(adapter);
|
|
15568
15885
|
return adapter;
|
|
15569
15886
|
}
|
|
15570
15887
|
function createShadowAdapter(options, config) {
|
|
@@ -15578,7 +15895,7 @@ function createShadowAdapter(options, config) {
|
|
|
15578
15895
|
...options.local ? { local: options.local } : {},
|
|
15579
15896
|
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
15580
15897
|
...postgresClient ? { postgresClient } : {},
|
|
15581
|
-
...options.
|
|
15898
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15582
15899
|
});
|
|
15583
15900
|
}
|
|
15584
15901
|
function createImplicitPostgresAdapter(options) {
|
|
@@ -15586,25 +15903,12 @@ function createImplicitPostgresAdapter(options) {
|
|
|
15586
15903
|
return null;
|
|
15587
15904
|
return createPostgresTodosStorageAdapter({
|
|
15588
15905
|
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
|
|
15906
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15600
15907
|
});
|
|
15601
15908
|
}
|
|
15602
|
-
function
|
|
15909
|
+
function assertPostgresAdapterCapabilities(adapter) {
|
|
15603
15910
|
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");
|
|
15911
|
+
throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
|
|
15608
15912
|
}
|
|
15609
15913
|
}
|
|
15610
15914
|
// src/storage/shadow-outbox.ts
|
|
@@ -15931,6 +16235,50 @@ async function closeRuntimeShadowCloud() {
|
|
|
15931
16235
|
await cloud.close().catch(() => {});
|
|
15932
16236
|
}
|
|
15933
16237
|
}
|
|
16238
|
+
// src/storage/hybrid.ts
|
|
16239
|
+
function createHybridTodosStorageAdapter(options) {
|
|
16240
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
16241
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
16242
|
+
if (!syncStore)
|
|
16243
|
+
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
16244
|
+
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
16245
|
+
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
16246
|
+
}
|
|
16247
|
+
const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
|
|
16248
|
+
const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
16249
|
+
const remote = {
|
|
16250
|
+
ensureSchema: () => syncStore.ensureSchema(),
|
|
16251
|
+
async pushSnapshot(context) {
|
|
16252
|
+
const snapshot = await exportSnapshot2(context);
|
|
16253
|
+
return syncStore.pushSnapshot(snapshot, context);
|
|
16254
|
+
},
|
|
16255
|
+
async pullSnapshot(options2, context) {
|
|
16256
|
+
const snapshot = await syncStore.pullSnapshot(options2);
|
|
16257
|
+
return importSnapshot2(snapshot, context);
|
|
16258
|
+
},
|
|
16259
|
+
async syncOnce(options2, context) {
|
|
16260
|
+
const pulled = await remote.pullSnapshot(options2, context);
|
|
16261
|
+
const pushed = await remote.pushSnapshot(context);
|
|
16262
|
+
return { pulled, pushed };
|
|
16263
|
+
}
|
|
16264
|
+
};
|
|
16265
|
+
return {
|
|
16266
|
+
...local,
|
|
16267
|
+
kind: "hybrid",
|
|
16268
|
+
capabilities: {
|
|
16269
|
+
...local.capabilities,
|
|
16270
|
+
localPersistence: true,
|
|
16271
|
+
remotePersistence: true,
|
|
16272
|
+
sync: true
|
|
16273
|
+
},
|
|
16274
|
+
sync: {
|
|
16275
|
+
...local.sync,
|
|
16276
|
+
exportSnapshot: exportSnapshot2,
|
|
16277
|
+
importSnapshot: importSnapshot2
|
|
16278
|
+
},
|
|
16279
|
+
remote
|
|
16280
|
+
};
|
|
16281
|
+
}
|
|
15934
16282
|
// src/storage/comment-redaction-backfill.ts
|
|
15935
16283
|
init_redaction();
|
|
15936
16284
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
@@ -16465,11 +16813,13 @@ export {
|
|
|
16465
16813
|
postgresTodosCommentCursorIndexSql,
|
|
16466
16814
|
planRunArtifactsS3Sync,
|
|
16467
16815
|
parseStorageMode,
|
|
16816
|
+
parseStorageBackend,
|
|
16468
16817
|
maybeInstallShadowCapture2 as maybeInstallShadowCapture,
|
|
16469
16818
|
loadTodosStorageConfig,
|
|
16470
16819
|
loadStorageConfig,
|
|
16471
16820
|
isTodosShadowEnabled,
|
|
16472
16821
|
isTodosRemoteStorageEnabled,
|
|
16822
|
+
isTodosPostgresBackend,
|
|
16473
16823
|
isCommentRedactionBackfillComplete,
|
|
16474
16824
|
installShadowOutboxSchema,
|
|
16475
16825
|
importSqliteTodosStorageSnapshot,
|
|
@@ -16478,6 +16828,7 @@ export {
|
|
|
16478
16828
|
getTodosStorageEnvName,
|
|
16479
16829
|
getTodosStorageDatabaseUrl,
|
|
16480
16830
|
getTodosStorageDatabaseEnv,
|
|
16831
|
+
getTodosStorageBackend,
|
|
16481
16832
|
getStorageMode,
|
|
16482
16833
|
getStorageDatabaseUrl,
|
|
16483
16834
|
getStorageDatabaseEnv,
|