@hasna/todos 0.11.95 → 0.12.2
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 +25 -5
- package/dist/cli/cloud-router.d.ts +5 -0
- 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 +7891 -1907
- package/dist/cli/stage-a.d.ts +17 -0
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +1638 -502
- 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/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/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4621 -833
- 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/mcp/index.js +6183 -1016
- 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 +1638 -502
- 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 +64 -0
- package/dist/sdk/v1.generated.d.ts +285 -0
- 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 +6079 -912
- package/dist/server/openapi.d.ts +1254 -0
- 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/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1860 -540
- 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 {
|
|
@@ -3004,77 +3557,405 @@ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
|
3004
3557
|
message: `Project path does not exist on this machine: ${project.path}`
|
|
3005
3558
|
});
|
|
3006
3559
|
}
|
|
3007
|
-
}
|
|
3008
|
-
return {
|
|
3009
|
-
generated_at: generatedAt,
|
|
3010
|
-
stale_after_minutes: staleAfter,
|
|
3011
|
-
local_machine: localMachine,
|
|
3012
|
-
machines: summaries,
|
|
3013
|
-
stale_machines: summaries.filter((summary) => summary.stale),
|
|
3014
|
-
path_issues: pathIssues
|
|
3560
|
+
}
|
|
3561
|
+
return {
|
|
3562
|
+
generated_at: generatedAt,
|
|
3563
|
+
stale_after_minutes: staleAfter,
|
|
3564
|
+
local_machine: localMachine,
|
|
3565
|
+
machines: summaries,
|
|
3566
|
+
stale_machines: summaries.filter((summary) => summary.stale),
|
|
3567
|
+
path_issues: pathIssues
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
function setPrimaryMachine(name, db) {
|
|
3571
|
+
const d = db || getDatabase();
|
|
3572
|
+
const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
3573
|
+
if (!row)
|
|
3574
|
+
throw new Error(`Machine '${name}' not found`);
|
|
3575
|
+
if (row.archived_at)
|
|
3576
|
+
throw new Error(`Cannot set archived machine '${name}' as primary`);
|
|
3577
|
+
d.run("UPDATE machines SET is_primary = 0 WHERE archived_at IS NULL");
|
|
3578
|
+
d.run("UPDATE machines SET is_primary = 1 WHERE id = ?", [row.id]);
|
|
3579
|
+
return rowToMachine({ ...row, is_primary: 1 });
|
|
3580
|
+
}
|
|
3581
|
+
function deleteMachine(id, db) {
|
|
3582
|
+
const d = db || getDatabase();
|
|
3583
|
+
const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
3584
|
+
if (!row)
|
|
3585
|
+
return false;
|
|
3586
|
+
if (row.is_primary)
|
|
3587
|
+
throw new Error("Cannot delete the primary machine");
|
|
3588
|
+
const activeCount = d.query("SELECT COUNT(*) as cnt FROM tasks WHERE machine_id = ? AND status IN ('pending', 'in_progress')").get(id);
|
|
3589
|
+
if (activeCount.cnt > 0) {
|
|
3590
|
+
throw new Error(`Cannot delete machine with ${activeCount.cnt} active/pending tasks`);
|
|
3591
|
+
}
|
|
3592
|
+
const result = d.run("DELETE FROM machines WHERE id = ?", [id]);
|
|
3593
|
+
return result.changes > 0;
|
|
3594
|
+
}
|
|
3595
|
+
function backfillMachineId(db, force = false) {
|
|
3596
|
+
if (!force && process.env["TODOS_DB_PATH"] === ":memory:")
|
|
3597
|
+
return;
|
|
3598
|
+
try {
|
|
3599
|
+
const machine = getOrCreateLocalMachine(db);
|
|
3600
|
+
for (const table of TABLES_WITH_MACHINE_ID) {
|
|
3601
|
+
try {
|
|
3602
|
+
db.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, [machine.id]);
|
|
3603
|
+
} catch {}
|
|
3604
|
+
}
|
|
3605
|
+
} catch {}
|
|
3606
|
+
}
|
|
3607
|
+
var _machineId = null, TABLES_WITH_MACHINE_ID;
|
|
3608
|
+
var init_machines = __esm(() => {
|
|
3609
|
+
init_database();
|
|
3610
|
+
TABLES_WITH_MACHINE_ID = [
|
|
3611
|
+
"projects",
|
|
3612
|
+
"tasks",
|
|
3613
|
+
"agents",
|
|
3614
|
+
"task_lists",
|
|
3615
|
+
"plans",
|
|
3616
|
+
"task_comments",
|
|
3617
|
+
"sessions",
|
|
3618
|
+
"task_history",
|
|
3619
|
+
"webhooks",
|
|
3620
|
+
"task_templates",
|
|
3621
|
+
"orgs",
|
|
3622
|
+
"handoffs",
|
|
3623
|
+
"task_checklists",
|
|
3624
|
+
"project_sources",
|
|
3625
|
+
"task_files",
|
|
3626
|
+
"task_relationships",
|
|
3627
|
+
"kg_edges",
|
|
3628
|
+
"project_agent_roles",
|
|
3629
|
+
"dispatches"
|
|
3630
|
+
];
|
|
3631
|
+
});
|
|
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
|
+
}
|
|
3015
3784
|
};
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
// src/db/identity-mapping.ts
|
|
3788
|
+
function timestamp() {
|
|
3789
|
+
return new Date().toISOString();
|
|
3016
3790
|
}
|
|
3017
|
-
function
|
|
3018
|
-
|
|
3019
|
-
const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
3020
|
-
if (!row)
|
|
3021
|
-
throw new Error(`Machine '${name}' not found`);
|
|
3022
|
-
if (row.archived_at)
|
|
3023
|
-
throw new Error(`Cannot set archived machine '${name}' as primary`);
|
|
3024
|
-
d.run("UPDATE machines SET is_primary = 0 WHERE archived_at IS NULL");
|
|
3025
|
-
d.run("UPDATE machines SET is_primary = 1 WHERE id = ?", [row.id]);
|
|
3026
|
-
return rowToMachine({ ...row, is_primary: 1 });
|
|
3791
|
+
function mappingId() {
|
|
3792
|
+
return crypto.randomUUID();
|
|
3027
3793
|
}
|
|
3028
|
-
function
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
if (row.is_primary)
|
|
3034
|
-
throw new Error("Cannot delete the primary machine");
|
|
3035
|
-
const activeCount = d.query("SELECT COUNT(*) as cnt FROM tasks WHERE machine_id = ? AND status IN ('pending', 'in_progress')").get(id);
|
|
3036
|
-
if (activeCount.cnt > 0) {
|
|
3037
|
-
throw new Error(`Cannot delete machine with ${activeCount.cnt} active/pending tasks`);
|
|
3038
|
-
}
|
|
3039
|
-
const result = d.run("DELETE FROM machines WHERE id = ?", [id]);
|
|
3040
|
-
return result.changes > 0;
|
|
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;
|
|
3041
3799
|
}
|
|
3042
|
-
function
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
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);
|
|
3051
3918
|
}
|
|
3052
|
-
|
|
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);
|
|
3053
3928
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
"
|
|
3070
|
-
|
|
3071
|
-
"
|
|
3072
|
-
|
|
3073
|
-
"
|
|
3074
|
-
|
|
3075
|
-
"
|
|
3076
|
-
"
|
|
3077
|
-
|
|
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
|
+
});
|
|
3078
3959
|
});
|
|
3079
3960
|
|
|
3080
3961
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -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));
|
|
@@ -7139,9 +8155,9 @@ function startTask(id, agentId, db) {
|
|
|
7139
8155
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
7140
8156
|
}
|
|
7141
8157
|
const cutoff = lockExpiryCutoff();
|
|
7142
|
-
const
|
|
8158
|
+
const timestamp2 = now();
|
|
7143
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 = ?
|
|
7144
|
-
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]);
|
|
7145
8161
|
if (result.changes === 0) {
|
|
7146
8162
|
const current = getTask(id, d);
|
|
7147
8163
|
if (!current)
|
|
@@ -7153,7 +8169,7 @@ function startTask(id, agentId, db) {
|
|
|
7153
8169
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
7154
8170
|
}
|
|
7155
8171
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
7156
|
-
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 };
|
|
7157
8173
|
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
7158
8174
|
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
7159
8175
|
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
@@ -7176,7 +8192,13 @@ function completeTask(id, agentId, db, options) {
|
|
|
7176
8192
|
throw new LockError(id, task.locked_by);
|
|
7177
8193
|
}
|
|
7178
8194
|
checkCompletionGuard(task, agentId || null, d);
|
|
7179
|
-
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;
|
|
7180
8202
|
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
7181
8203
|
const completionMeta = {};
|
|
7182
8204
|
if (hasEvidence)
|
|
@@ -7185,21 +8207,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
7185
8207
|
completionMeta._completion = { confidence: options.confidence };
|
|
7186
8208
|
}
|
|
7187
8209
|
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
7188
|
-
const
|
|
8210
|
+
const timestamp2 = options?.completed_at || now();
|
|
7189
8211
|
const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
|
|
7190
8212
|
const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
|
|
7191
8213
|
const finalVersion = versionBeforeStatus + 1;
|
|
7192
8214
|
const tx = d.transaction(() => {
|
|
7193
8215
|
if (hasMeta) {
|
|
7194
|
-
const meta2 = { ...task.metadata, ...completionMeta };
|
|
7195
|
-
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]);
|
|
7196
8218
|
if (metaResult.changes === 0) {
|
|
7197
8219
|
const current = getTask(id, d);
|
|
7198
8220
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
7199
8221
|
}
|
|
7200
8222
|
}
|
|
7201
8223
|
const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
7202
|
-
WHERE id = ? AND version = ?`, [
|
|
8224
|
+
WHERE id = ? AND version = ?`, [timestamp2, confidence, timestamp2, id, versionBeforeStatus]);
|
|
7203
8225
|
if (statusResult.changes === 0) {
|
|
7204
8226
|
const current = getTask(id, d);
|
|
7205
8227
|
throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
|
|
@@ -7212,20 +8234,20 @@ function completeTask(id, agentId, db, options) {
|
|
|
7212
8234
|
status: "completed",
|
|
7213
8235
|
locked_by: null,
|
|
7214
8236
|
locked_at: null,
|
|
7215
|
-
completed_at:
|
|
8237
|
+
completed_at: timestamp2,
|
|
7216
8238
|
confidence,
|
|
7217
8239
|
version: finalVersion,
|
|
7218
|
-
updated_at:
|
|
8240
|
+
updated_at: timestamp2,
|
|
7219
8241
|
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
7220
8242
|
};
|
|
7221
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at:
|
|
8243
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp2 });
|
|
7222
8244
|
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
7223
8245
|
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
7224
|
-
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 });
|
|
7225
8247
|
let spawnedTask = null;
|
|
7226
8248
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
7227
8249
|
try {
|
|
7228
|
-
spawnedTask = spawnNextRecurrence(task, d,
|
|
8250
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp2);
|
|
7229
8251
|
} catch (e) {
|
|
7230
8252
|
spawnedTask = null;
|
|
7231
8253
|
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -7275,7 +8297,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
7275
8297
|
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
7276
8298
|
}
|
|
7277
8299
|
}
|
|
7278
|
-
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 };
|
|
7279
8301
|
}
|
|
7280
8302
|
function lockTask(id, agentId, db) {
|
|
7281
8303
|
const d = db || getDatabase();
|
|
@@ -7289,15 +8311,15 @@ function lockTask(id, agentId, db) {
|
|
|
7289
8311
|
};
|
|
7290
8312
|
}
|
|
7291
8313
|
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
7292
|
-
const
|
|
7293
|
-
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]);
|
|
7294
8316
|
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
7295
|
-
return { success: true, locked_by: agentId, locked_at:
|
|
8317
|
+
return { success: true, locked_by: agentId, locked_at: timestamp3, expires_at: lockExpiresAt(timestamp3) };
|
|
7296
8318
|
}
|
|
7297
8319
|
const cutoff = lockExpiryCutoff();
|
|
7298
|
-
const
|
|
8320
|
+
const timestamp2 = now();
|
|
7299
8321
|
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
7300
|
-
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]);
|
|
7301
8323
|
if (result.changes === 0) {
|
|
7302
8324
|
const current = getTask(id, d);
|
|
7303
8325
|
if (!current)
|
|
@@ -7322,7 +8344,7 @@ function lockTask(id, agentId, db) {
|
|
|
7322
8344
|
};
|
|
7323
8345
|
}
|
|
7324
8346
|
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
7325
|
-
return { success: true, locked_by: agentId, locked_at:
|
|
8347
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
7326
8348
|
}
|
|
7327
8349
|
function unlockTask(id, agentId, db) {
|
|
7328
8350
|
const d = db || getDatabase();
|
|
@@ -7332,9 +8354,9 @@ function unlockTask(id, agentId, db) {
|
|
|
7332
8354
|
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
7333
8355
|
throw new LockError(id, task.locked_by);
|
|
7334
8356
|
}
|
|
7335
|
-
const
|
|
8357
|
+
const timestamp2 = now();
|
|
7336
8358
|
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
7337
|
-
WHERE id = ?`, [
|
|
8359
|
+
WHERE id = ?`, [timestamp2, id]);
|
|
7338
8360
|
return true;
|
|
7339
8361
|
}
|
|
7340
8362
|
function getTaskLockStatus(id, db) {
|
|
@@ -7416,7 +8438,7 @@ function getNextTask(agentId, filters, db) {
|
|
|
7416
8438
|
}
|
|
7417
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`;
|
|
7418
8440
|
const row = d.query(sql).get(...params);
|
|
7419
|
-
return row ?
|
|
8441
|
+
return row ? rowToTask2(row) : null;
|
|
7420
8442
|
}
|
|
7421
8443
|
function getActiveWork(filters, db) {
|
|
7422
8444
|
const d = db || getDatabase();
|
|
@@ -7451,7 +8473,7 @@ function getTasksChangedSince(since, filters, db) {
|
|
|
7451
8473
|
}
|
|
7452
8474
|
const where = conditions.join(" AND ");
|
|
7453
8475
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
7454
|
-
return rows.map(
|
|
8476
|
+
return rows.map(rowToTask2);
|
|
7455
8477
|
}
|
|
7456
8478
|
function failTask(id, agentId, reason, options, db) {
|
|
7457
8479
|
const d = db || getDatabase();
|
|
@@ -7459,20 +8481,23 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7459
8481
|
const task = getTask(id, d);
|
|
7460
8482
|
if (!task)
|
|
7461
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;
|
|
7462
8486
|
const meta = {
|
|
7463
8487
|
...task.metadata,
|
|
7464
8488
|
_failure: {
|
|
7465
|
-
reason:
|
|
7466
|
-
error_code:
|
|
8489
|
+
reason: safeReason,
|
|
8490
|
+
error_code: safeErrorCode,
|
|
7467
8491
|
failed_by: agentId || null,
|
|
7468
8492
|
failed_at: now(),
|
|
7469
8493
|
retry_requested: options?.retry || false
|
|
7470
8494
|
}
|
|
7471
8495
|
};
|
|
7472
|
-
const
|
|
8496
|
+
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
8497
|
+
const timestamp2 = now();
|
|
7473
8498
|
const failTx = d.transaction(() => {
|
|
7474
8499
|
const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
7475
|
-
WHERE id = ? AND version = ?`, [JSON.stringify(
|
|
8500
|
+
WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
7476
8501
|
if (res.changes === 0) {
|
|
7477
8502
|
const current = getTask(id, d);
|
|
7478
8503
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -7484,22 +8509,22 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7484
8509
|
status: "failed",
|
|
7485
8510
|
locked_by: null,
|
|
7486
8511
|
locked_at: null,
|
|
7487
|
-
metadata:
|
|
8512
|
+
metadata: safeMeta,
|
|
7488
8513
|
version: task.version + 1,
|
|
7489
|
-
updated_at:
|
|
8514
|
+
updated_at: timestamp2
|
|
7490
8515
|
};
|
|
7491
8516
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
7492
|
-
const failurePayload = taskEventData(failedTask, { reason, error_code:
|
|
8517
|
+
const failurePayload = taskEventData(failedTask, { reason: safeReason, error_code: safeErrorCode, agent_id: agentId });
|
|
7493
8518
|
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
7494
8519
|
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
7495
|
-
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 });
|
|
7496
8521
|
let retryTask;
|
|
7497
8522
|
if (options?.retry) {
|
|
7498
8523
|
const retryCount = (task.retry_count || 0) + 1;
|
|
7499
8524
|
const maxRetries = task.max_retries || 3;
|
|
7500
8525
|
if (retryCount > maxRetries) {
|
|
7501
8526
|
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
7502
|
-
JSON.stringify({ ...
|
|
8527
|
+
JSON.stringify(sanitizePreWriteValue({ ...safeMeta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }, "task.retry_exhausted.metadata")),
|
|
7503
8528
|
id
|
|
7504
8529
|
]);
|
|
7505
8530
|
} else {
|
|
@@ -7518,7 +8543,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
7518
8543
|
plan_id: task.plan_id ?? undefined,
|
|
7519
8544
|
assigned_to: task.assigned_to ?? undefined,
|
|
7520
8545
|
tags: task.tags,
|
|
7521
|
-
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 } },
|
|
7522
8547
|
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
7523
8548
|
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
7524
8549
|
due_at: retryAfter
|
|
@@ -7548,7 +8573,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
|
|
|
7548
8573
|
}
|
|
7549
8574
|
const where = conditions.join(" AND ");
|
|
7550
8575
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
7551
|
-
return rows.map(
|
|
8576
|
+
return rows.map(rowToTask2);
|
|
7552
8577
|
}
|
|
7553
8578
|
function stealTask(agentId, opts, db) {
|
|
7554
8579
|
const d = db || getDatabase();
|
|
@@ -7560,15 +8585,15 @@ function stealTask(agentId, opts, db) {
|
|
|
7560
8585
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
7561
8586
|
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
7562
8587
|
const target = staleTasks[0];
|
|
7563
|
-
const
|
|
8588
|
+
const timestamp2 = now();
|
|
7564
8589
|
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
7565
8590
|
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
7566
|
-
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]);
|
|
7567
8592
|
if (result.changes === 0)
|
|
7568
8593
|
return null;
|
|
7569
8594
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
7570
8595
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
7571
|
-
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 };
|
|
7572
8597
|
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
7573
8598
|
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
7574
8599
|
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
@@ -7629,10 +8654,11 @@ var init_task_lifecycle = __esm(() => {
|
|
|
7629
8654
|
init_templates();
|
|
7630
8655
|
init_task_crud();
|
|
7631
8656
|
init_task_graph();
|
|
8657
|
+
init_prewrite_secrets();
|
|
7632
8658
|
});
|
|
7633
8659
|
|
|
7634
8660
|
// src/db/task-crud.ts
|
|
7635
|
-
function
|
|
8661
|
+
function rowToTask2(row) {
|
|
7636
8662
|
return {
|
|
7637
8663
|
...row,
|
|
7638
8664
|
tags: JSON.parse(row.tags || "[]"),
|
|
@@ -7666,9 +8692,29 @@ function addMetadataConditions(metadata, conditions, params) {
|
|
|
7666
8692
|
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
7667
8693
|
}
|
|
7668
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
|
+
}
|
|
7669
8714
|
function createTask(input, db) {
|
|
8715
|
+
input = sanitizeCreateTaskInput(input);
|
|
7670
8716
|
const d = db || getDatabase();
|
|
7671
|
-
const
|
|
8717
|
+
const timestamp2 = now();
|
|
7672
8718
|
const tags = input.tags || [];
|
|
7673
8719
|
const machineId = currentStorageMachineId(d);
|
|
7674
8720
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
@@ -7695,8 +8741,8 @@ function createTask(input, db) {
|
|
|
7695
8741
|
input.working_dir || null,
|
|
7696
8742
|
JSON.stringify(tags),
|
|
7697
8743
|
JSON.stringify(input.metadata || {}),
|
|
7698
|
-
|
|
7699
|
-
|
|
8744
|
+
timestamp2,
|
|
8745
|
+
timestamp2,
|
|
7700
8746
|
input.due_at || null,
|
|
7701
8747
|
input.estimated_minutes || null,
|
|
7702
8748
|
input.sla_minutes ?? null,
|
|
@@ -7742,7 +8788,7 @@ function getTask(id, db) {
|
|
|
7742
8788
|
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
7743
8789
|
if (!row)
|
|
7744
8790
|
return null;
|
|
7745
|
-
return
|
|
8791
|
+
return rowToTask2(row);
|
|
7746
8792
|
}
|
|
7747
8793
|
function getTaskWithRelations(id, db) {
|
|
7748
8794
|
const d = db || getDatabase();
|
|
@@ -7750,15 +8796,15 @@ function getTaskWithRelations(id, db) {
|
|
|
7750
8796
|
if (!task)
|
|
7751
8797
|
return null;
|
|
7752
8798
|
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
7753
|
-
const subtasks = subtaskRows.map(
|
|
8799
|
+
const subtasks = subtaskRows.map(rowToTask2);
|
|
7754
8800
|
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
7755
8801
|
JOIN task_dependencies td ON td.depends_on = t.id
|
|
7756
8802
|
WHERE td.task_id = ?`).all(id);
|
|
7757
|
-
const dependencies = depRows.map(
|
|
8803
|
+
const dependencies = depRows.map(rowToTask2);
|
|
7758
8804
|
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
7759
8805
|
JOIN task_dependencies td ON td.task_id = t.id
|
|
7760
8806
|
WHERE td.depends_on = ?`).all(id);
|
|
7761
|
-
const blocked_by = blockedByRows.map(
|
|
8807
|
+
const blocked_by = blockedByRows.map(rowToTask2);
|
|
7762
8808
|
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
7763
8809
|
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
7764
8810
|
const checklist = getChecklist(id, d);
|
|
@@ -7874,7 +8920,7 @@ function listTasks(filter = {}, db) {
|
|
|
7874
8920
|
}
|
|
7875
8921
|
}
|
|
7876
8922
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
|
|
7877
|
-
return rows.map(
|
|
8923
|
+
return rows.map(rowToTask2);
|
|
7878
8924
|
}
|
|
7879
8925
|
function getTaskByFingerprint(fingerprint, db) {
|
|
7880
8926
|
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
@@ -8020,10 +9066,11 @@ function updateTask(id, input, db) {
|
|
|
8020
9066
|
if (task.version !== input.version) {
|
|
8021
9067
|
throw new VersionConflictError(id, input.version, task.version);
|
|
8022
9068
|
}
|
|
8023
|
-
|
|
8024
|
-
const
|
|
9069
|
+
input = sanitizeUpdateTaskInput(input);
|
|
9070
|
+
const timestamp2 = now();
|
|
9071
|
+
const completionTimestamp = input.completed_at ?? timestamp2;
|
|
8025
9072
|
const sets = ["version = version + 1", "updated_at = ?"];
|
|
8026
|
-
const params = [
|
|
9073
|
+
const params = [timestamp2];
|
|
8027
9074
|
if (input.title !== undefined) {
|
|
8028
9075
|
sets.push("title = ?");
|
|
8029
9076
|
params.push(input.title);
|
|
@@ -8172,7 +9219,7 @@ function updateTask(id, input, db) {
|
|
|
8172
9219
|
tags: input.tags ?? task.tags,
|
|
8173
9220
|
metadata: input.metadata ?? task.metadata,
|
|
8174
9221
|
version: task.version + 1,
|
|
8175
|
-
updated_at:
|
|
9222
|
+
updated_at: timestamp2,
|
|
8176
9223
|
locked_by: completedNow ? null : task.locked_by,
|
|
8177
9224
|
locked_at: completedNow ? null : task.locked_at,
|
|
8178
9225
|
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
@@ -8184,7 +9231,7 @@ function updateTask(id, input, db) {
|
|
|
8184
9231
|
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
8185
9232
|
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
8186
9233
|
approved_by: input.approved_by ?? task.approved_by,
|
|
8187
|
-
approved_at: input.approved_by ?
|
|
9234
|
+
approved_at: input.approved_by ? timestamp2 : task.approved_at
|
|
8188
9235
|
};
|
|
8189
9236
|
const databasePath = databasePathFromDatabase(d);
|
|
8190
9237
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
@@ -8216,7 +9263,7 @@ function deleteTask(id, db) {
|
|
|
8216
9263
|
recordStorageTombstone({
|
|
8217
9264
|
object_type: "tasks",
|
|
8218
9265
|
object_id: id,
|
|
8219
|
-
payload:
|
|
9266
|
+
payload: rowToTask2(row),
|
|
8220
9267
|
version: row.version
|
|
8221
9268
|
}, d);
|
|
8222
9269
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
@@ -8233,6 +9280,7 @@ var init_task_crud = __esm(() => {
|
|
|
8233
9280
|
init_webhooks();
|
|
8234
9281
|
init_checklists();
|
|
8235
9282
|
init_storage_tombstones();
|
|
9283
|
+
init_prewrite_secrets();
|
|
8236
9284
|
});
|
|
8237
9285
|
|
|
8238
9286
|
// src/db/task-status.ts
|
|
@@ -8364,10 +9412,10 @@ function redistributeStaleTasks(agentId, options, db) {
|
|
|
8364
9412
|
const maxAge = options?.max_age_minutes ?? 60;
|
|
8365
9413
|
const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
|
|
8366
9414
|
const limited = options?.limit ? stale.slice(0, options.limit) : stale;
|
|
8367
|
-
const
|
|
9415
|
+
const timestamp2 = now();
|
|
8368
9416
|
const released = [];
|
|
8369
9417
|
for (const t of limited) {
|
|
8370
|
-
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]);
|
|
8371
9419
|
released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
|
|
8372
9420
|
}
|
|
8373
9421
|
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
@@ -8507,7 +9555,7 @@ function getOverdueTasks(projectId, db, at = new Date) {
|
|
|
8507
9555
|
}
|
|
8508
9556
|
query += ` ORDER BY due_at ASC`;
|
|
8509
9557
|
const rows = d.query(query).all(...params);
|
|
8510
|
-
return rows.map(
|
|
9558
|
+
return rows.map(rowToTask2);
|
|
8511
9559
|
}
|
|
8512
9560
|
function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
8513
9561
|
const d = db || getDatabase();
|
|
@@ -8527,7 +9575,7 @@ function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
|
8527
9575
|
params.push(opts.agent_id);
|
|
8528
9576
|
}
|
|
8529
9577
|
const rows = d.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY due_at ASC, created_at ASC`).all(...params);
|
|
8530
|
-
return rows.map(
|
|
9578
|
+
return rows.map(rowToTask2).map((task) => {
|
|
8531
9579
|
const reasons = [];
|
|
8532
9580
|
const breachedTimes = [];
|
|
8533
9581
|
if (task.due_at) {
|
|
@@ -8839,7 +9887,7 @@ function resolveCreateSlug(input, projectId, db) {
|
|
|
8839
9887
|
function createPlan(input, db) {
|
|
8840
9888
|
const d = db || getDatabase();
|
|
8841
9889
|
const id = uuid();
|
|
8842
|
-
const
|
|
9890
|
+
const timestamp2 = now();
|
|
8843
9891
|
const projectId = input.project_id || null;
|
|
8844
9892
|
const slug = resolveCreateSlug(input, projectId, d);
|
|
8845
9893
|
const machineId = currentStorageMachineId(d);
|
|
@@ -8853,8 +9901,8 @@ function createPlan(input, db) {
|
|
|
8853
9901
|
input.name,
|
|
8854
9902
|
input.description || null,
|
|
8855
9903
|
input.status || "active",
|
|
8856
|
-
|
|
8857
|
-
|
|
9904
|
+
timestamp2,
|
|
9905
|
+
timestamp2,
|
|
8858
9906
|
machineId
|
|
8859
9907
|
]);
|
|
8860
9908
|
return getPlan(id, d);
|
|
@@ -8991,7 +10039,7 @@ function maybeFilter(value) {
|
|
|
8991
10039
|
function createTaskBoard(input, db) {
|
|
8992
10040
|
const d = db || getDatabase();
|
|
8993
10041
|
const id = uuid();
|
|
8994
|
-
const
|
|
10042
|
+
const timestamp2 = now();
|
|
8995
10043
|
const scope = input.scope || "tasks";
|
|
8996
10044
|
const lanes = normalizeLanes(scope, input.lanes);
|
|
8997
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)
|
|
@@ -9005,8 +10053,8 @@ function createTaskBoard(input, db) {
|
|
|
9005
10053
|
input.agent_id || null,
|
|
9006
10054
|
JSON.stringify(lanes),
|
|
9007
10055
|
JSON.stringify(input.filters || {}),
|
|
9008
|
-
|
|
9009
|
-
|
|
10056
|
+
timestamp2,
|
|
10057
|
+
timestamp2
|
|
9010
10058
|
]);
|
|
9011
10059
|
return getTaskBoard(id, d);
|
|
9012
10060
|
}
|
|
@@ -9407,14 +10455,14 @@ function storeArtifactContent(input) {
|
|
|
9407
10455
|
return null;
|
|
9408
10456
|
const sourceStat = statSync2(sourcePath);
|
|
9409
10457
|
if (!sourceStat.isFile())
|
|
9410
|
-
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")}`);
|
|
9411
10459
|
const sourceBuffer = readFileSync3(sourcePath);
|
|
9412
10460
|
const sourceSha = sha256(sourceBuffer);
|
|
9413
10461
|
const textLike = isTextLike(sourceBuffer, input.path);
|
|
9414
10462
|
let storedBuffer = sourceBuffer;
|
|
9415
10463
|
let redactionStatus = textLike ? "clean" : "binary_or_unknown";
|
|
9416
10464
|
if (textLike) {
|
|
9417
|
-
const redactedText =
|
|
10465
|
+
const redactedText = sanitizePreWriteText(sourceBuffer.toString("utf8"), "artifact.content");
|
|
9418
10466
|
storedBuffer = Buffer.from(redactedText);
|
|
9419
10467
|
if (!storedBuffer.equals(sourceBuffer))
|
|
9420
10468
|
redactionStatus = "redacted";
|
|
@@ -9448,7 +10496,7 @@ function storeArtifactContent(input) {
|
|
|
9448
10496
|
expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
|
|
9449
10497
|
},
|
|
9450
10498
|
source: {
|
|
9451
|
-
path: input.path,
|
|
10499
|
+
path: sanitizePreWriteText(input.path, "artifact.source_path"),
|
|
9452
10500
|
size_bytes: sourceBuffer.length,
|
|
9453
10501
|
sha256: sourceSha
|
|
9454
10502
|
}
|
|
@@ -9628,6 +10676,7 @@ var DEFAULT_DELETED_RETENTION_DAYS = 30;
|
|
|
9628
10676
|
var init_artifact_store = __esm(() => {
|
|
9629
10677
|
init_database();
|
|
9630
10678
|
init_redaction();
|
|
10679
|
+
init_prewrite_secrets();
|
|
9631
10680
|
});
|
|
9632
10681
|
|
|
9633
10682
|
// src/db/comments.ts
|
|
@@ -9637,17 +10686,17 @@ function addComment(input, db) {
|
|
|
9637
10686
|
throw new TaskNotFoundError(input.task_id);
|
|
9638
10687
|
}
|
|
9639
10688
|
const id = uuid();
|
|
9640
|
-
const
|
|
10689
|
+
const timestamp2 = now();
|
|
9641
10690
|
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
9642
10691
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9643
10692
|
id,
|
|
9644
10693
|
input.task_id,
|
|
9645
10694
|
input.agent_id || null,
|
|
9646
10695
|
input.session_id || null,
|
|
9647
|
-
|
|
10696
|
+
sanitizePreWriteText(input.content, "comment.content"),
|
|
9648
10697
|
input.type || "comment",
|
|
9649
10698
|
input.progress_pct ?? null,
|
|
9650
|
-
|
|
10699
|
+
timestamp2
|
|
9651
10700
|
]);
|
|
9652
10701
|
return getComment(id, d);
|
|
9653
10702
|
}
|
|
@@ -9671,21 +10720,21 @@ var init_comments = __esm(() => {
|
|
|
9671
10720
|
init_types();
|
|
9672
10721
|
init_database();
|
|
9673
10722
|
init_tasks();
|
|
9674
|
-
|
|
10723
|
+
init_prewrite_secrets();
|
|
9675
10724
|
});
|
|
9676
10725
|
|
|
9677
10726
|
// src/db/task-files.ts
|
|
9678
10727
|
function addTaskFile(input, db) {
|
|
9679
10728
|
const d = db || getDatabase();
|
|
9680
10729
|
const id = uuid();
|
|
9681
|
-
const
|
|
10730
|
+
const timestamp2 = now();
|
|
9682
10731
|
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
9683
10732
|
if (existing) {
|
|
9684
|
-
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]);
|
|
9685
10734
|
return getTaskFile(existing.id, d);
|
|
9686
10735
|
}
|
|
9687
10736
|
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
9688
|
-
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]);
|
|
9689
10738
|
return getTaskFile(id, d);
|
|
9690
10739
|
}
|
|
9691
10740
|
function getTaskFile(id, db) {
|
|
@@ -9702,8 +10751,8 @@ function findTasksByFile(path, db) {
|
|
|
9702
10751
|
}
|
|
9703
10752
|
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
9704
10753
|
const d = db || getDatabase();
|
|
9705
|
-
const
|
|
9706
|
-
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]);
|
|
9707
10756
|
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
9708
10757
|
return row;
|
|
9709
10758
|
}
|
|
@@ -9748,8 +10797,16 @@ function rowToVerification(row) {
|
|
|
9748
10797
|
function linkTaskToCommit(input, db) {
|
|
9749
10798
|
const d = db || getDatabase();
|
|
9750
10799
|
const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
|
|
9751
|
-
const ciJson = input.ci_snapshot ? JSON.stringify(input.ci_snapshot) : null;
|
|
9752
|
-
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;
|
|
9753
10810
|
if (existing) {
|
|
9754
10811
|
d.run(`UPDATE task_commits SET
|
|
9755
10812
|
message = COALESCE(?, message),
|
|
@@ -9765,17 +10822,17 @@ function linkTaskToCommit(input, db) {
|
|
|
9765
10822
|
repo_path = COALESCE(?, repo_path),
|
|
9766
10823
|
traceability = COALESCE(?, traceability)
|
|
9767
10824
|
WHERE id = ?`, [
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
10825
|
+
message,
|
|
10826
|
+
author,
|
|
10827
|
+
filesChanged,
|
|
9771
10828
|
input.committed_at ?? null,
|
|
9772
|
-
|
|
9773
|
-
|
|
10829
|
+
branch,
|
|
10830
|
+
prUrl,
|
|
9774
10831
|
input.pr_number ?? null,
|
|
9775
|
-
|
|
10832
|
+
prState,
|
|
9776
10833
|
ciJson,
|
|
9777
|
-
|
|
9778
|
-
|
|
10834
|
+
releaseTag,
|
|
10835
|
+
repoPath,
|
|
9779
10836
|
traceJson,
|
|
9780
10837
|
existing.id
|
|
9781
10838
|
]);
|
|
@@ -9789,17 +10846,17 @@ function linkTaskToCommit(input, db) {
|
|
|
9789
10846
|
id,
|
|
9790
10847
|
input.task_id,
|
|
9791
10848
|
input.sha,
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
10849
|
+
message,
|
|
10850
|
+
author,
|
|
10851
|
+
filesChanged,
|
|
9795
10852
|
input.committed_at ?? null,
|
|
9796
|
-
|
|
9797
|
-
|
|
10853
|
+
branch,
|
|
10854
|
+
prUrl,
|
|
9798
10855
|
input.pr_number ?? null,
|
|
9799
|
-
|
|
10856
|
+
prState,
|
|
9800
10857
|
ciJson,
|
|
9801
|
-
|
|
9802
|
-
|
|
10858
|
+
releaseTag,
|
|
10859
|
+
repoPath,
|
|
9803
10860
|
traceJson ?? "{}",
|
|
9804
10861
|
now()
|
|
9805
10862
|
]);
|
|
@@ -9828,13 +10885,16 @@ function addTaskVerification(input, db) {
|
|
|
9828
10885
|
const d = db || getDatabase();
|
|
9829
10886
|
const id = uuid();
|
|
9830
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;
|
|
9831
10891
|
d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
9832
10892
|
id,
|
|
9833
10893
|
input.task_id,
|
|
9834
|
-
|
|
10894
|
+
command,
|
|
9835
10895
|
input.status || "unknown",
|
|
9836
|
-
|
|
9837
|
-
|
|
10896
|
+
outputSummary,
|
|
10897
|
+
artifactPath,
|
|
9838
10898
|
input.agent_id ?? null,
|
|
9839
10899
|
runAt,
|
|
9840
10900
|
now()
|
|
@@ -9864,6 +10924,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
9864
10924
|
}
|
|
9865
10925
|
var init_task_commits = __esm(() => {
|
|
9866
10926
|
init_database();
|
|
10927
|
+
init_prewrite_secrets();
|
|
9867
10928
|
});
|
|
9868
10929
|
|
|
9869
10930
|
// src/db/task-runs.ts
|
|
@@ -9974,7 +11035,7 @@ function startTaskRun(input, db) {
|
|
|
9974
11035
|
if (!getTask(input.task_id, d))
|
|
9975
11036
|
throw new TaskNotFoundError(input.task_id);
|
|
9976
11037
|
const id = input.id ?? uuid();
|
|
9977
|
-
const
|
|
11038
|
+
const timestamp2 = input.started_at || now();
|
|
9978
11039
|
if (input.claim && input.agent_id) {
|
|
9979
11040
|
startTask(input.task_id, input.agent_id, d);
|
|
9980
11041
|
}
|
|
@@ -9982,12 +11043,12 @@ function startTaskRun(input, db) {
|
|
|
9982
11043
|
id,
|
|
9983
11044
|
input.task_id,
|
|
9984
11045
|
input.agent_id ?? null,
|
|
9985
|
-
input.title ?
|
|
9986
|
-
input.summary ?
|
|
9987
|
-
JSON.stringify(
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
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
|
|
9991
11052
|
]);
|
|
9992
11053
|
addTaskRunEvent({
|
|
9993
11054
|
run_id: id,
|
|
@@ -9995,7 +11056,7 @@ function startTaskRun(input, db) {
|
|
|
9995
11056
|
message: input.summary || input.title || "run started",
|
|
9996
11057
|
data: { title: input.title, claim: Boolean(input.claim) },
|
|
9997
11058
|
agent_id: input.agent_id,
|
|
9998
|
-
created_at:
|
|
11059
|
+
created_at: timestamp2
|
|
9999
11060
|
}, d);
|
|
10000
11061
|
if (input.claim && input.agent_id) {
|
|
10001
11062
|
addTaskRunEvent({
|
|
@@ -10004,7 +11065,7 @@ function startTaskRun(input, db) {
|
|
|
10004
11065
|
message: `claimed by ${input.agent_id}`,
|
|
10005
11066
|
data: { agent_id: input.agent_id },
|
|
10006
11067
|
agent_id: input.agent_id,
|
|
10007
|
-
created_at:
|
|
11068
|
+
created_at: timestamp2
|
|
10008
11069
|
}, d);
|
|
10009
11070
|
}
|
|
10010
11071
|
const run = getTaskRun(id, d);
|
|
@@ -10019,7 +11080,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10019
11080
|
const d = db || getDatabase();
|
|
10020
11081
|
if (!getTask(input.task_id, d))
|
|
10021
11082
|
throw new TaskNotFoundError(input.task_id);
|
|
10022
|
-
const
|
|
11083
|
+
const timestamp2 = input.started_at || now();
|
|
10023
11084
|
const key = normalizeTransactionKey(input);
|
|
10024
11085
|
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
10025
11086
|
const dryRun = !input.apply;
|
|
@@ -10028,7 +11089,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10028
11089
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10029
11090
|
local_only: true,
|
|
10030
11091
|
dry_run: dryRun,
|
|
10031
|
-
processed_at:
|
|
11092
|
+
processed_at: timestamp2,
|
|
10032
11093
|
action: "matched",
|
|
10033
11094
|
key,
|
|
10034
11095
|
run: summarizeTaskRun(existing),
|
|
@@ -10041,7 +11102,7 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10041
11102
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10042
11103
|
local_only: true,
|
|
10043
11104
|
dry_run: true,
|
|
10044
|
-
processed_at:
|
|
11105
|
+
processed_at: timestamp2,
|
|
10045
11106
|
action: "preview",
|
|
10046
11107
|
key,
|
|
10047
11108
|
run: null,
|
|
@@ -10049,14 +11110,14 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10049
11110
|
commands: loopRunCommands(null, key)
|
|
10050
11111
|
};
|
|
10051
11112
|
}
|
|
10052
|
-
const metadata =
|
|
11113
|
+
const metadata = sanitizePreWriteValue({
|
|
10053
11114
|
...input.metadata || {},
|
|
10054
11115
|
loop_transaction: {
|
|
10055
11116
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10056
11117
|
idempotency_key: key,
|
|
10057
11118
|
loop_id: input.loop_id ?? null,
|
|
10058
11119
|
loop_run_id: input.loop_run_id ?? null,
|
|
10059
|
-
first_seen_at:
|
|
11120
|
+
first_seen_at: timestamp2
|
|
10060
11121
|
},
|
|
10061
11122
|
idempotency_key: key
|
|
10062
11123
|
});
|
|
@@ -10070,8 +11131,8 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10070
11131
|
input.loop_id ?? null,
|
|
10071
11132
|
input.loop_run_id ?? null,
|
|
10072
11133
|
JSON.stringify(metadata),
|
|
10073
|
-
|
|
10074
|
-
|
|
11134
|
+
timestamp2,
|
|
11135
|
+
timestamp2
|
|
10075
11136
|
]);
|
|
10076
11137
|
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
10077
11138
|
if (!transaction)
|
|
@@ -10089,16 +11150,16 @@ function beginTaskRunTransaction(input, db) {
|
|
|
10089
11150
|
summary: input.summary,
|
|
10090
11151
|
metadata,
|
|
10091
11152
|
claim: input.claim,
|
|
10092
|
-
started_at:
|
|
11153
|
+
started_at: timestamp2
|
|
10093
11154
|
}, d);
|
|
10094
|
-
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]);
|
|
10095
11156
|
return { run, action: "created" };
|
|
10096
11157
|
})();
|
|
10097
11158
|
return {
|
|
10098
11159
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10099
11160
|
local_only: true,
|
|
10100
11161
|
dry_run: false,
|
|
10101
|
-
processed_at:
|
|
11162
|
+
processed_at: timestamp2,
|
|
10102
11163
|
action: created.action,
|
|
10103
11164
|
key,
|
|
10104
11165
|
run: summarizeTaskRun(created.run),
|
|
@@ -10113,19 +11174,19 @@ function addTaskRunEvent(input, db) {
|
|
|
10113
11174
|
if (!run)
|
|
10114
11175
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10115
11176
|
const id = uuid();
|
|
10116
|
-
const
|
|
11177
|
+
const timestamp2 = input.created_at || now();
|
|
10117
11178
|
d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10118
11179
|
id,
|
|
10119
11180
|
run.id,
|
|
10120
11181
|
run.task_id,
|
|
10121
11182
|
input.event_type,
|
|
10122
|
-
input.message ?
|
|
10123
|
-
JSON.stringify(
|
|
11183
|
+
input.message ? sanitizePreWriteText(input.message, "run_event.message") : null,
|
|
11184
|
+
JSON.stringify(sanitizePreWriteValue(input.data || {}, "run_event.data")),
|
|
10124
11185
|
input.agent_id ?? run.agent_id,
|
|
10125
|
-
|
|
11186
|
+
timestamp2
|
|
10126
11187
|
]);
|
|
10127
11188
|
if (input.event_type === "comment" && input.message) {
|
|
10128
|
-
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);
|
|
10129
11190
|
}
|
|
10130
11191
|
return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
|
|
10131
11192
|
}
|
|
@@ -10137,10 +11198,10 @@ function addTaskRunCommand(input, db) {
|
|
|
10137
11198
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10138
11199
|
const id = uuid();
|
|
10139
11200
|
const status = input.status || "unknown";
|
|
10140
|
-
const
|
|
10141
|
-
const command =
|
|
10142
|
-
const outputSummary = input.output_summary ?
|
|
10143
|
-
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;
|
|
10144
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10145
11206
|
id,
|
|
10146
11207
|
run.id,
|
|
@@ -10152,8 +11213,8 @@ function addTaskRunCommand(input, db) {
|
|
|
10152
11213
|
artifactPath,
|
|
10153
11214
|
input.agent_id ?? run.agent_id,
|
|
10154
11215
|
input.started_at ?? null,
|
|
10155
|
-
input.completed_at ??
|
|
10156
|
-
|
|
11216
|
+
input.completed_at ?? timestamp2,
|
|
11217
|
+
timestamp2
|
|
10157
11218
|
]);
|
|
10158
11219
|
addTaskVerification({
|
|
10159
11220
|
task_id: run.task_id,
|
|
@@ -10162,7 +11223,7 @@ function addTaskRunCommand(input, db) {
|
|
|
10162
11223
|
output_summary: outputSummary ?? undefined,
|
|
10163
11224
|
artifact_path: artifactPath ?? undefined,
|
|
10164
11225
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10165
|
-
run_at: input.completed_at ??
|
|
11226
|
+
run_at: input.completed_at ?? timestamp2
|
|
10166
11227
|
}, d);
|
|
10167
11228
|
addTaskRunEvent({
|
|
10168
11229
|
run_id: run.id,
|
|
@@ -10181,7 +11242,7 @@ function addTaskRunCommand(input, db) {
|
|
|
10181
11242
|
}
|
|
10182
11243
|
},
|
|
10183
11244
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10184
|
-
created_at:
|
|
11245
|
+
created_at: timestamp2
|
|
10185
11246
|
}, d);
|
|
10186
11247
|
return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
|
|
10187
11248
|
}
|
|
@@ -10195,7 +11256,7 @@ function addTaskRunFile(input, db) {
|
|
|
10195
11256
|
task_id: run.task_id,
|
|
10196
11257
|
path: input.path,
|
|
10197
11258
|
status: input.status || "modified",
|
|
10198
|
-
note: input.note ?
|
|
11259
|
+
note: input.note ? sanitizePreWriteText(input.note, "run_file.note") : undefined,
|
|
10199
11260
|
agent_id: input.agent_id ?? run.agent_id ?? undefined
|
|
10200
11261
|
}, d);
|
|
10201
11262
|
addTaskRunEvent({
|
|
@@ -10214,19 +11275,19 @@ function addTaskRunArtifact(input, db) {
|
|
|
10214
11275
|
if (!run)
|
|
10215
11276
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10216
11277
|
const id = uuid();
|
|
10217
|
-
const
|
|
10218
|
-
const path =
|
|
10219
|
-
const description = input.description ?
|
|
10220
|
-
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");
|
|
10221
11282
|
let sizeBytes = input.size_bytes ?? null;
|
|
10222
11283
|
let digest = input.sha256 ?? null;
|
|
10223
|
-
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;
|
|
10224
11285
|
if (stored) {
|
|
10225
11286
|
sizeBytes = stored.size_bytes;
|
|
10226
11287
|
digest = stored.sha256;
|
|
10227
11288
|
metadata["artifact_store"] = stored.store;
|
|
10228
11289
|
} else if (input.store_content === true) {
|
|
10229
|
-
throw new Error(`Artifact file not found: ${
|
|
11290
|
+
throw new Error(`Artifact file not found: ${path}`);
|
|
10230
11291
|
}
|
|
10231
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
10232
11293
|
id,
|
|
@@ -10239,7 +11300,7 @@ function addTaskRunArtifact(input, db) {
|
|
|
10239
11300
|
digest,
|
|
10240
11301
|
JSON.stringify(metadata),
|
|
10241
11302
|
input.agent_id ?? run.agent_id,
|
|
10242
|
-
|
|
11303
|
+
timestamp2
|
|
10243
11304
|
]);
|
|
10244
11305
|
addTaskRunEvent({
|
|
10245
11306
|
run_id: run.id,
|
|
@@ -10247,7 +11308,7 @@ function addTaskRunArtifact(input, db) {
|
|
|
10247
11308
|
message: description || path,
|
|
10248
11309
|
data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
|
|
10249
11310
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10250
|
-
created_at:
|
|
11311
|
+
created_at: timestamp2
|
|
10251
11312
|
}, d);
|
|
10252
11313
|
return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
|
|
10253
11314
|
}
|
|
@@ -10267,28 +11328,28 @@ function finishTaskRun(input, db) {
|
|
|
10267
11328
|
const run = getTaskRun(runId, d);
|
|
10268
11329
|
if (!run)
|
|
10269
11330
|
throw new Error(`Run not found: ${input.run_id}`);
|
|
10270
|
-
const
|
|
10271
|
-
const summary = input.summary ?
|
|
10272
|
-
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]);
|
|
10273
11334
|
addTaskRunEvent({
|
|
10274
11335
|
run_id: run.id,
|
|
10275
11336
|
event_type: input.status,
|
|
10276
11337
|
message: summary || `run ${input.status}`,
|
|
10277
11338
|
data: { status: input.status },
|
|
10278
11339
|
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
10279
|
-
created_at:
|
|
11340
|
+
created_at: timestamp2
|
|
10280
11341
|
}, d);
|
|
10281
11342
|
const updated = getTaskRun(run.id, d);
|
|
10282
11343
|
emitLocalEventHooksQuiet({
|
|
10283
11344
|
type: `run.${input.status}`,
|
|
10284
|
-
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 },
|
|
10285
11346
|
databasePath: databasePathFromDatabase(d)
|
|
10286
11347
|
});
|
|
10287
11348
|
return updated;
|
|
10288
11349
|
}
|
|
10289
11350
|
function finishTaskRunTransaction(input, db) {
|
|
10290
11351
|
const d = db || getDatabase();
|
|
10291
|
-
const
|
|
11352
|
+
const timestamp2 = input.completed_at || now();
|
|
10292
11353
|
const status = input.status || "completed";
|
|
10293
11354
|
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
10294
11355
|
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
@@ -10306,7 +11367,7 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10306
11367
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10307
11368
|
local_only: true,
|
|
10308
11369
|
dry_run: dryRun,
|
|
10309
|
-
processed_at:
|
|
11370
|
+
processed_at: timestamp2,
|
|
10310
11371
|
action: conflict ? "conflict" : "matched",
|
|
10311
11372
|
key: resolvedKey,
|
|
10312
11373
|
run: summarizeTaskRun(run),
|
|
@@ -10319,10 +11380,10 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10319
11380
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10320
11381
|
local_only: true,
|
|
10321
11382
|
dry_run: true,
|
|
10322
|
-
processed_at:
|
|
11383
|
+
processed_at: timestamp2,
|
|
10323
11384
|
action: "preview",
|
|
10324
11385
|
key: resolvedKey,
|
|
10325
|
-
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 }),
|
|
10326
11387
|
warnings: [],
|
|
10327
11388
|
commands: loopRunCommands(run, resolvedKey)
|
|
10328
11389
|
};
|
|
@@ -10332,13 +11393,13 @@ function finishTaskRunTransaction(input, db) {
|
|
|
10332
11393
|
status,
|
|
10333
11394
|
summary: input.summary,
|
|
10334
11395
|
agent_id: input.agent_id,
|
|
10335
|
-
completed_at:
|
|
11396
|
+
completed_at: timestamp2
|
|
10336
11397
|
}, d);
|
|
10337
11398
|
return {
|
|
10338
11399
|
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
10339
11400
|
local_only: true,
|
|
10340
11401
|
dry_run: false,
|
|
10341
|
-
processed_at:
|
|
11402
|
+
processed_at: timestamp2,
|
|
10342
11403
|
action: "finished",
|
|
10343
11404
|
key: resolvedKey,
|
|
10344
11405
|
run: summarizeTaskRun(finished),
|
|
@@ -10368,7 +11429,7 @@ var init_task_runs = __esm(() => {
|
|
|
10368
11429
|
init_artifact_store();
|
|
10369
11430
|
init_event_emission_safety();
|
|
10370
11431
|
init_event_hooks();
|
|
10371
|
-
|
|
11432
|
+
init_prewrite_secrets();
|
|
10372
11433
|
init_types();
|
|
10373
11434
|
init_comments();
|
|
10374
11435
|
init_database();
|
|
@@ -10496,7 +11557,7 @@ function inWindow(event, query) {
|
|
|
10496
11557
|
function createCalendarItem(input, db) {
|
|
10497
11558
|
const d = db || getDatabase();
|
|
10498
11559
|
const id = uuid();
|
|
10499
|
-
const
|
|
11560
|
+
const timestamp2 = now();
|
|
10500
11561
|
const kind = input.kind || "work_block";
|
|
10501
11562
|
d.run(`INSERT INTO local_calendar_items (
|
|
10502
11563
|
id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
|
|
@@ -10515,8 +11576,8 @@ function createCalendarItem(input, db) {
|
|
|
10515
11576
|
input.run_id || null,
|
|
10516
11577
|
input.recurrence_rule || null,
|
|
10517
11578
|
JSON.stringify(input.metadata || {}),
|
|
10518
|
-
|
|
10519
|
-
|
|
11579
|
+
timestamp2,
|
|
11580
|
+
timestamp2
|
|
10520
11581
|
]);
|
|
10521
11582
|
return getCalendarItem(id, d);
|
|
10522
11583
|
}
|
|
@@ -10763,18 +11824,172 @@ var init_tasks = __esm(() => {
|
|
|
10763
11824
|
// src/storage/index.ts
|
|
10764
11825
|
init_config();
|
|
10765
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
|
+
|
|
10766
11981
|
// src/storage/local-sqlite.ts
|
|
10767
11982
|
init_tasks();
|
|
10768
11983
|
init_projects();
|
|
10769
11984
|
init_plans();
|
|
10770
11985
|
|
|
10771
11986
|
// src/db/agents.ts
|
|
11987
|
+
init_types();
|
|
10772
11988
|
init_database();
|
|
11989
|
+
init_identity_mapping();
|
|
10773
11990
|
init_storage_tombstones();
|
|
10774
11991
|
|
|
10775
11992
|
// src/db/agent-names.ts
|
|
10776
|
-
init_database();
|
|
10777
|
-
|
|
10778
11993
|
class InvalidAgentNameError extends Error {
|
|
10779
11994
|
suggestions;
|
|
10780
11995
|
constructor(name, reason, suggestions = []) {
|
|
@@ -11055,45 +12270,10 @@ function validateAgentName(name, existingNames = []) {
|
|
|
11055
12270
|
}
|
|
11056
12271
|
return normalized;
|
|
11057
12272
|
}
|
|
11058
|
-
function tableHasColumn(db, table, column) {
|
|
11059
|
-
try {
|
|
11060
|
-
return db.query(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
11061
|
-
} catch {
|
|
11062
|
-
return false;
|
|
11063
|
-
}
|
|
11064
|
-
}
|
|
11065
|
-
function updateReferences(db, oldName, newName) {
|
|
11066
|
-
const refs = [
|
|
11067
|
-
["tasks", "assigned_to"],
|
|
11068
|
-
["tasks", "agent_id"],
|
|
11069
|
-
["tasks", "locked_by"],
|
|
11070
|
-
["tasks", "assigned_by"],
|
|
11071
|
-
["plans", "agent_id"],
|
|
11072
|
-
["sessions", "agent_id"],
|
|
11073
|
-
["task_comments", "agent_id"],
|
|
11074
|
-
["task_history", "agent_id"],
|
|
11075
|
-
["webhooks", "agent_id"],
|
|
11076
|
-
["task_files", "agent_id"],
|
|
11077
|
-
["task_time_logs", "agent_id"],
|
|
11078
|
-
["task_watchers", "agent_id"],
|
|
11079
|
-
["task_checkpoints", "agent_id"],
|
|
11080
|
-
["task_heartbeats", "agent_id"],
|
|
11081
|
-
["project_agent_roles", "agent_id"]
|
|
11082
|
-
];
|
|
11083
|
-
let changed = 0;
|
|
11084
|
-
for (const [table, column] of refs) {
|
|
11085
|
-
if (!tableHasColumn(db, table, column))
|
|
11086
|
-
continue;
|
|
11087
|
-
try {
|
|
11088
|
-
changed += db.run(`UPDATE ${table} SET ${column} = ? WHERE LOWER(${column}) = ?`, [newName, oldName]).changes;
|
|
11089
|
-
} catch {}
|
|
11090
|
-
}
|
|
11091
|
-
return changed;
|
|
11092
|
-
}
|
|
11093
12273
|
function normalizeGeneratedAgentNames(db) {
|
|
11094
12274
|
const rows = db.query("SELECT * FROM agents ORDER BY created_at, id").all();
|
|
11095
12275
|
const existing = new Set(rows.map((agent) => normalizeAgentNameInput(agent.name)));
|
|
11096
|
-
const
|
|
12276
|
+
const planned = [];
|
|
11097
12277
|
for (const agent of rows) {
|
|
11098
12278
|
const oldName = normalizeAgentNameInput(agent.name);
|
|
11099
12279
|
if (!isBlockedAgentName(oldName))
|
|
@@ -11103,18 +12283,20 @@ function normalizeGeneratedAgentNames(db) {
|
|
|
11103
12283
|
if (!replacement) {
|
|
11104
12284
|
throw new Error("No safe agent names are available for normalization");
|
|
11105
12285
|
}
|
|
11106
|
-
existing.delete(oldName);
|
|
11107
12286
|
existing.add(replacement);
|
|
11108
|
-
|
|
11109
|
-
const referenceUpdates = updateReferences(db, oldName, replacement);
|
|
11110
|
-
renamed.push({
|
|
12287
|
+
planned.push({
|
|
11111
12288
|
id: agent.id,
|
|
11112
12289
|
old_name: oldName,
|
|
11113
12290
|
new_name: replacement,
|
|
11114
|
-
|
|
12291
|
+
applied: false,
|
|
12292
|
+
disposition: "candidate",
|
|
12293
|
+
alias_kind: "candidate",
|
|
12294
|
+
status: "quarantined",
|
|
12295
|
+
name_updates: 0,
|
|
12296
|
+
reference_updates: 0
|
|
11115
12297
|
});
|
|
11116
12298
|
}
|
|
11117
|
-
return
|
|
12299
|
+
return planned;
|
|
11118
12300
|
}
|
|
11119
12301
|
|
|
11120
12302
|
// src/db/agents.ts
|
|
@@ -11136,7 +12318,6 @@ function autoReleaseStaleAgents(db) {
|
|
|
11136
12318
|
return result.changes;
|
|
11137
12319
|
}
|
|
11138
12320
|
function getAvailableNamesFromPool(pool, db) {
|
|
11139
|
-
autoReleaseStaleAgents(db);
|
|
11140
12321
|
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
11141
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()));
|
|
11142
12323
|
return pool.map(normalizeAgentNameInput).filter((name) => !activeNames.has(name));
|
|
@@ -11147,6 +12328,10 @@ function shortUuid() {
|
|
|
11147
12328
|
function rowToAgent(row) {
|
|
11148
12329
|
return {
|
|
11149
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,
|
|
11150
12335
|
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
11151
12336
|
capabilities: JSON.parse(row.capabilities || "[]"),
|
|
11152
12337
|
status: row.status || "active",
|
|
@@ -11155,11 +12340,37 @@ function rowToAgent(row) {
|
|
|
11155
12340
|
}
|
|
11156
12341
|
function registerAgent(input, db) {
|
|
11157
12342
|
const d = db || getDatabase();
|
|
12343
|
+
const register = d.transaction(() => registerAgentInCurrentTransaction(input, d));
|
|
12344
|
+
return register.immediate();
|
|
12345
|
+
}
|
|
12346
|
+
function registerAgentInCurrentTransaction(input, d) {
|
|
11158
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
|
+
}
|
|
11159
12352
|
const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
|
|
11160
12353
|
const normalizedName = validateAgentName(input.name, existingNames);
|
|
11161
|
-
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;
|
|
11162
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
|
+
}
|
|
11163
12374
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
11164
12375
|
const activeWindowMs = getActiveWindowMs();
|
|
11165
12376
|
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
@@ -11196,6 +12407,14 @@ function registerAgent(input, db) {
|
|
|
11196
12407
|
updates.push("active_project_id = ?");
|
|
11197
12408
|
params.push(input.project_id);
|
|
11198
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
|
+
}
|
|
11199
12418
|
if (!existing.machine_id && machineId) {
|
|
11200
12419
|
updates.push("machine_id = ?");
|
|
11201
12420
|
params.push(machineId);
|
|
@@ -11205,11 +12424,12 @@ function registerAgent(input, db) {
|
|
|
11205
12424
|
return getAgent(existing.id, d);
|
|
11206
12425
|
}
|
|
11207
12426
|
const id = shortUuid();
|
|
11208
|
-
const
|
|
11209
|
-
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)
|
|
11210
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
11211
12430
|
id,
|
|
11212
12431
|
normalizedName,
|
|
12432
|
+
identityId,
|
|
11213
12433
|
input.description || null,
|
|
11214
12434
|
input.role || "agent",
|
|
11215
12435
|
input.title || null,
|
|
@@ -11219,10 +12439,12 @@ function registerAgent(input, db) {
|
|
|
11219
12439
|
input.reports_to || null,
|
|
11220
12440
|
input.org_id || null,
|
|
11221
12441
|
JSON.stringify(input.metadata || {}),
|
|
11222
|
-
|
|
11223
|
-
|
|
12442
|
+
timestamp2,
|
|
12443
|
+
timestamp2,
|
|
11224
12444
|
input.session_id || null,
|
|
11225
12445
|
input.working_dir || null,
|
|
12446
|
+
input.project_id || null,
|
|
12447
|
+
input.runtime_instance_id || null,
|
|
11226
12448
|
input.project_id && input.session_id ? input.project_id : null,
|
|
11227
12449
|
machineId
|
|
11228
12450
|
]);
|
|
@@ -11262,12 +12484,15 @@ function getAgent(id, db) {
|
|
|
11262
12484
|
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
11263
12485
|
return row ? rowToAgent(row) : null;
|
|
11264
12486
|
}
|
|
11265
|
-
function
|
|
11266
|
-
const d = db || getDatabase();
|
|
12487
|
+
function getAgentByNameProjection(name, d) {
|
|
11267
12488
|
const normalizedName = name.trim().toLowerCase();
|
|
11268
12489
|
const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
11269
12490
|
return row ? rowToAgent(row) : null;
|
|
11270
12491
|
}
|
|
12492
|
+
function getAgentByName(name, db) {
|
|
12493
|
+
const agent = getAgentByNameProjection(name, db || getDatabase());
|
|
12494
|
+
return agent ? { ...agent, identity_id: null } : null;
|
|
12495
|
+
}
|
|
11271
12496
|
function listAgents(opts, db) {
|
|
11272
12497
|
let d;
|
|
11273
12498
|
let includeArchived = false;
|
|
@@ -11277,7 +12502,6 @@ function listAgents(opts, db) {
|
|
|
11277
12502
|
includeArchived = opts?.include_archived ?? false;
|
|
11278
12503
|
d = db || getDatabase();
|
|
11279
12504
|
}
|
|
11280
|
-
autoReleaseStaleAgents(d);
|
|
11281
12505
|
if (includeArchived) {
|
|
11282
12506
|
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
11283
12507
|
}
|
|
@@ -11294,21 +12518,19 @@ function updateAgent(id, input, db) {
|
|
|
11294
12518
|
throw new Error(`Agent not found: ${id}`);
|
|
11295
12519
|
const sets = ["last_seen_at = ?"];
|
|
11296
12520
|
const params = [now()];
|
|
12521
|
+
let historicalLabel = null;
|
|
11297
12522
|
if (input.name !== undefined) {
|
|
11298
12523
|
const existingNames = d.query("SELECT name FROM agents WHERE id != ?").all(id).map((row) => row.name);
|
|
11299
12524
|
const newName = validateAgentName(input.name, existingNames);
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
if (isActive && holder.status === "active") {
|
|
11305
|
-
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]);
|
|
11306
12529
|
}
|
|
11307
|
-
|
|
11308
|
-
|
|
12530
|
+
historicalLabel = agent.name;
|
|
12531
|
+
sets.push("name = ?");
|
|
12532
|
+
params.push(newName);
|
|
11309
12533
|
}
|
|
11310
|
-
sets.push("name = ?");
|
|
11311
|
-
params.push(newName);
|
|
11312
12534
|
}
|
|
11313
12535
|
if (input.description !== undefined) {
|
|
11314
12536
|
sets.push("description = ?");
|
|
@@ -11347,7 +12569,12 @@ function updateAgent(id, input, db) {
|
|
|
11347
12569
|
params.push(JSON.stringify(input.metadata));
|
|
11348
12570
|
}
|
|
11349
12571
|
params.push(id);
|
|
11350
|
-
|
|
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();
|
|
11351
12578
|
return getAgent(id, d);
|
|
11352
12579
|
}
|
|
11353
12580
|
function deleteAgent(id, db) {
|
|
@@ -11818,6 +13045,52 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
11818
13045
|
return getTask(shortIdRows[0].id, db);
|
|
11819
13046
|
return null;
|
|
11820
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
|
+
}
|
|
11821
13094
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
11822
13095
|
const database = () => options.db ?? getDatabase();
|
|
11823
13096
|
let adapter;
|
|
@@ -11834,8 +13107,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
11834
13107
|
create: (input) => createTask(input, database()),
|
|
11835
13108
|
get: (id) => getTask(id, database()),
|
|
11836
13109
|
resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
|
|
11837
|
-
list: (filter = {}) =>
|
|
11838
|
-
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()),
|
|
11839
13112
|
update: (id, input) => updateTask(id, input, database()),
|
|
11840
13113
|
unlock: (id, agentId) => {
|
|
11841
13114
|
unlockTask(id, agentId, database());
|
|
@@ -11945,6 +13218,27 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
11945
13218
|
`CREATE INDEX IF NOT EXISTS ${tableName}_task_status_idx ON ${tableName} ((payload->>'status')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
11946
13219
|
`CREATE INDEX IF NOT EXISTS ${tableName}_task_project_idx ON ${tableName} ((payload->>'project_id')) WHERE object_type = 'tasks' AND deleted_at IS NULL`,
|
|
11947
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`,
|
|
11948
13242
|
`CREATE TABLE IF NOT EXISTS ${cursorTableName} (
|
|
11949
13243
|
service text NOT NULL,
|
|
11950
13244
|
cursor_name text NOT NULL,
|
|
@@ -12582,12 +13876,23 @@ class PostgresJsonRecordStore {
|
|
|
12582
13876
|
}
|
|
12583
13877
|
if (filter.include_subtasks !== true)
|
|
12584
13878
|
conds.push(`(payload->>'parent_id' IS NULL OR payload->>'parent_id' = '')`);
|
|
12585
|
-
|
|
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 };
|
|
12586
13890
|
}
|
|
12587
13891
|
async listTasks(filter) {
|
|
12588
13892
|
await this.ensureSchema();
|
|
12589
|
-
const { where, params } = this.buildTaskFilterSql(filter);
|
|
12590
|
-
|
|
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}`;
|
|
12591
13896
|
if (filter.limit !== undefined) {
|
|
12592
13897
|
params.push(filter.limit);
|
|
12593
13898
|
sql += ` LIMIT $${params.length}`;
|
|
@@ -12758,7 +14063,7 @@ class PostgresJsonRecordStore {
|
|
|
12758
14063
|
}
|
|
12759
14064
|
async deleteTemplateWithTasks(id, context = {}) {
|
|
12760
14065
|
await this.ensureSchema();
|
|
12761
|
-
const
|
|
14066
|
+
const timestamp2 = new Date().toISOString();
|
|
12762
14067
|
const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
|
|
12763
14068
|
SELECT 1 FROM ${this.tableName}
|
|
12764
14069
|
WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
|
|
@@ -12770,7 +14075,7 @@ class PostgresJsonRecordStore {
|
|
|
12770
14075
|
WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
|
|
12771
14076
|
AND (record.object_type = 'templates' AND record.object_id = $2
|
|
12772
14077
|
OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
|
|
12773
|
-
RETURNING record.object_type`, [this.service, id,
|
|
14078
|
+
RETURNING record.object_type`, [this.service, id, timestamp2, this.machineId(context)]);
|
|
12774
14079
|
return result.rows.some((row) => row.object_type === "templates");
|
|
12775
14080
|
}
|
|
12776
14081
|
async completeTask(id, agentId, options) {
|
|
@@ -12834,7 +14139,7 @@ class PostgresJsonRecordStore {
|
|
|
12834
14139
|
const normalizedSlug = slugifyRaw(newSlug);
|
|
12835
14140
|
if (!normalizedSlug)
|
|
12836
14141
|
throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
|
|
12837
|
-
const
|
|
14142
|
+
const timestamp2 = new Date().toISOString();
|
|
12838
14143
|
try {
|
|
12839
14144
|
const result = await this.options.client.query(`/* todos:rename-project-atomic */ WITH target AS (
|
|
12840
14145
|
SELECT payload, payload->>'task_list_id' AS old_slug
|
|
@@ -12882,7 +14187,7 @@ class PostgresJsonRecordStore {
|
|
|
12882
14187
|
EXISTS (SELECT 1 FROM project_conflict) AS project_conflict,
|
|
12883
14188
|
EXISTS (SELECT 1 FROM task_list_conflict) AS task_list_conflict,
|
|
12884
14189
|
COALESCE((SELECT payload FROM updated_project), (SELECT payload FROM target)) AS project,
|
|
12885
|
-
(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)]);
|
|
12886
14191
|
const row = result.rows[0];
|
|
12887
14192
|
if (!row?.found)
|
|
12888
14193
|
throw new ProjectNotFoundError(id);
|
|
@@ -12935,19 +14240,19 @@ class PostgresJsonRecordStore {
|
|
|
12935
14240
|
const existing = await this.get(type, id);
|
|
12936
14241
|
if (!existing)
|
|
12937
14242
|
return false;
|
|
12938
|
-
const
|
|
14243
|
+
const timestamp2 = new Date().toISOString();
|
|
12939
14244
|
return this.tombstone({
|
|
12940
14245
|
object_type: type,
|
|
12941
14246
|
object_id: id,
|
|
12942
|
-
deleted_at:
|
|
12943
|
-
updated_at:
|
|
14247
|
+
deleted_at: timestamp2,
|
|
14248
|
+
updated_at: timestamp2,
|
|
12944
14249
|
payload: existing,
|
|
12945
14250
|
version: numberValue2(existing["version"])
|
|
12946
14251
|
}, context);
|
|
12947
14252
|
}
|
|
12948
14253
|
async deletePlan(id, context = {}) {
|
|
12949
14254
|
await this.ensureSchema();
|
|
12950
|
-
const
|
|
14255
|
+
const timestamp2 = new Date().toISOString();
|
|
12951
14256
|
const result = await this.options.client.query(`/* todos:delete-plan-atomic */ WITH target AS (
|
|
12952
14257
|
SELECT payload FROM ${this.tableName}
|
|
12953
14258
|
WHERE service = $1 AND object_type = 'plans' AND object_id = $2 AND deleted_at IS NULL
|
|
@@ -12977,7 +14282,7 @@ class PostgresJsonRecordStore {
|
|
|
12977
14282
|
RETURNING 1
|
|
12978
14283
|
) SELECT
|
|
12979
14284
|
EXISTS (SELECT 1 FROM target) AS found,
|
|
12980
|
-
(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)]);
|
|
12981
14286
|
return Boolean(result.rows[0]?.found);
|
|
12982
14287
|
}
|
|
12983
14288
|
async tombstone(tombstone, context = {}) {
|
|
@@ -13038,7 +14343,7 @@ class PostgresJsonRecordStore {
|
|
|
13038
14343
|
}
|
|
13039
14344
|
}
|
|
13040
14345
|
async function createTask2(input, store, context) {
|
|
13041
|
-
const
|
|
14346
|
+
const timestamp2 = new Date().toISOString();
|
|
13042
14347
|
const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
|
|
13043
14348
|
const task = {
|
|
13044
14349
|
id: randomUUID3(),
|
|
@@ -13060,8 +14365,8 @@ async function createTask2(input, store, context) {
|
|
|
13060
14365
|
version: 1,
|
|
13061
14366
|
locked_by: null,
|
|
13062
14367
|
locked_at: null,
|
|
13063
|
-
created_at:
|
|
13064
|
-
updated_at:
|
|
14368
|
+
created_at: timestamp2,
|
|
14369
|
+
updated_at: timestamp2,
|
|
13065
14370
|
started_at: null,
|
|
13066
14371
|
completed_at: null,
|
|
13067
14372
|
due_at: input.due_at ?? null,
|
|
@@ -13193,9 +14498,9 @@ async function lockTask2(id, agentId, store) {
|
|
|
13193
14498
|
if (task.locked_by && task.locked_by !== agentId && !cloudLockExpired(task.locked_at)) {
|
|
13194
14499
|
return { success: false, locked_by: task.locked_by, locked_at: task.locked_at ?? undefined, error: `Task is locked by ${task.locked_by}` };
|
|
13195
14500
|
}
|
|
13196
|
-
const
|
|
13197
|
-
await patchTask(task, { locked_by: agentId, locked_at:
|
|
13198
|
-
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) };
|
|
13199
14504
|
}
|
|
13200
14505
|
async function unlockTask2(id, agentId, store) {
|
|
13201
14506
|
const task = await requireRecord("tasks", id, store);
|
|
@@ -13234,8 +14539,8 @@ async function addDependency2(taskId, dependsOn, store, context) {
|
|
|
13234
14539
|
for (const next of adjacency.get(node) ?? [])
|
|
13235
14540
|
queue.push(next);
|
|
13236
14541
|
}
|
|
13237
|
-
const
|
|
13238
|
-
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 };
|
|
13239
14544
|
await store.upsert("dependencies", record, context);
|
|
13240
14545
|
return { task_id: taskId, depends_on: dependsOn };
|
|
13241
14546
|
}
|
|
@@ -13256,7 +14561,7 @@ async function listDependencies(taskId, store) {
|
|
|
13256
14561
|
async function addVerification(input, store, context) {
|
|
13257
14562
|
if (!await store.get("tasks", input.task_id))
|
|
13258
14563
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13259
|
-
const
|
|
14564
|
+
const timestamp2 = new Date().toISOString();
|
|
13260
14565
|
const verification = {
|
|
13261
14566
|
id: randomUUID3(),
|
|
13262
14567
|
task_id: input.task_id,
|
|
@@ -13265,10 +14570,10 @@ async function addVerification(input, store, context) {
|
|
|
13265
14570
|
output_summary: input.output_summary ?? null,
|
|
13266
14571
|
artifact_path: input.artifact_path ?? null,
|
|
13267
14572
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
13268
|
-
run_at:
|
|
13269
|
-
created_at:
|
|
14573
|
+
run_at: timestamp2,
|
|
14574
|
+
created_at: timestamp2
|
|
13270
14575
|
};
|
|
13271
|
-
await store.upsert("verifications", { ...verification, updated_at:
|
|
14576
|
+
await store.upsert("verifications", { ...verification, updated_at: timestamp2 }, context);
|
|
13272
14577
|
return verification;
|
|
13273
14578
|
}
|
|
13274
14579
|
async function listVerifications(taskId, store) {
|
|
@@ -13277,7 +14582,7 @@ async function listVerifications(taskId, store) {
|
|
|
13277
14582
|
async function addCommit(input, store, context) {
|
|
13278
14583
|
if (!await store.get("tasks", input.task_id))
|
|
13279
14584
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13280
|
-
const
|
|
14585
|
+
const timestamp2 = new Date().toISOString();
|
|
13281
14586
|
const commit = {
|
|
13282
14587
|
id: randomUUID3(),
|
|
13283
14588
|
task_id: input.task_id,
|
|
@@ -13285,9 +14590,9 @@ async function addCommit(input, store, context) {
|
|
|
13285
14590
|
message: input.message ?? null,
|
|
13286
14591
|
author: input.author ?? null,
|
|
13287
14592
|
files_changed: input.files_changed ?? null,
|
|
13288
|
-
created_at:
|
|
14593
|
+
created_at: timestamp2
|
|
13289
14594
|
};
|
|
13290
|
-
await store.upsert("commits", { ...commit, updated_at:
|
|
14595
|
+
await store.upsert("commits", { ...commit, updated_at: timestamp2 }, context);
|
|
13291
14596
|
return commit;
|
|
13292
14597
|
}
|
|
13293
14598
|
async function listCommits(taskId, store) {
|
|
@@ -13300,7 +14605,7 @@ async function findCommit(sha, store) {
|
|
|
13300
14605
|
async function addGitRef(input, store, context) {
|
|
13301
14606
|
if (!await store.get("tasks", input.task_id))
|
|
13302
14607
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
13303
|
-
const
|
|
14608
|
+
const timestamp2 = new Date().toISOString();
|
|
13304
14609
|
const gitRef = {
|
|
13305
14610
|
id: randomUUID3(),
|
|
13306
14611
|
task_id: input.task_id,
|
|
@@ -13309,8 +14614,8 @@ async function addGitRef(input, store, context) {
|
|
|
13309
14614
|
url: input.url ?? null,
|
|
13310
14615
|
provider: input.provider ?? null,
|
|
13311
14616
|
metadata: input.metadata ?? {},
|
|
13312
|
-
created_at:
|
|
13313
|
-
updated_at:
|
|
14617
|
+
created_at: timestamp2,
|
|
14618
|
+
updated_at: timestamp2
|
|
13314
14619
|
};
|
|
13315
14620
|
await store.upsert("refs", gitRef, context);
|
|
13316
14621
|
return gitRef;
|
|
@@ -13321,7 +14626,8 @@ async function listGitRefs(taskId, store) {
|
|
|
13321
14626
|
async function findGitRefs(ref, store) {
|
|
13322
14627
|
return (await store.list("refs")).filter((r) => r.name === ref).sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
13323
14628
|
}
|
|
13324
|
-
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}`;
|
|
13325
14631
|
function toFilterArray(value) {
|
|
13326
14632
|
return Array.isArray(value) ? value : [value];
|
|
13327
14633
|
}
|
|
@@ -13367,7 +14673,7 @@ async function getChangedSince(since, filters, store) {
|
|
|
13367
14673
|
return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
|
|
13368
14674
|
}
|
|
13369
14675
|
async function createProject2(input, store, context) {
|
|
13370
|
-
const
|
|
14676
|
+
const timestamp2 = new Date().toISOString();
|
|
13371
14677
|
const derivedSlug = slugifyRaw(input.name);
|
|
13372
14678
|
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
|
|
13373
14679
|
if (!derivedSlug || !taskListId)
|
|
@@ -13380,8 +14686,8 @@ async function createProject2(input, store, context) {
|
|
|
13380
14686
|
task_list_id: taskListId,
|
|
13381
14687
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
13382
14688
|
task_counter: 0,
|
|
13383
|
-
created_at:
|
|
13384
|
-
updated_at:
|
|
14689
|
+
created_at: timestamp2,
|
|
14690
|
+
updated_at: timestamp2,
|
|
13385
14691
|
machine_id: store.machineId(context),
|
|
13386
14692
|
synced_at: null
|
|
13387
14693
|
};
|
|
@@ -13396,7 +14702,7 @@ async function updateProject2(id, input, store) {
|
|
|
13396
14702
|
return store.upsert("projects", updated);
|
|
13397
14703
|
}
|
|
13398
14704
|
async function createPlan2(input, store, context) {
|
|
13399
|
-
const
|
|
14705
|
+
const timestamp2 = new Date().toISOString();
|
|
13400
14706
|
const projectId = input.project_id ?? context?.projectId ?? null;
|
|
13401
14707
|
const slug = await resolvePostgresPlanSlug({
|
|
13402
14708
|
name: input.name,
|
|
@@ -13413,8 +14719,8 @@ async function createPlan2(input, store, context) {
|
|
|
13413
14719
|
name: input.name,
|
|
13414
14720
|
description: input.description ?? null,
|
|
13415
14721
|
status: input.status ?? "active",
|
|
13416
|
-
created_at:
|
|
13417
|
-
updated_at:
|
|
14722
|
+
created_at: timestamp2,
|
|
14723
|
+
updated_at: timestamp2,
|
|
13418
14724
|
machine_id: store.machineId(context),
|
|
13419
14725
|
synced_at: null
|
|
13420
14726
|
}, context);
|
|
@@ -13438,7 +14744,7 @@ async function registerAgent2(input, store, context) {
|
|
|
13438
14744
|
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
13439
14745
|
return { conflict: true, message: `Agent name '${input.name}' is already active` };
|
|
13440
14746
|
}
|
|
13441
|
-
const
|
|
14747
|
+
const timestamp2 = new Date().toISOString();
|
|
13442
14748
|
const agent = {
|
|
13443
14749
|
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
13444
14750
|
name: input.name,
|
|
@@ -13452,8 +14758,8 @@ async function registerAgent2(input, store, context) {
|
|
|
13452
14758
|
capabilities: input.capabilities ?? existing?.capabilities ?? [],
|
|
13453
14759
|
status: "active",
|
|
13454
14760
|
metadata: input.metadata ?? existing?.metadata ?? {},
|
|
13455
|
-
created_at: existing?.created_at ??
|
|
13456
|
-
last_seen_at:
|
|
14761
|
+
created_at: existing?.created_at ?? timestamp2,
|
|
14762
|
+
last_seen_at: timestamp2,
|
|
13457
14763
|
session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
|
|
13458
14764
|
working_dir: input.working_dir ?? existing?.working_dir ?? null,
|
|
13459
14765
|
active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
|
|
@@ -13498,7 +14804,7 @@ async function releaseAgent2(idOrName, sessionId, store, context) {
|
|
|
13498
14804
|
return { agent: updated, released: true };
|
|
13499
14805
|
}
|
|
13500
14806
|
async function createTaskList2(input, store, context) {
|
|
13501
|
-
const
|
|
14807
|
+
const timestamp2 = new Date().toISOString();
|
|
13502
14808
|
const slug = slugifyRaw(input.slug === undefined ? input.name : input.slug);
|
|
13503
14809
|
if (!slug)
|
|
13504
14810
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
@@ -13509,8 +14815,8 @@ async function createTaskList2(input, store, context) {
|
|
|
13509
14815
|
name: input.name,
|
|
13510
14816
|
description: input.description ?? null,
|
|
13511
14817
|
metadata: input.metadata ?? {},
|
|
13512
|
-
created_at:
|
|
13513
|
-
updated_at:
|
|
14818
|
+
created_at: timestamp2,
|
|
14819
|
+
updated_at: timestamp2,
|
|
13514
14820
|
machine_id: store.machineId(context),
|
|
13515
14821
|
synced_at: null
|
|
13516
14822
|
}, context);
|
|
@@ -13536,7 +14842,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
13536
14842
|
});
|
|
13537
14843
|
}
|
|
13538
14844
|
async function createTemplate2(input, store, context) {
|
|
13539
|
-
const
|
|
14845
|
+
const timestamp2 = new Date().toISOString();
|
|
13540
14846
|
const template = {
|
|
13541
14847
|
id: randomUUID3(),
|
|
13542
14848
|
name: input.name,
|
|
@@ -13549,15 +14855,15 @@ async function createTemplate2(input, store, context) {
|
|
|
13549
14855
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
13550
14856
|
plan_id: input.plan_id ?? null,
|
|
13551
14857
|
metadata: input.metadata ?? {},
|
|
13552
|
-
created_at:
|
|
14858
|
+
created_at: timestamp2,
|
|
13553
14859
|
machine_id: store.machineId(context),
|
|
13554
14860
|
synced_at: null
|
|
13555
14861
|
};
|
|
13556
|
-
const tasks = buildTemplateTasks(template.id, input.tasks ?? [],
|
|
14862
|
+
const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp2);
|
|
13557
14863
|
await store.createTemplateWithTasks(template, tasks, context);
|
|
13558
14864
|
return template;
|
|
13559
14865
|
}
|
|
13560
|
-
function buildTemplateTasks(templateId, inputs,
|
|
14866
|
+
function buildTemplateTasks(templateId, inputs, timestamp2) {
|
|
13561
14867
|
return inputs.map((input, position) => ({
|
|
13562
14868
|
id: randomUUID3(),
|
|
13563
14869
|
template_id: templateId,
|
|
@@ -13571,7 +14877,7 @@ function buildTemplateTasks(templateId, inputs, timestamp) {
|
|
|
13571
14877
|
include_template_id: input.include_template_id ?? null,
|
|
13572
14878
|
depends_on_positions: input.depends_on ?? [],
|
|
13573
14879
|
metadata: input.metadata ?? {},
|
|
13574
|
-
created_at:
|
|
14880
|
+
created_at: timestamp2
|
|
13575
14881
|
}));
|
|
13576
14882
|
}
|
|
13577
14883
|
async function deleteTemplate2(id, store, context) {
|
|
@@ -14198,10 +15504,23 @@ function createTodosCloudQueryClient(url, options = {}) {
|
|
|
14198
15504
|
idleTimeout: options.idleTimeout ?? 20,
|
|
14199
15505
|
connectionTimeout: options.connectionTimeout ?? 15
|
|
14200
15506
|
});
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
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
|
+
}));
|
|
14205
15524
|
},
|
|
14206
15525
|
async close() {
|
|
14207
15526
|
if (typeof sql.end === "function")
|
|
@@ -14210,6 +15529,7 @@ function createTodosCloudQueryClient(url, options = {}) {
|
|
|
14210
15529
|
await sql.close();
|
|
14211
15530
|
}
|
|
14212
15531
|
};
|
|
15532
|
+
return client;
|
|
14213
15533
|
}
|
|
14214
15534
|
function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
14215
15535
|
const url = getTodosStorageDatabaseUrl(env);
|
|
@@ -15179,7 +16499,7 @@ export {
|
|
|
15179
16499
|
PostgresScopedSlugMigrationConflictError,
|
|
15180
16500
|
PostgresScopedSlugIndexBuildError,
|
|
15181
16501
|
COMMENT_REDACTION_BACKFILL_CONFIRMATION,
|
|
15182
|
-
|
|
16502
|
+
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
15183
16503
|
CANONICAL_TODOS_RDS_DATABASE,
|
|
15184
|
-
|
|
16504
|
+
CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
15185
16505
|
};
|