@hasna/todos 0.11.94 → 0.12.1
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 +5 -5
- package/dist/cli/cloud-router.d.ts +17 -1
- 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/api-key-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/help-commands.d.ts +2 -1
- package/dist/cli/commands/help-commands.d.ts.map +1 -1
- package/dist/cli/commands/knowledge-commands.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/pr-group-commands.d.ts +3 -0
- package/dist/cli/commands/pr-group-commands.d.ts.map +1 -0
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/risk-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +23 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +8539 -1940
- package/dist/cli/stage-a.d.ts +17 -0
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +1665 -523
- package/dist/db/agent-names.d.ts +13 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- package/dist/db/agents.d.ts +4 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/dispatches.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +40 -0
- package/dist/db/identity-mapping.d.ts.map +1 -0
- package/dist/db/inbox.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/storage-tombstones.d.ts +1 -1
- package/dist/db/storage-tombstones.d.ts.map +1 -1
- package/dist/db/task-commits.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/templates.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4784 -882
- package/dist/json-contracts.d.ts +4 -2
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/activity-audit.d.ts.map +1 -1
- package/dist/lib/artifact-store.d.ts.map +1 -1
- package/dist/lib/cli-help.d.ts +14 -3
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/evidence-redaction.d.ts +72 -0
- package/dist/lib/evidence-redaction.d.ts.map +1 -0
- package/dist/lib/headless-boundaries.d.ts +1 -1
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/lib/local-bridge.d.ts +1 -0
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-encryption.d.ts.map +1 -1
- package/dist/lib/prewrite-secrets.d.ts +29 -0
- package/dist/lib/prewrite-secrets.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/search.d.ts +3 -0
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/task-route-sources.d.ts +2 -0
- package/dist/lib/task-route-sources.d.ts.map +1 -1
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/lib/template-semantics.d.ts +8 -0
- package/dist/lib/template-semantics.d.ts.map +1 -0
- package/dist/mcp/index.js +6631 -1062
- package/dist/mcp.js +1 -1
- package/dist/pr-groups/http-client.d.ts +24 -0
- package/dist/pr-groups/http-client.d.ts.map +1 -0
- package/dist/pr-groups/index.d.ts +9 -0
- package/dist/pr-groups/index.d.ts.map +1 -0
- package/dist/pr-groups/ledger.d.ts +20 -0
- package/dist/pr-groups/ledger.d.ts.map +1 -0
- package/dist/pr-groups/postgres.d.ts +21 -0
- package/dist/pr-groups/postgres.d.ts.map +1 -0
- package/dist/pr-groups/sqlite.d.ts +16 -0
- package/dist/pr-groups/sqlite.d.ts.map +1 -0
- package/dist/pr-groups/types.d.ts +320 -0
- package/dist/pr-groups/types.d.ts.map +1 -0
- package/dist/registry.js +1665 -523
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/client.d.ts +16 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +99 -0
- package/dist/sdk/v1.generated.d.ts +388 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +3 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +6538 -969
- package/dist/server/openapi.d.ts +2020 -278
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/pr-groups.d.ts +7 -0
- package/dist/server/pr-groups.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +2 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-client.d.ts +1 -0
- package/dist/storage/cloud-client.d.ts.map +1 -1
- package/dist/storage/config.d.ts +5 -5
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +3 -2
- 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/postgres-sync.d.ts +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1958 -524
- package/dist/types/index.d.ts +90 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/storage.js
CHANGED
|
@@ -62,15 +62,15 @@ __export(exports_config, {
|
|
|
62
62
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
63
63
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
64
64
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
65
|
-
|
|
65
|
+
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
66
66
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
67
|
-
|
|
67
|
+
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
68
68
|
});
|
|
69
|
-
function getCanonicalTodosRdsConfig() {
|
|
69
|
+
function getCanonicalTodosRdsConfig(env = process.env) {
|
|
70
70
|
return {
|
|
71
|
-
cluster:
|
|
71
|
+
cluster: clean(env[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
|
|
72
72
|
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
73
|
-
runtimeSecretPath:
|
|
73
|
+
runtimeSecretPath: clean(env[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
|
|
74
74
|
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
75
75
|
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
76
76
|
};
|
|
@@ -203,7 +203,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
203
203
|
}
|
|
204
204
|
return parsed;
|
|
205
205
|
}
|
|
206
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV,
|
|
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";
|
|
207
207
|
var init_config = __esm(() => {
|
|
208
208
|
TODOS_STORAGE_TABLES = [
|
|
209
209
|
"todos_sync_records",
|
|
@@ -246,144 +246,6 @@ var init_config = __esm(() => {
|
|
|
246
246
|
};
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
// src/types/index.ts
|
|
250
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
251
|
-
var init_types = __esm(() => {
|
|
252
|
-
TASK_STATUSES = [
|
|
253
|
-
"pending",
|
|
254
|
-
"in_progress",
|
|
255
|
-
"completed",
|
|
256
|
-
"failed",
|
|
257
|
-
"cancelled"
|
|
258
|
-
];
|
|
259
|
-
TASK_PRIORITIES = [
|
|
260
|
-
"low",
|
|
261
|
-
"medium",
|
|
262
|
-
"high",
|
|
263
|
-
"critical"
|
|
264
|
-
];
|
|
265
|
-
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
266
|
-
VersionConflictError = class VersionConflictError extends Error {
|
|
267
|
-
taskId;
|
|
268
|
-
expectedVersion;
|
|
269
|
-
actualVersion;
|
|
270
|
-
static code = "VERSION_CONFLICT";
|
|
271
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
272
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
273
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
274
|
-
this.taskId = taskId;
|
|
275
|
-
this.expectedVersion = expectedVersion;
|
|
276
|
-
this.actualVersion = actualVersion;
|
|
277
|
-
this.name = "VersionConflictError";
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
281
|
-
taskId;
|
|
282
|
-
static code = "TASK_NOT_FOUND";
|
|
283
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
284
|
-
constructor(taskId) {
|
|
285
|
-
super(`Task not found: ${taskId}`);
|
|
286
|
-
this.taskId = taskId;
|
|
287
|
-
this.name = "TaskNotFoundError";
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
291
|
-
projectId;
|
|
292
|
-
static code = "PROJECT_NOT_FOUND";
|
|
293
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
294
|
-
constructor(projectId) {
|
|
295
|
-
super(`Project not found: ${projectId}`);
|
|
296
|
-
this.projectId = projectId;
|
|
297
|
-
this.name = "ProjectNotFoundError";
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
ResourceConflictError = class ResourceConflictError extends Error {
|
|
301
|
-
code;
|
|
302
|
-
constructor(code, message) {
|
|
303
|
-
super(message);
|
|
304
|
-
this.code = code;
|
|
305
|
-
this.name = "ResourceConflictError";
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
309
|
-
planId;
|
|
310
|
-
static code = "PLAN_NOT_FOUND";
|
|
311
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
312
|
-
constructor(planId) {
|
|
313
|
-
super(`Plan not found: ${planId}`);
|
|
314
|
-
this.planId = planId;
|
|
315
|
-
this.name = "PlanNotFoundError";
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
LockError = class LockError extends Error {
|
|
319
|
-
taskId;
|
|
320
|
-
lockedBy;
|
|
321
|
-
static code = "LOCK_ERROR";
|
|
322
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
323
|
-
constructor(taskId, lockedBy) {
|
|
324
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
325
|
-
this.taskId = taskId;
|
|
326
|
-
this.lockedBy = lockedBy;
|
|
327
|
-
this.name = "LockError";
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
331
|
-
agentId;
|
|
332
|
-
static code = "AGENT_NOT_FOUND";
|
|
333
|
-
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
334
|
-
constructor(agentId) {
|
|
335
|
-
super(`Agent not found: ${agentId}`);
|
|
336
|
-
this.agentId = agentId;
|
|
337
|
-
this.name = "AgentNotFoundError";
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
341
|
-
taskListId;
|
|
342
|
-
static code = "TASK_LIST_NOT_FOUND";
|
|
343
|
-
static suggestion = "Use list_task_lists to see available lists.";
|
|
344
|
-
constructor(taskListId) {
|
|
345
|
-
super(`Task list not found: ${taskListId}`);
|
|
346
|
-
this.taskListId = taskListId;
|
|
347
|
-
this.name = "TaskListNotFoundError";
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
DependencyCycleError = class DependencyCycleError extends Error {
|
|
351
|
-
taskId;
|
|
352
|
-
dependsOn;
|
|
353
|
-
static code = "DEPENDENCY_CYCLE";
|
|
354
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
355
|
-
constructor(taskId, dependsOn) {
|
|
356
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
357
|
-
this.taskId = taskId;
|
|
358
|
-
this.dependsOn = dependsOn;
|
|
359
|
-
this.name = "DependencyCycleError";
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
CompletionGuardError = class CompletionGuardError extends Error {
|
|
363
|
-
reason;
|
|
364
|
-
retryAfterSeconds;
|
|
365
|
-
static code = "COMPLETION_BLOCKED";
|
|
366
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
367
|
-
constructor(reason, retryAfterSeconds) {
|
|
368
|
-
super(reason);
|
|
369
|
-
this.reason = reason;
|
|
370
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
371
|
-
this.name = "CompletionGuardError";
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
375
|
-
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
376
|
-
dispatchId;
|
|
377
|
-
static code = "DISPATCH_NOT_FOUND";
|
|
378
|
-
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
379
|
-
constructor(dispatchId) {
|
|
380
|
-
super(`Dispatch not found: ${dispatchId}`);
|
|
381
|
-
this.dispatchId = dispatchId;
|
|
382
|
-
this.name = "DispatchNotFoundError";
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
});
|
|
386
|
-
|
|
387
249
|
// src/db/migrations.ts
|
|
388
250
|
var MIGRATIONS;
|
|
389
251
|
var init_migrations = __esm(() => {
|
|
@@ -1548,27 +1410,478 @@ var init_migrations = __esm(() => {
|
|
|
1548
1410
|
ALTER TABLE plans ADD COLUMN slug TEXT;
|
|
1549
1411
|
CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
|
|
1550
1412
|
INSERT OR IGNORE INTO _migrations (id) VALUES (64);
|
|
1413
|
+
`,
|
|
1414
|
+
`
|
|
1415
|
+
ALTER TABLE agents ADD COLUMN identity_id TEXT;
|
|
1416
|
+
ALTER TABLE agents ADD COLUMN project_id TEXT;
|
|
1417
|
+
ALTER TABLE agents ADD COLUMN runtime_instance_id TEXT;
|
|
1418
|
+
|
|
1419
|
+
CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
|
|
1420
|
+
id TEXT PRIMARY KEY,
|
|
1421
|
+
local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
|
|
1422
|
+
identity_id TEXT,
|
|
1423
|
+
source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
|
|
1424
|
+
source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
|
|
1425
|
+
source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
|
|
1426
|
+
source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
|
|
1427
|
+
source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
|
|
1428
|
+
observed_label TEXT,
|
|
1429
|
+
evidence TEXT NOT NULL DEFAULT '{}',
|
|
1430
|
+
mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
|
|
1431
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
|
|
1432
|
+
revision INTEGER NOT NULL CHECK(revision > 0),
|
|
1433
|
+
created_at TEXT NOT NULL,
|
|
1434
|
+
updated_at TEXT NOT NULL,
|
|
1435
|
+
CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
|
|
1436
|
+
CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
|
|
1437
|
+
);
|
|
1438
|
+
DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
|
|
1439
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
|
|
1440
|
+
ON agent_identity_source_mappings (
|
|
1441
|
+
source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
|
|
1442
|
+
)
|
|
1443
|
+
WHERE mapping_basis IN ('authoritative', 'imported');
|
|
1444
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
|
|
1445
|
+
ON agent_identity_source_mappings(identity_id);
|
|
1446
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
|
|
1447
|
+
ON agent_identity_source_mappings(local_agent_id);
|
|
1448
|
+
CREATE TABLE IF NOT EXISTS agent_identity_aliases (
|
|
1449
|
+
id TEXT PRIMARY KEY,
|
|
1450
|
+
local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
|
|
1451
|
+
label TEXT NOT NULL,
|
|
1452
|
+
normalized_label TEXT NOT NULL,
|
|
1453
|
+
alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
|
|
1454
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
|
|
1455
|
+
created_at TEXT NOT NULL,
|
|
1456
|
+
UNIQUE(local_agent_id, normalized_label)
|
|
1457
|
+
);
|
|
1458
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
|
|
1459
|
+
ON agent_identity_aliases(normalized_label, status);
|
|
1460
|
+
|
|
1461
|
+
CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
|
|
1462
|
+
BEFORE UPDATE OF identity_id ON agents
|
|
1463
|
+
WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
|
|
1464
|
+
BEGIN
|
|
1465
|
+
SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
|
|
1466
|
+
END;
|
|
1467
|
+
|
|
1468
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
|
|
1469
|
+
BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
|
|
1470
|
+
WHEN OLD.identity_id IS NOT NEW.identity_id
|
|
1471
|
+
BEGIN
|
|
1472
|
+
SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
|
|
1473
|
+
END;
|
|
1474
|
+
|
|
1475
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
|
|
1476
|
+
BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
|
|
1477
|
+
ON agent_identity_source_mappings
|
|
1478
|
+
WHEN OLD.source_authority IS NOT NEW.source_authority
|
|
1479
|
+
OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
|
|
1480
|
+
OR OLD.source_namespace IS NOT NEW.source_namespace
|
|
1481
|
+
OR OLD.source_entity_type IS NOT NEW.source_entity_type
|
|
1482
|
+
OR OLD.source_record_id IS NOT NEW.source_record_id
|
|
1483
|
+
BEGIN
|
|
1484
|
+
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
1485
|
+
END;
|
|
1486
|
+
|
|
1487
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
1488
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
1489
|
+
ON agent_identity_source_mappings
|
|
1490
|
+
BEGIN
|
|
1491
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
1492
|
+
END;
|
|
1493
|
+
|
|
1494
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
1495
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
1496
|
+
BEGIN
|
|
1497
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
1498
|
+
END;
|
|
1499
|
+
|
|
1500
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (65);
|
|
1501
|
+
`,
|
|
1502
|
+
`
|
|
1503
|
+
CREATE TABLE IF NOT EXISTS pr_groups (
|
|
1504
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1505
|
+
id TEXT PRIMARY KEY,
|
|
1506
|
+
identity_key TEXT NOT NULL UNIQUE,
|
|
1507
|
+
root_request_id TEXT NOT NULL,
|
|
1508
|
+
repository TEXT NOT NULL,
|
|
1509
|
+
state TEXT NOT NULL,
|
|
1510
|
+
active_attempt_id TEXT,
|
|
1511
|
+
active_generation TEXT,
|
|
1512
|
+
terminal_attempt_id TEXT,
|
|
1513
|
+
terminal_generation TEXT,
|
|
1514
|
+
terminal_outcome TEXT,
|
|
1515
|
+
terminal_head_sha TEXT,
|
|
1516
|
+
terminal_at TEXT,
|
|
1517
|
+
cleanup_eligible_at TEXT,
|
|
1518
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
1519
|
+
created_at TEXT NOT NULL,
|
|
1520
|
+
updated_at TEXT NOT NULL
|
|
1521
|
+
);
|
|
1522
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key);
|
|
1523
|
+
CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
|
|
1524
|
+
CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation);
|
|
1525
|
+
|
|
1526
|
+
CREATE TABLE IF NOT EXISTS pr_group_attempts (
|
|
1527
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1528
|
+
id TEXT PRIMARY KEY,
|
|
1529
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
1530
|
+
leaf_task_id TEXT NOT NULL,
|
|
1531
|
+
dispatch_attempt TEXT NOT NULL,
|
|
1532
|
+
writer_generation TEXT NOT NULL,
|
|
1533
|
+
previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
|
|
1534
|
+
worktree TEXT NOT NULL,
|
|
1535
|
+
branch TEXT NOT NULL,
|
|
1536
|
+
provider TEXT,
|
|
1537
|
+
provider_run_id TEXT,
|
|
1538
|
+
profile_alias TEXT,
|
|
1539
|
+
status TEXT NOT NULL,
|
|
1540
|
+
admitted_at TEXT NOT NULL,
|
|
1541
|
+
started_at TEXT,
|
|
1542
|
+
last_heartbeat_at TEXT,
|
|
1543
|
+
handed_off_at TEXT,
|
|
1544
|
+
fenced_at TEXT,
|
|
1545
|
+
terminal_at TEXT,
|
|
1546
|
+
created_at TEXT NOT NULL,
|
|
1547
|
+
updated_at TEXT NOT NULL,
|
|
1548
|
+
UNIQUE(group_id, leaf_task_id, dispatch_attempt),
|
|
1549
|
+
UNIQUE(group_id, writer_generation)
|
|
1550
|
+
);
|
|
1551
|
+
CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
|
|
1552
|
+
CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
|
|
1553
|
+
|
|
1554
|
+
CREATE TABLE IF NOT EXISTS pr_group_events (
|
|
1555
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1556
|
+
id TEXT PRIMARY KEY,
|
|
1557
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
1558
|
+
attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
|
|
1559
|
+
writer_generation TEXT NOT NULL,
|
|
1560
|
+
sequence INTEGER NOT NULL,
|
|
1561
|
+
idempotency_key TEXT NOT NULL,
|
|
1562
|
+
event_type TEXT NOT NULL,
|
|
1563
|
+
state TEXT NOT NULL,
|
|
1564
|
+
message TEXT,
|
|
1565
|
+
head_sha TEXT,
|
|
1566
|
+
receipt_key TEXT,
|
|
1567
|
+
outcome TEXT,
|
|
1568
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1569
|
+
payload_hash TEXT NOT NULL,
|
|
1570
|
+
created_at TEXT NOT NULL,
|
|
1571
|
+
UNIQUE(group_id, sequence),
|
|
1572
|
+
UNIQUE(group_id, idempotency_key),
|
|
1573
|
+
UNIQUE(group_id, receipt_key)
|
|
1574
|
+
);
|
|
1575
|
+
CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
|
|
1576
|
+
CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
|
|
1577
|
+
CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(group_id, receipt_key);
|
|
1578
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (66);
|
|
1579
|
+
`,
|
|
1580
|
+
`
|
|
1581
|
+
ALTER TABLE pr_groups ADD COLUMN leaf_task_id TEXT;
|
|
1582
|
+
ALTER TABLE pr_groups ADD COLUMN branch TEXT;
|
|
1583
|
+
ALTER TABLE pr_groups ADD COLUMN pr_number INTEGER;
|
|
1584
|
+
ALTER TABLE pr_groups ADD COLUMN base_sha TEXT;
|
|
1585
|
+
ALTER TABLE pr_groups ADD COLUMN repair_cycle_count INTEGER NOT NULL DEFAULT 0;
|
|
1586
|
+
ALTER TABLE pr_groups ADD COLUMN repair_cycle_limit INTEGER NOT NULL DEFAULT 2;
|
|
1587
|
+
ALTER TABLE pr_group_attempts ADD COLUMN repository TEXT;
|
|
1588
|
+
ALTER TABLE pr_group_attempts ADD COLUMN pr_number INTEGER;
|
|
1589
|
+
ALTER TABLE pr_group_attempts ADD COLUMN base_sha TEXT;
|
|
1590
|
+
ALTER TABLE pr_group_events ADD COLUMN review_receipt_key TEXT;
|
|
1591
|
+
ALTER TABLE pr_group_events ADD COLUMN conditional_merge_receipt_key TEXT;
|
|
1592
|
+
ALTER TABLE pr_group_events ADD COLUMN repository TEXT;
|
|
1593
|
+
ALTER TABLE pr_group_events ADD COLUMN pr_number INTEGER;
|
|
1594
|
+
ALTER TABLE pr_group_events ADD COLUMN base_sha TEXT;
|
|
1595
|
+
ALTER TABLE pr_group_events ADD COLUMN actor_id TEXT;
|
|
1596
|
+
ALTER TABLE pr_group_events ADD COLUMN actor_run_id TEXT;
|
|
1597
|
+
ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_id TEXT;
|
|
1598
|
+
ALTER TABLE pr_group_events ADD COLUMN expected_reviewer_run_id TEXT;
|
|
1599
|
+
ALTER TABLE pr_group_events ADD COLUMN repair_cycle INTEGER;
|
|
1600
|
+
ALTER TABLE pr_group_events ADD COLUMN cleanup_proof TEXT;
|
|
1601
|
+
|
|
1602
|
+
UPDATE pr_groups
|
|
1603
|
+
SET leaf_task_id = COALESCE(leaf_task_id, (
|
|
1604
|
+
SELECT attempt.leaf_task_id
|
|
1605
|
+
FROM pr_group_attempts AS attempt
|
|
1606
|
+
WHERE attempt.group_id = pr_groups.id
|
|
1607
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
1608
|
+
attempt.created_at ASC, attempt.id ASC
|
|
1609
|
+
LIMIT 1
|
|
1610
|
+
)),
|
|
1611
|
+
branch = COALESCE(branch, (
|
|
1612
|
+
SELECT attempt.branch
|
|
1613
|
+
FROM pr_group_attempts AS attempt
|
|
1614
|
+
WHERE attempt.group_id = pr_groups.id
|
|
1615
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
1616
|
+
attempt.created_at ASC, attempt.id ASC
|
|
1617
|
+
LIMIT 1
|
|
1618
|
+
));
|
|
1619
|
+
|
|
1620
|
+
UPDATE pr_group_attempts
|
|
1621
|
+
SET repository = COALESCE(repository, (
|
|
1622
|
+
SELECT groups.repository FROM pr_groups AS groups
|
|
1623
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
1624
|
+
)),
|
|
1625
|
+
pr_number = COALESCE(pr_number, (
|
|
1626
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
1627
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
1628
|
+
)),
|
|
1629
|
+
base_sha = COALESCE(base_sha, (
|
|
1630
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
1631
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
1632
|
+
));
|
|
1633
|
+
|
|
1634
|
+
UPDATE pr_groups
|
|
1635
|
+
SET pr_number = COALESCE(pr_number, (
|
|
1636
|
+
SELECT attempt.pr_number
|
|
1637
|
+
FROM pr_group_attempts AS attempt
|
|
1638
|
+
WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
|
|
1639
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
1640
|
+
attempt.created_at ASC, attempt.id ASC
|
|
1641
|
+
LIMIT 1
|
|
1642
|
+
)),
|
|
1643
|
+
base_sha = COALESCE(base_sha, (
|
|
1644
|
+
SELECT attempt.base_sha
|
|
1645
|
+
FROM pr_group_attempts AS attempt
|
|
1646
|
+
WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
|
|
1647
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
1648
|
+
attempt.created_at ASC, attempt.id ASC
|
|
1649
|
+
LIMIT 1
|
|
1650
|
+
));
|
|
1651
|
+
|
|
1652
|
+
UPDATE pr_group_attempts
|
|
1653
|
+
SET pr_number = COALESCE(pr_number, (
|
|
1654
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
1655
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
1656
|
+
)),
|
|
1657
|
+
base_sha = COALESCE(base_sha, (
|
|
1658
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
1659
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
1660
|
+
));
|
|
1661
|
+
|
|
1662
|
+
UPDATE pr_group_events
|
|
1663
|
+
SET repository = COALESCE(repository, (
|
|
1664
|
+
SELECT attempt.repository FROM pr_group_attempts AS attempt
|
|
1665
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
1666
|
+
), (
|
|
1667
|
+
SELECT groups.repository FROM pr_groups AS groups
|
|
1668
|
+
WHERE groups.id = pr_group_events.group_id
|
|
1669
|
+
)),
|
|
1670
|
+
pr_number = COALESCE(pr_number, (
|
|
1671
|
+
SELECT attempt.pr_number FROM pr_group_attempts AS attempt
|
|
1672
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
1673
|
+
), (
|
|
1674
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
1675
|
+
WHERE groups.id = pr_group_events.group_id
|
|
1676
|
+
)),
|
|
1677
|
+
base_sha = COALESCE(base_sha, (
|
|
1678
|
+
SELECT attempt.base_sha FROM pr_group_attempts AS attempt
|
|
1679
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
1680
|
+
), (
|
|
1681
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
1682
|
+
WHERE groups.id = pr_group_events.group_id
|
|
1683
|
+
));
|
|
1684
|
+
|
|
1685
|
+
INSERT OR IGNORE INTO _migrations (id)
|
|
1686
|
+
SELECT 66
|
|
1687
|
+
WHERE NOT EXISTS (
|
|
1688
|
+
SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
|
|
1689
|
+
)
|
|
1690
|
+
AND NOT EXISTS (
|
|
1691
|
+
SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
|
|
1692
|
+
)
|
|
1693
|
+
AND NOT EXISTS (
|
|
1694
|
+
SELECT 1 FROM pr_group_events WHERE repository IS NULL
|
|
1695
|
+
);
|
|
1696
|
+
`,
|
|
1697
|
+
`
|
|
1698
|
+
PRAGMA foreign_keys = OFF;
|
|
1699
|
+
BEGIN IMMEDIATE;
|
|
1700
|
+
|
|
1701
|
+
ALTER TABLE pr_group_events RENAME TO pr_group_events_v66;
|
|
1702
|
+
ALTER TABLE pr_group_attempts RENAME TO pr_group_attempts_v66;
|
|
1703
|
+
ALTER TABLE pr_groups RENAME TO pr_groups_v66;
|
|
1704
|
+
|
|
1705
|
+
CREATE TABLE pr_groups (
|
|
1706
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1707
|
+
id TEXT PRIMARY KEY,
|
|
1708
|
+
identity_key TEXT NOT NULL UNIQUE,
|
|
1709
|
+
root_request_id TEXT NOT NULL,
|
|
1710
|
+
repository TEXT NOT NULL,
|
|
1711
|
+
leaf_task_id TEXT NOT NULL,
|
|
1712
|
+
branch TEXT NOT NULL,
|
|
1713
|
+
pr_number INTEGER,
|
|
1714
|
+
base_sha TEXT,
|
|
1715
|
+
state TEXT NOT NULL,
|
|
1716
|
+
active_attempt_id TEXT,
|
|
1717
|
+
active_generation TEXT,
|
|
1718
|
+
repair_cycle_count INTEGER NOT NULL DEFAULT 0,
|
|
1719
|
+
repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
|
|
1720
|
+
terminal_attempt_id TEXT,
|
|
1721
|
+
terminal_generation TEXT,
|
|
1722
|
+
terminal_outcome TEXT,
|
|
1723
|
+
terminal_head_sha TEXT,
|
|
1724
|
+
terminal_at TEXT,
|
|
1725
|
+
cleanup_eligible_at TEXT,
|
|
1726
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
1727
|
+
created_at TEXT NOT NULL,
|
|
1728
|
+
updated_at TEXT NOT NULL
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
CREATE TABLE pr_group_attempts (
|
|
1732
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1733
|
+
id TEXT PRIMARY KEY,
|
|
1734
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
1735
|
+
leaf_task_id TEXT NOT NULL,
|
|
1736
|
+
dispatch_attempt TEXT NOT NULL,
|
|
1737
|
+
writer_generation TEXT NOT NULL,
|
|
1738
|
+
previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
|
|
1739
|
+
worktree TEXT NOT NULL,
|
|
1740
|
+
branch TEXT NOT NULL,
|
|
1741
|
+
repository TEXT NOT NULL,
|
|
1742
|
+
pr_number INTEGER,
|
|
1743
|
+
base_sha TEXT,
|
|
1744
|
+
provider TEXT,
|
|
1745
|
+
provider_run_id TEXT,
|
|
1746
|
+
profile_alias TEXT,
|
|
1747
|
+
status TEXT NOT NULL,
|
|
1748
|
+
admitted_at TEXT NOT NULL,
|
|
1749
|
+
started_at TEXT,
|
|
1750
|
+
last_heartbeat_at TEXT,
|
|
1751
|
+
handed_off_at TEXT,
|
|
1752
|
+
fenced_at TEXT,
|
|
1753
|
+
terminal_at TEXT,
|
|
1754
|
+
created_at TEXT NOT NULL,
|
|
1755
|
+
updated_at TEXT NOT NULL,
|
|
1756
|
+
UNIQUE(group_id, leaf_task_id, dispatch_attempt),
|
|
1757
|
+
UNIQUE(group_id, writer_generation)
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1760
|
+
CREATE TABLE pr_group_events (
|
|
1761
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1762
|
+
id TEXT PRIMARY KEY,
|
|
1763
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
1764
|
+
attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
|
|
1765
|
+
writer_generation TEXT NOT NULL,
|
|
1766
|
+
sequence INTEGER NOT NULL,
|
|
1767
|
+
idempotency_key TEXT NOT NULL,
|
|
1768
|
+
event_type TEXT NOT NULL,
|
|
1769
|
+
state TEXT NOT NULL,
|
|
1770
|
+
message TEXT,
|
|
1771
|
+
head_sha TEXT,
|
|
1772
|
+
receipt_key TEXT UNIQUE,
|
|
1773
|
+
review_receipt_key TEXT,
|
|
1774
|
+
conditional_merge_receipt_key TEXT,
|
|
1775
|
+
outcome TEXT,
|
|
1776
|
+
repository TEXT NOT NULL,
|
|
1777
|
+
pr_number INTEGER,
|
|
1778
|
+
base_sha TEXT,
|
|
1779
|
+
actor_id TEXT,
|
|
1780
|
+
actor_run_id TEXT,
|
|
1781
|
+
expected_reviewer_id TEXT,
|
|
1782
|
+
expected_reviewer_run_id TEXT,
|
|
1783
|
+
repair_cycle INTEGER,
|
|
1784
|
+
ci_proof TEXT,
|
|
1785
|
+
cleanup_proof TEXT,
|
|
1786
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1787
|
+
payload_hash TEXT NOT NULL,
|
|
1788
|
+
created_at TEXT NOT NULL,
|
|
1789
|
+
UNIQUE(group_id, sequence),
|
|
1790
|
+
UNIQUE(group_id, idempotency_key)
|
|
1791
|
+
);
|
|
1792
|
+
|
|
1793
|
+
INSERT INTO pr_groups (
|
|
1794
|
+
schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
|
|
1795
|
+
pr_number,base_sha,state,active_attempt_id,active_generation,
|
|
1796
|
+
repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
|
|
1797
|
+
terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
|
|
1798
|
+
revision,created_at,updated_at
|
|
1799
|
+
)
|
|
1800
|
+
SELECT
|
|
1801
|
+
schema_version,id,identity_key,root_request_id,repository,leaf_task_id,branch,
|
|
1802
|
+
pr_number,base_sha,state,active_attempt_id,active_generation,
|
|
1803
|
+
repair_cycle_count,repair_cycle_limit,terminal_attempt_id,terminal_generation,
|
|
1804
|
+
terminal_outcome,terminal_head_sha,terminal_at,cleanup_eligible_at,
|
|
1805
|
+
revision,created_at,updated_at
|
|
1806
|
+
FROM pr_groups_v66;
|
|
1807
|
+
|
|
1808
|
+
INSERT INTO pr_group_attempts (
|
|
1809
|
+
schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
|
|
1810
|
+
previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
|
|
1811
|
+
provider,provider_run_id,profile_alias,status,admitted_at,started_at,
|
|
1812
|
+
last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
|
|
1813
|
+
)
|
|
1814
|
+
SELECT
|
|
1815
|
+
schema_version,id,group_id,leaf_task_id,dispatch_attempt,writer_generation,
|
|
1816
|
+
previous_attempt_id,worktree,branch,repository,pr_number,base_sha,
|
|
1817
|
+
provider,provider_run_id,profile_alias,status,admitted_at,started_at,
|
|
1818
|
+
last_heartbeat_at,handed_off_at,fenced_at,terminal_at,created_at,updated_at
|
|
1819
|
+
FROM pr_group_attempts_v66;
|
|
1820
|
+
|
|
1821
|
+
INSERT INTO pr_group_events (
|
|
1822
|
+
schema_version,id,group_id,attempt_id,writer_generation,sequence,
|
|
1823
|
+
idempotency_key,event_type,state,message,head_sha,receipt_key,
|
|
1824
|
+
review_receipt_key,conditional_merge_receipt_key,outcome,repository,
|
|
1825
|
+
pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
|
|
1826
|
+
expected_reviewer_run_id,repair_cycle,ci_proof,cleanup_proof,
|
|
1827
|
+
metadata,payload_hash,created_at
|
|
1828
|
+
)
|
|
1829
|
+
SELECT
|
|
1830
|
+
schema_version,id,group_id,attempt_id,writer_generation,sequence,
|
|
1831
|
+
idempotency_key,event_type,state,message,head_sha,receipt_key,
|
|
1832
|
+
review_receipt_key,conditional_merge_receipt_key,outcome,repository,
|
|
1833
|
+
pr_number,base_sha,actor_id,actor_run_id,expected_reviewer_id,
|
|
1834
|
+
expected_reviewer_run_id,repair_cycle,NULL,cleanup_proof,
|
|
1835
|
+
metadata,payload_hash,created_at
|
|
1836
|
+
FROM pr_group_events_v66;
|
|
1837
|
+
|
|
1838
|
+
DROP TABLE pr_group_events_v66;
|
|
1839
|
+
DROP TABLE pr_group_attempts_v66;
|
|
1840
|
+
DROP TABLE pr_groups_v66;
|
|
1841
|
+
|
|
1842
|
+
CREATE UNIQUE INDEX idx_pr_groups_identity ON pr_groups(identity_key);
|
|
1843
|
+
CREATE INDEX idx_pr_groups_root_repository ON pr_groups(root_request_id, repository);
|
|
1844
|
+
CREATE INDEX idx_pr_groups_active_generation ON pr_groups(active_generation);
|
|
1845
|
+
CREATE INDEX idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id);
|
|
1846
|
+
CREATE INDEX idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation);
|
|
1847
|
+
CREATE INDEX idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence);
|
|
1848
|
+
CREATE INDEX idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence);
|
|
1849
|
+
CREATE INDEX idx_pr_group_events_receipt ON pr_group_events(receipt_key);
|
|
1850
|
+
|
|
1851
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (68);
|
|
1852
|
+
COMMIT;
|
|
1853
|
+
PRAGMA foreign_keys = ON;
|
|
1551
1854
|
`
|
|
1552
1855
|
];
|
|
1553
1856
|
});
|
|
1554
1857
|
|
|
1555
1858
|
// src/db/schema.ts
|
|
1556
1859
|
function runMigrations(db) {
|
|
1860
|
+
let strictMigrationFailure = null;
|
|
1861
|
+
const executeMigration = (migration, index) => {
|
|
1862
|
+
try {
|
|
1863
|
+
db.exec(migration);
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
try {
|
|
1866
|
+
db.exec("ROLLBACK");
|
|
1867
|
+
} catch {}
|
|
1868
|
+
if (index + 1 >= 68)
|
|
1869
|
+
strictMigrationFailure = error;
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1557
1872
|
try {
|
|
1558
1873
|
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
1559
1874
|
const currentLevel = result?.max_id ?? 0;
|
|
1560
1875
|
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
1561
|
-
|
|
1562
|
-
db.exec(MIGRATIONS[i]);
|
|
1563
|
-
} catch {}
|
|
1876
|
+
executeMigration(MIGRATIONS[i], i);
|
|
1564
1877
|
}
|
|
1565
1878
|
} catch {
|
|
1566
|
-
for (const migration of MIGRATIONS) {
|
|
1567
|
-
|
|
1568
|
-
db.exec(migration);
|
|
1569
|
-
} catch {}
|
|
1879
|
+
for (const [index, migration] of MIGRATIONS.entries()) {
|
|
1880
|
+
executeMigration(migration, index);
|
|
1570
1881
|
}
|
|
1571
1882
|
}
|
|
1883
|
+
if (strictMigrationFailure)
|
|
1884
|
+
throw strictMigrationFailure;
|
|
1572
1885
|
ensureSchema(db);
|
|
1573
1886
|
}
|
|
1574
1887
|
function planSlugBase(value) {
|
|
@@ -2737,6 +3050,246 @@ function ensureSchema(db) {
|
|
|
2737
3050
|
)`);
|
|
2738
3051
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
|
|
2739
3052
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
|
|
3053
|
+
ensureTable("pr_groups", `
|
|
3054
|
+
CREATE TABLE pr_groups (
|
|
3055
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
3056
|
+
id TEXT PRIMARY KEY,
|
|
3057
|
+
identity_key TEXT NOT NULL UNIQUE,
|
|
3058
|
+
root_request_id TEXT NOT NULL,
|
|
3059
|
+
repository TEXT NOT NULL,
|
|
3060
|
+
leaf_task_id TEXT NOT NULL,
|
|
3061
|
+
branch TEXT NOT NULL,
|
|
3062
|
+
pr_number INTEGER,
|
|
3063
|
+
base_sha TEXT,
|
|
3064
|
+
state TEXT NOT NULL,
|
|
3065
|
+
active_attempt_id TEXT,
|
|
3066
|
+
active_generation TEXT,
|
|
3067
|
+
repair_cycle_count INTEGER NOT NULL DEFAULT 0,
|
|
3068
|
+
repair_cycle_limit INTEGER NOT NULL DEFAULT 2,
|
|
3069
|
+
terminal_attempt_id TEXT,
|
|
3070
|
+
terminal_generation TEXT,
|
|
3071
|
+
terminal_outcome TEXT,
|
|
3072
|
+
terminal_head_sha TEXT,
|
|
3073
|
+
terminal_at TEXT,
|
|
3074
|
+
cleanup_eligible_at TEXT,
|
|
3075
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
3076
|
+
created_at TEXT NOT NULL,
|
|
3077
|
+
updated_at TEXT NOT NULL
|
|
3078
|
+
)`);
|
|
3079
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_groups_identity ON pr_groups(identity_key)");
|
|
3080
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_root_repository ON pr_groups(root_request_id, repository)");
|
|
3081
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_groups_active_generation ON pr_groups(active_generation)");
|
|
3082
|
+
ensureTable("pr_group_attempts", `
|
|
3083
|
+
CREATE TABLE pr_group_attempts (
|
|
3084
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
3085
|
+
id TEXT PRIMARY KEY,
|
|
3086
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
3087
|
+
leaf_task_id TEXT NOT NULL,
|
|
3088
|
+
dispatch_attempt TEXT NOT NULL,
|
|
3089
|
+
writer_generation TEXT NOT NULL,
|
|
3090
|
+
previous_attempt_id TEXT REFERENCES pr_group_attempts(id) ON DELETE SET NULL,
|
|
3091
|
+
worktree TEXT NOT NULL,
|
|
3092
|
+
branch TEXT NOT NULL,
|
|
3093
|
+
repository TEXT NOT NULL,
|
|
3094
|
+
pr_number INTEGER,
|
|
3095
|
+
base_sha TEXT,
|
|
3096
|
+
provider TEXT,
|
|
3097
|
+
provider_run_id TEXT,
|
|
3098
|
+
profile_alias TEXT,
|
|
3099
|
+
status TEXT NOT NULL,
|
|
3100
|
+
admitted_at TEXT NOT NULL,
|
|
3101
|
+
started_at TEXT,
|
|
3102
|
+
last_heartbeat_at TEXT,
|
|
3103
|
+
handed_off_at TEXT,
|
|
3104
|
+
fenced_at TEXT,
|
|
3105
|
+
terminal_at TEXT,
|
|
3106
|
+
created_at TEXT NOT NULL,
|
|
3107
|
+
updated_at TEXT NOT NULL,
|
|
3108
|
+
UNIQUE(group_id, leaf_task_id, dispatch_attempt),
|
|
3109
|
+
UNIQUE(group_id, writer_generation)
|
|
3110
|
+
)`);
|
|
3111
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_group ON pr_group_attempts(group_id, created_at, id)");
|
|
3112
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_attempts_generation ON pr_group_attempts(group_id, writer_generation)");
|
|
3113
|
+
ensureTable("pr_group_events", `
|
|
3114
|
+
CREATE TABLE pr_group_events (
|
|
3115
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
3116
|
+
id TEXT PRIMARY KEY,
|
|
3117
|
+
group_id TEXT NOT NULL REFERENCES pr_groups(id) ON DELETE CASCADE,
|
|
3118
|
+
attempt_id TEXT NOT NULL REFERENCES pr_group_attempts(id) ON DELETE CASCADE,
|
|
3119
|
+
writer_generation TEXT NOT NULL,
|
|
3120
|
+
sequence INTEGER NOT NULL,
|
|
3121
|
+
idempotency_key TEXT NOT NULL,
|
|
3122
|
+
event_type TEXT NOT NULL,
|
|
3123
|
+
state TEXT NOT NULL,
|
|
3124
|
+
message TEXT,
|
|
3125
|
+
head_sha TEXT,
|
|
3126
|
+
receipt_key TEXT UNIQUE,
|
|
3127
|
+
review_receipt_key TEXT,
|
|
3128
|
+
conditional_merge_receipt_key TEXT,
|
|
3129
|
+
outcome TEXT,
|
|
3130
|
+
repository TEXT NOT NULL,
|
|
3131
|
+
pr_number INTEGER,
|
|
3132
|
+
base_sha TEXT,
|
|
3133
|
+
actor_id TEXT,
|
|
3134
|
+
actor_run_id TEXT,
|
|
3135
|
+
expected_reviewer_id TEXT,
|
|
3136
|
+
expected_reviewer_run_id TEXT,
|
|
3137
|
+
repair_cycle INTEGER,
|
|
3138
|
+
ci_proof TEXT,
|
|
3139
|
+
cleanup_proof TEXT,
|
|
3140
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
3141
|
+
payload_hash TEXT NOT NULL,
|
|
3142
|
+
created_at TEXT NOT NULL,
|
|
3143
|
+
UNIQUE(group_id, sequence),
|
|
3144
|
+
UNIQUE(group_id, idempotency_key)
|
|
3145
|
+
)`);
|
|
3146
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_group_sequence ON pr_group_events(group_id, sequence)");
|
|
3147
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_attempt ON pr_group_events(attempt_id, sequence)");
|
|
3148
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_pr_group_events_receipt ON pr_group_events(receipt_key)");
|
|
3149
|
+
ensureColumn("pr_groups", "leaf_task_id", "TEXT");
|
|
3150
|
+
ensureColumn("pr_groups", "branch", "TEXT");
|
|
3151
|
+
ensureColumn("pr_groups", "pr_number", "INTEGER");
|
|
3152
|
+
ensureColumn("pr_groups", "base_sha", "TEXT");
|
|
3153
|
+
ensureColumn("pr_groups", "repair_cycle_count", "INTEGER NOT NULL DEFAULT 0");
|
|
3154
|
+
ensureColumn("pr_groups", "repair_cycle_limit", "INTEGER NOT NULL DEFAULT 2");
|
|
3155
|
+
ensureColumn("pr_group_attempts", "repository", "TEXT");
|
|
3156
|
+
ensureColumn("pr_group_attempts", "pr_number", "INTEGER");
|
|
3157
|
+
ensureColumn("pr_group_attempts", "base_sha", "TEXT");
|
|
3158
|
+
ensureColumn("pr_group_events", "review_receipt_key", "TEXT");
|
|
3159
|
+
ensureColumn("pr_group_events", "conditional_merge_receipt_key", "TEXT");
|
|
3160
|
+
ensureColumn("pr_group_events", "repository", "TEXT");
|
|
3161
|
+
ensureColumn("pr_group_events", "pr_number", "INTEGER");
|
|
3162
|
+
ensureColumn("pr_group_events", "base_sha", "TEXT");
|
|
3163
|
+
ensureColumn("pr_group_events", "actor_id", "TEXT");
|
|
3164
|
+
ensureColumn("pr_group_events", "actor_run_id", "TEXT");
|
|
3165
|
+
ensureColumn("pr_group_events", "expected_reviewer_id", "TEXT");
|
|
3166
|
+
ensureColumn("pr_group_events", "expected_reviewer_run_id", "TEXT");
|
|
3167
|
+
ensureColumn("pr_group_events", "repair_cycle", "INTEGER");
|
|
3168
|
+
ensureColumn("pr_group_events", "ci_proof", "TEXT");
|
|
3169
|
+
ensureColumn("pr_group_events", "cleanup_proof", "TEXT");
|
|
3170
|
+
try {
|
|
3171
|
+
db.exec(`
|
|
3172
|
+
UPDATE pr_groups
|
|
3173
|
+
SET leaf_task_id = COALESCE(leaf_task_id, (
|
|
3174
|
+
SELECT attempt.leaf_task_id
|
|
3175
|
+
FROM pr_group_attempts AS attempt
|
|
3176
|
+
WHERE attempt.group_id = pr_groups.id
|
|
3177
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
3178
|
+
attempt.created_at ASC, attempt.id ASC
|
|
3179
|
+
LIMIT 1
|
|
3180
|
+
)),
|
|
3181
|
+
branch = COALESCE(branch, (
|
|
3182
|
+
SELECT attempt.branch
|
|
3183
|
+
FROM pr_group_attempts AS attempt
|
|
3184
|
+
WHERE attempt.group_id = pr_groups.id
|
|
3185
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
3186
|
+
attempt.created_at ASC, attempt.id ASC
|
|
3187
|
+
LIMIT 1
|
|
3188
|
+
));
|
|
3189
|
+
UPDATE pr_group_attempts
|
|
3190
|
+
SET repository = COALESCE(repository, (
|
|
3191
|
+
SELECT groups.repository FROM pr_groups AS groups
|
|
3192
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
3193
|
+
)),
|
|
3194
|
+
pr_number = COALESCE(pr_number, (
|
|
3195
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
3196
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
3197
|
+
)),
|
|
3198
|
+
base_sha = COALESCE(base_sha, (
|
|
3199
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
3200
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
3201
|
+
));
|
|
3202
|
+
UPDATE pr_groups
|
|
3203
|
+
SET pr_number = COALESCE(pr_number, (
|
|
3204
|
+
SELECT attempt.pr_number
|
|
3205
|
+
FROM pr_group_attempts AS attempt
|
|
3206
|
+
WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
|
|
3207
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
3208
|
+
attempt.created_at ASC, attempt.id ASC
|
|
3209
|
+
LIMIT 1
|
|
3210
|
+
)),
|
|
3211
|
+
base_sha = COALESCE(base_sha, (
|
|
3212
|
+
SELECT attempt.base_sha
|
|
3213
|
+
FROM pr_group_attempts AS attempt
|
|
3214
|
+
WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
|
|
3215
|
+
ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
|
|
3216
|
+
attempt.created_at ASC, attempt.id ASC
|
|
3217
|
+
LIMIT 1
|
|
3218
|
+
));
|
|
3219
|
+
UPDATE pr_group_attempts
|
|
3220
|
+
SET pr_number = COALESCE(pr_number, (
|
|
3221
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
3222
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
3223
|
+
)),
|
|
3224
|
+
base_sha = COALESCE(base_sha, (
|
|
3225
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
3226
|
+
WHERE groups.id = pr_group_attempts.group_id
|
|
3227
|
+
));
|
|
3228
|
+
UPDATE pr_group_events
|
|
3229
|
+
SET repository = COALESCE(repository, (
|
|
3230
|
+
SELECT attempt.repository FROM pr_group_attempts AS attempt
|
|
3231
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
3232
|
+
), (
|
|
3233
|
+
SELECT groups.repository FROM pr_groups AS groups
|
|
3234
|
+
WHERE groups.id = pr_group_events.group_id
|
|
3235
|
+
)),
|
|
3236
|
+
pr_number = COALESCE(pr_number, (
|
|
3237
|
+
SELECT attempt.pr_number FROM pr_group_attempts AS attempt
|
|
3238
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
3239
|
+
), (
|
|
3240
|
+
SELECT groups.pr_number FROM pr_groups AS groups
|
|
3241
|
+
WHERE groups.id = pr_group_events.group_id
|
|
3242
|
+
)),
|
|
3243
|
+
base_sha = COALESCE(base_sha, (
|
|
3244
|
+
SELECT attempt.base_sha FROM pr_group_attempts AS attempt
|
|
3245
|
+
WHERE attempt.id = pr_group_events.attempt_id
|
|
3246
|
+
), (
|
|
3247
|
+
SELECT groups.base_sha FROM pr_groups AS groups
|
|
3248
|
+
WHERE groups.id = pr_group_events.group_id
|
|
3249
|
+
));
|
|
3250
|
+
INSERT OR IGNORE INTO _migrations (id)
|
|
3251
|
+
SELECT 66
|
|
3252
|
+
WHERE NOT EXISTS (
|
|
3253
|
+
SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
|
|
3254
|
+
)
|
|
3255
|
+
AND NOT EXISTS (
|
|
3256
|
+
SELECT 1 FROM pr_group_attempts WHERE repository IS NULL
|
|
3257
|
+
)
|
|
3258
|
+
AND NOT EXISTS (
|
|
3259
|
+
SELECT 1 FROM pr_group_events WHERE repository IS NULL
|
|
3260
|
+
);
|
|
3261
|
+
`);
|
|
3262
|
+
} catch {}
|
|
3263
|
+
const strictColumns = [
|
|
3264
|
+
["pr_groups", "leaf_task_id"],
|
|
3265
|
+
["pr_groups", "branch"],
|
|
3266
|
+
["pr_group_attempts", "repository"],
|
|
3267
|
+
["pr_group_events", "repository"]
|
|
3268
|
+
];
|
|
3269
|
+
for (const [table, column] of strictColumns) {
|
|
3270
|
+
const definition = db.query(`PRAGMA table_info(${table})`).all();
|
|
3271
|
+
if (definition.find((entry) => entry.name === column)?.notnull !== 1) {
|
|
3272
|
+
throw new Error(`PR-group schema integrity failure: ${table}.${column} is not NOT NULL`);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
const eventColumns = db.query("PRAGMA table_info(pr_group_events)").all();
|
|
3276
|
+
if (!eventColumns.some((entry) => entry.name === "ci_proof")) {
|
|
3277
|
+
throw new Error("PR-group schema integrity failure: pr_group_events.ci_proof is missing");
|
|
3278
|
+
}
|
|
3279
|
+
const receiptIndexes = db.query("PRAGMA index_list(pr_group_events)").all();
|
|
3280
|
+
const hasGlobalReceiptUniqueness = receiptIndexes.some((index) => {
|
|
3281
|
+
if (index.unique !== 1)
|
|
3282
|
+
return false;
|
|
3283
|
+
const columns = db.query(`PRAGMA index_info(${JSON.stringify(index.name)})`).all();
|
|
3284
|
+
return columns.length === 1 && columns[0]?.name === "receipt_key";
|
|
3285
|
+
});
|
|
3286
|
+
if (!hasGlobalReceiptUniqueness) {
|
|
3287
|
+
throw new Error("PR-group schema integrity failure: receipt_key is not globally unique");
|
|
3288
|
+
}
|
|
3289
|
+
const strictMigration = db.query("SELECT 1 AS ok FROM _migrations WHERE id = 68").get();
|
|
3290
|
+
if (strictMigration?.ok !== 1) {
|
|
3291
|
+
throw new Error("PR-group schema integrity failure: migration 68 is not recorded");
|
|
3292
|
+
}
|
|
2740
3293
|
}
|
|
2741
3294
|
function backfillTaskTags(db) {
|
|
2742
3295
|
try {
|
|
@@ -3077,6 +3630,334 @@ var init_machines = __esm(() => {
|
|
|
3077
3630
|
];
|
|
3078
3631
|
});
|
|
3079
3632
|
|
|
3633
|
+
// src/types/index.ts
|
|
3634
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3635
|
+
var init_types = __esm(() => {
|
|
3636
|
+
TASK_STATUSES = [
|
|
3637
|
+
"pending",
|
|
3638
|
+
"in_progress",
|
|
3639
|
+
"completed",
|
|
3640
|
+
"failed",
|
|
3641
|
+
"cancelled"
|
|
3642
|
+
];
|
|
3643
|
+
TASK_PRIORITIES = [
|
|
3644
|
+
"low",
|
|
3645
|
+
"medium",
|
|
3646
|
+
"high",
|
|
3647
|
+
"critical"
|
|
3648
|
+
];
|
|
3649
|
+
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
3650
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
3651
|
+
taskId;
|
|
3652
|
+
expectedVersion;
|
|
3653
|
+
actualVersion;
|
|
3654
|
+
static code = "VERSION_CONFLICT";
|
|
3655
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
3656
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
3657
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
3658
|
+
this.taskId = taskId;
|
|
3659
|
+
this.expectedVersion = expectedVersion;
|
|
3660
|
+
this.actualVersion = actualVersion;
|
|
3661
|
+
this.name = "VersionConflictError";
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3664
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
3665
|
+
taskId;
|
|
3666
|
+
static code = "TASK_NOT_FOUND";
|
|
3667
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
3668
|
+
constructor(taskId) {
|
|
3669
|
+
super(`Task not found: ${taskId}`);
|
|
3670
|
+
this.taskId = taskId;
|
|
3671
|
+
this.name = "TaskNotFoundError";
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
3675
|
+
projectId;
|
|
3676
|
+
static code = "PROJECT_NOT_FOUND";
|
|
3677
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
3678
|
+
constructor(projectId) {
|
|
3679
|
+
super(`Project not found: ${projectId}`);
|
|
3680
|
+
this.projectId = projectId;
|
|
3681
|
+
this.name = "ProjectNotFoundError";
|
|
3682
|
+
}
|
|
3683
|
+
};
|
|
3684
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
3685
|
+
code;
|
|
3686
|
+
constructor(code, message) {
|
|
3687
|
+
super(message);
|
|
3688
|
+
this.code = code;
|
|
3689
|
+
this.name = "ResourceConflictError";
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3693
|
+
planId;
|
|
3694
|
+
static code = "PLAN_NOT_FOUND";
|
|
3695
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
3696
|
+
constructor(planId) {
|
|
3697
|
+
super(`Plan not found: ${planId}`);
|
|
3698
|
+
this.planId = planId;
|
|
3699
|
+
this.name = "PlanNotFoundError";
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
LockError = class LockError extends Error {
|
|
3703
|
+
taskId;
|
|
3704
|
+
lockedBy;
|
|
3705
|
+
static code = "LOCK_ERROR";
|
|
3706
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
3707
|
+
constructor(taskId, lockedBy) {
|
|
3708
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
3709
|
+
this.taskId = taskId;
|
|
3710
|
+
this.lockedBy = lockedBy;
|
|
3711
|
+
this.name = "LockError";
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
3715
|
+
agentId;
|
|
3716
|
+
static code = "AGENT_NOT_FOUND";
|
|
3717
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
3718
|
+
constructor(agentId) {
|
|
3719
|
+
super(`Agent not found: ${agentId}`);
|
|
3720
|
+
this.agentId = agentId;
|
|
3721
|
+
this.name = "AgentNotFoundError";
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
3725
|
+
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
3726
|
+
candidates;
|
|
3727
|
+
constructor(subject, candidates = []) {
|
|
3728
|
+
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
3729
|
+
this.name = "IdentityAliasAmbiguousError";
|
|
3730
|
+
this.candidates = [...new Set(candidates)].sort();
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
3734
|
+
code = "IDENTITY_ID_IMMUTABLE";
|
|
3735
|
+
constructor(agentId) {
|
|
3736
|
+
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
3737
|
+
this.name = "IdentityIdImmutableError";
|
|
3738
|
+
}
|
|
3739
|
+
};
|
|
3740
|
+
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
3741
|
+
taskListId;
|
|
3742
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
3743
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
3744
|
+
constructor(taskListId) {
|
|
3745
|
+
super(`Task list not found: ${taskListId}`);
|
|
3746
|
+
this.taskListId = taskListId;
|
|
3747
|
+
this.name = "TaskListNotFoundError";
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
3751
|
+
taskId;
|
|
3752
|
+
dependsOn;
|
|
3753
|
+
static code = "DEPENDENCY_CYCLE";
|
|
3754
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
3755
|
+
constructor(taskId, dependsOn) {
|
|
3756
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
3757
|
+
this.taskId = taskId;
|
|
3758
|
+
this.dependsOn = dependsOn;
|
|
3759
|
+
this.name = "DependencyCycleError";
|
|
3760
|
+
}
|
|
3761
|
+
};
|
|
3762
|
+
CompletionGuardError = class CompletionGuardError extends Error {
|
|
3763
|
+
reason;
|
|
3764
|
+
retryAfterSeconds;
|
|
3765
|
+
static code = "COMPLETION_BLOCKED";
|
|
3766
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
3767
|
+
constructor(reason, retryAfterSeconds) {
|
|
3768
|
+
super(reason);
|
|
3769
|
+
this.reason = reason;
|
|
3770
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
3771
|
+
this.name = "CompletionGuardError";
|
|
3772
|
+
}
|
|
3773
|
+
};
|
|
3774
|
+
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
3775
|
+
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
3776
|
+
dispatchId;
|
|
3777
|
+
static code = "DISPATCH_NOT_FOUND";
|
|
3778
|
+
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
3779
|
+
constructor(dispatchId) {
|
|
3780
|
+
super(`Dispatch not found: ${dispatchId}`);
|
|
3781
|
+
this.dispatchId = dispatchId;
|
|
3782
|
+
this.name = "DispatchNotFoundError";
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
// src/db/identity-mapping.ts
|
|
3788
|
+
function timestamp() {
|
|
3789
|
+
return new Date().toISOString();
|
|
3790
|
+
}
|
|
3791
|
+
function mappingId() {
|
|
3792
|
+
return crypto.randomUUID();
|
|
3793
|
+
}
|
|
3794
|
+
function normalizedAlias(label) {
|
|
3795
|
+
const normalized = label.trim().toLocaleLowerCase("en-US");
|
|
3796
|
+
if (!normalized)
|
|
3797
|
+
throw new Error("IDENTITY_ALIAS_INVALID: label is required");
|
|
3798
|
+
return normalized;
|
|
3799
|
+
}
|
|
3800
|
+
function ensureColumn(db, table, column, type) {
|
|
3801
|
+
const columns = db.query(`PRAGMA table_info(${table})`).all();
|
|
3802
|
+
if (!columns.some((item) => item.name === column)) {
|
|
3803
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
function ensureAgentIdentitySchema(db) {
|
|
3807
|
+
const install = db.transaction(() => {
|
|
3808
|
+
ensureColumn(db, "agents", "identity_id", "TEXT");
|
|
3809
|
+
ensureColumn(db, "agents", "project_id", "TEXT");
|
|
3810
|
+
ensureColumn(db, "agents", "runtime_instance_id", "TEXT");
|
|
3811
|
+
db.exec(`
|
|
3812
|
+
CREATE TABLE IF NOT EXISTS agent_identity_source_mappings (
|
|
3813
|
+
id TEXT PRIMARY KEY,
|
|
3814
|
+
local_agent_id TEXT REFERENCES agents(id) ON DELETE RESTRICT,
|
|
3815
|
+
identity_id TEXT,
|
|
3816
|
+
source_authority TEXT NOT NULL CHECK(length(trim(source_authority)) > 0),
|
|
3817
|
+
source_tenant_id TEXT NOT NULL CHECK(length(trim(source_tenant_id)) > 0),
|
|
3818
|
+
source_namespace TEXT NOT NULL CHECK(length(trim(source_namespace)) > 0),
|
|
3819
|
+
source_entity_type TEXT NOT NULL CHECK(length(trim(source_entity_type)) > 0),
|
|
3820
|
+
source_record_id TEXT NOT NULL CHECK(length(trim(source_record_id)) > 0),
|
|
3821
|
+
observed_label TEXT,
|
|
3822
|
+
evidence TEXT NOT NULL DEFAULT '{}',
|
|
3823
|
+
mapping_basis TEXT NOT NULL CHECK(mapping_basis IN ('authoritative', 'imported', 'candidate', 'name_similarity')),
|
|
3824
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'retired', 'quarantined', 'revoked')),
|
|
3825
|
+
revision INTEGER NOT NULL CHECK(revision > 0),
|
|
3826
|
+
created_at TEXT NOT NULL,
|
|
3827
|
+
updated_at TEXT NOT NULL,
|
|
3828
|
+
CHECK(mapping_basis IN ('authoritative', 'imported') OR status IN ('quarantined', 'revoked')),
|
|
3829
|
+
CHECK(status != 'active' OR (mapping_basis IN ('authoritative', 'imported') AND identity_id IS NOT NULL))
|
|
3830
|
+
);
|
|
3831
|
+
`);
|
|
3832
|
+
ensureColumn(db, "agent_identity_source_mappings", "revision", "INTEGER NOT NULL DEFAULT 1");
|
|
3833
|
+
ensureColumn(db, "agent_identity_source_mappings", "evidence", "TEXT NOT NULL DEFAULT '{}'");
|
|
3834
|
+
db.exec(`
|
|
3835
|
+
DROP INDEX IF EXISTS idx_agent_identity_source_active_unique;
|
|
3836
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_identity_source_revision_unique
|
|
3837
|
+
ON agent_identity_source_mappings (
|
|
3838
|
+
source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id, revision
|
|
3839
|
+
)
|
|
3840
|
+
WHERE mapping_basis IN ('authoritative', 'imported');
|
|
3841
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_source_identity
|
|
3842
|
+
ON agent_identity_source_mappings(identity_id);
|
|
3843
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_source_local
|
|
3844
|
+
ON agent_identity_source_mappings(local_agent_id);
|
|
3845
|
+
DROP INDEX IF EXISTS idx_agents_identity_unique;
|
|
3846
|
+
|
|
3847
|
+
CREATE TABLE IF NOT EXISTS agent_identity_aliases (
|
|
3848
|
+
id TEXT PRIMARY KEY,
|
|
3849
|
+
local_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,
|
|
3850
|
+
label TEXT NOT NULL,
|
|
3851
|
+
normalized_label TEXT NOT NULL,
|
|
3852
|
+
alias_kind TEXT NOT NULL CHECK(alias_kind IN ('historical', 'candidate')),
|
|
3853
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'quarantined', 'revoked')),
|
|
3854
|
+
created_at TEXT NOT NULL,
|
|
3855
|
+
UNIQUE(local_agent_id, normalized_label)
|
|
3856
|
+
);
|
|
3857
|
+
CREATE INDEX IF NOT EXISTS idx_agent_identity_alias_lookup
|
|
3858
|
+
ON agent_identity_aliases(normalized_label, status);
|
|
3859
|
+
|
|
3860
|
+
CREATE TRIGGER IF NOT EXISTS trg_agents_identity_id_immutable
|
|
3861
|
+
BEFORE UPDATE OF identity_id ON agents
|
|
3862
|
+
WHEN OLD.identity_id IS NOT NULL AND OLD.identity_id IS NOT NEW.identity_id
|
|
3863
|
+
BEGIN
|
|
3864
|
+
SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
|
|
3865
|
+
END;
|
|
3866
|
+
|
|
3867
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_identity_immutable
|
|
3868
|
+
BEFORE UPDATE OF identity_id ON agent_identity_source_mappings
|
|
3869
|
+
WHEN OLD.identity_id IS NOT NEW.identity_id
|
|
3870
|
+
BEGIN
|
|
3871
|
+
SELECT RAISE(ABORT, 'IDENTITY_ID_IMMUTABLE');
|
|
3872
|
+
END;
|
|
3873
|
+
|
|
3874
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_lineage_immutable
|
|
3875
|
+
BEFORE UPDATE OF source_authority, source_tenant_id, source_namespace, source_entity_type, source_record_id
|
|
3876
|
+
ON agent_identity_source_mappings
|
|
3877
|
+
WHEN OLD.source_authority IS NOT NEW.source_authority
|
|
3878
|
+
OR OLD.source_tenant_id IS NOT NEW.source_tenant_id
|
|
3879
|
+
OR OLD.source_namespace IS NOT NEW.source_namespace
|
|
3880
|
+
OR OLD.source_entity_type IS NOT NEW.source_entity_type
|
|
3881
|
+
OR OLD.source_record_id IS NOT NEW.source_record_id
|
|
3882
|
+
BEGIN
|
|
3883
|
+
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
3884
|
+
END;
|
|
3885
|
+
|
|
3886
|
+
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
3887
|
+
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
3888
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
3889
|
+
ON agent_identity_source_mappings
|
|
3890
|
+
BEGIN
|
|
3891
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
3892
|
+
END;
|
|
3893
|
+
|
|
3894
|
+
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
3895
|
+
CREATE TRIGGER trg_agent_identity_mapping_history_append_only
|
|
3896
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
3897
|
+
BEGIN
|
|
3898
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
3899
|
+
END;
|
|
3900
|
+
`);
|
|
3901
|
+
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
3902
|
+
});
|
|
3903
|
+
install.immediate();
|
|
3904
|
+
}
|
|
3905
|
+
function storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db) {
|
|
3906
|
+
const normalized = normalizedAlias(label);
|
|
3907
|
+
const exists = db.query("SELECT id FROM agents WHERE id = ?").get(agentId);
|
|
3908
|
+
if (!exists)
|
|
3909
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
3910
|
+
const existing = db.query(`
|
|
3911
|
+
SELECT * FROM agent_identity_aliases
|
|
3912
|
+
WHERE local_agent_id = ? AND normalized_label = ?
|
|
3913
|
+
`).get(agentId, normalized);
|
|
3914
|
+
if (existing) {
|
|
3915
|
+
if (aliasKind === "historical" && (existing.alias_kind !== "historical" || existing.status !== "active")) {
|
|
3916
|
+
db.run("UPDATE agent_identity_aliases SET label = ?, alias_kind = 'historical', status = 'active' WHERE id = ?", [label.trim(), existing.id]);
|
|
3917
|
+
return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(existing.id);
|
|
3918
|
+
}
|
|
3919
|
+
return existing;
|
|
3920
|
+
}
|
|
3921
|
+
const id = mappingId();
|
|
3922
|
+
db.run(`
|
|
3923
|
+
INSERT INTO agent_identity_aliases (
|
|
3924
|
+
id, local_agent_id, label, normalized_label, alias_kind, status, created_at
|
|
3925
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3926
|
+
`, [id, agentId, label.trim(), normalized, aliasKind, status, timestamp()]);
|
|
3927
|
+
return db.query("SELECT * FROM agent_identity_aliases WHERE id = ?").get(id);
|
|
3928
|
+
}
|
|
3929
|
+
function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
3930
|
+
if (db.inTransaction) {
|
|
3931
|
+
return storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db);
|
|
3932
|
+
}
|
|
3933
|
+
const store = db.transaction(() => storeAgentAliasInCurrentTransaction(agentId, label, aliasKind, status, db));
|
|
3934
|
+
return store.immediate();
|
|
3935
|
+
}
|
|
3936
|
+
function recordAgentAlias(agentId, label, db) {
|
|
3937
|
+
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
3938
|
+
}
|
|
3939
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
|
|
3940
|
+
var init_identity_mapping = __esm(() => {
|
|
3941
|
+
init_types();
|
|
3942
|
+
init_types();
|
|
3943
|
+
IDENTITY_PROJECTION_CONTRACT = Object.freeze({
|
|
3944
|
+
name: "hasna.todos.agent-identity-projection/v1",
|
|
3945
|
+
version: 1,
|
|
3946
|
+
foundation_repository: "hasna/identities",
|
|
3947
|
+
foundation_pull_request: 14,
|
|
3948
|
+
foundation_contract: "hasna.identities.agent-identity/v1",
|
|
3949
|
+
foundation_version: 1,
|
|
3950
|
+
foundation_commit: "14d83e3e45df5748944a871a07c4aacece893169",
|
|
3951
|
+
foundation_fixture_id: "hasna.identities.agent-identity/v1/conformance/1",
|
|
3952
|
+
foundation_fixture_source_path: "docs/fixtures/agent-identity-v1.conformance.json",
|
|
3953
|
+
foundation_fixture_local_path: "src/db/fixtures/agent-identity-v1.conformance.json",
|
|
3954
|
+
foundation_fixture_sha256: "sha256:2d22e72b7315d0b06b3c67c71674cfd8c2dff552727eea00efb325f52c9420af",
|
|
3955
|
+
foundation_default_read_preference: "canonical_first",
|
|
3956
|
+
consumer_rollout_default_read_preference: "legacy_first",
|
|
3957
|
+
lease_fence_authority: "external Runtime Coordination"
|
|
3958
|
+
});
|
|
3959
|
+
});
|
|
3960
|
+
|
|
3080
3961
|
// src/storage/shadow-outbox-schema.ts
|
|
3081
3962
|
var exports_shadow_outbox_schema = {};
|
|
3082
3963
|
__export(exports_shadow_outbox_schema, {
|
|
@@ -3096,7 +3977,7 @@ function installShadowOutboxSchema(db) {
|
|
|
3096
3977
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
3097
3978
|
revision INTEGER NOT NULL DEFAULT 0
|
|
3098
3979
|
)`);
|
|
3099
|
-
|
|
3980
|
+
ensureColumn2(db, "shadow_outbox", "revision", "INTEGER NOT NULL DEFAULT 0");
|
|
3100
3981
|
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS shadow_outbox_pending_uq
|
|
3101
3982
|
ON shadow_outbox(object_type, object_id) WHERE status='pending'`);
|
|
3102
3983
|
db.run(`CREATE INDEX IF NOT EXISTS shadow_outbox_ready_idx
|
|
@@ -3118,7 +3999,7 @@ function installShadowOutboxSchema(db) {
|
|
|
3118
3999
|
}
|
|
3119
4000
|
}
|
|
3120
4001
|
}
|
|
3121
|
-
function
|
|
4002
|
+
function ensureColumn2(db, table, column, definition) {
|
|
3122
4003
|
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
3123
4004
|
if (rows.some((row) => row.name === column))
|
|
3124
4005
|
return;
|
|
@@ -3246,6 +4127,7 @@ function openDatabase(path) {
|
|
|
3246
4127
|
db.run("PRAGMA busy_timeout = 5000");
|
|
3247
4128
|
db.run("PRAGMA foreign_keys = ON");
|
|
3248
4129
|
runMigrations(db);
|
|
4130
|
+
ensureAgentIdentitySchema(db);
|
|
3249
4131
|
backfillTaskTags(db);
|
|
3250
4132
|
backfillMachineId(db);
|
|
3251
4133
|
maybeInstallShadowCapture(db);
|
|
@@ -3354,9 +4236,19 @@ function resolvePartialId(db, table, partialId) {
|
|
|
3354
4236
|
return nameRow.id;
|
|
3355
4237
|
}
|
|
3356
4238
|
if (table === "agents") {
|
|
3357
|
-
const
|
|
3358
|
-
|
|
3359
|
-
|
|
4239
|
+
const normalized = partialId.trim().toLowerCase();
|
|
4240
|
+
const matches = db.query(`
|
|
4241
|
+
SELECT id FROM agents WHERE lower(name) = ?
|
|
4242
|
+
UNION
|
|
4243
|
+
SELECT local_agent_id AS id FROM agent_identity_aliases
|
|
4244
|
+
WHERE normalized_label = ? AND status = 'active'
|
|
4245
|
+
ORDER BY id
|
|
4246
|
+
`).all(normalized, normalized);
|
|
4247
|
+
if (matches.length === 1)
|
|
4248
|
+
return matches[0].id;
|
|
4249
|
+
if (matches.length > 1) {
|
|
4250
|
+
throw new IdentityAliasAmbiguousError(partialId, matches.map((match) => match.id));
|
|
4251
|
+
}
|
|
3360
4252
|
}
|
|
3361
4253
|
return null;
|
|
3362
4254
|
}
|
|
@@ -3364,6 +4256,8 @@ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
|
3364
4256
|
var init_database = __esm(() => {
|
|
3365
4257
|
init_schema();
|
|
3366
4258
|
init_machines();
|
|
4259
|
+
init_identity_mapping();
|
|
4260
|
+
init_types();
|
|
3367
4261
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
3368
4262
|
});
|
|
3369
4263
|
|
|
@@ -3706,7 +4600,7 @@ function createProject(input, db) {
|
|
|
3706
4600
|
const d = db || getDatabase();
|
|
3707
4601
|
return d.transaction(() => {
|
|
3708
4602
|
const id = uuid();
|
|
3709
|
-
const
|
|
4603
|
+
const timestamp2 = now();
|
|
3710
4604
|
const derivedSlug = slugify(input.name);
|
|
3711
4605
|
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
|
|
3712
4606
|
if (!derivedSlug || !taskListId)
|
|
@@ -3718,7 +4612,7 @@ function createProject(input, db) {
|
|
|
3718
4612
|
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
3719
4613
|
const machineId = currentStorageMachineId(d);
|
|
3720
4614
|
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
3721
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix,
|
|
4615
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp2, timestamp2, machineId]);
|
|
3722
4616
|
return getProject(id, d);
|
|
3723
4617
|
})();
|
|
3724
4618
|
}
|
|
@@ -3831,7 +4725,7 @@ function rowToSource(row) {
|
|
|
3831
4725
|
function addProjectSource(input, db) {
|
|
3832
4726
|
const d = db || getDatabase();
|
|
3833
4727
|
const id = uuid();
|
|
3834
|
-
const
|
|
4728
|
+
const timestamp2 = now();
|
|
3835
4729
|
d.run(`INSERT INTO project_sources (id, project_id, type, name, uri, description, metadata, created_at, updated_at)
|
|
3836
4730
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3837
4731
|
id,
|
|
@@ -3841,8 +4735,8 @@ function addProjectSource(input, db) {
|
|
|
3841
4735
|
input.uri,
|
|
3842
4736
|
input.description || null,
|
|
3843
4737
|
JSON.stringify(input.metadata || {}),
|
|
3844
|
-
|
|
3845
|
-
|
|
4738
|
+
timestamp2,
|
|
4739
|
+
timestamp2
|
|
3846
4740
|
]);
|
|
3847
4741
|
return rowToSource(d.query("SELECT * FROM project_sources WHERE id = ?").get(id));
|
|
3848
4742
|
}
|
|
@@ -4060,6 +4954,11 @@ function customPatterns() {
|
|
|
4060
4954
|
function secretPatterns() {
|
|
4061
4955
|
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
4062
4956
|
}
|
|
4957
|
+
function isRedactionPlaceholderMatch(match) {
|
|
4958
|
+
const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
4959
|
+
const trimmed = match.trim();
|
|
4960
|
+
return new RegExp(`^${placeholder}$`).test(trimmed) || new RegExp(`=\\s*['"]?${placeholder}['"]?$`).test(trimmed);
|
|
4961
|
+
}
|
|
4063
4962
|
function isSecretKey(key) {
|
|
4064
4963
|
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
4065
4964
|
return false;
|
|
@@ -4097,7 +4996,7 @@ function redactValue(value) {
|
|
|
4097
4996
|
function listSecretFindings(value) {
|
|
4098
4997
|
const findings = [];
|
|
4099
4998
|
for (const pattern of secretPatterns()) {
|
|
4100
|
-
const matches = value.match(cloneRegex(pattern.regex));
|
|
4999
|
+
const matches = value.match(cloneRegex(pattern.regex))?.filter((match) => !isRedactionPlaceholderMatch(match));
|
|
4101
5000
|
if (matches?.length)
|
|
4102
5001
|
findings.push({ pattern: pattern.name, count: matches.length });
|
|
4103
5002
|
}
|
|
@@ -4128,6 +5027,9 @@ var init_redaction = __esm(() => {
|
|
|
4128
5027
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
4129
5028
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
4130
5029
|
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
5030
|
+
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
5031
|
+
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
5032
|
+
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
4131
5033
|
{ name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
|
|
4132
5034
|
{ name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
|
|
4133
5035
|
];
|
|
@@ -4202,7 +5104,7 @@ function upsertWorkspaceTrustProfile(input) {
|
|
|
4202
5104
|
const preset = input.preset || existing?.preset || "standard";
|
|
4203
5105
|
const presetChanged = Boolean(existing && input.preset && input.preset !== existing.preset);
|
|
4204
5106
|
const base = presetChanged ? defaultProfile(root, preset) : existing || defaultProfile(root, preset);
|
|
4205
|
-
const
|
|
5107
|
+
const timestamp2 = new Date().toISOString();
|
|
4206
5108
|
const profile = {
|
|
4207
5109
|
...base,
|
|
4208
5110
|
...PRESET_DEFAULTS[preset],
|
|
@@ -4215,8 +5117,8 @@ function upsertWorkspaceTrustProfile(input) {
|
|
|
4215
5117
|
write_scopes: unique2(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
|
|
4216
5118
|
env_redactions: unique2(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].env_redactions),
|
|
4217
5119
|
require_prompt_for_unsafe: input.require_prompt_for_unsafe ?? base.require_prompt_for_unsafe ?? PRESET_DEFAULTS[preset].require_prompt_for_unsafe,
|
|
4218
|
-
created_at: existing?.created_at ||
|
|
4219
|
-
updated_at:
|
|
5120
|
+
created_at: existing?.created_at || timestamp2,
|
|
5121
|
+
updated_at: timestamp2
|
|
4220
5122
|
};
|
|
4221
5123
|
saveConfig({
|
|
4222
5124
|
...config,
|
|
@@ -4417,7 +5319,7 @@ function upsertRunnerSandboxProfile(input) {
|
|
|
4417
5319
|
const existing = config.runner_sandboxes?.[input.name];
|
|
4418
5320
|
const root = normalizePath2(input.root || existing?.root || process.cwd());
|
|
4419
5321
|
const base = existing || defaultProfile2(input.name, root);
|
|
4420
|
-
const
|
|
5322
|
+
const timestamp2 = new Date().toISOString();
|
|
4421
5323
|
const profile = {
|
|
4422
5324
|
...base,
|
|
4423
5325
|
name: input.name,
|
|
@@ -4431,8 +5333,8 @@ function upsertRunnerSandboxProfile(input) {
|
|
|
4431
5333
|
network_policy: input.network_policy || base.network_policy,
|
|
4432
5334
|
require_approval: input.require_approval ?? base.require_approval,
|
|
4433
5335
|
audit_evidence: input.audit_evidence ?? base.audit_evidence,
|
|
4434
|
-
created_at: existing?.created_at ||
|
|
4435
|
-
updated_at:
|
|
5336
|
+
created_at: existing?.created_at || timestamp2,
|
|
5337
|
+
updated_at: timestamp2
|
|
4436
5338
|
};
|
|
4437
5339
|
saveConfig({
|
|
4438
5340
|
...config,
|
|
@@ -4549,7 +5451,7 @@ function normalizeHook(input, existing) {
|
|
|
4549
5451
|
throw new Error("socket event hooks require socket_path");
|
|
4550
5452
|
if (input.target === "script" && !input.command && !existing?.command)
|
|
4551
5453
|
throw new Error("script event hooks require command");
|
|
4552
|
-
const
|
|
5454
|
+
const timestamp2 = new Date().toISOString();
|
|
4553
5455
|
return {
|
|
4554
5456
|
...existing,
|
|
4555
5457
|
name: safeName(input.name),
|
|
@@ -4566,8 +5468,8 @@ function normalizeHook(input, existing) {
|
|
|
4566
5468
|
attempts: clampAttempts(input.retry?.attempts ?? existing?.retry?.attempts ?? 1),
|
|
4567
5469
|
backoff_ms: Math.max(0, input.retry?.backoff_ms ?? existing?.retry?.backoff_ms ?? 0)
|
|
4568
5470
|
},
|
|
4569
|
-
created_at: existing?.created_at ||
|
|
4570
|
-
updated_at:
|
|
5471
|
+
created_at: existing?.created_at || timestamp2,
|
|
5472
|
+
updated_at: timestamp2
|
|
4571
5473
|
};
|
|
4572
5474
|
}
|
|
4573
5475
|
function clampAttempts(value) {
|
|
@@ -4581,11 +5483,11 @@ function eventMatches(hook, eventType) {
|
|
|
4581
5483
|
function canonicalEvent(input) {
|
|
4582
5484
|
return JSON.stringify(input);
|
|
4583
5485
|
}
|
|
4584
|
-
function buildEnvelope(type, payload,
|
|
5486
|
+
function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
|
|
4585
5487
|
const base = {
|
|
4586
5488
|
id: randomUUID(),
|
|
4587
5489
|
type,
|
|
4588
|
-
timestamp,
|
|
5490
|
+
timestamp: timestamp2,
|
|
4589
5491
|
payload: redactValue(payload ?? {}),
|
|
4590
5492
|
source: { package: "@hasna/todos", local_only: true }
|
|
4591
5493
|
};
|
|
@@ -4999,11 +5901,11 @@ class JsonEventsStore {
|
|
|
4999
5901
|
});
|
|
5000
5902
|
}
|
|
5001
5903
|
}
|
|
5002
|
-
function buildSignatureBase(
|
|
5003
|
-
return `${
|
|
5904
|
+
function buildSignatureBase(timestamp2, body) {
|
|
5905
|
+
return `${timestamp2}.${body}`;
|
|
5004
5906
|
}
|
|
5005
|
-
function signPayload(secret,
|
|
5006
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(
|
|
5907
|
+
function signPayload(secret, timestamp2, body) {
|
|
5908
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
|
|
5007
5909
|
return `sha256=${digest}`;
|
|
5008
5910
|
}
|
|
5009
5911
|
function now2() {
|
|
@@ -5016,17 +5918,17 @@ function buildWebhookRequest(event, channel) {
|
|
|
5016
5918
|
if (!channel.webhook)
|
|
5017
5919
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
5018
5920
|
const body = JSON.stringify(event);
|
|
5019
|
-
const
|
|
5921
|
+
const timestamp2 = event.time;
|
|
5020
5922
|
const headers = {
|
|
5021
5923
|
"Content-Type": "application/json",
|
|
5022
5924
|
"User-Agent": "@hasna/events",
|
|
5023
5925
|
"X-Hasna-Event-Id": event.id,
|
|
5024
5926
|
"X-Hasna-Event-Type": event.type,
|
|
5025
|
-
"X-Hasna-Timestamp":
|
|
5927
|
+
"X-Hasna-Timestamp": timestamp2,
|
|
5026
5928
|
...channel.webhook.headers
|
|
5027
5929
|
};
|
|
5028
5930
|
if (channel.webhook.secret) {
|
|
5029
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret,
|
|
5931
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
|
|
5030
5932
|
}
|
|
5031
5933
|
return { body, headers };
|
|
5032
5934
|
}
|
|
@@ -5180,11 +6082,11 @@ class EventsClient {
|
|
|
5180
6082
|
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
5181
6083
|
}
|
|
5182
6084
|
async addChannel(input) {
|
|
5183
|
-
const
|
|
6085
|
+
const timestamp2 = new Date().toISOString();
|
|
5184
6086
|
return this.store.addChannel({
|
|
5185
6087
|
...input,
|
|
5186
|
-
createdAt: input.createdAt ??
|
|
5187
|
-
updatedAt: input.updatedAt ??
|
|
6088
|
+
createdAt: input.createdAt ?? timestamp2,
|
|
6089
|
+
updatedAt: input.updatedAt ?? timestamp2
|
|
5188
6090
|
});
|
|
5189
6091
|
}
|
|
5190
6092
|
async listChannels() {
|
|
@@ -5256,12 +6158,12 @@ class EventsClient {
|
|
|
5256
6158
|
const match = await this.matchChannel(id, input);
|
|
5257
6159
|
const event = match.event;
|
|
5258
6160
|
if (options.honorFilters && !match.matched) {
|
|
5259
|
-
const
|
|
6161
|
+
const timestamp2 = new Date().toISOString();
|
|
5260
6162
|
const result2 = createDeliveryResult(event, channel, [{
|
|
5261
6163
|
attempt: 1,
|
|
5262
6164
|
status: "skipped",
|
|
5263
|
-
startedAt:
|
|
5264
|
-
completedAt:
|
|
6165
|
+
startedAt: timestamp2,
|
|
6166
|
+
completedAt: timestamp2,
|
|
5265
6167
|
error: match.reason
|
|
5266
6168
|
}]);
|
|
5267
6169
|
result2.metadata = { reason: "filter_mismatch" };
|
|
@@ -5382,7 +6284,7 @@ function createTaskList(input, db) {
|
|
|
5382
6284
|
const d = db || getDatabase();
|
|
5383
6285
|
return d.transaction(() => {
|
|
5384
6286
|
const id = uuid();
|
|
5385
|
-
const
|
|
6287
|
+
const timestamp2 = now();
|
|
5386
6288
|
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
5387
6289
|
if (!slug)
|
|
5388
6290
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
@@ -5393,7 +6295,7 @@ function createTaskList(input, db) {
|
|
|
5393
6295
|
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
5394
6296
|
}
|
|
5395
6297
|
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
|
|
5396
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}),
|
|
6298
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp2, timestamp2, machineId]);
|
|
5397
6299
|
return getTaskList(id, d);
|
|
5398
6300
|
})();
|
|
5399
6301
|
}
|
|
@@ -5866,6 +6768,110 @@ var init_secret_redaction = __esm(() => {
|
|
|
5866
6768
|
customRedactors = [];
|
|
5867
6769
|
});
|
|
5868
6770
|
|
|
6771
|
+
// src/lib/prewrite-secrets.ts
|
|
6772
|
+
function mergeFindings(findings) {
|
|
6773
|
+
const byPattern = new Map;
|
|
6774
|
+
for (const finding of findings) {
|
|
6775
|
+
const existing = byPattern.get(finding.pattern);
|
|
6776
|
+
if (!existing) {
|
|
6777
|
+
byPattern.set(finding.pattern, {
|
|
6778
|
+
pattern: finding.pattern,
|
|
6779
|
+
count: finding.count,
|
|
6780
|
+
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
6781
|
+
});
|
|
6782
|
+
continue;
|
|
6783
|
+
}
|
|
6784
|
+
existing.count += finding.count;
|
|
6785
|
+
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
6786
|
+
}
|
|
6787
|
+
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
6788
|
+
}
|
|
6789
|
+
function redactPreWriteText(value) {
|
|
6790
|
+
return redactText(redactEvidenceText(value));
|
|
6791
|
+
}
|
|
6792
|
+
function scanPreWriteText(value, context = "text") {
|
|
6793
|
+
const redacted = redactPreWriteText(value);
|
|
6794
|
+
if (redacted === value) {
|
|
6795
|
+
return {
|
|
6796
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
6797
|
+
clean: true,
|
|
6798
|
+
context,
|
|
6799
|
+
findings: []
|
|
6800
|
+
};
|
|
6801
|
+
}
|
|
6802
|
+
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
6803
|
+
pattern: match.pattern,
|
|
6804
|
+
count: 1,
|
|
6805
|
+
lines: match.line ? [match.line] : []
|
|
6806
|
+
}));
|
|
6807
|
+
const configured = listSecretFindings(value).map((finding) => ({
|
|
6808
|
+
pattern: finding.pattern,
|
|
6809
|
+
count: finding.count,
|
|
6810
|
+
lines: []
|
|
6811
|
+
}));
|
|
6812
|
+
return {
|
|
6813
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
6814
|
+
clean: false,
|
|
6815
|
+
context,
|
|
6816
|
+
findings: mergeFindings([...structural, ...configured])
|
|
6817
|
+
};
|
|
6818
|
+
}
|
|
6819
|
+
function assertPreWriteTextClean(value, context = "text") {
|
|
6820
|
+
const scan = scanPreWriteText(value, context);
|
|
6821
|
+
if (!scan.clean)
|
|
6822
|
+
throw new PreWriteSecretError(context, scan.findings);
|
|
6823
|
+
}
|
|
6824
|
+
function sanitizePreWriteText(value, context = "text", options = {}) {
|
|
6825
|
+
const mode = options.mode ?? "redact";
|
|
6826
|
+
if (mode === "block") {
|
|
6827
|
+
assertPreWriteTextClean(value, context);
|
|
6828
|
+
return value;
|
|
6829
|
+
}
|
|
6830
|
+
return redactPreWriteText(value);
|
|
6831
|
+
}
|
|
6832
|
+
function sanitizePreWriteValue(value, context = "value", options = {}) {
|
|
6833
|
+
if (typeof value === "string")
|
|
6834
|
+
return sanitizePreWriteText(value, context, options);
|
|
6835
|
+
if (Array.isArray(value)) {
|
|
6836
|
+
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
6837
|
+
}
|
|
6838
|
+
if (value && typeof value === "object") {
|
|
6839
|
+
const sanitized = {};
|
|
6840
|
+
for (const [key, child] of Object.entries(value)) {
|
|
6841
|
+
const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
|
|
6842
|
+
sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
|
|
6843
|
+
}
|
|
6844
|
+
return redactValue(sanitized);
|
|
6845
|
+
}
|
|
6846
|
+
return value;
|
|
6847
|
+
}
|
|
6848
|
+
function sanitizePreWriteJsonString(value, context = "json") {
|
|
6849
|
+
if (value === undefined || value === null)
|
|
6850
|
+
return null;
|
|
6851
|
+
try {
|
|
6852
|
+
return JSON.stringify(sanitizePreWriteValue(JSON.parse(value), context));
|
|
6853
|
+
} catch {
|
|
6854
|
+
return sanitizePreWriteText(value, context);
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
|
|
6858
|
+
var init_prewrite_secrets = __esm(() => {
|
|
6859
|
+
init_redaction();
|
|
6860
|
+
init_secret_redaction();
|
|
6861
|
+
PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
|
|
6862
|
+
PreWriteSecretError = class PreWriteSecretError extends Error {
|
|
6863
|
+
context;
|
|
6864
|
+
findings;
|
|
6865
|
+
constructor(context, findings) {
|
|
6866
|
+
const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
|
|
6867
|
+
super(`Secret pattern detected before persistence in ${context}: ${summary}`);
|
|
6868
|
+
this.name = "PreWriteSecretError";
|
|
6869
|
+
this.context = context;
|
|
6870
|
+
this.findings = findings;
|
|
6871
|
+
}
|
|
6872
|
+
};
|
|
6873
|
+
});
|
|
6874
|
+
|
|
5869
6875
|
// src/lib/activity-audit.ts
|
|
5870
6876
|
var exports_activity_audit = {};
|
|
5871
6877
|
__export(exports_activity_audit, {
|
|
@@ -5914,6 +6920,9 @@ function logActivity(input, db) {
|
|
|
5914
6920
|
const id = uuid();
|
|
5915
6921
|
const ts = input.created_at ?? now();
|
|
5916
6922
|
const machineId = input.machine_id ?? getMachineId2();
|
|
6923
|
+
const oldValue = input.old_value == null ? null : sanitizePreWriteText(input.old_value, "activity.old_value");
|
|
6924
|
+
const newValue = input.new_value == null ? null : sanitizePreWriteText(input.new_value, "activity.new_value");
|
|
6925
|
+
const metadata = sanitizePreWriteValue(input.metadata ?? {}, "activity.metadata");
|
|
5917
6926
|
d.run(`INSERT INTO activity_log (
|
|
5918
6927
|
id, entity_type, entity_id, action, field, old_value, new_value,
|
|
5919
6928
|
actor_id, session_id, machine_id, metadata, created_at
|
|
@@ -5923,12 +6932,12 @@ function logActivity(input, db) {
|
|
|
5923
6932
|
input.entity_id,
|
|
5924
6933
|
input.action,
|
|
5925
6934
|
input.field ?? null,
|
|
5926
|
-
|
|
5927
|
-
|
|
6935
|
+
oldValue,
|
|
6936
|
+
newValue,
|
|
5928
6937
|
input.actor_id ?? null,
|
|
5929
6938
|
input.session_id ?? null,
|
|
5930
6939
|
machineId,
|
|
5931
|
-
JSON.stringify(
|
|
6940
|
+
JSON.stringify(metadata),
|
|
5932
6941
|
ts
|
|
5933
6942
|
]);
|
|
5934
6943
|
return getActivityRecord(id, d);
|
|
@@ -6039,6 +7048,7 @@ var ACTIVITY_LOG_SCHEMA = "todos.activity_log.v1", ACTIVITY_ENTITY_TYPES;
|
|
|
6039
7048
|
var init_activity_audit = __esm(() => {
|
|
6040
7049
|
init_database();
|
|
6041
7050
|
init_secret_redaction();
|
|
7051
|
+
init_prewrite_secrets();
|
|
6042
7052
|
ACTIVITY_ENTITY_TYPES = [
|
|
6043
7053
|
"task",
|
|
6044
7054
|
"project",
|
|
@@ -6051,13 +7061,18 @@ var init_activity_audit = __esm(() => {
|
|
|
6051
7061
|
});
|
|
6052
7062
|
|
|
6053
7063
|
// src/db/audit.ts
|
|
7064
|
+
function sanitizeHistoryValue(value, context) {
|
|
7065
|
+
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
7066
|
+
}
|
|
6054
7067
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
6055
7068
|
const d = db || getDatabase();
|
|
6056
7069
|
const id = uuid();
|
|
6057
|
-
const
|
|
7070
|
+
const timestamp2 = now();
|
|
6058
7071
|
const machineId = currentStorageMachineId(d);
|
|
7072
|
+
const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
|
|
7073
|
+
const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
|
|
6059
7074
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
6060
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null,
|
|
7075
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
|
|
6061
7076
|
try {
|
|
6062
7077
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
6063
7078
|
logActivity2({
|
|
@@ -6065,12 +7080,12 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
|
6065
7080
|
entity_id: taskId,
|
|
6066
7081
|
action,
|
|
6067
7082
|
field,
|
|
6068
|
-
old_value:
|
|
6069
|
-
new_value:
|
|
7083
|
+
old_value: safeOldValue,
|
|
7084
|
+
new_value: safeNewValue,
|
|
6070
7085
|
actor_id: agentId ?? undefined
|
|
6071
7086
|
}, d);
|
|
6072
7087
|
} catch {}
|
|
6073
|
-
return { id, task_id: taskId, action, field: field || null, old_value:
|
|
7088
|
+
return { id, task_id: taskId, action, field: field || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
|
|
6074
7089
|
}
|
|
6075
7090
|
function getTaskHistory(taskId, db) {
|
|
6076
7091
|
const d = db || getDatabase();
|
|
@@ -6109,6 +7124,7 @@ function getRecap(hours = 8, projectId, db) {
|
|
|
6109
7124
|
var init_audit = __esm(() => {
|
|
6110
7125
|
init_database();
|
|
6111
7126
|
init_storage_tombstones();
|
|
7127
|
+
init_prewrite_secrets();
|
|
6112
7128
|
});
|
|
6113
7129
|
|
|
6114
7130
|
// src/db/webhooks.ts
|
|
@@ -6341,27 +7357,27 @@ function getChecklist(taskId, db) {
|
|
|
6341
7357
|
function addChecklistItem(input, db) {
|
|
6342
7358
|
const d = db || getDatabase();
|
|
6343
7359
|
const id = uuid();
|
|
6344
|
-
const
|
|
7360
|
+
const timestamp2 = now();
|
|
6345
7361
|
let position = input.position;
|
|
6346
7362
|
if (position === undefined) {
|
|
6347
7363
|
const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
|
|
6348
7364
|
position = (maxRow?.max_pos ?? -1) + 1;
|
|
6349
7365
|
}
|
|
6350
|
-
d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text,
|
|
7366
|
+
d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp2, timestamp2]);
|
|
6351
7367
|
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
6352
7368
|
}
|
|
6353
7369
|
function checkChecklistItem(id, checked, db) {
|
|
6354
7370
|
const d = db || getDatabase();
|
|
6355
|
-
const
|
|
6356
|
-
const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0,
|
|
7371
|
+
const timestamp2 = now();
|
|
7372
|
+
const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp2, id]);
|
|
6357
7373
|
if (result.changes === 0)
|
|
6358
7374
|
return null;
|
|
6359
7375
|
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
6360
7376
|
}
|
|
6361
7377
|
function updateChecklistItemText(id, text, db) {
|
|
6362
7378
|
const d = db || getDatabase();
|
|
6363
|
-
const
|
|
6364
|
-
const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text,
|
|
7379
|
+
const timestamp2 = now();
|
|
7380
|
+
const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp2, id]);
|
|
6365
7381
|
if (result.changes === 0)
|
|
6366
7382
|
return null;
|
|
6367
7383
|
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
@@ -6483,6 +7499,50 @@ var init_recurrence = __esm(() => {
|
|
|
6483
7499
|
};
|
|
6484
7500
|
});
|
|
6485
7501
|
|
|
7502
|
+
// src/lib/template-semantics.ts
|
|
7503
|
+
function resolveTemplateVariables(templateVars, provided) {
|
|
7504
|
+
const merged = { ...provided };
|
|
7505
|
+
for (const variable of templateVars) {
|
|
7506
|
+
if (merged[variable.name] === undefined && variable.default !== undefined) {
|
|
7507
|
+
merged[variable.name] = variable.default;
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7510
|
+
const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
|
|
7511
|
+
if (missing.length > 0) {
|
|
7512
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
7513
|
+
}
|
|
7514
|
+
return merged;
|
|
7515
|
+
}
|
|
7516
|
+
function substituteTemplateVariables(text, variables) {
|
|
7517
|
+
let result = text;
|
|
7518
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
7519
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
7520
|
+
}
|
|
7521
|
+
return result;
|
|
7522
|
+
}
|
|
7523
|
+
function evaluateTemplateCondition(condition, variables) {
|
|
7524
|
+
if (!condition || condition.trim() === "")
|
|
7525
|
+
return true;
|
|
7526
|
+
const trimmed = condition.trim();
|
|
7527
|
+
const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
7528
|
+
if (equal)
|
|
7529
|
+
return (variables[equal[1]] ?? "") === equal[2].trim();
|
|
7530
|
+
const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
7531
|
+
if (unequal)
|
|
7532
|
+
return (variables[unequal[1]] ?? "") !== unequal[2].trim();
|
|
7533
|
+
const falsy = trimmed.match(/^!\{([^}]+)\}$/);
|
|
7534
|
+
if (falsy) {
|
|
7535
|
+
const value = variables[falsy[1]];
|
|
7536
|
+
return !value || value === "" || value === "false";
|
|
7537
|
+
}
|
|
7538
|
+
const truthy = trimmed.match(/^\{([^}]+)\}$/);
|
|
7539
|
+
if (truthy) {
|
|
7540
|
+
const value = variables[truthy[1]];
|
|
7541
|
+
return !!value && value !== "" && value !== "false";
|
|
7542
|
+
}
|
|
7543
|
+
return true;
|
|
7544
|
+
}
|
|
7545
|
+
|
|
6486
7546
|
// src/db/templates.ts
|
|
6487
7547
|
function rowToTemplate(row) {
|
|
6488
7548
|
return {
|
|
@@ -6558,6 +7618,14 @@ function deleteTemplate(id, db) {
|
|
|
6558
7618
|
payload: template,
|
|
6559
7619
|
version: template.version
|
|
6560
7620
|
}, d);
|
|
7621
|
+
for (const task of getTemplateTasks(resolved, d)) {
|
|
7622
|
+
recordStorageTombstone({
|
|
7623
|
+
object_type: "template_tasks",
|
|
7624
|
+
object_id: task.id,
|
|
7625
|
+
payload: task,
|
|
7626
|
+
version: 1
|
|
7627
|
+
}, d);
|
|
7628
|
+
}
|
|
6561
7629
|
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
6562
7630
|
}
|
|
6563
7631
|
function updateTemplate(id, updates, db) {
|
|
@@ -6689,34 +7757,7 @@ function getTemplateTasks(templateId, db) {
|
|
|
6689
7757
|
return rows.map(rowToTemplateTask);
|
|
6690
7758
|
}
|
|
6691
7759
|
function evaluateCondition(condition, variables) {
|
|
6692
|
-
|
|
6693
|
-
return true;
|
|
6694
|
-
const trimmed = condition.trim();
|
|
6695
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
6696
|
-
if (eqMatch) {
|
|
6697
|
-
const varName = eqMatch[1];
|
|
6698
|
-
const expected = eqMatch[2].trim();
|
|
6699
|
-
return (variables[varName] ?? "") === expected;
|
|
6700
|
-
}
|
|
6701
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
6702
|
-
if (neqMatch) {
|
|
6703
|
-
const varName = neqMatch[1];
|
|
6704
|
-
const expected = neqMatch[2].trim();
|
|
6705
|
-
return (variables[varName] ?? "") !== expected;
|
|
6706
|
-
}
|
|
6707
|
-
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
6708
|
-
if (falsyMatch) {
|
|
6709
|
-
const varName = falsyMatch[1];
|
|
6710
|
-
const val = variables[varName];
|
|
6711
|
-
return !val || val === "" || val === "false";
|
|
6712
|
-
}
|
|
6713
|
-
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
6714
|
-
if (truthyMatch) {
|
|
6715
|
-
const varName = truthyMatch[1];
|
|
6716
|
-
const val = variables[varName];
|
|
6717
|
-
return !!val && val !== "" && val !== "false";
|
|
6718
|
-
}
|
|
6719
|
-
return true;
|
|
7760
|
+
return evaluateTemplateCondition(condition, variables);
|
|
6720
7761
|
}
|
|
6721
7762
|
function exportTemplate(id, db) {
|
|
6722
7763
|
const d = db || getDatabase();
|
|
@@ -6789,29 +7830,10 @@ function listTemplateVersions(id, db) {
|
|
|
6789
7830
|
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
6790
7831
|
}
|
|
6791
7832
|
function resolveVariables(templateVars, provided) {
|
|
6792
|
-
|
|
6793
|
-
for (const v of templateVars) {
|
|
6794
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
6795
|
-
merged[v.name] = v.default;
|
|
6796
|
-
}
|
|
6797
|
-
}
|
|
6798
|
-
const missing = [];
|
|
6799
|
-
for (const v of templateVars) {
|
|
6800
|
-
if (v.required && merged[v.name] === undefined) {
|
|
6801
|
-
missing.push(v.name);
|
|
6802
|
-
}
|
|
6803
|
-
}
|
|
6804
|
-
if (missing.length > 0) {
|
|
6805
|
-
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
6806
|
-
}
|
|
6807
|
-
return merged;
|
|
7833
|
+
return resolveTemplateVariables(templateVars, provided);
|
|
6808
7834
|
}
|
|
6809
7835
|
function substituteVars(text, variables) {
|
|
6810
|
-
|
|
6811
|
-
for (const [key, val] of Object.entries(variables)) {
|
|
6812
|
-
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
6813
|
-
}
|
|
6814
|
-
return result;
|
|
7836
|
+
return substituteTemplateVariables(text, variables);
|
|
6815
7837
|
}
|
|
6816
7838
|
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
6817
7839
|
const d = db || getDatabase();
|
|
@@ -7133,9 +8155,9 @@ function startTask(id, agentId, db) {
|
|
|
7133
8155
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
7134
8156
|
}
|
|
7135
8157
|
const cutoff = lockExpiryCutoff();
|
|
7136
|
-
const
|
|
8158
|
+
const timestamp2 = now();
|
|
7137
8159
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
7138
|
-
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId,
|
|
8160
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp2, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
7139
8161
|
if (result.changes === 0) {
|
|
7140
8162
|
const current = getTask(id, d);
|
|
7141
8163
|
if (!current)
|
|
@@ -7147,7 +8169,7 @@ function startTask(id, agentId, db) {
|
|
|
7147
8169
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
7148
8170
|
}
|
|
7149
8171
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
7150
|
-
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at:
|
|
8172
|
+
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp2, started_at: task.started_at || timestamp2, version: task.version + 1, updated_at: timestamp2 };
|
|
7151
8173
|
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
7152
8174
|
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
7153
8175
|
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
@@ -7170,7 +8192,13 @@ function completeTask(id, agentId, db, options) {
|
|
|
7170
8192
|
throw new LockError(id, task.locked_by);
|
|
7171
8193
|
}
|
|
7172
8194
|
checkCompletionGuard(task, agentId || null, d);
|
|
7173
|
-
const evidence = options ? {
|
|
8195
|
+
const evidence = options ? sanitizePreWriteValue({
|
|
8196
|
+
files_changed: options.files_changed,
|
|
8197
|
+
test_results: options.test_results,
|
|
8198
|
+
commit_hash: options.commit_hash,
|
|
8199
|
+
notes: options.notes,
|
|
8200
|
+
attachment_ids: options.attachment_ids
|
|
8201
|
+
}, "task.completion_evidence") : undefined;
|
|
7174
8202
|
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
7175
8203
|
const completionMeta = {};
|
|
7176
8204
|
if (hasEvidence)
|
|
@@ -7179,21 +8207,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
7179
8207
|
completionMeta._completion = { confidence: options.confidence };
|
|
7180
8208
|
}
|
|
7181
8209
|
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
7182
|
-
const
|
|
8210
|
+
const timestamp2 = options?.completed_at || now();
|
|
7183
8211
|
const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
|
|
7184
8212
|
const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
|
|
7185
8213
|
const finalVersion = versionBeforeStatus + 1;
|
|
7186
8214
|
const tx = d.transaction(() => {
|
|
7187
8215
|
if (hasMeta) {
|
|
7188
|
-
const meta2 = { ...task.metadata, ...completionMeta };
|
|
7189
|
-
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2),
|
|
8216
|
+
const meta2 = sanitizePreWriteValue({ ...task.metadata, ...completionMeta }, "task.metadata");
|
|
8217
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp2, id, task.version]);
|
|
7190
8218
|
if (metaResult.changes === 0) {
|
|
7191
8219
|
const current = getTask(id, d);
|
|
7192
8220
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
7193
8221
|
}
|
|
7194
8222
|
}
|
|
7195
8223
|
const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
7196
|
-
WHERE id = ? AND version = ?`, [
|
|
8224
|
+
WHERE id = ? AND version = ?`, [timestamp2, confidence, timestamp2, id, versionBeforeStatus]);
|
|
7197
8225
|
if (statusResult.changes === 0) {
|
|
7198
8226
|
const current = getTask(id, d);
|
|
7199
8227
|
throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
|
|
@@ -7206,20 +8234,20 @@ function completeTask(id, agentId, db, options) {
|
|
|
7206
8234
|
status: "completed",
|
|
7207
8235
|
locked_by: null,
|
|
7208
8236
|
locked_at: null,
|
|
7209
|
-
completed_at:
|
|
8237
|
+
completed_at: timestamp2,
|
|
7210
8238
|
confidence,
|
|
7211
8239
|
version: finalVersion,
|
|
7212
|
-
updated_at:
|
|
8240
|
+
updated_at: timestamp2,
|
|
7213
8241
|
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
7214
8242
|
};
|
|
7215
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at:
|
|
8243
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp2 });
|
|
7216
8244
|
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7217
8245
|
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
7218
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at:
|
|
8246
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp2 }, databasePath });
|
|
7219
8247
|
let spawnedTask = null;
|
|
7220
8248
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7221
8249
|
try {
|
|
7222
|
-
spawnedTask = spawnNextRecurrence(task, d,
|
|
8250
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp2);
|
|
7223
8251
|
} catch (e) {
|
|
7224
8252
|
spawnedTask = null;
|
|
7225
8253
|
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -7269,7 +8297,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
7269
8297
|
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
7270
8298
|
}
|
|
7271
8299
|
}
|
|
7272
|
-
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at:
|
|
8300
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp2, confidence, version: finalVersion, updated_at: timestamp2, metadata: meta };
|
|
7273
8301
|
}
|
|
7274
8302
|
function lockTask(id, agentId, db) {
|
|
7275
8303
|
const d = db || getDatabase();
|
|
@@ -7283,15 +8311,15 @@ function lockTask(id, agentId, db) {
|
|
|
7283
8311
|
};
|
|
7284
8312
|
}
|
|
7285
8313
|
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
7286
|
-
const
|
|
7287
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [
|
|
8314
|
+
const timestamp3 = now();
|
|
8315
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp3, timestamp3, id, agentId]);
|
|
7288
8316
|
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
7289
|
-
return { success: true, locked_by: agentId, locked_at:
|
|
8317
|
+
return { success: true, locked_by: agentId, locked_at: timestamp3, expires_at: lockExpiresAt(timestamp3) };
|
|
7290
8318
|
}
|
|
7291
8319
|
const cutoff = lockExpiryCutoff();
|
|
7292
|
-
const
|
|
8320
|
+
const timestamp2 = now();
|
|
7293
8321
|
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
7294
|
-
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId,
|
|
8322
|
+
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
7295
8323
|
if (result.changes === 0) {
|
|
7296
8324
|
const current = getTask(id, d);
|
|
7297
8325
|
if (!current)
|
|
@@ -7316,7 +8344,7 @@ function lockTask(id, agentId, db) {
|
|
|
7316
8344
|
};
|
|
7317
8345
|
}
|
|
7318
8346
|
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
7319
|
-
return { success: true, locked_by: agentId, locked_at:
|
|
8347
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
7320
8348
|
}
|
|
7321
8349
|
function unlockTask(id, agentId, db) {
|
|
7322
8350
|
const d = db || getDatabase();
|
|
@@ -7326,9 +8354,9 @@ function unlockTask(id, agentId, db) {
|
|
|
7326
8354
|
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
7327
8355
|
throw new LockError(id, task.locked_by);
|
|
7328
8356
|
}
|
|
7329
|
-
const
|
|
8357
|
+
const timestamp2 = now();
|
|
7330
8358
|
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
7331
|
-
WHERE id = ?`, [
|
|
8359
|
+
WHERE id = ?`, [timestamp2, id]);
|
|
7332
8360
|
return true;
|
|
7333
8361
|
}
|
|
7334
8362
|
function getTaskLockStatus(id, db) {
|
|
@@ -7410,7 +8438,7 @@ function getNextTask(agentId, filters, db) {
|
|
|
7410
8438
|
}
|
|
7411
8439
|
sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
|
|
7412
8440
|
const row = d.query(sql).get(...params);
|
|
7413
|
-
return row ?
|
|
8441
|
+
return row ? rowToTask2(row) : null;
|
|
7414
8442
|
}
|
|
7415
8443
|
function getActiveWork(filters, db) {
|
|
7416
8444
|
const d = db || getDatabase();
|
|
@@ -7445,7 +8473,7 @@ function getTasksChangedSince(since, filters, db) {
|
|
|
7445
8473
|
}
|
|
7446
8474
|
const where = conditions.join(" AND ");
|
|
7447
8475
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
7448
|
-
return rows.map(
|
|
8476
|
+
return rows.map(rowToTask2);
|
|
7449
8477
|
}
|
|
7450
8478
|
function failTask(id, agentId, reason, options, db) {
|
|
7451
8479
|
const d = db || getDatabase();
|
|
@@ -7453,20 +8481,23 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7453
8481
|
const task = getTask(id, d);
|
|
7454
8482
|
if (!task)
|
|
7455
8483
|
throw new TaskNotFoundError(id);
|
|
8484
|
+
const safeReason = sanitizePreWriteText(reason || "Unknown failure", "task.failure.reason");
|
|
8485
|
+
const safeErrorCode = options?.error_code ? sanitizePreWriteText(options.error_code, "task.failure.error_code") : null;
|
|
7456
8486
|
const meta = {
|
|
7457
8487
|
...task.metadata,
|
|
7458
8488
|
_failure: {
|
|
7459
|
-
reason:
|
|
7460
|
-
error_code:
|
|
8489
|
+
reason: safeReason,
|
|
8490
|
+
error_code: safeErrorCode,
|
|
7461
8491
|
failed_by: agentId || null,
|
|
7462
8492
|
failed_at: now(),
|
|
7463
8493
|
retry_requested: options?.retry || false
|
|
7464
8494
|
}
|
|
7465
8495
|
};
|
|
7466
|
-
const
|
|
8496
|
+
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
8497
|
+
const timestamp2 = now();
|
|
7467
8498
|
const failTx = d.transaction(() => {
|
|
7468
8499
|
const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
7469
|
-
WHERE id = ? AND version = ?`, [JSON.stringify(
|
|
8500
|
+
WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
7470
8501
|
if (res.changes === 0) {
|
|
7471
8502
|
const current = getTask(id, d);
|
|
7472
8503
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -7478,22 +8509,22 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7478
8509
|
status: "failed",
|
|
7479
8510
|
locked_by: null,
|
|
7480
8511
|
locked_at: null,
|
|
7481
|
-
metadata:
|
|
8512
|
+
metadata: safeMeta,
|
|
7482
8513
|
version: task.version + 1,
|
|
7483
|
-
updated_at:
|
|
8514
|
+
updated_at: timestamp2
|
|
7484
8515
|
};
|
|
7485
8516
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
7486
|
-
const failurePayload = taskEventData(failedTask, { reason, error_code:
|
|
8517
|
+
const failurePayload = taskEventData(failedTask, { reason: safeReason, error_code: safeErrorCode, agent_id: agentId });
|
|
7487
8518
|
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
7488
8519
|
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
7489
|
-
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code:
|
|
8520
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason: safeReason, error_code: safeErrorCode, agent_id: agentId }, severity: "warning", databasePath });
|
|
7490
8521
|
let retryTask;
|
|
7491
8522
|
if (options?.retry) {
|
|
7492
8523
|
const retryCount = (task.retry_count || 0) + 1;
|
|
7493
8524
|
const maxRetries = task.max_retries || 3;
|
|
7494
8525
|
if (retryCount > maxRetries) {
|
|
7495
8526
|
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
7496
|
-
JSON.stringify({ ...
|
|
8527
|
+
JSON.stringify(sanitizePreWriteValue({ ...safeMeta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }, "task.retry_exhausted.metadata")),
|
|
7497
8528
|
id
|
|
7498
8529
|
]);
|
|
7499
8530
|
} else {
|
|
@@ -7512,7 +8543,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7512
8543
|
plan_id: task.plan_id ?? undefined,
|
|
7513
8544
|
assigned_to: task.assigned_to ?? undefined,
|
|
7514
8545
|
tags: task.tags,
|
|
7515
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason:
|
|
8546
|
+
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: safeReason } },
|
|
7516
8547
|
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
7517
8548
|
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
7518
8549
|
due_at: retryAfter
|
|
@@ -7542,7 +8573,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
|
|
|
7542
8573
|
}
|
|
7543
8574
|
const where = conditions.join(" AND ");
|
|
7544
8575
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
7545
|
-
return rows.map(
|
|
8576
|
+
return rows.map(rowToTask2);
|
|
7546
8577
|
}
|
|
7547
8578
|
function stealTask(agentId, opts, db) {
|
|
7548
8579
|
const d = db || getDatabase();
|
|
@@ -7554,15 +8585,15 @@ function stealTask(agentId, opts, db) {
|
|
|
7554
8585
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7555
8586
|
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
7556
8587
|
const target = staleTasks[0];
|
|
7557
|
-
const
|
|
8588
|
+
const timestamp2 = now();
|
|
7558
8589
|
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7559
8590
|
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
7560
|
-
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId,
|
|
8591
|
+
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp2, timestamp2, target.id, cutoff, cutoff]);
|
|
7561
8592
|
if (result.changes === 0)
|
|
7562
8593
|
return null;
|
|
7563
8594
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
7564
8595
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
7565
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at:
|
|
8596
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp2, updated_at: timestamp2, version: target.version + 1 };
|
|
7566
8597
|
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7567
8598
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7568
8599
|
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
@@ -7623,10 +8654,11 @@ var init_task_lifecycle = __esm(() => {
|
|
|
7623
8654
|
init_templates();
|
|
7624
8655
|
init_task_crud();
|
|
7625
8656
|
init_task_graph();
|
|
8657
|
+
init_prewrite_secrets();
|
|
7626
8658
|
});
|
|
7627
8659
|
|
|
7628
8660
|
// src/db/task-crud.ts
|
|
7629
|
-
function
|
|
8661
|
+
function rowToTask2(row) {
|
|
7630
8662
|
return {
|
|
7631
8663
|
...row,
|
|
7632
8664
|
tags: JSON.parse(row.tags || "[]"),
|
|
@@ -7660,9 +8692,29 @@ function addMetadataConditions(metadata, conditions, params) {
|
|
|
7660
8692
|
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
7661
8693
|
}
|
|
7662
8694
|
}
|
|
8695
|
+
function sanitizeCreateTaskInput(input) {
|
|
8696
|
+
return {
|
|
8697
|
+
...input,
|
|
8698
|
+
title: sanitizePreWriteText(input.title, "task.title"),
|
|
8699
|
+
description: input.description !== undefined ? sanitizePreWriteText(input.description, "task.description") : undefined,
|
|
8700
|
+
tags: input.tags !== undefined ? sanitizePreWriteValue(input.tags, "task.tags") : undefined,
|
|
8701
|
+
metadata: input.metadata !== undefined ? sanitizePreWriteValue(input.metadata, "task.metadata") : undefined,
|
|
8702
|
+
reason: input.reason !== undefined ? sanitizePreWriteText(input.reason, "task.reason") : undefined
|
|
8703
|
+
};
|
|
8704
|
+
}
|
|
8705
|
+
function sanitizeUpdateTaskInput(input) {
|
|
8706
|
+
return {
|
|
8707
|
+
...input,
|
|
8708
|
+
title: input.title !== undefined ? sanitizePreWriteText(input.title, "task.title") : undefined,
|
|
8709
|
+
description: input.description !== undefined ? sanitizePreWriteText(input.description, "task.description") : undefined,
|
|
8710
|
+
tags: input.tags !== undefined ? sanitizePreWriteValue(input.tags, "task.tags") : undefined,
|
|
8711
|
+
metadata: input.metadata !== undefined ? sanitizePreWriteValue(input.metadata, "task.metadata") : undefined
|
|
8712
|
+
};
|
|
8713
|
+
}
|
|
7663
8714
|
function createTask(input, db) {
|
|
8715
|
+
input = sanitizeCreateTaskInput(input);
|
|
7664
8716
|
const d = db || getDatabase();
|
|
7665
|
-
const
|
|
8717
|
+
const timestamp2 = now();
|
|
7666
8718
|
const tags = input.tags || [];
|
|
7667
8719
|
const machineId = currentStorageMachineId(d);
|
|
7668
8720
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
@@ -7689,8 +8741,8 @@ function createTask(input, db) {
|
|
|
7689
8741
|
input.working_dir || null,
|
|
7690
8742
|
JSON.stringify(tags),
|
|
7691
8743
|
JSON.stringify(input.metadata || {}),
|
|
7692
|
-
|
|
7693
|
-
|
|
8744
|
+
timestamp2,
|
|
8745
|
+
timestamp2,
|
|
7694
8746
|
input.due_at || null,
|
|
7695
8747
|
input.estimated_minutes || null,
|
|
7696
8748
|
input.sla_minutes ?? null,
|
|
@@ -7736,7 +8788,7 @@ function getTask(id, db) {
|
|
|
7736
8788
|
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
7737
8789
|
if (!row)
|
|
7738
8790
|
return null;
|
|
7739
|
-
return
|
|
8791
|
+
return rowToTask2(row);
|
|
7740
8792
|
}
|
|
7741
8793
|
function getTaskWithRelations(id, db) {
|
|
7742
8794
|
const d = db || getDatabase();
|
|
@@ -7744,15 +8796,15 @@ function getTaskWithRelations(id, db) {
|
|
|
7744
8796
|
if (!task)
|
|
7745
8797
|
return null;
|
|
7746
8798
|
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
7747
|
-
const subtasks = subtaskRows.map(
|
|
8799
|
+
const subtasks = subtaskRows.map(rowToTask2);
|
|
7748
8800
|
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
7749
8801
|
JOIN task_dependencies td ON td.depends_on = t.id
|
|
7750
8802
|
WHERE td.task_id = ?`).all(id);
|
|
7751
|
-
const dependencies = depRows.map(
|
|
8803
|
+
const dependencies = depRows.map(rowToTask2);
|
|
7752
8804
|
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
7753
8805
|
JOIN task_dependencies td ON td.task_id = t.id
|
|
7754
8806
|
WHERE td.depends_on = ?`).all(id);
|
|
7755
|
-
const blocked_by = blockedByRows.map(
|
|
8807
|
+
const blocked_by = blockedByRows.map(rowToTask2);
|
|
7756
8808
|
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
7757
8809
|
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
7758
8810
|
const checklist = getChecklist(id, d);
|
|
@@ -7868,7 +8920,7 @@ function listTasks(filter = {}, db) {
|
|
|
7868
8920
|
}
|
|
7869
8921
|
}
|
|
7870
8922
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
|
|
7871
|
-
return rows.map(
|
|
8923
|
+
return rows.map(rowToTask2);
|
|
7872
8924
|
}
|
|
7873
8925
|
function getTaskByFingerprint(fingerprint, db) {
|
|
7874
8926
|
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
@@ -8014,10 +9066,11 @@ function updateTask(id, input, db) {
|
|
|
8014
9066
|
if (task.version !== input.version) {
|
|
8015
9067
|
throw new VersionConflictError(id, input.version, task.version);
|
|
8016
9068
|
}
|
|
8017
|
-
|
|
8018
|
-
const
|
|
9069
|
+
input = sanitizeUpdateTaskInput(input);
|
|
9070
|
+
const timestamp2 = now();
|
|
9071
|
+
const completionTimestamp = input.completed_at ?? timestamp2;
|
|
8019
9072
|
const sets = ["version = version + 1", "updated_at = ?"];
|
|
8020
|
-
const params = [
|
|
9073
|
+
const params = [timestamp2];
|
|
8021
9074
|
if (input.title !== undefined) {
|
|
8022
9075
|
sets.push("title = ?");
|
|
8023
9076
|
params.push(input.title);
|
|
@@ -8166,7 +9219,7 @@ function updateTask(id, input, db) {
|
|
|
8166
9219
|
tags: input.tags ?? task.tags,
|
|
8167
9220
|
metadata: input.metadata ?? task.metadata,
|
|
8168
9221
|
version: task.version + 1,
|
|
8169
|
-
updated_at:
|
|
9222
|
+
updated_at: timestamp2,
|
|
8170
9223
|
locked_by: completedNow ? null : task.locked_by,
|
|
8171
9224
|
locked_at: completedNow ? null : task.locked_at,
|
|
8172
9225
|
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
@@ -8178,7 +9231,7 @@ function updateTask(id, input, db) {
|
|
|
8178
9231
|
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
8179
9232
|
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
8180
9233
|
approved_by: input.approved_by ?? task.approved_by,
|
|
8181
|
-
approved_at: input.approved_by ?
|
|
9234
|
+
approved_at: input.approved_by ? timestamp2 : task.approved_at
|
|
8182
9235
|
};
|
|
8183
9236
|
const databasePath = databasePathFromDatabase(d);
|
|
8184
9237
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
@@ -8210,7 +9263,7 @@ function deleteTask(id, db) {
|
|
|
8210
9263
|
recordStorageTombstone({
|
|
8211
9264
|
object_type: "tasks",
|
|
8212
9265
|
object_id: id,
|
|
8213
|
-
payload:
|
|
9266
|
+
payload: rowToTask2(row),
|
|
8214
9267
|
version: row.version
|
|
8215
9268
|
}, d);
|
|
8216
9269
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
@@ -8227,6 +9280,7 @@ var init_task_crud = __esm(() => {
|
|
|
8227
9280
|
init_webhooks();
|
|
8228
9281
|
init_checklists();
|
|
8229
9282
|
init_storage_tombstones();
|
|
9283
|
+
init_prewrite_secrets();
|
|
8230
9284
|
});
|
|
8231
9285
|
|
|
8232
9286
|
// src/db/task-status.ts
|
|
@@ -8358,10 +9412,10 @@ function redistributeStaleTasks(agentId, options, db) {
|
|
|
8358
9412
|
const maxAge = options?.max_age_minutes ?? 60;
|
|
8359
9413
|
const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
|
|
8360
9414
|
const limited = options?.limit ? stale.slice(0, options.limit) : stale;
|
|
8361
|
-
const
|
|
9415
|
+
const timestamp2 = now();
|
|
8362
9416
|
const released = [];
|
|
8363
9417
|
for (const t of limited) {
|
|
8364
|
-
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [
|
|
9418
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [timestamp2, t.id]);
|
|
8365
9419
|
released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
|
|
8366
9420
|
}
|
|
8367
9421
|
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
@@ -8501,7 +9555,7 @@ function getOverdueTasks(projectId, db, at = new Date) {
|
|
|
8501
9555
|
}
|
|
8502
9556
|
query += ` ORDER BY due_at ASC`;
|
|
8503
9557
|
const rows = d.query(query).all(...params);
|
|
8504
|
-
return rows.map(
|
|
9558
|
+
return rows.map(rowToTask2);
|
|
8505
9559
|
}
|
|
8506
9560
|
function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
8507
9561
|
const d = db || getDatabase();
|
|
@@ -8521,7 +9575,7 @@ function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
|
8521
9575
|
params.push(opts.agent_id);
|
|
8522
9576
|
}
|
|
8523
9577
|
const rows = d.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY due_at ASC, created_at ASC`).all(...params);
|
|
8524
|
-
return rows.map(
|
|
9578
|
+
return rows.map(rowToTask2).map((task) => {
|
|
8525
9579
|
const reasons = [];
|
|
8526
9580
|
const breachedTimes = [];
|
|
8527
9581
|
if (task.due_at) {
|
|
@@ -8833,7 +9887,7 @@ function resolveCreateSlug(input, projectId, db) {
|
|
|
8833
9887
|
function createPlan(input, db) {
|
|
8834
9888
|
const d = db || getDatabase();
|
|
8835
9889
|
const id = uuid();
|
|
8836
|
-
const
|
|
9890
|
+
const timestamp2 = now();
|
|
8837
9891
|
const projectId = input.project_id || null;
|
|
8838
9892
|
const slug = resolveCreateSlug(input, projectId, d);
|
|
8839
9893
|
const machineId = currentStorageMachineId(d);
|
|
@@ -8847,8 +9901,8 @@ function createPlan(input, db) {
|
|
|
8847
9901
|
input.name,
|
|
8848
9902
|
input.description || null,
|
|
8849
9903
|
input.status || "active",
|
|
8850
|
-
|
|
8851
|
-
|
|
9904
|
+
timestamp2,
|
|
9905
|
+
timestamp2,
|
|
8852
9906
|
machineId
|
|
8853
9907
|
]);
|
|
8854
9908
|
return getPlan(id, d);
|
|
@@ -8985,7 +10039,7 @@ function maybeFilter(value) {
|
|
|
8985
10039
|
function createTaskBoard(input, db) {
|
|
8986
10040
|
const d = db || getDatabase();
|
|
8987
10041
|
const id = uuid();
|
|
8988
|
-
const
|
|
10042
|
+
const timestamp2 = now();
|
|
8989
10043
|
const scope = input.scope || "tasks";
|
|
8990
10044
|
const lanes = normalizeLanes(scope, input.lanes);
|
|
8991
10045
|
d.run(`INSERT INTO task_boards (id, name, scope, project_id, task_list_id, plan_id, agent_id, lanes, filters, created_at, updated_at)
|
|
@@ -8999,8 +10053,8 @@ function createTaskBoard(input, db) {
|
|
|
8999
10053
|
input.agent_id || null,
|
|
9000
10054
|
JSON.stringify(lanes),
|
|
9001
10055
|
JSON.stringify(input.filters || {}),
|
|
9002
|
-
|
|
9003
|
-
|
|
10056
|
+
timestamp2,
|
|
10057
|
+
timestamp2
|
|
9004
10058
|
]);
|
|
9005
10059
|
return getTaskBoard(id, d);
|
|
9006
10060
|
}
|
|
@@ -9401,14 +10455,14 @@ function storeArtifactContent(input) {
|
|
|
9401
10455
|
return null;
|
|
9402
10456
|
const sourceStat = statSync2(sourcePath);
|
|
9403
10457
|
if (!sourceStat.isFile())
|
|
9404
|
-
throw new Error(`Artifact path is not a file: ${input.path}`);
|
|
10458
|
+
throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
|
|
9405
10459
|
const sourceBuffer = readFileSync3(sourcePath);
|
|
9406
10460
|
const sourceSha = sha256(sourceBuffer);
|
|
9407
10461
|
const textLike = isTextLike(sourceBuffer, input.path);
|
|
9408
10462
|
let storedBuffer = sourceBuffer;
|
|
9409
10463
|
let redactionStatus = textLike ? "clean" : "binary_or_unknown";
|
|
9410
10464
|
if (textLike) {
|
|
9411
|
-
const redactedText =
|
|
10465
|
+
const redactedText = sanitizePreWriteText(sourceBuffer.toString("utf8"), "artifact.content");
|
|
9412
10466
|
storedBuffer = Buffer.from(redactedText);
|
|
9413
10467
|
if (!storedBuffer.equals(sourceBuffer))
|
|
9414
10468
|
redactionStatus = "redacted";
|
|
@@ -9442,7 +10496,7 @@ function storeArtifactContent(input) {
|
|
|
9442
10496
|
expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
|
|
9443
10497
|
},
|
|
9444
10498
|
source: {
|
|
9445
|
-
path: input.path,
|
|
10499
|
+
path: sanitizePreWriteText(input.path, "artifact.source_path"),
|
|
9446
10500
|
size_bytes: sourceBuffer.length,
|
|
9447
10501
|
sha256: sourceSha
|
|
9448
10502
|
}
|
|
@@ -9622,6 +10676,7 @@ var DEFAULT_DELETED_RETENTION_DAYS = 30;
|
|
|
9622
10676
|
var init_artifact_store = __esm(() => {
|
|
9623
10677
|
init_database();
|
|
9624
10678
|
init_redaction();
|
|
10679
|
+
init_prewrite_secrets();
|
|
9625
10680
|
});
|
|
9626
10681
|
|
|
9627
10682
|
// src/db/comments.ts
|
|
@@ -9631,17 +10686,17 @@ function addComment(input, db) {
|
|
|
9631
10686
|
throw new TaskNotFoundError(input.task_id);
|
|
9632
10687
|
}
|
|
9633
10688
|
const id = uuid();
|
|
9634
|
-
const
|
|
10689
|
+
const timestamp2 = now();
|
|
9635
10690
|
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
9636
10691
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9637
10692
|
id,
|
|
9638
10693
|
input.task_id,
|
|
9639
10694
|
input.agent_id || null,
|
|
9640
10695
|
input.session_id || null,
|
|
9641
|
-
|
|
10696
|
+
sanitizePreWriteText(input.content, "comment.content"),
|
|
9642
10697
|
input.type || "comment",
|
|
9643
10698
|
input.progress_pct ?? null,
|
|
9644
|
-
|
|
10699
|
+
timestamp2
|
|
9645
10700
|
]);
|
|
9646
10701
|
return getComment(id, d);
|
|
9647
10702
|
}
|
|
@@ -9665,21 +10720,21 @@ var init_comments = __esm(() => {
|
|
|
9665
10720
|
init_types();
|
|
9666
10721
|
init_database();
|
|
9667
10722
|
init_tasks();
|
|
9668
|
-
|
|
10723
|
+
init_prewrite_secrets();
|
|
9669
10724
|
});
|
|
9670
10725
|
|
|
9671
10726
|
// src/db/task-files.ts
|
|
9672
10727
|
function addTaskFile(input, db) {
|
|
9673
10728
|
const d = db || getDatabase();
|
|
9674
10729
|
const id = uuid();
|
|
9675
|
-
const
|
|
10730
|
+
const timestamp2 = now();
|
|
9676
10731
|
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
9677
10732
|
if (existing) {
|
|
9678
|
-
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null,
|
|
10733
|
+
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp2, existing.id]);
|
|
9679
10734
|
return getTaskFile(existing.id, d);
|
|
9680
10735
|
}
|
|
9681
10736
|
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
9682
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null,
|
|
10737
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp2, timestamp2]);
|
|
9683
10738
|
return getTaskFile(id, d);
|
|
9684
10739
|
}
|
|
9685
10740
|
function getTaskFile(id, db) {
|
|
@@ -9696,8 +10751,8 @@ function findTasksByFile(path, db) {
|
|
|
9696
10751
|
}
|
|
9697
10752
|
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
9698
10753
|
const d = db || getDatabase();
|
|
9699
|
-
const
|
|
9700
|
-
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null,
|
|
10754
|
+
const timestamp2 = now();
|
|
10755
|
+
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp2, taskId, path]);
|
|
9701
10756
|
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
9702
10757
|
return row;
|
|
9703
10758
|
}
|
|
@@ -9742,8 +10797,16 @@ function rowToVerification(row) {
|
|
|
9742
10797
|
function linkTaskToCommit(input, db) {
|
|
9743
10798
|
const d = db || getDatabase();
|
|
9744
10799
|
const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
|
|
9745
|
-
const ciJson = input.ci_snapshot ? JSON.stringify(input.ci_snapshot) : null;
|
|
9746
|
-
const traceJson = input.traceability ? JSON.stringify(input.traceability) : null;
|
|
10800
|
+
const ciJson = input.ci_snapshot ? JSON.stringify(sanitizePreWriteValue(input.ci_snapshot, "commit.ci_snapshot")) : null;
|
|
10801
|
+
const traceJson = input.traceability ? JSON.stringify(sanitizePreWriteValue(input.traceability, "commit.traceability")) : null;
|
|
10802
|
+
const message = input.message ? sanitizePreWriteText(input.message, "commit.message") : null;
|
|
10803
|
+
const author = input.author ? sanitizePreWriteText(input.author, "commit.author") : null;
|
|
10804
|
+
const filesChanged = input.files_changed ? JSON.stringify(sanitizePreWriteValue(input.files_changed, "commit.files_changed")) : null;
|
|
10805
|
+
const branch = input.branch ? sanitizePreWriteText(input.branch, "commit.branch") : null;
|
|
10806
|
+
const prUrl = input.pr_url ? sanitizePreWriteText(input.pr_url, "commit.pr_url") : null;
|
|
10807
|
+
const prState = input.pr_state ? sanitizePreWriteText(input.pr_state, "commit.pr_state") : null;
|
|
10808
|
+
const releaseTag = input.release_tag ? sanitizePreWriteText(input.release_tag, "commit.release_tag") : null;
|
|
10809
|
+
const repoPath = input.repo_path ? sanitizePreWriteText(input.repo_path, "commit.repo_path") : null;
|
|
9747
10810
|
if (existing) {
|
|
9748
10811
|
d.run(`UPDATE task_commits SET
|
|
9749
10812
|
message = COALESCE(?, message),
|
|
@@ -9759,17 +10822,17 @@ function linkTaskToCommit(input, db) {
|
|
|
9759
10822
|
repo_path = COALESCE(?, repo_path),
|
|
9760
10823
|
traceability = COALESCE(?, traceability)
|
|
9761
10824
|
WHERE id = ?`, [
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
10825
|
+
message,
|
|
10826
|
+
author,
|
|
10827
|
+
filesChanged,
|
|
9765
10828
|
input.committed_at ?? null,
|
|
9766
|
-
|
|
9767
|
-
|
|
10829
|
+
branch,
|
|
10830
|
+
prUrl,
|
|
9768
10831
|
input.pr_number ?? null,
|
|
9769
|
-
|
|
10832
|
+
prState,
|
|
9770
10833
|
ciJson,
|
|
9771
|
-
|
|
9772
|
-
|
|
10834
|
+
releaseTag,
|
|
10835
|
+
repoPath,
|
|
9773
10836
|
traceJson,
|
|
9774
10837
|
existing.id
|
|
9775
10838
|
]);
|
|
@@ -9783,17 +10846,17 @@ function linkTaskToCommit(input, db) {
|
|
|
9783
10846
|
id,
|
|
9784
10847
|
input.task_id,
|
|
9785
10848
|
input.sha,
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
10849
|
+
message,
|
|
10850
|
+
author,
|
|
10851
|
+
filesChanged,
|
|
9789
10852
|
input.committed_at ?? null,
|
|
9790
|
-
|
|
9791
|
-
|
|
10853
|
+
branch,
|
|
10854
|
+
prUrl,
|
|
9792
10855
|
input.pr_number ?? null,
|
|
9793
|
-
|
|
10856
|
+
prState,
|
|
9794
10857
|
ciJson,
|
|
9795
|
-
|
|
9796
|
-
|
|
10858
|
+
releaseTag,
|
|
10859
|
+
repoPath,
|
|
9797
10860
|
traceJson ?? "{}",
|
|
9798
10861
|
now()
|
|
9799
10862
|
]);
|
|
@@ -9822,13 +10885,16 @@ function addTaskVerification(input, db) {
|
|
|
9822
10885
|
const d = db || getDatabase();
|
|
9823
10886
|
const id = uuid();
|
|
9824
10887
|
const runAt = input.run_at || now();
|
|
10888
|
+
const command = sanitizePreWriteText(input.command, "verification.command");
|
|
10889
|
+
const outputSummary = input.output_summary ? sanitizePreWriteText(input.output_summary, "verification.output_summary") : null;
|
|
10890
|
+
const artifactPath = input.artifact_path ? sanitizePreWriteText(input.artifact_path, "verification.artifact_path") : null;
|
|
9825
10891
|
d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
9826
10892
|
id,
|
|
9827
10893
|
input.task_id,
|
|
9828
|
-
|
|
10894
|
+
command,
|
|
9829
10895
|
input.status || "unknown",
|
|
9830
|
-
|
|
9831
|
-
|
|
10896
|
+
outputSummary,
|
|
10897
|
+
artifactPath,
|
|
9832
10898
|
input.agent_id ?? null,
|
|
9833
10899
|
runAt,
|
|
9834
10900
|
now()
|
|
@@ -9858,6 +10924,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
9858
10924
|
}
|
|
9859
10925
|
var init_task_commits = __esm(() => {
|
|
9860
10926
|
init_database();
|
|
10927
|
+
init_prewrite_secrets();
|
|
9861
10928
|
});
|
|
9862
10929
|
|
|
9863
10930
|
// src/db/task-runs.ts
|
|
@@ -9968,7 +11035,7 @@ function startTaskRun(input, db) {
|
|
|
9968
11035
|
if (!getTask(input.task_id, d))
|
|
9969
11036
|
throw new TaskNotFoundError(input.task_id);
|
|
9970
11037
|
const id = input.id ?? uuid();
|
|
9971
|
-
const
|
|
11038
|
+
const timestamp2 = input.started_at || now();
|
|
9972
11039
|
if (input.claim && input.agent_id) {
|
|
9973
11040
|
startTask(input.task_id, input.agent_id, d);
|
|
9974
11041
|
}
|
|
@@ -9976,12 +11043,12 @@ function startTaskRun(input, db) {
|
|
|
9976
11043
|
id,
|
|
9977
11044
|
input.task_id,
|
|
9978
11045
|
input.agent_id ?? null,
|
|
9979
|
-
input.title ?
|
|
9980
|
-
input.summary ?
|
|
9981
|
-
JSON.stringify(
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
11046
|
+
input.title ? sanitizePreWriteText(input.title, "run.title") : null,
|
|
11047
|
+
input.summary ? sanitizePreWriteText(input.summary, "run.summary") : null,
|
|
11048
|
+
JSON.stringify(sanitizePreWriteValue(input.metadata || {}, "run.metadata")),
|
|
11049
|
+
timestamp2,
|
|
11050
|
+
timestamp2,
|
|
11051
|
+
timestamp2
|
|
9985
11052
|
]);
|
|
9986
11053
|
addTaskRunEvent({
|
|
9987
11054
|
run_id: id,
|
|
@@ -9989,7 +11056,7 @@ function startTaskRun(input, db) {
|
|
|
9989
11056
|
message: input.summary || input.title || "run started",
|
|
9990
11057
|
data: { title: input.title, claim: Boolean(input.claim) },
|
|
9991
11058
|
agent_id: input.agent_id,
|
|
9992
|
-
created_at:
|
|
11059
|
+
created_at: timestamp2
|
|
9993
11060
|
}, d);
|
|
9994
11061
|
if (input.claim && input.agent_id) {
|
|
9995
11062
|
addTaskRunEvent({
|
|
@@ -9998,7 +11065,7 @@ function startTaskRun(input, db) {
|
|
|
9998
11065
|
message: `claimed by ${input.agent_id}`,
|
|
9999
11066
|
data: { agent_id: input.agent_id },
|
|
10000
11067
|
agent_id: input.agent_id,
|
|
10001
|
-
created_at:
|
|
11068
|
+
created_at: timestamp2
|
|
10002
11069
|
}, d);
|
|
10003
11070
|
}
|
|
10004
11071
|
const run = getTaskRun(id, d);
|
|
@@ -10013,7 +11080,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10013
11080
|
const d = db || getDatabase();
|
|
10014
11081
|
if (!getTask(input.task_id, d))
|
|
10015
11082
|
throw new TaskNotFoundError(input.task_id);
|
|
10016
|
-
const
|
|
11083
|
+
const timestamp2 = input.started_at || now();
|
|
10017
11084
|
const key = normalizeTransactionKey(input);
|
|
10018
11085
|
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
10019
11086
|
const dryRun = !input.apply;
|
|
@@ -10022,7 +11089,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10022
11089
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10023
11090
|
local_only: true,
|
|
10024
11091
|
dry_run: dryRun,
|
|
10025
|
-
processed_at:
|
|
11092
|
+
processed_at: timestamp2,
|
|
10026
11093
|
action: "matched",
|
|
10027
11094
|
key,
|
|
10028
11095
|
run: summarizeTaskRun(existing),
|
|
@@ -10035,7 +11102,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10035
11102
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10036
11103
|
local_only: true,
|
|
10037
11104
|
dry_run: true,
|
|
10038
|
-
processed_at:
|
|
11105
|
+
processed_at: timestamp2,
|
|
10039
11106
|
action: "preview",
|
|
10040
11107
|
key,
|
|
10041
11108
|
run: null,
|
|
@@ -10043,14 +11110,14 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10043
11110
|
commands: loopRunCommands(null, key)
|
|
10044
11111
|
};
|
|
10045
11112
|
}
|
|
10046
|
-
const metadata =
|
|
11113
|
+
const metadata = sanitizePreWriteValue({
|
|
10047
11114
|
...input.metadata || {},
|
|
10048
11115
|
loop_transaction: {
|
|
10049
11116
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10050
11117
|
idempotency_key: key,
|
|
10051
11118
|
loop_id: input.loop_id ?? null,
|
|
10052
11119
|
loop_run_id: input.loop_run_id ?? null,
|
|
10053
|
-
first_seen_at:
|
|
11120
|
+
first_seen_at: timestamp2
|
|
10054
11121
|
},
|
|
10055
11122
|
idempotency_key: key
|
|
10056
11123
|
});
|
|
@@ -10064,8 +11131,8 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10064
11131
|
input.loop_id ?? null,
|
|
10065
11132
|
input.loop_run_id ?? null,
|
|
10066
11133
|
JSON.stringify(metadata),
|
|
10067
|
-
|
|
10068
|
-
|
|
11134
|
+
timestamp2,
|
|
11135
|
+
timestamp2
|
|
10069
11136
|
]);
|
|
10070
11137
|
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
10071
11138
|
if (!transaction)
|
|
@@ -10083,16 +11150,16 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10083
11150
|
summary: input.summary,
|
|
10084
11151
|
metadata,
|
|
10085
11152
|
claim: input.claim,
|
|
10086
|
-
started_at:
|
|
11153
|
+
started_at: timestamp2
|
|
10087
11154
|
}, d);
|
|
10088
|
-
d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata),
|
|
11155
|
+
d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp2, transaction.id]);
|
|
10089
11156
|
return { run, action: "created" };
|
|
10090
11157
|
})();
|
|
10091
11158
|
return {
|
|
10092
11159
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10093
11160
|
local_only: true,
|
|
10094
11161
|
dry_run: false,
|
|
10095
|
-
processed_at:
|
|
11162
|
+
processed_at: timestamp2,
|
|
10096
11163
|
action: created.action,
|
|
10097
11164
|
key,
|
|
10098
11165
|
run: summarizeTaskRun(created.run),
|
|
@@ -10107,19 +11174,19 @@ function addTaskRunEvent(input, db) {
|
|
|
10107
11174
|
if (!run)
|
|
10108
11175
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10109
11176
|
const id = uuid();
|
|
10110
|
-
const
|
|
11177
|
+
const timestamp2 = input.created_at || now();
|
|
10111
11178
|
d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10112
11179
|
id,
|
|
10113
11180
|
run.id,
|
|
10114
11181
|
run.task_id,
|
|
10115
11182
|
input.event_type,
|
|
10116
|
-
input.message ?
|
|
10117
|
-
JSON.stringify(
|
|
11183
|
+
input.message ? sanitizePreWriteText(input.message, "run_event.message") : null,
|
|
11184
|
+
JSON.stringify(sanitizePreWriteValue(input.data || {}, "run_event.data")),
|
|
10118
11185
|
input.agent_id ?? run.agent_id,
|
|
10119
|
-
|
|
11186
|
+
timestamp2
|
|
10120
11187
|
]);
|
|
10121
11188
|
if (input.event_type === "comment" && input.message) {
|
|
10122
|
-
addComment({ task_id: run.task_id, content:
|
|
11189
|
+
addComment({ task_id: run.task_id, content: sanitizePreWriteText(input.message, "run_event.comment"), type: "comment", agent_id: input.agent_id ?? run.agent_id ?? undefined }, d);
|
|
10123
11190
|
}
|
|
10124
11191
|
return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
|
|
10125
11192
|
}
|
|
@@ -10131,10 +11198,10 @@ function addTaskRunCommand(input, db) {
|
|
|
10131
11198
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10132
11199
|
const id = uuid();
|
|
10133
11200
|
const status = input.status || "unknown";
|
|
10134
|
-
const
|
|
10135
|
-
const command =
|
|
10136
|
-
const outputSummary = input.output_summary ?
|
|
10137
|
-
const artifactPath = input.artifact_path ?
|
|
11201
|
+
const timestamp2 = now();
|
|
11202
|
+
const command = sanitizePreWriteText(input.command, "run_command.command");
|
|
11203
|
+
const outputSummary = input.output_summary ? sanitizePreWriteText(input.output_summary, "run_command.output_summary") : null;
|
|
11204
|
+
const artifactPath = input.artifact_path ? sanitizePreWriteText(input.artifact_path, "run_command.artifact_path") : null;
|
|
10138
11205
|
d.run("INSERT INTO task_run_commands (id, run_id, task_id, command, status, exit_code, output_summary, artifact_path, agent_id, started_at, completed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10139
11206
|
id,
|
|
10140
11207
|
run.id,
|
|
@@ -10146,8 +11213,8 @@ function addTaskRunCommand(input, db) {
|
|
|
10146
11213
|
artifactPath,
|
|
10147
11214
|
input.agent_id ?? run.agent_id,
|
|
10148
11215
|
input.started_at ?? null,
|
|
10149
|
-
input.completed_at ??
|
|
10150
|
-
|
|
11216
|
+
input.completed_at ?? timestamp2,
|
|
11217
|
+
timestamp2
|
|
10151
11218
|
]);
|
|
10152
11219
|
addTaskVerification({
|
|
10153
11220
|
task_id: run.task_id,
|
|
@@ -10156,7 +11223,7 @@ function addTaskRunCommand(input, db) {
|
|
|
10156
11223
|
output_summary: outputSummary ?? undefined,
|
|
10157
11224
|
artifact_path: artifactPath ?? undefined,
|
|
10158
11225
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10159
|
-
run_at: input.completed_at ??
|
|
11226
|
+
run_at: input.completed_at ?? timestamp2
|
|
10160
11227
|
}, d);
|
|
10161
11228
|
addTaskRunEvent({
|
|
10162
11229
|
run_id: run.id,
|
|
@@ -10175,7 +11242,7 @@ function addTaskRunCommand(input, db) {
|
|
|
10175
11242
|
}
|
|
10176
11243
|
},
|
|
10177
11244
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10178
|
-
created_at:
|
|
11245
|
+
created_at: timestamp2
|
|
10179
11246
|
}, d);
|
|
10180
11247
|
return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
|
|
10181
11248
|
}
|
|
@@ -10189,7 +11256,7 @@ function addTaskRunFile(input, db) {
|
|
|
10189
11256
|
task_id: run.task_id,
|
|
10190
11257
|
path: input.path,
|
|
10191
11258
|
status: input.status || "modified",
|
|
10192
|
-
note: input.note ?
|
|
11259
|
+
note: input.note ? sanitizePreWriteText(input.note, "run_file.note") : undefined,
|
|
10193
11260
|
agent_id: input.agent_id ?? run.agent_id ?? undefined
|
|
10194
11261
|
}, d);
|
|
10195
11262
|
addTaskRunEvent({
|
|
@@ -10208,19 +11275,19 @@ function addTaskRunArtifact(input, db) {
|
|
|
10208
11275
|
if (!run)
|
|
10209
11276
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10210
11277
|
const id = uuid();
|
|
10211
|
-
const
|
|
10212
|
-
const path =
|
|
10213
|
-
const description = input.description ?
|
|
10214
|
-
const metadata =
|
|
11278
|
+
const timestamp2 = now();
|
|
11279
|
+
const path = sanitizePreWriteText(input.path, "run_artifact.path");
|
|
11280
|
+
const description = input.description ? sanitizePreWriteText(input.description, "run_artifact.description") : null;
|
|
11281
|
+
const metadata = sanitizePreWriteValue(input.metadata || {}, "run_artifact.metadata");
|
|
10215
11282
|
let sizeBytes = input.size_bytes ?? null;
|
|
10216
11283
|
let digest = input.sha256 ?? null;
|
|
10217
|
-
const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at:
|
|
11284
|
+
const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at: timestamp2 }) : null;
|
|
10218
11285
|
if (stored) {
|
|
10219
11286
|
sizeBytes = stored.size_bytes;
|
|
10220
11287
|
digest = stored.sha256;
|
|
10221
11288
|
metadata["artifact_store"] = stored.store;
|
|
10222
11289
|
} else if (input.store_content === true) {
|
|
10223
|
-
throw new Error(`Artifact file not found: ${
|
|
11290
|
+
throw new Error(`Artifact file not found: ${path}`);
|
|
10224
11291
|
}
|
|
10225
11292
|
d.run("INSERT INTO task_run_artifacts (id, run_id, task_id, path, artifact_type, description, size_bytes, sha256, metadata, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10226
11293
|
id,
|
|
@@ -10233,7 +11300,7 @@ function addTaskRunArtifact(input, db) {
|
|
|
10233
11300
|
digest,
|
|
10234
11301
|
JSON.stringify(metadata),
|
|
10235
11302
|
input.agent_id ?? run.agent_id,
|
|
10236
|
-
|
|
11303
|
+
timestamp2
|
|
10237
11304
|
]);
|
|
10238
11305
|
addTaskRunEvent({
|
|
10239
11306
|
run_id: run.id,
|
|
@@ -10241,7 +11308,7 @@ function addTaskRunArtifact(input, db) {
|
|
|
10241
11308
|
message: description || path,
|
|
10242
11309
|
data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
|
|
10243
11310
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10244
|
-
created_at:
|
|
11311
|
+
created_at: timestamp2
|
|
10245
11312
|
}, d);
|
|
10246
11313
|
return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
|
|
10247
11314
|
}
|
|
@@ -10261,28 +11328,28 @@ function finishTaskRun(input, db) {
|
|
|
10261
11328
|
const run = getTaskRun(runId, d);
|
|
10262
11329
|
if (!run)
|
|
10263
11330
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10264
|
-
const
|
|
10265
|
-
const summary = input.summary ?
|
|
10266
|
-
d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary,
|
|
11331
|
+
const timestamp2 = input.completed_at || now();
|
|
11332
|
+
const summary = input.summary ? sanitizePreWriteText(input.summary, "run.summary") : null;
|
|
11333
|
+
d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary, timestamp2, timestamp2, run.id]);
|
|
10267
11334
|
addTaskRunEvent({
|
|
10268
11335
|
run_id: run.id,
|
|
10269
11336
|
event_type: input.status,
|
|
10270
11337
|
message: summary || `run ${input.status}`,
|
|
10271
11338
|
data: { status: input.status },
|
|
10272
11339
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10273
|
-
created_at:
|
|
11340
|
+
created_at: timestamp2
|
|
10274
11341
|
}, d);
|
|
10275
11342
|
const updated = getTaskRun(run.id, d);
|
|
10276
11343
|
emitLocalEventHooksQuiet({
|
|
10277
11344
|
type: `run.${input.status}`,
|
|
10278
|
-
payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at:
|
|
11345
|
+
payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp2 },
|
|
10279
11346
|
databasePath: databasePathFromDatabase(d)
|
|
10280
11347
|
});
|
|
10281
11348
|
return updated;
|
|
10282
11349
|
}
|
|
10283
11350
|
function finishTaskRunTransaction(input, db) {
|
|
10284
11351
|
const d = db || getDatabase();
|
|
10285
|
-
const
|
|
11352
|
+
const timestamp2 = input.completed_at || now();
|
|
10286
11353
|
const status = input.status || "completed";
|
|
10287
11354
|
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
10288
11355
|
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
@@ -10300,7 +11367,7 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10300
11367
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10301
11368
|
local_only: true,
|
|
10302
11369
|
dry_run: dryRun,
|
|
10303
|
-
processed_at:
|
|
11370
|
+
processed_at: timestamp2,
|
|
10304
11371
|
action: conflict ? "conflict" : "matched",
|
|
10305
11372
|
key: resolvedKey,
|
|
10306
11373
|
run: summarizeTaskRun(run),
|
|
@@ -10313,10 +11380,10 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10313
11380
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10314
11381
|
local_only: true,
|
|
10315
11382
|
dry_run: true,
|
|
10316
|
-
processed_at:
|
|
11383
|
+
processed_at: timestamp2,
|
|
10317
11384
|
action: "preview",
|
|
10318
11385
|
key: resolvedKey,
|
|
10319
|
-
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at:
|
|
11386
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp2, updated_at: timestamp2 }),
|
|
10320
11387
|
warnings: [],
|
|
10321
11388
|
commands: loopRunCommands(run, resolvedKey)
|
|
10322
11389
|
};
|
|
@@ -10326,13 +11393,13 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10326
11393
|
status,
|
|
10327
11394
|
summary: input.summary,
|
|
10328
11395
|
agent_id: input.agent_id,
|
|
10329
|
-
completed_at:
|
|
11396
|
+
completed_at: timestamp2
|
|
10330
11397
|
}, d);
|
|
10331
11398
|
return {
|
|
10332
11399
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10333
11400
|
local_only: true,
|
|
10334
11401
|
dry_run: false,
|
|
10335
|
-
processed_at:
|
|
11402
|
+
processed_at: timestamp2,
|
|
10336
11403
|
action: "finished",
|
|
10337
11404
|
key: resolvedKey,
|
|
10338
11405
|
run: summarizeTaskRun(finished),
|
|
@@ -10362,7 +11429,7 @@ var init_task_runs = __esm(() => {
|
|
|
10362
11429
|
init_artifact_store();
|
|
10363
11430
|
init_event_emission_safety();
|
|
10364
11431
|
init_event_hooks();
|
|
10365
|
-
|
|
11432
|
+
init_prewrite_secrets();
|
|
10366
11433
|
init_types();
|
|
10367
11434
|
init_comments();
|
|
10368
11435
|
init_database();
|
|
@@ -10490,7 +11557,7 @@ function inWindow(event, query) {
|
|
|
10490
11557
|
function createCalendarItem(input, db) {
|
|
10491
11558
|
const d = db || getDatabase();
|
|
10492
11559
|
const id = uuid();
|
|
10493
|
-
const
|
|
11560
|
+
const timestamp2 = now();
|
|
10494
11561
|
const kind = input.kind || "work_block";
|
|
10495
11562
|
d.run(`INSERT INTO local_calendar_items (
|
|
10496
11563
|
id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
|
|
@@ -10509,8 +11576,8 @@ function createCalendarItem(input, db) {
|
|
|
10509
11576
|
input.run_id || null,
|
|
10510
11577
|
input.recurrence_rule || null,
|
|
10511
11578
|
JSON.stringify(input.metadata || {}),
|
|
10512
|
-
|
|
10513
|
-
|
|
11579
|
+
timestamp2,
|
|
11580
|
+
timestamp2
|
|
10514
11581
|
]);
|
|
10515
11582
|
return getCalendarItem(id, d);
|
|
10516
11583
|
}
|
|
@@ -10757,18 +11824,172 @@ var init_tasks = __esm(() => {
|
|
|
10757
11824
|
// src/storage/index.ts
|
|
10758
11825
|
init_config();
|
|
10759
11826
|
|
|
11827
|
+
// src/lib/search.ts
|
|
11828
|
+
init_database();
|
|
11829
|
+
function rowToTask(row) {
|
|
11830
|
+
return {
|
|
11831
|
+
...row,
|
|
11832
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
11833
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
11834
|
+
status: row.status,
|
|
11835
|
+
priority: row.priority,
|
|
11836
|
+
requires_approval: Boolean(row.requires_approval)
|
|
11837
|
+
};
|
|
11838
|
+
}
|
|
11839
|
+
var DEFAULT_SEARCH_LIMIT = 1000;
|
|
11840
|
+
var BM25_WEIGHTS = "0.0, 10.0, 5.0, 3.0";
|
|
11841
|
+
function hasFts(db) {
|
|
11842
|
+
try {
|
|
11843
|
+
const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
|
|
11844
|
+
return result !== null;
|
|
11845
|
+
} catch {
|
|
11846
|
+
return false;
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
function buildFtsMatchQuery(raw) {
|
|
11850
|
+
const terms = [];
|
|
11851
|
+
const re = /"([^"]*)"|(\S+)/g;
|
|
11852
|
+
let m;
|
|
11853
|
+
while ((m = re.exec(raw)) !== null) {
|
|
11854
|
+
if (m[1] !== undefined) {
|
|
11855
|
+
const phrase = m[1].replace(/"/g, " ").replace(/\s+/g, " ").trim();
|
|
11856
|
+
if (phrase)
|
|
11857
|
+
terms.push(`"${phrase}"`);
|
|
11858
|
+
} else {
|
|
11859
|
+
const cleaned = m[2].replace(/["*^():]/g, " ").replace(/\s+/g, " ").trim();
|
|
11860
|
+
if (cleaned)
|
|
11861
|
+
terms.push(`"${cleaned}"*`);
|
|
11862
|
+
}
|
|
11863
|
+
}
|
|
11864
|
+
return terms.join(" AND ");
|
|
11865
|
+
}
|
|
11866
|
+
function boundedLimit(limit) {
|
|
11867
|
+
if (!Number.isFinite(limit) || !limit || limit <= 0)
|
|
11868
|
+
return DEFAULT_SEARCH_LIMIT;
|
|
11869
|
+
return Math.trunc(limit);
|
|
11870
|
+
}
|
|
11871
|
+
function searchTasks(options, projectId, taskListId, db) {
|
|
11872
|
+
const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
|
|
11873
|
+
const d = db || getDatabase();
|
|
11874
|
+
clearExpiredLocks(d);
|
|
11875
|
+
const params = [];
|
|
11876
|
+
let sql;
|
|
11877
|
+
const raw = opts.query?.trim() ?? "";
|
|
11878
|
+
const q = raw === "*" ? "" : raw;
|
|
11879
|
+
const ftsMatch = q ? buildFtsMatchQuery(q) : "";
|
|
11880
|
+
const useFts = hasFts(d) && q !== "" && ftsMatch !== "";
|
|
11881
|
+
if (useFts) {
|
|
11882
|
+
const pattern = `%${q}%`;
|
|
11883
|
+
sql = `SELECT t.* FROM tasks t
|
|
11884
|
+
LEFT JOIN (
|
|
11885
|
+
SELECT rowid, bm25(tasks_fts, ${BM25_WEIGHTS}) AS rank
|
|
11886
|
+
FROM tasks_fts WHERE tasks_fts MATCH ?
|
|
11887
|
+
) fts ON fts.rowid = t.rowid
|
|
11888
|
+
WHERE (
|
|
11889
|
+
fts.rowid IS NOT NULL
|
|
11890
|
+
OR t.id LIKE ?
|
|
11891
|
+
OR t.short_id LIKE ?
|
|
11892
|
+
OR t.title LIKE ?
|
|
11893
|
+
OR t.description LIKE ?
|
|
11894
|
+
OR t.working_dir LIKE ?
|
|
11895
|
+
OR t.metadata LIKE ?
|
|
11896
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
11897
|
+
)`;
|
|
11898
|
+
params.push(ftsMatch, pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
11899
|
+
} else if (q) {
|
|
11900
|
+
const pattern = `%${q}%`;
|
|
11901
|
+
sql = `SELECT t.* FROM tasks t WHERE (
|
|
11902
|
+
t.id LIKE ?
|
|
11903
|
+
OR t.short_id LIKE ?
|
|
11904
|
+
OR t.title LIKE ?
|
|
11905
|
+
OR t.description LIKE ?
|
|
11906
|
+
OR t.working_dir LIKE ?
|
|
11907
|
+
OR t.metadata LIKE ?
|
|
11908
|
+
OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?)
|
|
11909
|
+
)`;
|
|
11910
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
|
11911
|
+
} else {
|
|
11912
|
+
sql = `SELECT t.* FROM tasks t WHERE 1=1`;
|
|
11913
|
+
}
|
|
11914
|
+
if (opts.project_id) {
|
|
11915
|
+
sql += " AND t.project_id = ?";
|
|
11916
|
+
params.push(opts.project_id);
|
|
11917
|
+
}
|
|
11918
|
+
if (opts.task_list_id) {
|
|
11919
|
+
sql += " AND t.task_list_id = ?";
|
|
11920
|
+
params.push(opts.task_list_id);
|
|
11921
|
+
}
|
|
11922
|
+
if (opts.status) {
|
|
11923
|
+
if (Array.isArray(opts.status)) {
|
|
11924
|
+
sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
|
|
11925
|
+
params.push(...opts.status);
|
|
11926
|
+
} else {
|
|
11927
|
+
sql += " AND t.status = ?";
|
|
11928
|
+
params.push(opts.status);
|
|
11929
|
+
}
|
|
11930
|
+
}
|
|
11931
|
+
if (opts.priority) {
|
|
11932
|
+
if (Array.isArray(opts.priority)) {
|
|
11933
|
+
sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
|
|
11934
|
+
params.push(...opts.priority);
|
|
11935
|
+
} else {
|
|
11936
|
+
sql += " AND t.priority = ?";
|
|
11937
|
+
params.push(opts.priority);
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
if (opts.assigned_to) {
|
|
11941
|
+
sql += " AND t.assigned_to = ?";
|
|
11942
|
+
params.push(opts.assigned_to);
|
|
11943
|
+
}
|
|
11944
|
+
if (opts.agent_id) {
|
|
11945
|
+
sql += " AND t.agent_id = ?";
|
|
11946
|
+
params.push(opts.agent_id);
|
|
11947
|
+
}
|
|
11948
|
+
if (opts.created_after) {
|
|
11949
|
+
sql += " AND t.created_at > ?";
|
|
11950
|
+
params.push(opts.created_after);
|
|
11951
|
+
}
|
|
11952
|
+
if (opts.updated_after) {
|
|
11953
|
+
sql += " AND t.updated_at > ?";
|
|
11954
|
+
params.push(opts.updated_after);
|
|
11955
|
+
}
|
|
11956
|
+
if (opts.has_dependencies === true) {
|
|
11957
|
+
sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
|
|
11958
|
+
} else if (opts.has_dependencies === false) {
|
|
11959
|
+
sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
|
|
11960
|
+
}
|
|
11961
|
+
if (opts.is_blocked === true) {
|
|
11962
|
+
sql += " AND t.id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
11963
|
+
} else if (opts.is_blocked === false) {
|
|
11964
|
+
sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
11965
|
+
}
|
|
11966
|
+
if (useFts) {
|
|
11967
|
+
sql += ` ORDER BY (fts.rowid IS NULL) ASC, fts.rank ASC,
|
|
11968
|
+
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
11969
|
+
t.created_at DESC`;
|
|
11970
|
+
} else {
|
|
11971
|
+
sql += ` ORDER BY
|
|
11972
|
+
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
11973
|
+
t.created_at DESC`;
|
|
11974
|
+
}
|
|
11975
|
+
sql += ` LIMIT ?`;
|
|
11976
|
+
params.push(boundedLimit(opts.limit));
|
|
11977
|
+
const rows = d.query(sql).all(...params);
|
|
11978
|
+
return rows.map(rowToTask);
|
|
11979
|
+
}
|
|
11980
|
+
|
|
10760
11981
|
// src/storage/local-sqlite.ts
|
|
10761
11982
|
init_tasks();
|
|
10762
11983
|
init_projects();
|
|
10763
11984
|
init_plans();
|
|
10764
11985
|
|
|
10765
11986
|
// src/db/agents.ts
|
|
11987
|
+
init_types();
|
|
10766
11988
|
init_database();
|
|
11989
|
+
init_identity_mapping();
|
|
10767
11990
|
init_storage_tombstones();
|
|
10768
11991
|
|
|
10769
11992
|
// src/db/agent-names.ts
|
|
10770
|
-
init_database();
|
|
10771
|
-
|
|
10772
11993
|
class InvalidAgentNameError extends Error {
|
|
10773
11994
|
suggestions;
|
|
10774
11995
|
constructor(name, reason, suggestions = []) {
|
|
@@ -11049,45 +12270,10 @@ function validateAgentName(name, existingNames = []) {
|
|
|
11049
12270
|
}
|
|
11050
12271
|
return normalized;
|
|
11051
12272
|
}
|
|
11052
|
-
function tableHasColumn(db, table, column) {
|
|
11053
|
-
try {
|
|
11054
|
-
return db.query(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
11055
|
-
} catch {
|
|
11056
|
-
return false;
|
|
11057
|
-
}
|
|
11058
|
-
}
|
|
11059
|
-
function updateReferences(db, oldName, newName) {
|
|
11060
|
-
const refs = [
|
|
11061
|
-
["tasks", "assigned_to"],
|
|
11062
|
-
["tasks", "agent_id"],
|
|
11063
|
-
["tasks", "locked_by"],
|
|
11064
|
-
["tasks", "assigned_by"],
|
|
11065
|
-
["plans", "agent_id"],
|
|
11066
|
-
["sessions", "agent_id"],
|
|
11067
|
-
["task_comments", "agent_id"],
|
|
11068
|
-
["task_history", "agent_id"],
|
|
11069
|
-
["webhooks", "agent_id"],
|
|
11070
|
-
["task_files", "agent_id"],
|
|
11071
|
-
["task_time_logs", "agent_id"],
|
|
11072
|
-
["task_watchers", "agent_id"],
|
|
11073
|
-
["task_checkpoints", "agent_id"],
|
|
11074
|
-
["task_heartbeats", "agent_id"],
|
|
11075
|
-
["project_agent_roles", "agent_id"]
|
|
11076
|
-
];
|
|
11077
|
-
let changed = 0;
|
|
11078
|
-
for (const [table, column] of refs) {
|
|
11079
|
-
if (!tableHasColumn(db, table, column))
|
|
11080
|
-
continue;
|
|
11081
|
-
try {
|
|
11082
|
-
changed += db.run(`UPDATE ${table} SET ${column} = ? WHERE LOWER(${column}) = ?`, [newName, oldName]).changes;
|
|
11083
|
-
} catch {}
|
|
11084
|
-
}
|
|
11085
|
-
return changed;
|
|
11086
|
-
}
|
|
11087
12273
|
function normalizeGeneratedAgentNames(db) {
|
|
11088
12274
|
const rows = db.query("SELECT * FROM agents ORDER BY created_at, id").all();
|
|
11089
12275
|
const existing = new Set(rows.map((agent) => normalizeAgentNameInput(agent.name)));
|
|
11090
|
-
const
|
|
12276
|
+
const planned = [];
|
|
11091
12277
|
for (const agent of rows) {
|
|
11092
12278
|
const oldName = normalizeAgentNameInput(agent.name);
|
|
11093
12279
|
if (!isBlockedAgentName(oldName))
|
|
@@ -11097,18 +12283,20 @@ function normalizeGeneratedAgentNames(db) {
|
|
|
11097
12283
|
if (!replacement) {
|
|
11098
12284
|
throw new Error("No safe agent names are available for normalization");
|
|
11099
12285
|
}
|
|
11100
|
-
existing.delete(oldName);
|
|
11101
12286
|
existing.add(replacement);
|
|
11102
|
-
|
|
11103
|
-
const referenceUpdates = updateReferences(db, oldName, replacement);
|
|
11104
|
-
renamed.push({
|
|
12287
|
+
planned.push({
|
|
11105
12288
|
id: agent.id,
|
|
11106
12289
|
old_name: oldName,
|
|
11107
12290
|
new_name: replacement,
|
|
11108
|
-
|
|
12291
|
+
applied: false,
|
|
12292
|
+
disposition: "candidate",
|
|
12293
|
+
alias_kind: "candidate",
|
|
12294
|
+
status: "quarantined",
|
|
12295
|
+
name_updates: 0,
|
|
12296
|
+
reference_updates: 0
|
|
11109
12297
|
});
|
|
11110
12298
|
}
|
|
11111
|
-
return
|
|
12299
|
+
return planned;
|
|
11112
12300
|
}
|
|
11113
12301
|
|
|
11114
12302
|
// src/db/agents.ts
|
|
@@ -11130,7 +12318,6 @@ function autoReleaseStaleAgents(db) {
|
|
|
11130
12318
|
return result.changes;
|
|
11131
12319
|
}
|
|
11132
12320
|
function getAvailableNamesFromPool(pool, db) {
|
|
11133
|
-
autoReleaseStaleAgents(db);
|
|
11134
12321
|
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
11135
12322
|
const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
11136
12323
|
return pool.map(normalizeAgentNameInput).filter((name) => !activeNames.has(name));
|
|
@@ -11141,6 +12328,10 @@ function shortUuid() {
|
|
|
11141
12328
|
function rowToAgent(row) {
|
|
11142
12329
|
return {
|
|
11143
12330
|
...row,
|
|
12331
|
+
identity_id: row.identity_id ?? null,
|
|
12332
|
+
project_id: row.project_id ?? null,
|
|
12333
|
+
runtime_instance_id: row.runtime_instance_id ?? null,
|
|
12334
|
+
machine_id: row.machine_id ?? null,
|
|
11144
12335
|
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
11145
12336
|
capabilities: JSON.parse(row.capabilities || "[]"),
|
|
11146
12337
|
status: row.status || "active",
|
|
@@ -11149,11 +12340,37 @@ function rowToAgent(row) {
|
|
|
11149
12340
|
}
|
|
11150
12341
|
function registerAgent(input, db) {
|
|
11151
12342
|
const d = db || getDatabase();
|
|
12343
|
+
const register = d.transaction(() => registerAgentInCurrentTransaction(input, d));
|
|
12344
|
+
return register.immediate();
|
|
12345
|
+
}
|
|
12346
|
+
function registerAgentInCurrentTransaction(input, d) {
|
|
11152
12347
|
const machineId = currentStorageMachineId(d);
|
|
12348
|
+
const identityId = input.identity_id === undefined ? null : input.identity_id.trim();
|
|
12349
|
+
if (input.identity_id !== undefined && !identityId) {
|
|
12350
|
+
throw new Error("identity_id must be a non-empty canonical identifier");
|
|
12351
|
+
}
|
|
11153
12352
|
const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
|
|
11154
12353
|
const normalizedName = validateAgentName(input.name, existingNames);
|
|
11155
|
-
const
|
|
12354
|
+
const existingByName = getAgentByNameProjection(normalizedName, d);
|
|
12355
|
+
const identityRows = identityId ? d.query("SELECT id FROM agents WHERE identity_id = ? ORDER BY id").all(identityId) : [];
|
|
12356
|
+
if (identityRows.length > 1) {
|
|
12357
|
+
throw new IdentityAliasAmbiguousError(identityId, identityRows.map((row) => row.id));
|
|
12358
|
+
}
|
|
12359
|
+
const existingByIdentity = identityRows[0] ? getAgent(identityRows[0].id, d) : null;
|
|
12360
|
+
if (existingByIdentity && existingByName && existingByIdentity.id !== existingByName.id) {
|
|
12361
|
+
throw new IdentityAliasAmbiguousError(normalizedName, [existingByIdentity.id, existingByName.id]);
|
|
12362
|
+
}
|
|
12363
|
+
if (identityId && existingByName && !existingByIdentity && existingByName.identity_id !== identityId) {
|
|
12364
|
+
throw new IdentityAliasAmbiguousError(normalizedName, [existingByName.id]);
|
|
12365
|
+
}
|
|
12366
|
+
const existing = existingByIdentity || existingByName;
|
|
11156
12367
|
if (existing) {
|
|
12368
|
+
if (existing.identity_id && !identityId) {
|
|
12369
|
+
return buildConflictError(existing, new Date(existing.last_seen_at).getTime(), input.pool, d);
|
|
12370
|
+
}
|
|
12371
|
+
if (identityId && existing.identity_id && identityId !== existing.identity_id) {
|
|
12372
|
+
throw new IdentityIdImmutableError(existing.id);
|
|
12373
|
+
}
|
|
11157
12374
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
11158
12375
|
const activeWindowMs = getActiveWindowMs();
|
|
11159
12376
|
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
@@ -11190,6 +12407,14 @@ function registerAgent(input, db) {
|
|
|
11190
12407
|
updates.push("active_project_id = ?");
|
|
11191
12408
|
params.push(input.project_id);
|
|
11192
12409
|
}
|
|
12410
|
+
if (input.project_id) {
|
|
12411
|
+
updates.push("project_id = ?");
|
|
12412
|
+
params.push(input.project_id);
|
|
12413
|
+
}
|
|
12414
|
+
if (input.runtime_instance_id) {
|
|
12415
|
+
updates.push("runtime_instance_id = ?");
|
|
12416
|
+
params.push(input.runtime_instance_id);
|
|
12417
|
+
}
|
|
11193
12418
|
if (!existing.machine_id && machineId) {
|
|
11194
12419
|
updates.push("machine_id = ?");
|
|
11195
12420
|
params.push(machineId);
|
|
@@ -11199,11 +12424,12 @@ function registerAgent(input, db) {
|
|
|
11199
12424
|
return getAgent(existing.id, d);
|
|
11200
12425
|
}
|
|
11201
12426
|
const id = shortUuid();
|
|
11202
|
-
const
|
|
11203
|
-
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id, machine_id)
|
|
11204
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12427
|
+
const timestamp2 = now();
|
|
12428
|
+
d.run(`INSERT INTO agents (id, name, identity_id, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, project_id, runtime_instance_id, active_project_id, machine_id)
|
|
12429
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
11205
12430
|
id,
|
|
11206
12431
|
normalizedName,
|
|
12432
|
+
identityId,
|
|
11207
12433
|
input.description || null,
|
|
11208
12434
|
input.role || "agent",
|
|
11209
12435
|
input.title || null,
|
|
@@ -11213,10 +12439,12 @@ function registerAgent(input, db) {
|
|
|
11213
12439
|
input.reports_to || null,
|
|
11214
12440
|
input.org_id || null,
|
|
11215
12441
|
JSON.stringify(input.metadata || {}),
|
|
11216
|
-
|
|
11217
|
-
|
|
12442
|
+
timestamp2,
|
|
12443
|
+
timestamp2,
|
|
11218
12444
|
input.session_id || null,
|
|
11219
12445
|
input.working_dir || null,
|
|
12446
|
+
input.project_id || null,
|
|
12447
|
+
input.runtime_instance_id || null,
|
|
11220
12448
|
input.project_id && input.session_id ? input.project_id : null,
|
|
11221
12449
|
machineId
|
|
11222
12450
|
]);
|
|
@@ -11256,12 +12484,15 @@ function getAgent(id, db) {
|
|
|
11256
12484
|
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
11257
12485
|
return row ? rowToAgent(row) : null;
|
|
11258
12486
|
}
|
|
11259
|
-
function
|
|
11260
|
-
const d = db || getDatabase();
|
|
12487
|
+
function getAgentByNameProjection(name, d) {
|
|
11261
12488
|
const normalizedName = name.trim().toLowerCase();
|
|
11262
12489
|
const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
11263
12490
|
return row ? rowToAgent(row) : null;
|
|
11264
12491
|
}
|
|
12492
|
+
function getAgentByName(name, db) {
|
|
12493
|
+
const agent = getAgentByNameProjection(name, db || getDatabase());
|
|
12494
|
+
return agent ? { ...agent, identity_id: null } : null;
|
|
12495
|
+
}
|
|
11265
12496
|
function listAgents(opts, db) {
|
|
11266
12497
|
let d;
|
|
11267
12498
|
let includeArchived = false;
|
|
@@ -11271,7 +12502,6 @@ function listAgents(opts, db) {
|
|
|
11271
12502
|
includeArchived = opts?.include_archived ?? false;
|
|
11272
12503
|
d = db || getDatabase();
|
|
11273
12504
|
}
|
|
11274
|
-
autoReleaseStaleAgents(d);
|
|
11275
12505
|
if (includeArchived) {
|
|
11276
12506
|
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
11277
12507
|
}
|
|
@@ -11288,21 +12518,19 @@ function updateAgent(id, input, db) {
|
|
|
11288
12518
|
throw new Error(`Agent not found: ${id}`);
|
|
11289
12519
|
const sets = ["last_seen_at = ?"];
|
|
11290
12520
|
const params = [now()];
|
|
12521
|
+
let historicalLabel = null;
|
|
11291
12522
|
if (input.name !== undefined) {
|
|
11292
12523
|
const existingNames = d.query("SELECT name FROM agents WHERE id != ?").all(id).map((row) => row.name);
|
|
11293
12524
|
const newName = validateAgentName(input.name, existingNames);
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
if (isActive && holder.status === "active") {
|
|
11299
|
-
throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
|
|
12525
|
+
if (newName !== agent.name) {
|
|
12526
|
+
const holder = getAgentByName(newName, d);
|
|
12527
|
+
if (holder && holder.id !== id) {
|
|
12528
|
+
throw new IdentityAliasAmbiguousError(newName, [holder.id, id]);
|
|
11300
12529
|
}
|
|
11301
|
-
|
|
11302
|
-
|
|
12530
|
+
historicalLabel = agent.name;
|
|
12531
|
+
sets.push("name = ?");
|
|
12532
|
+
params.push(newName);
|
|
11303
12533
|
}
|
|
11304
|
-
sets.push("name = ?");
|
|
11305
|
-
params.push(newName);
|
|
11306
12534
|
}
|
|
11307
12535
|
if (input.description !== undefined) {
|
|
11308
12536
|
sets.push("description = ?");
|
|
@@ -11341,7 +12569,12 @@ function updateAgent(id, input, db) {
|
|
|
11341
12569
|
params.push(JSON.stringify(input.metadata));
|
|
11342
12570
|
}
|
|
11343
12571
|
params.push(id);
|
|
11344
|
-
|
|
12572
|
+
const apply = d.transaction(() => {
|
|
12573
|
+
if (historicalLabel)
|
|
12574
|
+
recordAgentAlias(id, historicalLabel, d);
|
|
12575
|
+
d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
12576
|
+
});
|
|
12577
|
+
apply.immediate();
|
|
11345
12578
|
return getAgent(id, d);
|
|
11346
12579
|
}
|
|
11347
12580
|
function deleteAgent(id, db) {
|
|
@@ -11499,6 +12732,21 @@ var TEMPLATE_COLUMNS = [
|
|
|
11499
12732
|
"machine_id",
|
|
11500
12733
|
"synced_at"
|
|
11501
12734
|
];
|
|
12735
|
+
var TEMPLATE_TASK_COLUMNS = [
|
|
12736
|
+
"id",
|
|
12737
|
+
"template_id",
|
|
12738
|
+
"position",
|
|
12739
|
+
"title_pattern",
|
|
12740
|
+
"description",
|
|
12741
|
+
"priority",
|
|
12742
|
+
"tags",
|
|
12743
|
+
"task_type",
|
|
12744
|
+
"condition",
|
|
12745
|
+
"include_template_id",
|
|
12746
|
+
"depends_on_positions",
|
|
12747
|
+
"metadata",
|
|
12748
|
+
"created_at"
|
|
12749
|
+
];
|
|
11502
12750
|
var TASK_COLUMNS = [
|
|
11503
12751
|
"id",
|
|
11504
12752
|
"short_id",
|
|
@@ -11567,7 +12815,7 @@ var AUDIT_COLUMNS = [
|
|
|
11567
12815
|
"created_at",
|
|
11568
12816
|
"machine_id"
|
|
11569
12817
|
];
|
|
11570
|
-
var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
|
|
12818
|
+
var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
11571
12819
|
var BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
11572
12820
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
11573
12821
|
const d = db ?? getDatabase();
|
|
@@ -11581,6 +12829,7 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
11581
12829
|
agents: listAgents({ include_archived: true }, d),
|
|
11582
12830
|
taskLists: listTaskLists(undefined, d),
|
|
11583
12831
|
templates: listTemplates(d),
|
|
12832
|
+
templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
|
|
11584
12833
|
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
11585
12834
|
tombstones: listStorageTombstones(d)
|
|
11586
12835
|
};
|
|
@@ -11630,6 +12879,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
11630
12879
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
11631
12880
|
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
11632
12881
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
12882
|
+
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
11633
12883
|
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
11634
12884
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
11635
12885
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
@@ -11734,6 +12984,8 @@ function tableForTombstone(objectType) {
|
|
|
11734
12984
|
return "task_lists";
|
|
11735
12985
|
if (objectType === "templates")
|
|
11736
12986
|
return "task_templates";
|
|
12987
|
+
if (objectType === "template_tasks")
|
|
12988
|
+
return "template_tasks";
|
|
11737
12989
|
return "task_history";
|
|
11738
12990
|
}
|
|
11739
12991
|
function listRows(db, table, columns) {
|
|
@@ -11793,6 +13045,52 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
11793
13045
|
return getTask(shortIdRows[0].id, db);
|
|
11794
13046
|
return null;
|
|
11795
13047
|
}
|
|
13048
|
+
function isSearchQuery(filter) {
|
|
13049
|
+
const q = filter.query?.trim();
|
|
13050
|
+
return !!q && q !== "*";
|
|
13051
|
+
}
|
|
13052
|
+
function matchesExtraFilters(task, filter) {
|
|
13053
|
+
if (filter.ids && !filter.ids.includes(task.id))
|
|
13054
|
+
return false;
|
|
13055
|
+
if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
|
|
13056
|
+
return false;
|
|
13057
|
+
if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
|
|
13058
|
+
return false;
|
|
13059
|
+
if (filter.session_id !== undefined && task.session_id !== filter.session_id)
|
|
13060
|
+
return false;
|
|
13061
|
+
if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
|
|
13062
|
+
return false;
|
|
13063
|
+
if (filter.task_type !== undefined) {
|
|
13064
|
+
const allowed = Array.isArray(filter.task_type) ? filter.task_type : [filter.task_type];
|
|
13065
|
+
if (!allowed.includes(task.task_type ?? ""))
|
|
13066
|
+
return false;
|
|
13067
|
+
}
|
|
13068
|
+
if (filter.tags?.length) {
|
|
13069
|
+
const taskTags = new Set(task.tags ?? []);
|
|
13070
|
+
if (!filter.tags.every((tag) => taskTags.has(tag)))
|
|
13071
|
+
return false;
|
|
13072
|
+
}
|
|
13073
|
+
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|
|
13074
|
+
return false;
|
|
13075
|
+
return true;
|
|
13076
|
+
}
|
|
13077
|
+
function listTasksMaybeSearch(filter, db) {
|
|
13078
|
+
if (!isSearchQuery(filter))
|
|
13079
|
+
return listTasks(filter, db);
|
|
13080
|
+
const matched = searchTasks({
|
|
13081
|
+
query: filter.query,
|
|
13082
|
+
project_id: filter.project_id,
|
|
13083
|
+
task_list_id: filter.task_list_id,
|
|
13084
|
+
status: filter.status,
|
|
13085
|
+
priority: filter.priority,
|
|
13086
|
+
assigned_to: filter.assigned_to,
|
|
13087
|
+
agent_id: filter.agent_id
|
|
13088
|
+
}, undefined, undefined, db).filter((task) => matchesExtraFilters(task, filter));
|
|
13089
|
+
const offset = filter.offset && filter.offset > 0 ? Math.trunc(filter.offset) : 0;
|
|
13090
|
+
if (filter.limit !== undefined && filter.limit >= 0)
|
|
13091
|
+
return matched.slice(offset, offset + filter.limit);
|
|
13092
|
+
return offset ? matched.slice(offset) : matched;
|
|
13093
|
+
}
|
|
11796
13094
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
11797
13095
|
const database = () => options.db ?? getDatabase();
|
|
11798
13096
|
let adapter;
|
|
@@ -11809,8 +13107,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
11809
13107
|
create: (input) => createTask(input, database()),
|
|
11810
13108
|
get: (id) => getTask(id, database()),
|
|
11811
13109
|
resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
|
|
11812
|
-
list: (filter = {}) =>
|
|
11813
|
-
count: (filter = {}) => countTasks(filter, database()),
|
|
13110
|
+
list: (filter = {}) => listTasksMaybeSearch(filter, database()),
|
|
13111
|
+
count: (filter = {}) => isSearchQuery(filter) ? listTasksMaybeSearch({ ...filter, limit: undefined, offset: undefined }, database()).length : countTasks(filter, database()),
|
|
11814
13112
|
update: (id, input) => updateTask(id, input, database()),
|
|
11815
13113
|
unlock: (id, agentId) => {
|
|
11816
13114
|
unlockTask(id, agentId, database());
|
|
@@ -11920,6 +13218,27 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
11920
13218
|
`CREATE INDEX IF NOT EXISTS ${tableName}_task_status_idx ON ${tableName} ((payload->>'status')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
11921
13219
|
`CREATE INDEX IF NOT EXISTS ${tableName}_task_project_idx ON ${tableName} ((payload->>'project_id')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
11922
13220
|
`CREATE INDEX IF NOT EXISTS ${tableName}_payload_gin ON ${tableName} USING gin (payload jsonb_path_ops)`,
|
|
13221
|
+
`CREATE EXTENSION IF NOT EXISTS pg_trgm`,
|
|
13222
|
+
`CREATE EXTENSION IF NOT EXISTS unaccent`,
|
|
13223
|
+
`CREATE OR REPLACE FUNCTION todos_immutable_unaccent(text)
|
|
13224
|
+
RETURNS text
|
|
13225
|
+
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
|
|
13226
|
+
AS $$ SELECT unaccent('unaccent', $1) $$`,
|
|
13227
|
+
`ALTER TABLE ${tableName}
|
|
13228
|
+
ADD COLUMN IF NOT EXISTS task_search_tsv tsvector
|
|
13229
|
+
GENERATED ALWAYS AS (
|
|
13230
|
+
setweight(to_tsvector('simple', todos_immutable_unaccent(COALESCE(payload->>'title', ''))), 'A')
|
|
13231
|
+
|| setweight(to_tsvector('simple', todos_immutable_unaccent(COALESCE(payload->>'description', ''))), 'B')
|
|
13232
|
+
|| setweight(to_tsvector('simple', todos_immutable_unaccent(translate(COALESCE(payload->>'tags', '[]'), '[]",', ' '))), 'C')
|
|
13233
|
+
) STORED`,
|
|
13234
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_task_search_tsv_idx
|
|
13235
|
+
ON ${tableName} USING gin (task_search_tsv)
|
|
13236
|
+
WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
13237
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_task_search_trgm_idx
|
|
13238
|
+
ON ${tableName} USING gin (
|
|
13239
|
+
todos_immutable_unaccent(COALESCE(payload->>'title', '') || ' ' || COALESCE(payload->>'description', '')) gin_trgm_ops
|
|
13240
|
+
)
|
|
13241
|
+
WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
11923
13242
|
`CREATE TABLE IF NOT EXISTS ${cursorTableName} (
|
|
11924
13243
|
service text NOT NULL,
|
|
11925
13244
|
cursor_name text NOT NULL,
|
|
@@ -12152,6 +13471,7 @@ function snapshotEntries(snapshot) {
|
|
|
12152
13471
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
12153
13472
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
12154
13473
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
13474
|
+
...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
|
|
12155
13475
|
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
|
|
12156
13476
|
...(snapshot.tombstones ?? []).map((tombstone) => ({
|
|
12157
13477
|
type: tombstone.object_type,
|
|
@@ -12203,6 +13523,7 @@ function rowsToSnapshot(rows) {
|
|
|
12203
13523
|
agents: [],
|
|
12204
13524
|
taskLists: [],
|
|
12205
13525
|
templates: [],
|
|
13526
|
+
templateTasks: [],
|
|
12206
13527
|
auditHistory: [],
|
|
12207
13528
|
tombstones: []
|
|
12208
13529
|
};
|
|
@@ -12237,6 +13558,8 @@ function rowsToSnapshot(rows) {
|
|
|
12237
13558
|
snapshot.taskLists.push(payload);
|
|
12238
13559
|
else if (row.object_type === "templates")
|
|
12239
13560
|
snapshot.templates.push(payload);
|
|
13561
|
+
else if (row.object_type === "template_tasks")
|
|
13562
|
+
snapshot.templateTasks.push(payload);
|
|
12240
13563
|
else if (row.object_type === "audit_history")
|
|
12241
13564
|
snapshot.auditHistory.push(payload);
|
|
12242
13565
|
}
|
|
@@ -12399,10 +13722,13 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
12399
13722
|
get: (id) => store.get("templates", id),
|
|
12400
13723
|
list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
12401
13724
|
update: (id, input) => updateTemplate2(id, input, store),
|
|
12402
|
-
delete: (id, context) =>
|
|
13725
|
+
delete: (id, context) => deleteTemplate2(id, store, context),
|
|
12403
13726
|
getWithTasks: async (id) => {
|
|
12404
13727
|
const template = await store.get("templates", id);
|
|
12405
|
-
|
|
13728
|
+
if (!template)
|
|
13729
|
+
return null;
|
|
13730
|
+
const tasks = (await store.list("template_tasks")).filter((task) => task.template_id === id).sort((left, right) => left.position - right.position || left.id.localeCompare(right.id));
|
|
13731
|
+
return { ...template, tasks };
|
|
12406
13732
|
}
|
|
12407
13733
|
},
|
|
12408
13734
|
audit: {
|
|
@@ -12550,12 +13876,23 @@ class PostgresJsonRecordStore {
|
|
|
12550
13876
|
}
|
|
12551
13877
|
if (filter.include_subtasks !== true)
|
|
12552
13878
|
conds.push(`(payload->>'parent_id' IS NULL OR payload->>'parent_id' = '')`);
|
|
12553
|
-
|
|
13879
|
+
let queryRef;
|
|
13880
|
+
const rawQuery = filter.query?.trim() ?? "";
|
|
13881
|
+
if (rawQuery && rawQuery !== "*") {
|
|
13882
|
+
queryRef = p(rawQuery);
|
|
13883
|
+
const clauses = [`task_search_tsv @@ websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))`];
|
|
13884
|
+
if (!/\s/.test(rawQuery)) {
|
|
13885
|
+
clauses.push(`todos_immutable_unaccent(${queryRef}) <% todos_immutable_unaccent(` + `COALESCE(payload->>'title', '') || ' ' || COALESCE(payload->>'description', ''))`);
|
|
13886
|
+
}
|
|
13887
|
+
conds.push(`(${clauses.join(" OR ")})`);
|
|
13888
|
+
}
|
|
13889
|
+
return { where: conds.join(" AND "), params, queryRef };
|
|
12554
13890
|
}
|
|
12555
13891
|
async listTasks(filter) {
|
|
12556
13892
|
await this.ensureSchema();
|
|
12557
|
-
const { where, params } = this.buildTaskFilterSql(filter);
|
|
12558
|
-
|
|
13893
|
+
const { where, params, queryRef } = this.buildTaskFilterSql(filter);
|
|
13894
|
+
const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
13895
|
+
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
|
|
12559
13896
|
if (filter.limit !== undefined) {
|
|
12560
13897
|
params.push(filter.limit);
|
|
12561
13898
|
sql += ` LIMIT $${params.length}`;
|
|
@@ -12690,6 +14027,57 @@ class PostgresJsonRecordStore {
|
|
|
12690
14027
|
}
|
|
12691
14028
|
return value;
|
|
12692
14029
|
}
|
|
14030
|
+
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
14031
|
+
await this.ensureSchema();
|
|
14032
|
+
const records = [
|
|
14033
|
+
{ object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
|
|
14034
|
+
...tasks.map((task) => ({ object_type: "template_tasks", object_id: task.id, payload: task, updated_at: task.created_at, version: 1 }))
|
|
14035
|
+
];
|
|
14036
|
+
const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
|
|
14037
|
+
SELECT value->>'object_type' AS object_type,
|
|
14038
|
+
value->>'object_id' AS object_id,
|
|
14039
|
+
value->'payload' AS payload,
|
|
14040
|
+
value->>'updated_at' AS updated_at,
|
|
14041
|
+
COALESCE((value->>'version')::integer, 1) AS version
|
|
14042
|
+
FROM jsonb_array_elements($2::jsonb) AS value
|
|
14043
|
+
) INSERT INTO ${this.tableName} (
|
|
14044
|
+
service, object_type, object_id, payload, updated_at,
|
|
14045
|
+
deleted_at, source_machine_id, version
|
|
14046
|
+
) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
|
|
14047
|
+
NULL, $3, version
|
|
14048
|
+
FROM input
|
|
14049
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
14050
|
+
payload = EXCLUDED.payload,
|
|
14051
|
+
updated_at = EXCLUDED.updated_at,
|
|
14052
|
+
deleted_at = NULL,
|
|
14053
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
14054
|
+
version = EXCLUDED.version
|
|
14055
|
+
WHERE ${this.tableName}.updated_at IS NULL
|
|
14056
|
+
OR ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
14057
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
14058
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
14059
|
+
RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
|
|
14060
|
+
if (result.rows.length !== records.length) {
|
|
14061
|
+
throw new Error("Template checklist write was rejected before completion; no partial template was committed");
|
|
14062
|
+
}
|
|
14063
|
+
}
|
|
14064
|
+
async deleteTemplateWithTasks(id, context = {}) {
|
|
14065
|
+
await this.ensureSchema();
|
|
14066
|
+
const timestamp2 = new Date().toISOString();
|
|
14067
|
+
const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
|
|
14068
|
+
SELECT 1 FROM ${this.tableName}
|
|
14069
|
+
WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
|
|
14070
|
+
) UPDATE ${this.tableName} AS record SET
|
|
14071
|
+
deleted_at = $3::timestamptz,
|
|
14072
|
+
updated_at = $3::timestamptz,
|
|
14073
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
14074
|
+
version = COALESCE(record.version, 0) + 1
|
|
14075
|
+
WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
|
|
14076
|
+
AND (record.object_type = 'templates' AND record.object_id = $2
|
|
14077
|
+
OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
|
|
14078
|
+
RETURNING record.object_type`, [this.service, id, timestamp2, this.machineId(context)]);
|
|
14079
|
+
return result.rows.some((row) => row.object_type === "templates");
|
|
14080
|
+
}
|
|
12693
14081
|
async completeTask(id, agentId, options) {
|
|
12694
14082
|
await this.ensureSchema();
|
|
12695
14083
|
const operationTimestamp = new Date().toISOString();
|
|
@@ -12751,7 +14139,7 @@ class PostgresJsonRecordStore {
|
|
|
12751
14139
|
const normalizedSlug = slugifyRaw(newSlug);
|
|
12752
14140
|
if (!normalizedSlug)
|
|
12753
14141
|
throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
|
|
12754
|
-
const
|
|
14142
|
+
const timestamp2 = new Date().toISOString();
|
|
12755
14143
|
try {
|
|
12756
14144
|
const result = await this.options.client.query(`/* todos:rename-project-atomic */ WITH target AS (
|
|
12757
14145
|
SELECT payload, payload->>'task_list_id' AS old_slug
|
|
@@ -12799,7 +14187,7 @@ class PostgresJsonRecordStore {
|
|
|
12799
14187
|
EXISTS (SELECT 1 FROM project_conflict) AS project_conflict,
|
|
12800
14188
|
EXISTS (SELECT 1 FROM task_list_conflict) AS task_list_conflict,
|
|
12801
14189
|
COALESCE((SELECT payload FROM updated_project), (SELECT payload FROM target)) AS project,
|
|
12802
|
-
(SELECT count(*) FROM updated_lists) AS task_lists_updated`, [this.service, id, normalizedSlug, name ?? null,
|
|
14190
|
+
(SELECT count(*) FROM updated_lists) AS task_lists_updated`, [this.service, id, normalizedSlug, name ?? null, timestamp2, this.machineId(context)]);
|
|
12803
14191
|
const row = result.rows[0];
|
|
12804
14192
|
if (!row?.found)
|
|
12805
14193
|
throw new ProjectNotFoundError(id);
|
|
@@ -12852,19 +14240,19 @@ class PostgresJsonRecordStore {
|
|
|
12852
14240
|
const existing = await this.get(type, id);
|
|
12853
14241
|
if (!existing)
|
|
12854
14242
|
return false;
|
|
12855
|
-
const
|
|
14243
|
+
const timestamp2 = new Date().toISOString();
|
|
12856
14244
|
return this.tombstone({
|
|
12857
14245
|
object_type: type,
|
|
12858
14246
|
object_id: id,
|
|
12859
|
-
deleted_at:
|
|
12860
|
-
updated_at:
|
|
14247
|
+
deleted_at: timestamp2,
|
|
14248
|
+
updated_at: timestamp2,
|
|
12861
14249
|
payload: existing,
|
|
12862
14250
|
version: numberValue2(existing["version"])
|
|
12863
14251
|
}, context);
|
|
12864
14252
|
}
|
|
12865
14253
|
async deletePlan(id, context = {}) {
|
|
12866
14254
|
await this.ensureSchema();
|
|
12867
|
-
const
|
|
14255
|
+
const timestamp2 = new Date().toISOString();
|
|
12868
14256
|
const result = await this.options.client.query(`/* todos:delete-plan-atomic */ WITH target AS (
|
|
12869
14257
|
SELECT payload FROM ${this.tableName}
|
|
12870
14258
|
WHERE service = $1 AND object_type = 'plans' AND object_id = $2 AND deleted_at IS NULL
|
|
@@ -12894,7 +14282,7 @@ class PostgresJsonRecordStore {
|
|
|
12894
14282
|
RETURNING 1
|
|
12895
14283
|
) SELECT
|
|
12896
14284
|
EXISTS (SELECT 1 FROM target) AS found,
|
|
12897
|
-
(SELECT count(*) FROM updated_tasks) AS tasks_updated`, [this.service, id,
|
|
14285
|
+
(SELECT count(*) FROM updated_tasks) AS tasks_updated`, [this.service, id, timestamp2, this.machineId(context)]);
|
|
12898
14286
|
return Boolean(result.rows[0]?.found);
|
|
12899
14287
|
}
|
|
12900
14288
|
async tombstone(tombstone, context = {}) {
|
|
@@ -12955,7 +14343,7 @@ class PostgresJsonRecordStore {
|
|
|
12955
14343
|
}
|
|
12956
14344
|
}
|
|
12957
14345
|
async function createTask2(input, store, context) {
|
|
12958
|
-
const
|
|
14346
|
+
const timestamp2 = new Date().toISOString();
|
|
12959
14347
|
const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
|
|
12960
14348
|
const task = {
|
|
12961
14349
|
id: randomUUID3(),
|
|
@@ -12977,8 +14365,8 @@ async function createTask2(input, store, context) {
|
|
|
12977
14365
|
version: 1,
|
|
12978
14366
|
locked_by: null,
|
|
12979
14367
|
locked_at: null,
|
|
12980
|
-
created_at:
|
|
12981
|
-
updated_at:
|
|
14368
|
+
created_at: timestamp2,
|
|
14369
|
+
updated_at: timestamp2,
|
|
12982
14370
|
started_at: null,
|
|
12983
14371
|
completed_at: null,
|
|
12984
14372
|
due_at: input.due_at ?? null,
|
|
@@ -13110,9 +14498,9 @@ async function lockTask2(id, agentId, store) {
|
|
|
13110
14498
|
if (task.locked_by && task.locked_by !== agentId && !cloudLockExpired(task.locked_at)) {
|
|
13111
14499
|
return { success: false, locked_by: task.locked_by, locked_at: task.locked_at ?? undefined, error: `Task is locked by ${task.locked_by}` };
|
|
13112
14500
|
}
|
|
13113
|
-
const
|
|
13114
|
-
await patchTask(task, { locked_by: agentId, locked_at:
|
|
13115
|
-
return { success: true, locked_by: agentId, locked_at:
|
|
14501
|
+
const timestamp2 = new Date().toISOString();
|
|
14502
|
+
await patchTask(task, { locked_by: agentId, locked_at: timestamp2 }, store);
|
|
14503
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: cloudLockExpiresAt(timestamp2) };
|
|
13116
14504
|
}
|
|
13117
14505
|
async function unlockTask2(id, agentId, store) {
|
|
13118
14506
|
const task = await requireRecord("tasks", id, store);
|
|
@@ -13151,8 +14539,8 @@ async function addDependency2(taskId, dependsOn, store, context) {
|
|
|
13151
14539
|
for (const next of adjacency.get(node) ?? [])
|
|
13152
14540
|
queue.push(next);
|
|
13153
14541
|
}
|
|
13154
|
-
const
|
|
13155
|
-
const record = { id: dependencyId(taskId, dependsOn), task_id: taskId, depends_on: dependsOn, created_at:
|
|
14542
|
+
const timestamp2 = new Date().toISOString();
|
|
14543
|
+
const record = { id: dependencyId(taskId, dependsOn), task_id: taskId, depends_on: dependsOn, created_at: timestamp2, updated_at: timestamp2 };
|
|
13156
14544
|
await store.upsert("dependencies", record, context);
|
|
13157
14545
|
return { task_id: taskId, depends_on: dependsOn };
|
|
13158
14546
|
}
|
|
@@ -13173,7 +14561,7 @@ async function listDependencies(taskId, store) {
|
|
|
13173
14561
|
async function addVerification(input, store, context) {
|
|
13174
14562
|
if (!await store.get("tasks", input.task_id))
|
|
13175
14563
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13176
|
-
const
|
|
14564
|
+
const timestamp2 = new Date().toISOString();
|
|
13177
14565
|
const verification = {
|
|
13178
14566
|
id: randomUUID3(),
|
|
13179
14567
|
task_id: input.task_id,
|
|
@@ -13182,10 +14570,10 @@ async function addVerification(input, store, context) {
|
|
|
13182
14570
|
output_summary: input.output_summary ?? null,
|
|
13183
14571
|
artifact_path: input.artifact_path ?? null,
|
|
13184
14572
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
13185
|
-
run_at:
|
|
13186
|
-
created_at:
|
|
14573
|
+
run_at: timestamp2,
|
|
14574
|
+
created_at: timestamp2
|
|
13187
14575
|
};
|
|
13188
|
-
await store.upsert("verifications", { ...verification, updated_at:
|
|
14576
|
+
await store.upsert("verifications", { ...verification, updated_at: timestamp2 }, context);
|
|
13189
14577
|
return verification;
|
|
13190
14578
|
}
|
|
13191
14579
|
async function listVerifications(taskId, store) {
|
|
@@ -13194,7 +14582,7 @@ async function listVerifications(taskId, store) {
|
|
|
13194
14582
|
async function addCommit(input, store, context) {
|
|
13195
14583
|
if (!await store.get("tasks", input.task_id))
|
|
13196
14584
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13197
|
-
const
|
|
14585
|
+
const timestamp2 = new Date().toISOString();
|
|
13198
14586
|
const commit = {
|
|
13199
14587
|
id: randomUUID3(),
|
|
13200
14588
|
task_id: input.task_id,
|
|
@@ -13202,9 +14590,9 @@ async function addCommit(input, store, context) {
|
|
|
13202
14590
|
message: input.message ?? null,
|
|
13203
14591
|
author: input.author ?? null,
|
|
13204
14592
|
files_changed: input.files_changed ?? null,
|
|
13205
|
-
created_at:
|
|
14593
|
+
created_at: timestamp2
|
|
13206
14594
|
};
|
|
13207
|
-
await store.upsert("commits", { ...commit, updated_at:
|
|
14595
|
+
await store.upsert("commits", { ...commit, updated_at: timestamp2 }, context);
|
|
13208
14596
|
return commit;
|
|
13209
14597
|
}
|
|
13210
14598
|
async function listCommits(taskId, store) {
|
|
@@ -13217,7 +14605,7 @@ async function findCommit(sha, store) {
|
|
|
13217
14605
|
async function addGitRef(input, store, context) {
|
|
13218
14606
|
if (!await store.get("tasks", input.task_id))
|
|
13219
14607
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13220
|
-
const
|
|
14608
|
+
const timestamp2 = new Date().toISOString();
|
|
13221
14609
|
const gitRef = {
|
|
13222
14610
|
id: randomUUID3(),
|
|
13223
14611
|
task_id: input.task_id,
|
|
@@ -13226,8 +14614,8 @@ async function addGitRef(input, store, context) {
|
|
|
13226
14614
|
url: input.url ?? null,
|
|
13227
14615
|
provider: input.provider ?? null,
|
|
13228
14616
|
metadata: input.metadata ?? {},
|
|
13229
|
-
created_at:
|
|
13230
|
-
updated_at:
|
|
14617
|
+
created_at: timestamp2,
|
|
14618
|
+
updated_at: timestamp2
|
|
13231
14619
|
};
|
|
13232
14620
|
await store.upsert("refs", gitRef, context);
|
|
13233
14621
|
return gitRef;
|
|
@@ -13238,7 +14626,8 @@ async function listGitRefs(taskId, store) {
|
|
|
13238
14626
|
async function findGitRefs(ref, store) {
|
|
13239
14627
|
return (await store.list("refs")).filter((r) => r.name === ref).sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
13240
14628
|
}
|
|
13241
|
-
var
|
|
14629
|
+
var TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
|
|
14630
|
+
var TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
13242
14631
|
function toFilterArray(value) {
|
|
13243
14632
|
return Array.isArray(value) ? value : [value];
|
|
13244
14633
|
}
|
|
@@ -13284,7 +14673,7 @@ async function getChangedSince(since, filters, store) {
|
|
|
13284
14673
|
return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
|
|
13285
14674
|
}
|
|
13286
14675
|
async function createProject2(input, store, context) {
|
|
13287
|
-
const
|
|
14676
|
+
const timestamp2 = new Date().toISOString();
|
|
13288
14677
|
const derivedSlug = slugifyRaw(input.name);
|
|
13289
14678
|
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
|
|
13290
14679
|
if (!derivedSlug || !taskListId)
|
|
@@ -13297,8 +14686,8 @@ async function createProject2(input, store, context) {
|
|
|
13297
14686
|
task_list_id: taskListId,
|
|
13298
14687
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
13299
14688
|
task_counter: 0,
|
|
13300
|
-
created_at:
|
|
13301
|
-
updated_at:
|
|
14689
|
+
created_at: timestamp2,
|
|
14690
|
+
updated_at: timestamp2,
|
|
13302
14691
|
machine_id: store.machineId(context),
|
|
13303
14692
|
synced_at: null
|
|
13304
14693
|
};
|
|
@@ -13313,7 +14702,7 @@ async function updateProject2(id, input, store) {
|
|
|
13313
14702
|
return store.upsert("projects", updated);
|
|
13314
14703
|
}
|
|
13315
14704
|
async function createPlan2(input, store, context) {
|
|
13316
|
-
const
|
|
14705
|
+
const timestamp2 = new Date().toISOString();
|
|
13317
14706
|
const projectId = input.project_id ?? context?.projectId ?? null;
|
|
13318
14707
|
const slug = await resolvePostgresPlanSlug({
|
|
13319
14708
|
name: input.name,
|
|
@@ -13330,8 +14719,8 @@ async function createPlan2(input, store, context) {
|
|
|
13330
14719
|
name: input.name,
|
|
13331
14720
|
description: input.description ?? null,
|
|
13332
14721
|
status: input.status ?? "active",
|
|
13333
|
-
created_at:
|
|
13334
|
-
updated_at:
|
|
14722
|
+
created_at: timestamp2,
|
|
14723
|
+
updated_at: timestamp2,
|
|
13335
14724
|
machine_id: store.machineId(context),
|
|
13336
14725
|
synced_at: null
|
|
13337
14726
|
}, context);
|
|
@@ -13355,7 +14744,7 @@ async function registerAgent2(input, store, context) {
|
|
|
13355
14744
|
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
13356
14745
|
return { conflict: true, message: `Agent name '${input.name}' is already active` };
|
|
13357
14746
|
}
|
|
13358
|
-
const
|
|
14747
|
+
const timestamp2 = new Date().toISOString();
|
|
13359
14748
|
const agent = {
|
|
13360
14749
|
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
13361
14750
|
name: input.name,
|
|
@@ -13369,8 +14758,8 @@ async function registerAgent2(input, store, context) {
|
|
|
13369
14758
|
capabilities: input.capabilities ?? existing?.capabilities ?? [],
|
|
13370
14759
|
status: "active",
|
|
13371
14760
|
metadata: input.metadata ?? existing?.metadata ?? {},
|
|
13372
|
-
created_at: existing?.created_at ??
|
|
13373
|
-
last_seen_at:
|
|
14761
|
+
created_at: existing?.created_at ?? timestamp2,
|
|
14762
|
+
last_seen_at: timestamp2,
|
|
13374
14763
|
session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
|
|
13375
14764
|
working_dir: input.working_dir ?? existing?.working_dir ?? null,
|
|
13376
14765
|
active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
|
|
@@ -13415,7 +14804,7 @@ async function releaseAgent2(idOrName, sessionId, store, context) {
|
|
|
13415
14804
|
return { agent: updated, released: true };
|
|
13416
14805
|
}
|
|
13417
14806
|
async function createTaskList2(input, store, context) {
|
|
13418
|
-
const
|
|
14807
|
+
const timestamp2 = new Date().toISOString();
|
|
13419
14808
|
const slug = slugifyRaw(input.slug === undefined ? input.name : input.slug);
|
|
13420
14809
|
if (!slug)
|
|
13421
14810
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
@@ -13426,8 +14815,8 @@ async function createTaskList2(input, store, context) {
|
|
|
13426
14815
|
name: input.name,
|
|
13427
14816
|
description: input.description ?? null,
|
|
13428
14817
|
metadata: input.metadata ?? {},
|
|
13429
|
-
created_at:
|
|
13430
|
-
updated_at:
|
|
14818
|
+
created_at: timestamp2,
|
|
14819
|
+
updated_at: timestamp2,
|
|
13431
14820
|
machine_id: store.machineId(context),
|
|
13432
14821
|
synced_at: null
|
|
13433
14822
|
}, context);
|
|
@@ -13453,8 +14842,8 @@ async function updateTaskList2(id, input, store) {
|
|
|
13453
14842
|
});
|
|
13454
14843
|
}
|
|
13455
14844
|
async function createTemplate2(input, store, context) {
|
|
13456
|
-
const
|
|
13457
|
-
|
|
14845
|
+
const timestamp2 = new Date().toISOString();
|
|
14846
|
+
const template = {
|
|
13458
14847
|
id: randomUUID3(),
|
|
13459
14848
|
name: input.name,
|
|
13460
14849
|
title_pattern: input.title_pattern,
|
|
@@ -13466,10 +14855,33 @@ async function createTemplate2(input, store, context) {
|
|
|
13466
14855
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
13467
14856
|
plan_id: input.plan_id ?? null,
|
|
13468
14857
|
metadata: input.metadata ?? {},
|
|
13469
|
-
created_at:
|
|
14858
|
+
created_at: timestamp2,
|
|
13470
14859
|
machine_id: store.machineId(context),
|
|
13471
14860
|
synced_at: null
|
|
13472
|
-
}
|
|
14861
|
+
};
|
|
14862
|
+
const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp2);
|
|
14863
|
+
await store.createTemplateWithTasks(template, tasks, context);
|
|
14864
|
+
return template;
|
|
14865
|
+
}
|
|
14866
|
+
function buildTemplateTasks(templateId, inputs, timestamp2) {
|
|
14867
|
+
return inputs.map((input, position) => ({
|
|
14868
|
+
id: randomUUID3(),
|
|
14869
|
+
template_id: templateId,
|
|
14870
|
+
position,
|
|
14871
|
+
title_pattern: input.title_pattern,
|
|
14872
|
+
description: input.description ?? null,
|
|
14873
|
+
priority: input.priority ?? "medium",
|
|
14874
|
+
tags: input.tags ?? [],
|
|
14875
|
+
task_type: input.task_type ?? null,
|
|
14876
|
+
condition: input.condition ?? null,
|
|
14877
|
+
include_template_id: input.include_template_id ?? null,
|
|
14878
|
+
depends_on_positions: input.depends_on ?? [],
|
|
14879
|
+
metadata: input.metadata ?? {},
|
|
14880
|
+
created_at: timestamp2
|
|
14881
|
+
}));
|
|
14882
|
+
}
|
|
14883
|
+
async function deleteTemplate2(id, store, context) {
|
|
14884
|
+
return store.deleteTemplateWithTasks(id, context);
|
|
13473
14885
|
}
|
|
13474
14886
|
async function updateTemplate2(id, input, store) {
|
|
13475
14887
|
const template = await store.get("templates", id);
|
|
@@ -13525,6 +14937,7 @@ async function exportSnapshot(store) {
|
|
|
13525
14937
|
agents: await store.list("agents"),
|
|
13526
14938
|
taskLists: await store.list("task_lists"),
|
|
13527
14939
|
templates: await store.list("templates"),
|
|
14940
|
+
templateTasks: await store.list("template_tasks"),
|
|
13528
14941
|
auditHistory: await store.list("audit_history"),
|
|
13529
14942
|
tombstones: await store.listTombstones()
|
|
13530
14943
|
};
|
|
@@ -13549,6 +14962,7 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
13549
14962
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
13550
14963
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
13551
14964
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
14965
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
|
|
13552
14966
|
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
13553
14967
|
];
|
|
13554
14968
|
for (const [type, row] of entries) {
|
|
@@ -13694,6 +15108,7 @@ var SNAPSHOT_TO_OBJECT_TYPE = {
|
|
|
13694
15108
|
agents: "agents",
|
|
13695
15109
|
taskLists: "task_lists",
|
|
13696
15110
|
templates: "templates",
|
|
15111
|
+
templateTasks: "template_tasks",
|
|
13697
15112
|
auditHistory: "audit_history"
|
|
13698
15113
|
};
|
|
13699
15114
|
|
|
@@ -13885,6 +15300,7 @@ function emptySnapshot() {
|
|
|
13885
15300
|
agents: [],
|
|
13886
15301
|
taskLists: [],
|
|
13887
15302
|
templates: [],
|
|
15303
|
+
templateTasks: [],
|
|
13888
15304
|
auditHistory: [],
|
|
13889
15305
|
tombstones: []
|
|
13890
15306
|
};
|
|
@@ -14088,10 +15504,23 @@ function createTodosCloudQueryClient(url, options = {}) {
|
|
|
14088
15504
|
idleTimeout: options.idleTimeout ?? 20,
|
|
14089
15505
|
connectionTimeout: options.connectionTimeout ?? 15
|
|
14090
15506
|
});
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
15507
|
+
const queryWith = async (handle, text, values = []) => {
|
|
15508
|
+
const result = await handle.unsafe(text, values.length ? [...values] : []);
|
|
15509
|
+
return { rows: toRows(result) };
|
|
15510
|
+
};
|
|
15511
|
+
const client = {
|
|
15512
|
+
query(text, values = []) {
|
|
15513
|
+
return queryWith(sql, text, values);
|
|
15514
|
+
},
|
|
15515
|
+
async transaction(fn) {
|
|
15516
|
+
if (typeof sql.begin !== "function") {
|
|
15517
|
+
throw new Error("PR_GROUP_ATOMICITY_UNAVAILABLE: Bun.SQL transaction support is required");
|
|
15518
|
+
}
|
|
15519
|
+
return sql.begin(async (handle) => fn({
|
|
15520
|
+
query(text, values = []) {
|
|
15521
|
+
return queryWith(handle, text, values);
|
|
15522
|
+
}
|
|
15523
|
+
}));
|
|
14095
15524
|
},
|
|
14096
15525
|
async close() {
|
|
14097
15526
|
if (typeof sql.end === "function")
|
|
@@ -14100,6 +15529,7 @@ function createTodosCloudQueryClient(url, options = {}) {
|
|
|
14100
15529
|
await sql.close();
|
|
14101
15530
|
}
|
|
14102
15531
|
};
|
|
15532
|
+
return client;
|
|
14103
15533
|
}
|
|
14104
15534
|
function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
14105
15535
|
const url = getTodosStorageDatabaseUrl(env);
|
|
@@ -14341,6 +15771,9 @@ class TodosShadowOutbox {
|
|
|
14341
15771
|
case "templates":
|
|
14342
15772
|
snapshot.templates.push(record);
|
|
14343
15773
|
break;
|
|
15774
|
+
case "template_tasks":
|
|
15775
|
+
snapshot.templateTasks.push(record);
|
|
15776
|
+
break;
|
|
14344
15777
|
case "audit_history":
|
|
14345
15778
|
snapshot.auditHistory.push(record);
|
|
14346
15779
|
break;
|
|
@@ -14407,6 +15840,7 @@ function emptySnapshot2() {
|
|
|
14407
15840
|
agents: [],
|
|
14408
15841
|
taskLists: [],
|
|
14409
15842
|
templates: [],
|
|
15843
|
+
templateTasks: [],
|
|
14410
15844
|
auditHistory: [],
|
|
14411
15845
|
tombstones: []
|
|
14412
15846
|
};
|
|
@@ -15065,7 +16499,7 @@ export {
|
|
|
15065
16499
|
PostgresScopedSlugMigrationConflictError,
|
|
15066
16500
|
PostgresScopedSlugIndexBuildError,
|
|
15067
16501
|
COMMENT_REDACTION_BACKFILL_CONFIRMATION,
|
|
15068
|
-
|
|
16502
|
+
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
15069
16503
|
CANONICAL_TODOS_RDS_DATABASE,
|
|
15070
|
-
|
|
16504
|
+
CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
15071
16505
|
};
|