@cleocode/core 2026.9.25 → 2026.9.26
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/dist/admin/import-tasks.d.ts.map +1 -1
- package/dist/admin/import-tasks.js +5 -2
- package/dist/admin/import-tasks.js.map +1 -1
- package/dist/admin/import.d.ts.map +1 -1
- package/dist/admin/import.js +7 -2
- package/dist/admin/import.js.map +1 -1
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/index.d.ts +1 -0
- package/dist/cloud/index.d.ts.map +1 -1
- package/dist/cloud/index.js +1 -0
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/nexus-auth.d.ts +26 -1
- package/dist/cloud/nexus-auth.d.ts.map +1 -1
- package/dist/cloud/nexus-auth.js +61 -32
- package/dist/cloud/nexus-auth.js.map +1 -1
- package/dist/cloud/nexus-credentials.js +2 -2
- package/dist/cloud/nexus-credentials.js.map +1 -1
- package/dist/cloud/nexus-device.d.ts +809 -0
- package/dist/cloud/nexus-device.d.ts.map +1 -0
- package/dist/cloud/nexus-device.js +1680 -0
- package/dist/cloud/nexus-device.js.map +1 -0
- package/dist/cloud/nexus-enrol.d.ts +210 -0
- package/dist/cloud/nexus-enrol.d.ts.map +1 -0
- package/dist/cloud/nexus-enrol.js +1023 -0
- package/dist/cloud/nexus-enrol.js.map +1 -0
- package/dist/cloud/nexus-link.d.ts +3 -0
- package/dist/cloud/nexus-link.d.ts.map +1 -1
- package/dist/cloud/nexus-link.js +38 -4
- package/dist/cloud/nexus-link.js.map +1 -1
- package/dist/cloud/signing.d.ts +20 -0
- package/dist/cloud/signing.d.ts.map +1 -1
- package/dist/cloud/signing.js +14 -0
- package/dist/cloud/signing.js.map +1 -1
- package/dist/crypto/credentials.d.ts +61 -0
- package/dist/crypto/credentials.d.ts.map +1 -1
- package/dist/crypto/credentials.js +154 -58
- package/dist/crypto/credentials.js.map +1 -1
- package/dist/docs/export-document.js +830 -190
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/dependency-cycles.d.ts +41 -0
- package/dist/doctor/dependency-cycles.d.ts.map +1 -0
- package/dist/doctor/dependency-cycles.js +39 -0
- package/dist/doctor/dependency-cycles.js.map +1 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/row-identity.d.ts +11 -0
- package/dist/doctor/row-identity.d.ts.map +1 -1
- package/dist/doctor/row-identity.js +67 -9
- package/dist/doctor/row-identity.js.map +1 -1
- package/dist/error-catalog.d.ts.map +1 -1
- package/dist/error-catalog.js +4 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/error-registry.d.ts.map +1 -1
- package/dist/error-registry.js +8 -0
- package/dist/error-registry.js.map +1 -1
- package/dist/llm/plugin-facade.js +857 -217
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/workspace.d.ts.map +1 -1
- package/dist/nexus/workspace.js +54 -4
- package/dist/nexus/workspace.js.map +1 -1
- package/dist/orchestrate/lifecycle-ops.js +1 -1
- package/dist/orchestrate/lifecycle-ops.js.map +1 -1
- package/dist/orchestrate/pivot.d.ts.map +1 -1
- package/dist/orchestrate/pivot.js +4 -2
- package/dist/orchestrate/pivot.js.map +1 -1
- package/dist/orchestrate/query-ops.d.ts.map +1 -1
- package/dist/orchestrate/query-ops.js +3 -3
- package/dist/orchestrate/query-ops.js.map +1 -1
- package/dist/orchestrate/spawn-ops.js +1 -1
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/lead-rollup.d.ts.map +1 -1
- package/dist/orchestration/lead-rollup.js +3 -3
- package/dist/orchestration/lead-rollup.js.map +1 -1
- package/dist/orchestration/parallel.js +1 -1
- package/dist/orchestration/parallel.js.map +1 -1
- package/dist/orchestration/wave-topic.d.ts +4 -2
- package/dist/orchestration/wave-topic.d.ts.map +1 -1
- package/dist/orchestration/wave-topic.js +6 -4
- package/dist/orchestration/wave-topic.js.map +1 -1
- package/dist/orchestration/waves.d.ts +2 -1
- package/dist/orchestration/waves.d.ts.map +1 -1
- package/dist/orchestration/waves.js +4 -3
- package/dist/orchestration/waves.js.map +1 -1
- package/dist/phases/index.d.ts.map +1 -1
- package/dist/phases/index.js +4 -2
- package/dist/phases/index.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +856 -216
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +77 -49
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/focus-state-store.d.ts +69 -23
- package/dist/sessions/focus-state-store.d.ts.map +1 -1
- package/dist/sessions/focus-state-store.js +113 -27
- package/dist/sessions/focus-state-store.js.map +1 -1
- package/dist/sessions/handoff.js +1 -1
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +24 -13
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/terminal-identity.d.ts +165 -17
- package/dist/sessions/terminal-identity.d.ts.map +1 -1
- package/dist/sessions/terminal-identity.js +341 -32
- package/dist/sessions/terminal-identity.js.map +1 -1
- package/dist/skills/orchestrator/startup.js +2 -2
- package/dist/skills/orchestrator/startup.js.map +1 -1
- package/dist/snapshot/index.d.ts +4 -0
- package/dist/snapshot/index.d.ts.map +1 -1
- package/dist/snapshot/index.js +50 -3
- package/dist/snapshot/index.js.map +1 -1
- package/dist/store/db-helpers.d.ts +15 -2
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/db-helpers.js +84 -3
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/dependency-cycles.d.ts +164 -0
- package/dist/store/dependency-cycles.d.ts.map +1 -0
- package/dist/store/dependency-cycles.js +360 -0
- package/dist/store/dependency-cycles.js.map +1 -0
- package/dist/store/display-id-alias.d.ts +93 -37
- package/dist/store/display-id-alias.d.ts.map +1 -1
- package/dist/store/display-id-alias.js +590 -115
- package/dist/store/display-id-alias.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +34 -7
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +8 -1
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/global-salt.d.ts +35 -0
- package/dist/store/global-salt.d.ts.map +1 -1
- package/dist/store/global-salt.js +136 -16
- package/dist/store/global-salt.js.map +1 -1
- package/dist/store/migration-sqlite.d.ts.map +1 -1
- package/dist/store/migration-sqlite.js +19 -11
- package/dist/store/migration-sqlite.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +6 -0
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +11 -3
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts +91 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +276 -7
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/safety-data-accessor.d.ts +3 -2
- package/dist/store/safety-data-accessor.d.ts.map +1 -1
- package/dist/store/safety-data-accessor.js +5 -2
- package/dist/store/safety-data-accessor.js.map +1 -1
- package/dist/store/session-store.d.ts +63 -3
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +121 -8
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/split-brain-import.d.ts.map +1 -1
- package/dist/store/split-brain-import.js +36 -0
- package/dist/store/split-brain-import.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +50 -3
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +246 -13
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +14 -2
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/store/umbrella-data-accessor.d.ts +3 -2
- package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
- package/dist/store/umbrella-data-accessor.js +5 -2
- package/dist/store/umbrella-data-accessor.js.map +1 -1
- package/dist/system/health.d.ts +2 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +54 -0
- package/dist/system/health.js.map +1 -1
- package/dist/task-work/index.d.ts.map +1 -1
- package/dist/task-work/index.js +9 -6
- package/dist/task-work/index.js.map +1 -1
- package/dist/tasks/analyze.d.ts.map +1 -1
- package/dist/tasks/analyze.js +3 -2
- package/dist/tasks/analyze.js.map +1 -1
- package/dist/tasks/evidence.d.ts +19 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +79 -18
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/infer-add-params.js +1 -1
- package/dist/tasks/infer-add-params.js.map +1 -1
- package/dist/tasks/task-context.d.ts.map +1 -1
- package/dist/tasks/task-context.js +3 -1
- package/dist/tasks/task-context.js.map +1 -1
- package/dist/tasks/task-import.d.ts.map +1 -1
- package/dist/tasks/task-import.js +11 -3
- package/dist/tasks/task-import.js.map +1 -1
- package/dist/tasks/task-next.d.ts +5 -1
- package/dist/tasks/task-next.d.ts.map +1 -1
- package/dist/tasks/task-next.js +7 -2
- package/dist/tasks/task-next.js.map +1 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +3 -2
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
- package/dist/workgraph/scaffold-apply.js +14 -5
- package/dist/workgraph/scaffold-apply.js.map +1 -1
- package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/migration.sql +76 -0
- package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/revert.sql +7 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +2 -2
- package/templates/CLEO-REFERENCE.md +1 -1
|
@@ -22,13 +22,15 @@ import { withBackgroundOpCommitBoundary } from './background-ops.js';
|
|
|
22
22
|
import { archivedTaskToRow, rowToSession, rowToTask, taskToRow } from './converters.js';
|
|
23
23
|
import { cleanupBrainRefsOnSessionDelete } from './cross-db-cleanup.js';
|
|
24
24
|
import { batchUpdateDependencies, insertNewTask, loadDependenciesForTasks, loadRelationsForTasks, parseLabels, updateDependencies, updateTaskLabels, upsertSession, upsertTask, } from './db-helpers.js';
|
|
25
|
-
import { registerRowIdentityWriters } from './row-identity.js';
|
|
25
|
+
import { fillRowUids, parseStoreTimestamp, registerRowIdentityWriters, rowIdentityShared, } from './row-identity.js';
|
|
26
|
+
import { rowUidFillEnabled } from './row-identity-flag.js';
|
|
27
|
+
import { ROW_IDENTITY } from './row-identity-registry.js';
|
|
26
28
|
import { tasksAuditLog } from './schema/cleo-project/audit.js';
|
|
27
29
|
import { resolveCurrentSession } from './session-store.js';
|
|
28
30
|
import { closeDb, getDb, getNativeTasksDb } from './sqlite.js';
|
|
29
31
|
import { TERMINAL_TASK_STATUSES } from './status-registry.js';
|
|
30
32
|
import { claimAllows, claimColumnsOf, claimFromColumns, isClaimExpired, leaseExpiresAtFrom, taskClaimedError, taskClaimLeaseMs, } from './task-claim.js';
|
|
31
|
-
import { nextTaskVersion, taskConflictError, taskVersion } from './task-version.js';
|
|
33
|
+
import { nextTaskVersion, taskConflictError, taskVersion, } from './task-version.js';
|
|
32
34
|
import * as schema from './tasks-schema.js';
|
|
33
35
|
import { assertTwinCollapseWritable, bareCounterOf, mirrorCounterToBare } from './twin-collapse.js';
|
|
34
36
|
import { runHeartbeatWrite, withWriteRetry } from './with-retry.js';
|
|
@@ -234,6 +236,7 @@ export function renameTaskDisplayIdNative(nativeDb, uid, toId, refs, guard = {})
|
|
|
234
236
|
if (Number(changed) !== 1) {
|
|
235
237
|
throw taskConflictError(current.id, taskVersion(current), null);
|
|
236
238
|
}
|
|
239
|
+
recordLocalRename(nativeDb, uid, current.id, toId, taskVersion(current));
|
|
237
240
|
const rewritten = {};
|
|
238
241
|
const q = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
239
242
|
for (const ref of refs) {
|
|
@@ -311,19 +314,42 @@ export function rederiveAcIdsNative(nativeDb, fromId, toId) {
|
|
|
311
314
|
return count;
|
|
312
315
|
}
|
|
313
316
|
/**
|
|
314
|
-
* Insert a display-id alias row
|
|
317
|
+
* Insert a display-id alias row. Idempotent on its uid, and order-independent
|
|
318
|
+
* (T12800): when two displacements write one alias uid, the stored row is
|
|
319
|
+
* replaced only when `precedes(incoming, stored)` says the incoming one ranks
|
|
320
|
+
* first, so every replica keeps the same row whatever order the ops arrived
|
|
321
|
+
* in. The caller ranks by HLC order (`compareHlc`), never by text: stored
|
|
322
|
+
* values may be in either HLC format (T12802).
|
|
315
323
|
*
|
|
316
324
|
* @param nativeDb - The project `cleo.db` handle.
|
|
317
325
|
* @param row - The alias.
|
|
326
|
+
* @param precedes - Whether `incoming` ranks before the `stored` row of the same uid.
|
|
318
327
|
* @task T12341
|
|
319
328
|
*/
|
|
320
|
-
export function insertDisplayIdAliasNative(nativeDb, row) {
|
|
329
|
+
export function insertDisplayIdAliasNative(nativeDb, row, precedes) {
|
|
330
|
+
const stored = nativeDb
|
|
331
|
+
.prepare(`SELECT uid, entity_table AS entityTable, display_id AS displayId, entity_uid AS entityUid,
|
|
332
|
+
reason, origin, displaced_hlc AS displacedHlc, created_at AS createdAt,
|
|
333
|
+
entity_birth_fp AS entityBirthFp
|
|
334
|
+
FROM tasks_display_id_aliases WHERE uid = ?`)
|
|
335
|
+
.get(row.uid);
|
|
336
|
+
if (!stored) {
|
|
337
|
+
nativeDb
|
|
338
|
+
.prepare(`INSERT INTO tasks_display_id_aliases
|
|
339
|
+
(uid, entity_table, display_id, entity_uid, reason, origin, displaced_hlc, created_at,
|
|
340
|
+
entity_birth_fp)
|
|
341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
342
|
+
.run(row.uid, row.entityTable, row.displayId, row.entityUid, row.reason, row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!precedes(row, stored))
|
|
346
|
+
return;
|
|
321
347
|
nativeDb
|
|
322
|
-
.prepare(`
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.run(row.
|
|
348
|
+
.prepare(`UPDATE tasks_display_id_aliases
|
|
349
|
+
SET origin = ?, displaced_hlc = ?, created_at = ?,
|
|
350
|
+
entity_birth_fp = coalesce(entity_birth_fp, ?)
|
|
351
|
+
WHERE uid = ?`)
|
|
352
|
+
.run(row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp, row.uid);
|
|
327
353
|
}
|
|
328
354
|
/**
|
|
329
355
|
* Delete one display-id alias row (a local alias whose entity was re-keyed is
|
|
@@ -420,6 +446,103 @@ export function relinkAcUidNative(nativeDb, acId, uid, birthFp) {
|
|
|
420
446
|
export function clearAcUidGraveyardNative(nativeDb) {
|
|
421
447
|
nativeDb.exec('DELETE FROM tasks_ac_uid_graveyard');
|
|
422
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Read the birth of task `taskId` before an overwrite replaces it.
|
|
451
|
+
*
|
|
452
|
+
* @param nativeDb - The project `cleo.db` handle.
|
|
453
|
+
* @param taskId - The task about to be overwritten.
|
|
454
|
+
* @returns Its birth, or `undefined` when there is no such row.
|
|
455
|
+
* @task T12806
|
|
456
|
+
*/
|
|
457
|
+
export function readReplacedTaskBirthNative(nativeDb, taskId) {
|
|
458
|
+
return nativeDb
|
|
459
|
+
.prepare('SELECT created_at AS createdAt, uid FROM tasks_tasks WHERE id = ?')
|
|
460
|
+
.get(taskId);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* After an overwrite import replaced task `taskId`, keep or re-key its
|
|
464
|
+
* identity (T12806 review):
|
|
465
|
+
*
|
|
466
|
+
* - the SAME task (same id, same birth instant as before the overwrite) keeps
|
|
467
|
+
* its identity. Sameness is decided on the immutable birth facts, never on
|
|
468
|
+
* the title or type: a task retitled here and re-imported unchanged stays
|
|
469
|
+
* the same row;
|
|
470
|
+
* - refused once identity is shared: the overwrite would re-point a uid other
|
|
471
|
+
* devices hold at different work;
|
|
472
|
+
* - otherwise the task, the natural rows keyed by it (dependencies,
|
|
473
|
+
* relations, labels) and its criteria are re-derived from the new row, now,
|
|
474
|
+
* and every AC history row and binding that pointed at one of those
|
|
475
|
+
* criteria is RE-KEYED to the criterion's new uid, found by the criterion's
|
|
476
|
+
* old uid as well as by its current id. A binding whose `ac_id` is stale
|
|
477
|
+
* (#1731) keeps resolving: its evidence is kept, never dropped.
|
|
478
|
+
*
|
|
479
|
+
* @param nativeDb - The project `cleo.db` handle holding the transaction.
|
|
480
|
+
* @param taskId - The overwritten task.
|
|
481
|
+
* @param before - Its birth before the overwrite ({@link readReplacedTaskBirthNative}).
|
|
482
|
+
* @task T12806
|
|
483
|
+
*/
|
|
484
|
+
export function clearReplacedTaskIdentityNative(nativeDb, taskId, before) {
|
|
485
|
+
const row = nativeDb
|
|
486
|
+
.prepare('SELECT uid, birth_fp AS fp, created_at AS createdAt FROM tasks_tasks WHERE id = ?')
|
|
487
|
+
.get(taskId);
|
|
488
|
+
if (!row || (row.uid === null && row.fp === null))
|
|
489
|
+
return;
|
|
490
|
+
if (before) {
|
|
491
|
+
const was = parseStoreTimestamp(before.createdAt);
|
|
492
|
+
if (was !== null && was === parseStoreTimestamp(row.createdAt)) {
|
|
493
|
+
return; // the same task, re-imported: its identity stands
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (rowIdentityShared(nativeDb)) {
|
|
497
|
+
throw new CleoError(ExitCode.VALIDATION_ERROR, `Task ${taskId} cannot be overwritten by an import: its identity is shared with other devices`, {
|
|
498
|
+
fix: 'Import it under a new id (the default duplicate strategy), or edit the task instead of overwriting it.',
|
|
499
|
+
details: { field: 'taskId', actual: taskId },
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
const has = (table) => nativeDb
|
|
503
|
+
.prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
504
|
+
.get(table) !== undefined;
|
|
505
|
+
nativeDb.prepare('UPDATE tasks_tasks SET uid = NULL, birth_fp = NULL WHERE id = ?').run(taskId);
|
|
506
|
+
for (const spec of ROW_IDENTITY.project) {
|
|
507
|
+
if (spec.kind !== 'natural' || !has(spec.table))
|
|
508
|
+
continue;
|
|
509
|
+
const cols = (spec.keyRefs ?? []).filter((r) => r.table === 'tasks_tasks').map((r) => r.column);
|
|
510
|
+
if (cols.length === 0)
|
|
511
|
+
continue;
|
|
512
|
+
nativeDb
|
|
513
|
+
.prepare(`UPDATE main.${quoteIdent(spec.table)} SET uid = NULL WHERE ${cols.map((c) => `${quoteIdent(c)} = ?`).join(' OR ')}`)
|
|
514
|
+
.run(...cols.map(() => taskId));
|
|
515
|
+
}
|
|
516
|
+
const children = ['tasks_task_acceptance_criteria_history', 'tasks_evidence_ac_bindings'].filter(has);
|
|
517
|
+
const criteria = has('tasks_task_acceptance_criteria')
|
|
518
|
+
? nativeDb
|
|
519
|
+
.prepare('SELECT id, uid FROM tasks_task_acceptance_criteria WHERE task_id = ?')
|
|
520
|
+
.all(taskId)
|
|
521
|
+
: [];
|
|
522
|
+
if (criteria.length > 0) {
|
|
523
|
+
nativeDb
|
|
524
|
+
.prepare('UPDATE tasks_task_acceptance_criteria SET uid = NULL, birth_fp = NULL WHERE task_id = ?')
|
|
525
|
+
.run(taskId);
|
|
526
|
+
}
|
|
527
|
+
// Re-derive the task, its natural rows and its criteria now, inside the
|
|
528
|
+
// import transaction, so the children can be re-keyed to the new uids.
|
|
529
|
+
fillRowUids(nativeDb, 'project');
|
|
530
|
+
const newUid = nativeDb.prepare('SELECT uid FROM tasks_task_acceptance_criteria WHERE id = ?');
|
|
531
|
+
let rekeyed = 0;
|
|
532
|
+
for (const ac of criteria) {
|
|
533
|
+
const now = newUid.get(ac.id)?.uid ?? null;
|
|
534
|
+
for (const table of children) {
|
|
535
|
+
// By the criterion's old uid (a stale ac_id keeps it) or its current id.
|
|
536
|
+
rekeyed += Number(nativeDb
|
|
537
|
+
.prepare(`UPDATE main.${quoteIdent(table)} SET ac_uid = ?, birth_fp = NULL
|
|
538
|
+
WHERE ac_id = ? OR (? IS NOT NULL AND ac_uid = ?)`)
|
|
539
|
+
.run(now, ac.id, ac.uid, ac.uid).changes);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Their fingerprints hash the criterion's: re-derive them against the new one.
|
|
543
|
+
if (rekeyed > 0)
|
|
544
|
+
fillRowUids(nativeDb, 'project');
|
|
545
|
+
}
|
|
423
546
|
/** Quote an identifier for the dynamic row-identity writes below. */
|
|
424
547
|
function quoteIdent(name) {
|
|
425
548
|
return `"${name.replaceAll('"', '""')}"`;
|
|
@@ -668,6 +791,83 @@ function claimPredicate(guard, allowedHolders) {
|
|
|
668
791
|
: undefined;
|
|
669
792
|
return or(isNull(holder), mine, related, expired);
|
|
670
793
|
}
|
|
794
|
+
/** How long after a local rename a write to the old id is still read as meant for the renamed task. */
|
|
795
|
+
export const RENAMED_RECENT_MS = 24 * 60 * 60 * 1000;
|
|
796
|
+
function renameRecordKey(fromId) {
|
|
797
|
+
return `renamed_from:tasks_tasks:${fromId}`;
|
|
798
|
+
}
|
|
799
|
+
function hasIdentityMeta(nativeDb) {
|
|
800
|
+
return (nativeDb
|
|
801
|
+
.prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = 'tasks_row_identity_meta'")
|
|
802
|
+
.get() !== undefined);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Remember, locally, that `fromId` named the task `uid` at version
|
|
806
|
+
* `fromVersion` until now (T12800). Local-only (`tasks_row_identity_meta`):
|
|
807
|
+
* it answers "what did a caller of THIS store mean by `fromId`", which only
|
|
808
|
+
* this store's history can.
|
|
809
|
+
*/
|
|
810
|
+
function recordLocalRename(nativeDb, uid, fromId, toId, fromVersion) {
|
|
811
|
+
if (!hasIdentityMeta(nativeDb))
|
|
812
|
+
return;
|
|
813
|
+
const record = { uid, fromVersion, toId, at: new Date().toISOString() };
|
|
814
|
+
writeRowIdentityMetaNative(nativeDb, renameRecordKey(fromId), JSON.stringify(record));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Refuse a guarded write that names a display id its task no longer carries
|
|
818
|
+
* (T12800), when the caller can only mean the renamed task:
|
|
819
|
+
*
|
|
820
|
+
* - it expects EXACTLY the version the renamed task had when it left the id
|
|
821
|
+
* (the version the caller read under the old id), and that is not the
|
|
822
|
+
* current holder's version: a stale write on the current holder stays a
|
|
823
|
+
* plain `E_CONFLICT`;
|
|
824
|
+
* - or it renews or releases a claim the caller's session holds on the
|
|
825
|
+
* renamed task (acquiring or taking over the new holder is legitimate: a
|
|
826
|
+
* pivot claims its target before it releases its source);
|
|
827
|
+
* - or no row holds the old id any more.
|
|
828
|
+
*
|
|
829
|
+
* Only a RECENT rename counts ({@link RENAMED_RECENT_MS}), and only one this
|
|
830
|
+
* store performed (the local rename record). `E_TASK_RENAMED` carries the new
|
|
831
|
+
* id in its details.
|
|
832
|
+
*/
|
|
833
|
+
function assertNotRenamed(nativeDb, taskId, guard, current) {
|
|
834
|
+
if (!nativeDb || !hasIdentityMeta(nativeDb))
|
|
835
|
+
return;
|
|
836
|
+
const raw = nativeDb
|
|
837
|
+
.prepare('SELECT value FROM tasks_row_identity_meta WHERE key = ?')
|
|
838
|
+
.get(renameRecordKey(taskId));
|
|
839
|
+
if (!raw)
|
|
840
|
+
return;
|
|
841
|
+
let record;
|
|
842
|
+
try {
|
|
843
|
+
record = JSON.parse(raw.value);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (Date.now() - Date.parse(record.at) > RENAMED_RECENT_MS)
|
|
849
|
+
return;
|
|
850
|
+
const renamed = nativeDb
|
|
851
|
+
.prepare('SELECT id, claimed_by_session AS claimedBySession FROM tasks_tasks WHERE uid = ?')
|
|
852
|
+
.get(record.uid);
|
|
853
|
+
if (!renamed || renamed.id === taskId)
|
|
854
|
+
return;
|
|
855
|
+
const expected = guard.expectedUpdatedAt;
|
|
856
|
+
const session = guard.claim?.sessionId ?? null;
|
|
857
|
+
const holdsRenamedClaim = session !== null &&
|
|
858
|
+
(guard.claim?.mode === 'renew' || guard.claim?.mode === 'release') &&
|
|
859
|
+
renamed.claimedBySession === session &&
|
|
860
|
+
current?.claimedBySession !== session;
|
|
861
|
+
const readRenamed = expected !== undefined &&
|
|
862
|
+
expected === record.fromVersion &&
|
|
863
|
+
(current === undefined || taskVersion(current) !== expected);
|
|
864
|
+
if (!(current === undefined || holdsRenamedClaim || readRenamed))
|
|
865
|
+
return;
|
|
866
|
+
throw new CleoError(ExitCode.TASK_RENAMED, `Task ${taskId} is now ${renamed.id}: sync re-numbered it after a display-id collision, and ${taskId} ${current ? 'names another task' : 'names no task'}; nothing was written`, {
|
|
867
|
+
fix: `Re-read it with 'cleo show ${renamed.id}' and retry against ${renamed.id}.`,
|
|
868
|
+
details: { field: 'taskId', actual: taskId, expected: renamed.id },
|
|
869
|
+
});
|
|
870
|
+
}
|
|
671
871
|
/** Throw `E_TASK_CLAIMED` when the stored holder refuses this claim write. */
|
|
672
872
|
function assertClaimAllowed(taskId, row, guard, allowedHolders) {
|
|
673
873
|
const stored = claimFromColumns(row);
|
|
@@ -977,8 +1177,18 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
|
|
|
977
1177
|
async upsertSingleTask(task) {
|
|
978
1178
|
await accessor.transaction((tx) => tx.upsertSingleTask(task));
|
|
979
1179
|
},
|
|
980
|
-
async insertNewTask(task) {
|
|
981
|
-
await accessor.transaction((tx) => tx.insertNewTask(task));
|
|
1180
|
+
async insertNewTask(task, identity) {
|
|
1181
|
+
await accessor.transaction((tx) => tx.insertNewTask(task, identity));
|
|
1182
|
+
},
|
|
1183
|
+
async getTaskIdentities(taskIds) {
|
|
1184
|
+
if (taskIds.length === 0)
|
|
1185
|
+
return [];
|
|
1186
|
+
const db = await getDb(cwd);
|
|
1187
|
+
return db
|
|
1188
|
+
.select({ id: schema.tasks.id, uid: schema.tasks.uid, birthFp: schema.tasks.birthFp })
|
|
1189
|
+
.from(schema.tasks)
|
|
1190
|
+
.where(inArray(schema.tasks.id, taskIds))
|
|
1191
|
+
.all();
|
|
982
1192
|
},
|
|
983
1193
|
async addRelation(taskId, relatedTo, relationType, reason) {
|
|
984
1194
|
return accessor.transaction(async () => {
|
|
@@ -1556,6 +1766,11 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
|
|
|
1556
1766
|
.where(eq(schema.tasks.id, taskId))
|
|
1557
1767
|
.limit(1)
|
|
1558
1768
|
.all();
|
|
1769
|
+
// T12800: a guarded write aimed at a task that sync re-numbered must not
|
|
1770
|
+
// land on whatever row holds the old id now.
|
|
1771
|
+
if (guard?.claim || guard?.expectedUpdatedAt !== undefined) {
|
|
1772
|
+
assertNotRenamed(getNativeTasksDb(cwd), taskId, guard, current);
|
|
1773
|
+
}
|
|
1559
1774
|
if (!current)
|
|
1560
1775
|
throw new Error(`Task not found: ${taskId}`);
|
|
1561
1776
|
// T12502: refuse a claim write the stored holder does not allow before
|
|
@@ -1638,24 +1853,42 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
|
|
|
1638
1853
|
nativeDb.prepare(`SAVEPOINT ${spName}`).run();
|
|
1639
1854
|
}
|
|
1640
1855
|
try {
|
|
1856
|
+
// The birth of each task an upsert replaced, for clearTaskIdentity
|
|
1857
|
+
// (T12806): sameness is decided on the birth before the overwrite.
|
|
1858
|
+
const replacedBirths = new Map();
|
|
1641
1859
|
const tx = {
|
|
1642
1860
|
async upsertSingleTask(task) {
|
|
1643
1861
|
scope.assertActive();
|
|
1644
1862
|
return accessor.transaction(async () => {
|
|
1645
1863
|
scope.assertActive();
|
|
1864
|
+
if (rowUidFillEnabled() && !replacedBirths.has(task.id)) {
|
|
1865
|
+
const before = readReplacedTaskBirthNative(nativeDb, task.id);
|
|
1866
|
+
if (before)
|
|
1867
|
+
replacedBirths.set(task.id, before);
|
|
1868
|
+
}
|
|
1646
1869
|
const row = taskToRow(task);
|
|
1647
1870
|
await upsertTask(db, row);
|
|
1648
1871
|
await updateDependencies(db, task.id, task.depends ?? []);
|
|
1649
1872
|
});
|
|
1650
1873
|
},
|
|
1651
|
-
async insertNewTask(task) {
|
|
1874
|
+
async insertNewTask(task, identity) {
|
|
1652
1875
|
scope.assertActive();
|
|
1653
1876
|
return accessor.transaction(async () => {
|
|
1654
1877
|
scope.assertActive();
|
|
1655
|
-
await insertNewTask(db, taskToRow(task));
|
|
1878
|
+
await insertNewTask(db, taskToRow(task), identity);
|
|
1656
1879
|
await updateDependencies(db, task.id, task.depends ?? []);
|
|
1657
1880
|
});
|
|
1658
1881
|
},
|
|
1882
|
+
async clearTaskIdentity(taskId) {
|
|
1883
|
+
scope.assertActive();
|
|
1884
|
+
// Row uids off: an overwrite changes no identity value (T12806 review).
|
|
1885
|
+
if (!rowUidFillEnabled())
|
|
1886
|
+
return;
|
|
1887
|
+
return accessor.transaction(async () => {
|
|
1888
|
+
scope.assertActive();
|
|
1889
|
+
clearReplacedTaskIdentityNative(nativeDb, taskId, replacedBirths.get(taskId));
|
|
1890
|
+
});
|
|
1891
|
+
},
|
|
1659
1892
|
async archiveSingleTask(taskId, fields) {
|
|
1660
1893
|
scope.assertActive();
|
|
1661
1894
|
return accessor.transaction(async () => {
|