@hasna/todos 0.15.19 → 0.15.20
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/cli/cloud-router.d.ts +20 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +851 -47
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +228 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +7 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.js +479 -20
- package/dist/lib/cli-help.d.ts +3 -2
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/stale-lock-handoff.d.ts +25 -0
- package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
- package/dist/mcp/index.js +702 -36
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +1147 -686
- package/dist/registry.js +228 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +27 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1035 -369
- package/dist/server/openapi.d.ts +182 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/audit-history-import.d.ts +14 -0
- package/dist/storage/audit-history-import.d.ts.map +1 -0
- package/dist/storage/interfaces.d.ts +12 -1
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +476 -19
- package/dist/task-manifest.js +11 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
package/dist/mcp/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
47
47
|
function isTerminalStatus(status) {
|
|
48
48
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
49
49
|
}
|
|
50
|
-
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
50
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
51
51
|
var init_types = __esm(() => {
|
|
52
52
|
TASK_STATUSES = [
|
|
53
53
|
"pending",
|
|
@@ -169,6 +169,16 @@ var init_types = __esm(() => {
|
|
|
169
169
|
this.name = "LockError";
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
173
|
+
code;
|
|
174
|
+
details;
|
|
175
|
+
constructor(code, message, details = {}) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.code = code;
|
|
178
|
+
this.details = details;
|
|
179
|
+
this.name = "StaleLockHandoffError";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
172
182
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
173
183
|
agentId;
|
|
174
184
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -12170,28 +12180,55 @@ var init_activity_audit = __esm(() => {
|
|
|
12170
12180
|
function sanitizeHistoryValue(value, context) {
|
|
12171
12181
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
12172
12182
|
}
|
|
12173
|
-
function
|
|
12183
|
+
function insertTaskHistory(entry, db) {
|
|
12174
12184
|
const d = db || getDatabase();
|
|
12175
|
-
const
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12185
|
+
const safeEntry = {
|
|
12186
|
+
...entry,
|
|
12187
|
+
field: entry.field || null,
|
|
12188
|
+
old_value: sanitizeHistoryValue(entry.old_value, "task_history.old_value"),
|
|
12189
|
+
new_value: sanitizeHistoryValue(entry.new_value, "task_history.new_value"),
|
|
12190
|
+
agent_id: entry.agent_id || null,
|
|
12191
|
+
machine_id: entry.machine_id ?? currentStorageMachineId(d)
|
|
12192
|
+
};
|
|
12180
12193
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
12181
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12195
|
+
safeEntry.id,
|
|
12196
|
+
safeEntry.task_id,
|
|
12197
|
+
safeEntry.action,
|
|
12198
|
+
safeEntry.field,
|
|
12199
|
+
safeEntry.old_value,
|
|
12200
|
+
safeEntry.new_value,
|
|
12201
|
+
safeEntry.agent_id,
|
|
12202
|
+
safeEntry.created_at,
|
|
12203
|
+
safeEntry.machine_id ?? null
|
|
12204
|
+
]);
|
|
12182
12205
|
try {
|
|
12183
12206
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
12184
12207
|
logActivity2({
|
|
12185
12208
|
entity_type: "task",
|
|
12186
|
-
entity_id:
|
|
12187
|
-
action,
|
|
12188
|
-
field,
|
|
12189
|
-
old_value:
|
|
12190
|
-
new_value:
|
|
12191
|
-
actor_id:
|
|
12209
|
+
entity_id: safeEntry.task_id,
|
|
12210
|
+
action: safeEntry.action,
|
|
12211
|
+
field: safeEntry.field ?? undefined,
|
|
12212
|
+
old_value: safeEntry.old_value,
|
|
12213
|
+
new_value: safeEntry.new_value,
|
|
12214
|
+
actor_id: safeEntry.agent_id ?? undefined
|
|
12192
12215
|
}, d);
|
|
12193
12216
|
} catch {}
|
|
12194
|
-
return
|
|
12217
|
+
return safeEntry;
|
|
12218
|
+
}
|
|
12219
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
12220
|
+
const d = db || getDatabase();
|
|
12221
|
+
return insertTaskHistory({
|
|
12222
|
+
id: uuid(),
|
|
12223
|
+
task_id: taskId,
|
|
12224
|
+
action,
|
|
12225
|
+
field: field || null,
|
|
12226
|
+
old_value: oldValue ?? null,
|
|
12227
|
+
new_value: newValue ?? null,
|
|
12228
|
+
agent_id: agentId || null,
|
|
12229
|
+
created_at: now(),
|
|
12230
|
+
machine_id: currentStorageMachineId(d)
|
|
12231
|
+
}, d);
|
|
12195
12232
|
}
|
|
12196
12233
|
function getTaskHistory(taskId, db) {
|
|
12197
12234
|
const d = db || getDatabase();
|
|
@@ -13194,6 +13231,145 @@ var init_task_graph = __esm(() => {
|
|
|
13194
13231
|
init_task_crud();
|
|
13195
13232
|
});
|
|
13196
13233
|
|
|
13234
|
+
// src/lib/stale-lock-handoff.ts
|
|
13235
|
+
function normalizeExactTaskId(value) {
|
|
13236
|
+
if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
|
|
13237
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
|
|
13238
|
+
}
|
|
13239
|
+
return value.trim().toLowerCase();
|
|
13240
|
+
}
|
|
13241
|
+
function requireNonEmptyString(value, field) {
|
|
13242
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
13243
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
|
|
13244
|
+
}
|
|
13245
|
+
const trimmed = value.trim();
|
|
13246
|
+
if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
|
|
13247
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
|
|
13248
|
+
}
|
|
13249
|
+
return trimmed;
|
|
13250
|
+
}
|
|
13251
|
+
function requireCanonicalLockVersion(value) {
|
|
13252
|
+
if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
|
|
13253
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
|
|
13254
|
+
}
|
|
13255
|
+
const parsed = Date.parse(value);
|
|
13256
|
+
if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
|
|
13257
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
|
|
13258
|
+
}
|
|
13259
|
+
return value;
|
|
13260
|
+
}
|
|
13261
|
+
function requireStaleThreshold(value) {
|
|
13262
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
13263
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
|
|
13264
|
+
}
|
|
13265
|
+
return Number(value);
|
|
13266
|
+
}
|
|
13267
|
+
function prepareStaleLockHandoff(input, options = {}) {
|
|
13268
|
+
const taskId = normalizeExactTaskId(input.task_id);
|
|
13269
|
+
const actor = requireNonEmptyString(input.actor, "actor");
|
|
13270
|
+
const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
|
|
13271
|
+
const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
|
|
13272
|
+
const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
|
|
13273
|
+
const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
|
|
13274
|
+
const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
|
|
13275
|
+
if (!reason) {
|
|
13276
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
|
|
13277
|
+
}
|
|
13278
|
+
if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
|
|
13279
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
|
|
13280
|
+
}
|
|
13281
|
+
if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
|
|
13282
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
|
|
13283
|
+
}
|
|
13284
|
+
const operationTimestamp = options.now ?? new Date().toISOString();
|
|
13285
|
+
if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
|
|
13286
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
|
|
13287
|
+
}
|
|
13288
|
+
const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
|
|
13289
|
+
return {
|
|
13290
|
+
task_id: taskId,
|
|
13291
|
+
actor,
|
|
13292
|
+
expected_holder: expectedHolder,
|
|
13293
|
+
expected_lock_version: expectedLockVersion,
|
|
13294
|
+
stale_after_seconds: staleAfterSeconds,
|
|
13295
|
+
new_holder: newHolder,
|
|
13296
|
+
reason,
|
|
13297
|
+
operation_timestamp: operationTimestamp,
|
|
13298
|
+
stale_cutoff: staleCutoff,
|
|
13299
|
+
receipt_id: options.receiptId ?? crypto.randomUUID()
|
|
13300
|
+
};
|
|
13301
|
+
}
|
|
13302
|
+
function buildStaleLockHandoffReceipt(input) {
|
|
13303
|
+
return {
|
|
13304
|
+
schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
|
|
13305
|
+
receipt_id: input.receipt_id,
|
|
13306
|
+
task_id: input.task_id,
|
|
13307
|
+
actor: input.actor,
|
|
13308
|
+
previous_holder: input.expected_holder,
|
|
13309
|
+
previous_lock_version: input.expected_lock_version,
|
|
13310
|
+
new_holder: input.new_holder,
|
|
13311
|
+
new_lock_version: input.operation_timestamp,
|
|
13312
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
13313
|
+
stale_cutoff: input.stale_cutoff,
|
|
13314
|
+
reason: input.reason,
|
|
13315
|
+
created_at: input.operation_timestamp
|
|
13316
|
+
};
|
|
13317
|
+
}
|
|
13318
|
+
function staleLockHandoffHistory(receipt, machineId) {
|
|
13319
|
+
return {
|
|
13320
|
+
id: receipt.receipt_id,
|
|
13321
|
+
task_id: receipt.task_id,
|
|
13322
|
+
action: STALE_LOCK_HANDOFF_ACTION,
|
|
13323
|
+
field: STALE_LOCK_HANDOFF_FIELD,
|
|
13324
|
+
old_value: JSON.stringify({
|
|
13325
|
+
holder: receipt.previous_holder,
|
|
13326
|
+
lock_version: receipt.previous_lock_version
|
|
13327
|
+
}),
|
|
13328
|
+
new_value: JSON.stringify(receipt),
|
|
13329
|
+
agent_id: receipt.actor,
|
|
13330
|
+
created_at: receipt.created_at,
|
|
13331
|
+
machine_id: machineId
|
|
13332
|
+
};
|
|
13333
|
+
}
|
|
13334
|
+
function throwStaleLockHandoffConflict(task, input) {
|
|
13335
|
+
if (!task.locked_by || !task.locked_at) {
|
|
13336
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
|
|
13337
|
+
}
|
|
13338
|
+
if (task.locked_at !== input.expected_lock_version) {
|
|
13339
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
|
|
13340
|
+
task_id: input.task_id,
|
|
13341
|
+
expected_lock_version: input.expected_lock_version,
|
|
13342
|
+
current_lock_version: task.locked_at
|
|
13343
|
+
});
|
|
13344
|
+
}
|
|
13345
|
+
if (task.locked_by !== input.expected_holder) {
|
|
13346
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
|
|
13347
|
+
task_id: input.task_id,
|
|
13348
|
+
expected_holder: input.expected_holder,
|
|
13349
|
+
current_holder: task.locked_by
|
|
13350
|
+
});
|
|
13351
|
+
}
|
|
13352
|
+
if (isTerminalStatus(task.status)) {
|
|
13353
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
|
|
13354
|
+
}
|
|
13355
|
+
if (task.locked_at >= input.stale_cutoff) {
|
|
13356
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
|
|
13357
|
+
task_id: input.task_id,
|
|
13358
|
+
current_lock_version: task.locked_at,
|
|
13359
|
+
stale_cutoff: input.stale_cutoff
|
|
13360
|
+
});
|
|
13361
|
+
}
|
|
13362
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
|
|
13363
|
+
}
|
|
13364
|
+
var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
|
|
13365
|
+
var init_stale_lock_handoff = __esm(() => {
|
|
13366
|
+
init_types();
|
|
13367
|
+
init_creator_identity();
|
|
13368
|
+
init_prewrite_secrets();
|
|
13369
|
+
EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13370
|
+
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13371
|
+
});
|
|
13372
|
+
|
|
13197
13373
|
// src/db/task-lifecycle.ts
|
|
13198
13374
|
var exports_task_lifecycle = {};
|
|
13199
13375
|
__export(exports_task_lifecycle, {
|
|
@@ -13202,6 +13378,7 @@ __export(exports_task_lifecycle, {
|
|
|
13202
13378
|
startTask: () => startTask,
|
|
13203
13379
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
13204
13380
|
lockTask: () => lockTask,
|
|
13381
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
13205
13382
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
13206
13383
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
13207
13384
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -13470,6 +13647,38 @@ function unlockTask(id, agentId, db) {
|
|
|
13470
13647
|
WHERE id = ?`, [timestamp2, id]);
|
|
13471
13648
|
return true;
|
|
13472
13649
|
}
|
|
13650
|
+
function handoffStaleTaskLock(input, db) {
|
|
13651
|
+
const d = db || getDatabase();
|
|
13652
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
13653
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
13654
|
+
const history = staleLockHandoffHistory(receipt, null);
|
|
13655
|
+
const transfer = d.transaction(() => {
|
|
13656
|
+
const result = d.run(`UPDATE tasks
|
|
13657
|
+
SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
13658
|
+
WHERE id = ?
|
|
13659
|
+
AND locked_by = ?
|
|
13660
|
+
AND locked_at = ?
|
|
13661
|
+
AND julianday(locked_at) < julianday(?)
|
|
13662
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`, [
|
|
13663
|
+
prepared.new_holder,
|
|
13664
|
+
prepared.operation_timestamp,
|
|
13665
|
+
prepared.operation_timestamp,
|
|
13666
|
+
prepared.task_id,
|
|
13667
|
+
prepared.expected_holder,
|
|
13668
|
+
prepared.expected_lock_version,
|
|
13669
|
+
prepared.stale_cutoff
|
|
13670
|
+
]);
|
|
13671
|
+
if (result.changes === 0) {
|
|
13672
|
+
const current = getTask(prepared.task_id, d);
|
|
13673
|
+
if (!current)
|
|
13674
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
13675
|
+
throwStaleLockHandoffConflict(current, prepared);
|
|
13676
|
+
}
|
|
13677
|
+
insertTaskHistory(history, d);
|
|
13678
|
+
});
|
|
13679
|
+
transfer();
|
|
13680
|
+
return receipt;
|
|
13681
|
+
}
|
|
13473
13682
|
function getTaskLockStatus(id, db) {
|
|
13474
13683
|
const d = db || getDatabase();
|
|
13475
13684
|
const task = getTask(id, d);
|
|
@@ -13770,6 +13979,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
13770
13979
|
init_task_crud();
|
|
13771
13980
|
init_task_graph();
|
|
13772
13981
|
init_prewrite_secrets();
|
|
13982
|
+
init_stale_lock_handoff();
|
|
13773
13983
|
});
|
|
13774
13984
|
|
|
13775
13985
|
// src/db/task-crud.ts
|
|
@@ -17342,6 +17552,7 @@ __export(exports_tasks, {
|
|
|
17342
17552
|
insertTaskTags: () => insertTaskTags,
|
|
17343
17553
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
17344
17554
|
importCalendarIcs: () => importCalendarIcs,
|
|
17555
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
17345
17556
|
getTimeReport: () => getTimeReport,
|
|
17346
17557
|
getTimeLogs: () => getTimeLogs,
|
|
17347
17558
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -20799,6 +21010,7 @@ var init_http_client = __esm(() => {
|
|
|
20799
21010
|
// src/cli/cloud-router.ts
|
|
20800
21011
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
20801
21012
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
21013
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20802
21014
|
import { resolve as resolvePath } from "path";
|
|
20803
21015
|
function cleanMode(value) {
|
|
20804
21016
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -21201,7 +21413,10 @@ async function cloudGetTask(client, id) {
|
|
|
21201
21413
|
}
|
|
21202
21414
|
async function cloudCreateTask(client, input) {
|
|
21203
21415
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
21204
|
-
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.
|
|
21416
|
+
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
|
|
21417
|
+
idempotencyKey: randomUUID3(),
|
|
21418
|
+
retry: false
|
|
21419
|
+
}), ["PARENT_TASK_NOT_FOUND"]));
|
|
21205
21420
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
21206
21421
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
21207
21422
|
}
|
|
@@ -21212,7 +21427,7 @@ async function cloudCreateTask(client, input) {
|
|
|
21212
21427
|
return persisted;
|
|
21213
21428
|
}
|
|
21214
21429
|
async function cloudUpdateTask(client, id, patch) {
|
|
21215
|
-
return unwrapTask(await client.
|
|
21430
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
21216
21431
|
}
|
|
21217
21432
|
async function cloudDeleteTask(client, id) {
|
|
21218
21433
|
try {
|
|
@@ -35537,7 +35752,7 @@ var package_default;
|
|
|
35537
35752
|
var init_package = __esm(() => {
|
|
35538
35753
|
package_default = {
|
|
35539
35754
|
name: "@hasna/todos",
|
|
35540
|
-
version: "0.15.
|
|
35755
|
+
version: "0.15.20",
|
|
35541
35756
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35542
35757
|
type: "module",
|
|
35543
35758
|
main: "dist/index.js",
|
|
@@ -35608,6 +35823,8 @@ var init_package = __esm(() => {
|
|
|
35608
35823
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
35609
35824
|
"dev:serve": "bun run src/server/index.ts",
|
|
35610
35825
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
35826
|
+
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
35827
|
+
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
35611
35828
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
35612
35829
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
35613
35830
|
},
|
|
@@ -45098,6 +45315,58 @@ var init_plan_project_links = __esm(() => {
|
|
|
45098
45315
|
init_tasks();
|
|
45099
45316
|
});
|
|
45100
45317
|
|
|
45318
|
+
// src/storage/audit-history-import.ts
|
|
45319
|
+
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
45320
|
+
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
45321
|
+
const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
|
|
45322
|
+
const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
|
|
45323
|
+
return leftValue === rightValue;
|
|
45324
|
+
});
|
|
45325
|
+
}
|
|
45326
|
+
function divergentAuditHistoryReplayError(id) {
|
|
45327
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
45328
|
+
}
|
|
45329
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
45330
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
45331
|
+
}
|
|
45332
|
+
function parseAuditHistoryImportFailure(message) {
|
|
45333
|
+
const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
|
|
45334
|
+
const divergentSuffix = " differs from stored row";
|
|
45335
|
+
if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
|
|
45336
|
+
return {
|
|
45337
|
+
code: AUDIT_HISTORY_DIVERGENT_REPLAY,
|
|
45338
|
+
auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
|
|
45339
|
+
conflict: true,
|
|
45340
|
+
status: 409
|
|
45341
|
+
};
|
|
45342
|
+
}
|
|
45343
|
+
const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
|
|
45344
|
+
const tombstoneSuffix = " is not allowed";
|
|
45345
|
+
if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
|
|
45346
|
+
return {
|
|
45347
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
45348
|
+
auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
|
|
45349
|
+
conflict: false,
|
|
45350
|
+
status: 400
|
|
45351
|
+
};
|
|
45352
|
+
}
|
|
45353
|
+
return null;
|
|
45354
|
+
}
|
|
45355
|
+
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
|
|
45356
|
+
var init_audit_history_import = __esm(() => {
|
|
45357
|
+
AUDIT_HISTORY_FIELDS = [
|
|
45358
|
+
"id",
|
|
45359
|
+
"task_id",
|
|
45360
|
+
"action",
|
|
45361
|
+
"field",
|
|
45362
|
+
"old_value",
|
|
45363
|
+
"new_value",
|
|
45364
|
+
"agent_id",
|
|
45365
|
+
"created_at",
|
|
45366
|
+
"machine_id"
|
|
45367
|
+
];
|
|
45368
|
+
});
|
|
45369
|
+
|
|
45101
45370
|
// src/storage/sqlite-snapshot.ts
|
|
45102
45371
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
45103
45372
|
const d = db ?? getDatabase();
|
|
@@ -45131,8 +45400,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45131
45400
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
45132
45401
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
45133
45402
|
}
|
|
45403
|
+
const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
45404
|
+
result.errors.push(...auditImport.errors);
|
|
45134
45405
|
if (result.errors.length > 0)
|
|
45135
45406
|
return result;
|
|
45407
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
45136
45408
|
const applyRows = (objectType2, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
45137
45409
|
for (const row of rows) {
|
|
45138
45410
|
try {
|
|
@@ -45167,10 +45439,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45167
45439
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
45168
45440
|
}
|
|
45169
45441
|
});
|
|
45170
|
-
|
|
45442
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
45171
45443
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
45172
45444
|
return result;
|
|
45173
45445
|
}
|
|
45446
|
+
function preflightAuditHistoryImport(db, rows, tombstones) {
|
|
45447
|
+
const errors2 = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
45448
|
+
const rowsToInsert = [];
|
|
45449
|
+
const seen = new Map;
|
|
45450
|
+
let identicalReplayCount = 0;
|
|
45451
|
+
for (const rawRow of rows) {
|
|
45452
|
+
try {
|
|
45453
|
+
const row = asRecord2(rawRow);
|
|
45454
|
+
if (typeof row.id !== "string" || !row.id) {
|
|
45455
|
+
throw new Error("task_history row is missing id");
|
|
45456
|
+
}
|
|
45457
|
+
const prior = seen.get(row.id);
|
|
45458
|
+
if (prior) {
|
|
45459
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
45460
|
+
identicalReplayCount += 1;
|
|
45461
|
+
else
|
|
45462
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
45463
|
+
continue;
|
|
45464
|
+
}
|
|
45465
|
+
seen.set(row.id, row);
|
|
45466
|
+
const existing = getAuditHistoryById(db, row.id);
|
|
45467
|
+
if (!existing) {
|
|
45468
|
+
rowsToInsert.push(row);
|
|
45469
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
45470
|
+
identicalReplayCount += 1;
|
|
45471
|
+
} else {
|
|
45472
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
45473
|
+
}
|
|
45474
|
+
} catch (error) {
|
|
45475
|
+
errors2.push(error instanceof Error ? error.message : String(error));
|
|
45476
|
+
}
|
|
45477
|
+
}
|
|
45478
|
+
return { rowsToInsert, identicalReplayCount, errors: errors2 };
|
|
45479
|
+
}
|
|
45480
|
+
function insertAuditHistoryRows(db, rows, result) {
|
|
45481
|
+
for (const rawRow of rows) {
|
|
45482
|
+
try {
|
|
45483
|
+
const row = asRecord2(rawRow);
|
|
45484
|
+
const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
|
|
45485
|
+
if (!presentColumns.includes("id"))
|
|
45486
|
+
presentColumns.unshift("id");
|
|
45487
|
+
const placeholders2 = presentColumns.map(() => "?").join(", ");
|
|
45488
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
45489
|
+
const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders2})`, values).changes;
|
|
45490
|
+
if (changes > 0) {
|
|
45491
|
+
result.inserted += 1;
|
|
45492
|
+
continue;
|
|
45493
|
+
}
|
|
45494
|
+
const existing = getAuditHistoryById(db, String(row["id"]));
|
|
45495
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
45496
|
+
result.skipped += 1;
|
|
45497
|
+
} else {
|
|
45498
|
+
result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
|
|
45499
|
+
}
|
|
45500
|
+
} catch (error) {
|
|
45501
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
45502
|
+
}
|
|
45503
|
+
}
|
|
45504
|
+
}
|
|
45505
|
+
function getAuditHistoryById(db, id) {
|
|
45506
|
+
return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
|
|
45507
|
+
}
|
|
45174
45508
|
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
45175
45509
|
const id = row["id"];
|
|
45176
45510
|
if (typeof id !== "string" || !id)
|
|
@@ -45268,7 +45602,7 @@ function tableForTombstone(objectType2) {
|
|
|
45268
45602
|
return "task_templates";
|
|
45269
45603
|
if (objectType2 === "template_tasks")
|
|
45270
45604
|
return "template_tasks";
|
|
45271
|
-
|
|
45605
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType2)}`);
|
|
45272
45606
|
}
|
|
45273
45607
|
function listRows(db, table, columns) {
|
|
45274
45608
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -45315,6 +45649,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
45315
45649
|
init_tasks();
|
|
45316
45650
|
init_templates();
|
|
45317
45651
|
init_storage_tombstones();
|
|
45652
|
+
init_audit_history_import();
|
|
45318
45653
|
PROJECT_COLUMNS = [
|
|
45319
45654
|
"id",
|
|
45320
45655
|
"name",
|
|
@@ -45579,6 +45914,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45579
45914
|
unlockTask(id, agentId, database());
|
|
45580
45915
|
return true;
|
|
45581
45916
|
},
|
|
45917
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
45582
45918
|
delete: (id) => deleteTask(id, database()),
|
|
45583
45919
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
45584
45920
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -46498,7 +46834,7 @@ var init_api_keys = __esm(() => {
|
|
|
46498
46834
|
});
|
|
46499
46835
|
|
|
46500
46836
|
// src/storage/postgres-adapter.ts
|
|
46501
|
-
import { randomUUID as
|
|
46837
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
46502
46838
|
function createPostgresTodosStorageAdapter(options) {
|
|
46503
46839
|
const store = new PostgresJsonRecordStore(options);
|
|
46504
46840
|
const adapter = {
|
|
@@ -46527,6 +46863,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46527
46863
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
46528
46864
|
lock: (id, agentId) => lockTask2(id, agentId, store),
|
|
46529
46865
|
unlock: (id, agentId) => unlockTask2(id, agentId, store),
|
|
46866
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
46530
46867
|
getByFingerprint: (fingerprint3) => store.getTaskByFingerprint(fingerprint3)
|
|
46531
46868
|
},
|
|
46532
46869
|
dependencies: {
|
|
@@ -46673,6 +47010,91 @@ class PostgresJsonRecordStore {
|
|
|
46673
47010
|
LIMIT 1`, [this.service, type, id]);
|
|
46674
47011
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
46675
47012
|
}
|
|
47013
|
+
async handoffStaleLock(input, context = {}) {
|
|
47014
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
47015
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
47016
|
+
const history = staleLockHandoffHistory(receipt, this.machineId(context));
|
|
47017
|
+
await this.ensureSchema();
|
|
47018
|
+
const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
|
|
47019
|
+
target AS MATERIALIZED (
|
|
47020
|
+
SELECT payload
|
|
47021
|
+
FROM ${this.tableName}
|
|
47022
|
+
WHERE service = $1
|
|
47023
|
+
AND object_type = 'tasks'
|
|
47024
|
+
AND object_id = $2
|
|
47025
|
+
AND deleted_at IS NULL
|
|
47026
|
+
FOR UPDATE
|
|
47027
|
+
),
|
|
47028
|
+
updated AS (
|
|
47029
|
+
UPDATE ${this.tableName} AS task_record
|
|
47030
|
+
SET payload = jsonb_set(
|
|
47031
|
+
jsonb_set(
|
|
47032
|
+
jsonb_set(
|
|
47033
|
+
jsonb_set(
|
|
47034
|
+
task_record.payload,
|
|
47035
|
+
'{locked_by}',
|
|
47036
|
+
to_jsonb($6::text),
|
|
47037
|
+
true
|
|
47038
|
+
),
|
|
47039
|
+
'{locked_at}',
|
|
47040
|
+
to_jsonb($7::text),
|
|
47041
|
+
true
|
|
47042
|
+
),
|
|
47043
|
+
'{updated_at}',
|
|
47044
|
+
to_jsonb($7::text),
|
|
47045
|
+
true
|
|
47046
|
+
),
|
|
47047
|
+
'{version}',
|
|
47048
|
+
to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
|
|
47049
|
+
true
|
|
47050
|
+
),
|
|
47051
|
+
updated_at = $7::timestamptz,
|
|
47052
|
+
source_machine_id = $10,
|
|
47053
|
+
version = COALESCE(task_record.version, 0) + 1
|
|
47054
|
+
FROM target
|
|
47055
|
+
WHERE task_record.service = $1
|
|
47056
|
+
AND task_record.object_type = 'tasks'
|
|
47057
|
+
AND task_record.object_id = $2
|
|
47058
|
+
AND task_record.deleted_at IS NULL
|
|
47059
|
+
AND target.payload->>'locked_by' = $3
|
|
47060
|
+
AND target.payload->>'locked_at' = $4
|
|
47061
|
+
AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
|
|
47062
|
+
AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
|
|
47063
|
+
RETURNING task_record.payload
|
|
47064
|
+
),
|
|
47065
|
+
audit AS (
|
|
47066
|
+
INSERT INTO ${this.tableName} (
|
|
47067
|
+
service, object_type, object_id, payload, updated_at,
|
|
47068
|
+
deleted_at, source_machine_id, version
|
|
47069
|
+
)
|
|
47070
|
+
SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
|
|
47071
|
+
NULL, $10, NULL
|
|
47072
|
+
FROM updated
|
|
47073
|
+
RETURNING payload
|
|
47074
|
+
)
|
|
47075
|
+
SELECT
|
|
47076
|
+
(SELECT payload FROM target) AS current_payload,
|
|
47077
|
+
(SELECT payload FROM updated) AS updated_payload,
|
|
47078
|
+
(SELECT payload FROM audit) AS audit_payload`, [
|
|
47079
|
+
this.service,
|
|
47080
|
+
prepared.task_id,
|
|
47081
|
+
prepared.expected_holder,
|
|
47082
|
+
prepared.expected_lock_version,
|
|
47083
|
+
prepared.stale_cutoff,
|
|
47084
|
+
prepared.new_holder,
|
|
47085
|
+
prepared.operation_timestamp,
|
|
47086
|
+
receipt.receipt_id,
|
|
47087
|
+
jsonbParam(history),
|
|
47088
|
+
this.machineId(context)
|
|
47089
|
+
]);
|
|
47090
|
+
const row = result.rows[0];
|
|
47091
|
+
if (!row?.current_payload)
|
|
47092
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
47093
|
+
if (!row.updated_payload || !row.audit_payload) {
|
|
47094
|
+
throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
|
|
47095
|
+
}
|
|
47096
|
+
return receipt;
|
|
47097
|
+
}
|
|
46676
47098
|
async list(type) {
|
|
46677
47099
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
46678
47100
|
}
|
|
@@ -46942,6 +47364,28 @@ class PostgresJsonRecordStore {
|
|
|
46942
47364
|
}
|
|
46943
47365
|
return value;
|
|
46944
47366
|
}
|
|
47367
|
+
async insertImmutableAuditHistory(value, context = {}) {
|
|
47368
|
+
await this.ensureSchema();
|
|
47369
|
+
const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
47370
|
+
service, object_type, object_id, payload, updated_at,
|
|
47371
|
+
deleted_at, source_machine_id, version
|
|
47372
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
|
|
47373
|
+
ON CONFLICT (service, object_type, object_id) DO NOTHING
|
|
47374
|
+
RETURNING object_id`, [
|
|
47375
|
+
this.service,
|
|
47376
|
+
"audit_history",
|
|
47377
|
+
value.id,
|
|
47378
|
+
jsonbParam(value),
|
|
47379
|
+
value.created_at,
|
|
47380
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
47381
|
+
]);
|
|
47382
|
+
if (inserted.rows.length > 0)
|
|
47383
|
+
return "inserted";
|
|
47384
|
+
const existing = await this.get("audit_history", value.id);
|
|
47385
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
|
|
47386
|
+
return "identical";
|
|
47387
|
+
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47388
|
+
}
|
|
46945
47389
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
46946
47390
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
46947
47391
|
if (planIds.length === 0)
|
|
@@ -47768,7 +48212,7 @@ async function createTask3(input, store, context) {
|
|
|
47768
48212
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
47769
48213
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
47770
48214
|
const task2 = {
|
|
47771
|
-
id:
|
|
48215
|
+
id: randomUUID4(),
|
|
47772
48216
|
short_id: shortId,
|
|
47773
48217
|
project_id: effectiveProjectId,
|
|
47774
48218
|
parent_id: input.parent_id ?? null,
|
|
@@ -48014,7 +48458,7 @@ async function addVerification(input, store, context) {
|
|
|
48014
48458
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48015
48459
|
const timestamp4 = new Date().toISOString();
|
|
48016
48460
|
const verification = {
|
|
48017
|
-
id:
|
|
48461
|
+
id: randomUUID4(),
|
|
48018
48462
|
task_id: input.task_id,
|
|
48019
48463
|
command: input.command,
|
|
48020
48464
|
status: input.status ?? "unknown",
|
|
@@ -48035,7 +48479,7 @@ async function addCommit(input, store, context) {
|
|
|
48035
48479
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48036
48480
|
const timestamp4 = new Date().toISOString();
|
|
48037
48481
|
const commit = {
|
|
48038
|
-
id:
|
|
48482
|
+
id: randomUUID4(),
|
|
48039
48483
|
task_id: input.task_id,
|
|
48040
48484
|
sha: input.sha,
|
|
48041
48485
|
message: input.message ?? null,
|
|
@@ -48058,7 +48502,7 @@ async function addGitRef(input, store, context) {
|
|
|
48058
48502
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48059
48503
|
const timestamp4 = new Date().toISOString();
|
|
48060
48504
|
const gitRef = {
|
|
48061
|
-
id:
|
|
48505
|
+
id: randomUUID4(),
|
|
48062
48506
|
task_id: input.task_id,
|
|
48063
48507
|
ref_type: input.ref_type,
|
|
48064
48508
|
name: input.name,
|
|
@@ -48128,7 +48572,7 @@ async function createProject2(input, store, context) {
|
|
|
48128
48572
|
if (!derivedSlug || !taskListId)
|
|
48129
48573
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
48130
48574
|
const project = {
|
|
48131
|
-
id:
|
|
48575
|
+
id: randomUUID4(),
|
|
48132
48576
|
name: input.name,
|
|
48133
48577
|
path: input.path,
|
|
48134
48578
|
description: input.description ?? null,
|
|
@@ -48160,7 +48604,7 @@ async function createPlan2(input, store, context) {
|
|
|
48160
48604
|
store
|
|
48161
48605
|
});
|
|
48162
48606
|
return store.upsert("plans", {
|
|
48163
|
-
id:
|
|
48607
|
+
id: randomUUID4(),
|
|
48164
48608
|
slug,
|
|
48165
48609
|
project_id: projectId,
|
|
48166
48610
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
@@ -48212,7 +48656,7 @@ async function registerAgent2(input, store, context) {
|
|
|
48212
48656
|
}
|
|
48213
48657
|
const timestamp4 = new Date().toISOString();
|
|
48214
48658
|
const agent = {
|
|
48215
|
-
id: existing?.id ??
|
|
48659
|
+
id: existing?.id ?? randomUUID4().slice(0, 8),
|
|
48216
48660
|
name: canonicalName,
|
|
48217
48661
|
description: input.description ?? existing?.description ?? null,
|
|
48218
48662
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -48285,7 +48729,7 @@ async function createTaskList2(input, store, context) {
|
|
|
48285
48729
|
if (!slug)
|
|
48286
48730
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
48287
48731
|
return store.upsert("task_lists", {
|
|
48288
|
-
id:
|
|
48732
|
+
id: randomUUID4(),
|
|
48289
48733
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
48290
48734
|
slug,
|
|
48291
48735
|
name: input.name,
|
|
@@ -48320,7 +48764,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
48320
48764
|
async function createTemplate2(input, store, context) {
|
|
48321
48765
|
const timestamp4 = new Date().toISOString();
|
|
48322
48766
|
const template = {
|
|
48323
|
-
id:
|
|
48767
|
+
id: randomUUID4(),
|
|
48324
48768
|
name: input.name,
|
|
48325
48769
|
title_pattern: input.title_pattern,
|
|
48326
48770
|
description: input.description ?? null,
|
|
@@ -48341,7 +48785,7 @@ async function createTemplate2(input, store, context) {
|
|
|
48341
48785
|
}
|
|
48342
48786
|
function buildTemplateTasks(templateId, inputs, timestamp4) {
|
|
48343
48787
|
return inputs.map((input, position) => ({
|
|
48344
|
-
id:
|
|
48788
|
+
id: randomUUID4(),
|
|
48345
48789
|
template_id: templateId,
|
|
48346
48790
|
position,
|
|
48347
48791
|
title_pattern: input.title_pattern,
|
|
@@ -48374,7 +48818,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
48374
48818
|
}
|
|
48375
48819
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
48376
48820
|
const entry2 = {
|
|
48377
|
-
id:
|
|
48821
|
+
id: randomUUID4(),
|
|
48378
48822
|
task_id: taskId,
|
|
48379
48823
|
action,
|
|
48380
48824
|
field: field ?? null,
|
|
@@ -48388,7 +48832,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
48388
48832
|
}
|
|
48389
48833
|
async function addComment2(input, store, context) {
|
|
48390
48834
|
const comment = {
|
|
48391
|
-
id:
|
|
48835
|
+
id: randomUUID4(),
|
|
48392
48836
|
task_id: input.task_id,
|
|
48393
48837
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
48394
48838
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -48430,6 +48874,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48430
48874
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
48431
48875
|
if (result.errors.length > 0)
|
|
48432
48876
|
return result;
|
|
48877
|
+
const auditHistory = await preflightAuditHistoryImport2(snapshot.auditHistory, snapshot.tombstones ?? [], store);
|
|
48878
|
+
result.errors.push(...auditHistory.errors);
|
|
48879
|
+
if (result.errors.length > 0)
|
|
48880
|
+
return result;
|
|
48881
|
+
result.skipped += auditHistory.identical;
|
|
48433
48882
|
const entries = [
|
|
48434
48883
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
48435
48884
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -48438,9 +48887,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48438
48887
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
48439
48888
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
48440
48889
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
48441
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48442
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
48890
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48443
48891
|
];
|
|
48892
|
+
for (const row of auditHistory.rowsToInsert) {
|
|
48893
|
+
try {
|
|
48894
|
+
const outcome = await store.insertImmutableAuditHistory(row, context);
|
|
48895
|
+
if (outcome === "inserted")
|
|
48896
|
+
result.inserted += 1;
|
|
48897
|
+
else
|
|
48898
|
+
result.skipped += 1;
|
|
48899
|
+
} catch (error) {
|
|
48900
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
48901
|
+
return result;
|
|
48902
|
+
}
|
|
48903
|
+
}
|
|
48444
48904
|
for (const [type, row] of entries) {
|
|
48445
48905
|
try {
|
|
48446
48906
|
const existing = await store.get(type, row.id);
|
|
@@ -48474,6 +48934,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48474
48934
|
}
|
|
48475
48935
|
return result;
|
|
48476
48936
|
}
|
|
48937
|
+
async function preflightAuditHistoryImport2(rows, tombstones, store) {
|
|
48938
|
+
const errors2 = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
48939
|
+
const rowsToInsert = [];
|
|
48940
|
+
const seen = new Map;
|
|
48941
|
+
let identical = 0;
|
|
48942
|
+
for (const row of rows) {
|
|
48943
|
+
const prior = seen.get(row.id);
|
|
48944
|
+
if (prior) {
|
|
48945
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
48946
|
+
identical += 1;
|
|
48947
|
+
else
|
|
48948
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
48949
|
+
continue;
|
|
48950
|
+
}
|
|
48951
|
+
seen.set(row.id, row);
|
|
48952
|
+
const existing = await store.get("audit_history", row.id);
|
|
48953
|
+
if (!existing) {
|
|
48954
|
+
rowsToInsert.push(row);
|
|
48955
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
48956
|
+
identical += 1;
|
|
48957
|
+
} else {
|
|
48958
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
48959
|
+
}
|
|
48960
|
+
}
|
|
48961
|
+
return { rowsToInsert, identical, errors: errors2 };
|
|
48962
|
+
}
|
|
48477
48963
|
async function requireRecord(type, id, store) {
|
|
48478
48964
|
const record = await store.get(type, id);
|
|
48479
48965
|
if (!record)
|
|
@@ -48580,9 +49066,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
48580
49066
|
init_types();
|
|
48581
49067
|
init_creator_identity();
|
|
48582
49068
|
init_plan_project_link_contract();
|
|
49069
|
+
init_stale_lock_handoff();
|
|
48583
49070
|
init_postgres_sync();
|
|
48584
49071
|
init_integrity();
|
|
48585
49072
|
init_redaction();
|
|
49073
|
+
init_audit_history_import();
|
|
48586
49074
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
48587
49075
|
});
|
|
48588
49076
|
|
|
@@ -53927,6 +54415,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
53927
54415
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
53928
54416
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
53929
54417
|
TaskComment: taskCommentSchema,
|
|
54418
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
54419
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
53930
54420
|
TaskGitRef: taskGitRefSchema,
|
|
53931
54421
|
Plan: planSchema,
|
|
53932
54422
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -55134,6 +55624,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55134
55624
|
}
|
|
55135
55625
|
}
|
|
55136
55626
|
},
|
|
55627
|
+
"/v1/tasks/{id}/stale-lock-handoff": {
|
|
55628
|
+
post: {
|
|
55629
|
+
operationId: "handoffStaleTaskLock",
|
|
55630
|
+
summary: "Atomically transfer one exact stale task lock",
|
|
55631
|
+
description: "Compares one full task UUID, current holder, and exact locked_at version, verifies the lock is strictly older than the supplied threshold, then transfers it directly and writes an immutable task-history receipt in the same backend transaction.",
|
|
55632
|
+
parameters: [
|
|
55633
|
+
{
|
|
55634
|
+
name: "id",
|
|
55635
|
+
in: "path",
|
|
55636
|
+
required: true,
|
|
55637
|
+
schema: { type: "string", format: "uuid" },
|
|
55638
|
+
description: "Exact full task UUID. Short ids and prefixes are rejected."
|
|
55639
|
+
}
|
|
55640
|
+
],
|
|
55641
|
+
requestBody: {
|
|
55642
|
+
required: true,
|
|
55643
|
+
content: {
|
|
55644
|
+
"application/json": {
|
|
55645
|
+
schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
|
|
55646
|
+
}
|
|
55647
|
+
}
|
|
55648
|
+
},
|
|
55649
|
+
responses: {
|
|
55650
|
+
"200": {
|
|
55651
|
+
content: {
|
|
55652
|
+
"application/json": {
|
|
55653
|
+
schema: {
|
|
55654
|
+
type: "object",
|
|
55655
|
+
additionalProperties: false,
|
|
55656
|
+
required: ["receipt"],
|
|
55657
|
+
properties: {
|
|
55658
|
+
receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
|
|
55659
|
+
}
|
|
55660
|
+
}
|
|
55661
|
+
}
|
|
55662
|
+
}
|
|
55663
|
+
},
|
|
55664
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55665
|
+
"403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55666
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55667
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55668
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
55669
|
+
}
|
|
55670
|
+
}
|
|
55671
|
+
},
|
|
55137
55672
|
"/v1/tasks/{id}/refs": {
|
|
55138
55673
|
get: {
|
|
55139
55674
|
operationId: "listTaskGitRefs",
|
|
@@ -55730,7 +56265,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55730
56265
|
}
|
|
55731
56266
|
};
|
|
55732
56267
|
}
|
|
55733
|
-
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
56268
|
+
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
55734
56269
|
var init_openapi = __esm(() => {
|
|
55735
56270
|
init_package_version();
|
|
55736
56271
|
init_types();
|
|
@@ -55749,6 +56284,8 @@ var init_openapi = __esm(() => {
|
|
|
55749
56284
|
reason: { type: "string", nullable: true },
|
|
55750
56285
|
tags: { type: "array", items: { type: "string" } },
|
|
55751
56286
|
version: { type: "number" },
|
|
56287
|
+
locked_by: { type: "string", nullable: true },
|
|
56288
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
55752
56289
|
created_at: { type: "string" },
|
|
55753
56290
|
updated_at: { type: "string" }
|
|
55754
56291
|
}
|
|
@@ -55913,6 +56450,69 @@ var init_openapi = __esm(() => {
|
|
|
55913
56450
|
created_at: { type: "string", format: "date-time" }
|
|
55914
56451
|
}
|
|
55915
56452
|
};
|
|
56453
|
+
staleLockHandoffInputSchema = {
|
|
56454
|
+
type: "object",
|
|
56455
|
+
additionalProperties: false,
|
|
56456
|
+
required: [
|
|
56457
|
+
"expected_holder",
|
|
56458
|
+
"expected_lock_version",
|
|
56459
|
+
"stale_after_seconds",
|
|
56460
|
+
"new_holder",
|
|
56461
|
+
"reason"
|
|
56462
|
+
],
|
|
56463
|
+
properties: {
|
|
56464
|
+
expected_holder: { type: "string", minLength: 1 },
|
|
56465
|
+
expected_lock_version: {
|
|
56466
|
+
type: "string",
|
|
56467
|
+
format: "date-time",
|
|
56468
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
56469
|
+
description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
|
|
56470
|
+
},
|
|
56471
|
+
stale_after_seconds: {
|
|
56472
|
+
type: "integer",
|
|
56473
|
+
minimum: 1,
|
|
56474
|
+
description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
|
|
56475
|
+
},
|
|
56476
|
+
new_holder: {
|
|
56477
|
+
type: "string",
|
|
56478
|
+
minLength: 1,
|
|
56479
|
+
description: "Must match the agent bound to the authenticated API key."
|
|
56480
|
+
},
|
|
56481
|
+
reason: { type: "string", minLength: 1, maxLength: 4096 }
|
|
56482
|
+
}
|
|
56483
|
+
};
|
|
56484
|
+
staleLockHandoffReceiptSchema = {
|
|
56485
|
+
type: "object",
|
|
56486
|
+
additionalProperties: false,
|
|
56487
|
+
required: [
|
|
56488
|
+
"schema_version",
|
|
56489
|
+
"receipt_id",
|
|
56490
|
+
"task_id",
|
|
56491
|
+
"actor",
|
|
56492
|
+
"previous_holder",
|
|
56493
|
+
"previous_lock_version",
|
|
56494
|
+
"new_holder",
|
|
56495
|
+
"new_lock_version",
|
|
56496
|
+
"stale_after_seconds",
|
|
56497
|
+
"stale_cutoff",
|
|
56498
|
+
"reason",
|
|
56499
|
+
"created_at"
|
|
56500
|
+
],
|
|
56501
|
+
properties: {
|
|
56502
|
+
schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
|
|
56503
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
56504
|
+
task_id: { type: "string", format: "uuid" },
|
|
56505
|
+
actor: { type: "string" },
|
|
56506
|
+
previous_holder: { type: "string" },
|
|
56507
|
+
previous_lock_version: { type: "string", format: "date-time" },
|
|
56508
|
+
new_holder: { type: "string" },
|
|
56509
|
+
new_lock_version: { type: "string", format: "date-time" },
|
|
56510
|
+
stale_after_seconds: { type: "integer", minimum: 1 },
|
|
56511
|
+
stale_cutoff: { type: "string", format: "date-time" },
|
|
56512
|
+
reason: { type: "string" },
|
|
56513
|
+
created_at: { type: "string", format: "date-time" }
|
|
56514
|
+
}
|
|
56515
|
+
};
|
|
55916
56516
|
taskGitRefSchema = {
|
|
55917
56517
|
type: "object",
|
|
55918
56518
|
additionalProperties: false,
|
|
@@ -57154,6 +57754,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57154
57754
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
57155
57755
|
}
|
|
57156
57756
|
if (action) {
|
|
57757
|
+
if (action === "stale-lock-handoff") {
|
|
57758
|
+
if (method !== "POST") {
|
|
57759
|
+
return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
|
|
57760
|
+
}
|
|
57761
|
+
const exactId = normalizeExactTaskId(id);
|
|
57762
|
+
if (!principal.agent) {
|
|
57763
|
+
return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
|
|
57764
|
+
code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
|
|
57765
|
+
});
|
|
57766
|
+
}
|
|
57767
|
+
if (typeof store.tasks.handoffStaleLock !== "function") {
|
|
57768
|
+
return error(501, "stale-lock handoff is not supported by this storage backend");
|
|
57769
|
+
}
|
|
57770
|
+
const body3 = await readJson3(req) ?? {};
|
|
57771
|
+
const allowed = new Set([
|
|
57772
|
+
"expected_holder",
|
|
57773
|
+
"expected_lock_version",
|
|
57774
|
+
"stale_after_seconds",
|
|
57775
|
+
"new_holder",
|
|
57776
|
+
"reason"
|
|
57777
|
+
]);
|
|
57778
|
+
const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
|
|
57779
|
+
if (unknown) {
|
|
57780
|
+
return error(400, `unknown stale-lock handoff field: ${unknown}`, {
|
|
57781
|
+
code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
|
|
57782
|
+
field: unknown
|
|
57783
|
+
});
|
|
57784
|
+
}
|
|
57785
|
+
const receipt = await store.tasks.handoffStaleLock({
|
|
57786
|
+
task_id: exactId,
|
|
57787
|
+
actor: principal.agent,
|
|
57788
|
+
expected_holder: body3.expected_holder,
|
|
57789
|
+
expected_lock_version: body3.expected_lock_version,
|
|
57790
|
+
stale_after_seconds: body3.stale_after_seconds,
|
|
57791
|
+
new_holder: body3.new_holder,
|
|
57792
|
+
reason: body3.reason
|
|
57793
|
+
}, contextFromPrincipal(principal));
|
|
57794
|
+
return json5({ receipt });
|
|
57795
|
+
}
|
|
57157
57796
|
if (action === "comments") {
|
|
57158
57797
|
if (method === "GET") {
|
|
57159
57798
|
if (!await store.tasks.get(id))
|
|
@@ -57942,7 +58581,24 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57942
58581
|
if (received === 0) {
|
|
57943
58582
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
57944
58583
|
}
|
|
58584
|
+
const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
|
|
58585
|
+
if (forbiddenAuditTombstone) {
|
|
58586
|
+
return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
|
|
58587
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
58588
|
+
conflict: false,
|
|
58589
|
+
audit_history_id: forbiddenAuditTombstone.object_id
|
|
58590
|
+
});
|
|
58591
|
+
}
|
|
57945
58592
|
const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
|
|
58593
|
+
const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
|
|
58594
|
+
if (auditFailureMessage) {
|
|
58595
|
+
const failure = parseAuditHistoryImportFailure(auditFailureMessage);
|
|
58596
|
+
return error(failure.status, auditFailureMessage, {
|
|
58597
|
+
code: failure.code,
|
|
58598
|
+
conflict: failure.conflict,
|
|
58599
|
+
audit_history_id: failure.auditHistoryId
|
|
58600
|
+
});
|
|
58601
|
+
}
|
|
57946
58602
|
return json5({ result, received });
|
|
57947
58603
|
}
|
|
57948
58604
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -57965,6 +58621,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57965
58621
|
if (e instanceof TaskNotFoundError) {
|
|
57966
58622
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
57967
58623
|
}
|
|
58624
|
+
if (e instanceof StaleLockHandoffError) {
|
|
58625
|
+
const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
58626
|
+
return error(status2, e.message, {
|
|
58627
|
+
code: e.code,
|
|
58628
|
+
conflict: status2 === 409,
|
|
58629
|
+
...e.details
|
|
58630
|
+
});
|
|
58631
|
+
}
|
|
57968
58632
|
if (e instanceof PlanNotFoundError) {
|
|
57969
58633
|
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
57970
58634
|
}
|
|
@@ -57999,6 +58663,8 @@ var init_v1 = __esm(() => {
|
|
|
57999
58663
|
init_redaction();
|
|
58000
58664
|
init_project_task_list_ensure();
|
|
58001
58665
|
init_plan_project_link();
|
|
58666
|
+
init_stale_lock_handoff();
|
|
58667
|
+
init_audit_history_import();
|
|
58002
58668
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
58003
58669
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
58004
58670
|
});
|