@hasna/todos 0.15.18 → 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/project-commands.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 +1306 -122
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +283 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/plans.d.ts +4 -0
- package/dist/db/plans.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 +599 -26
- 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 +954 -46
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +4250 -3684
- package/dist/registry.js +283 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +40 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1287 -379
- package/dist/server/openapi.d.ts +232 -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 +22 -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/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +596 -25
- package/dist/task-manifest.js +24 -1
- package/dist/types/index.d.ts +50 -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, 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",
|
|
@@ -134,6 +134,19 @@ var init_types = __esm(() => {
|
|
|
134
134
|
this.name = "ResourceConflictError";
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
138
|
+
planId;
|
|
139
|
+
expectedUpdatedAt;
|
|
140
|
+
currentUpdatedAt;
|
|
141
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
142
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
143
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
144
|
+
this.planId = planId;
|
|
145
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
146
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
147
|
+
this.name = "PlanRevisionConflictError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
137
150
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
138
151
|
planId;
|
|
139
152
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -156,6 +169,16 @@ var init_types = __esm(() => {
|
|
|
156
169
|
this.name = "LockError";
|
|
157
170
|
}
|
|
158
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
|
+
};
|
|
159
182
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
160
183
|
agentId;
|
|
161
184
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -12157,28 +12180,55 @@ var init_activity_audit = __esm(() => {
|
|
|
12157
12180
|
function sanitizeHistoryValue(value, context) {
|
|
12158
12181
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
12159
12182
|
}
|
|
12160
|
-
function
|
|
12183
|
+
function insertTaskHistory(entry, db) {
|
|
12161
12184
|
const d = db || getDatabase();
|
|
12162
|
-
const
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
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
|
+
};
|
|
12167
12193
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
12168
|
-
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
|
+
]);
|
|
12169
12205
|
try {
|
|
12170
12206
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
12171
12207
|
logActivity2({
|
|
12172
12208
|
entity_type: "task",
|
|
12173
|
-
entity_id:
|
|
12174
|
-
action,
|
|
12175
|
-
field,
|
|
12176
|
-
old_value:
|
|
12177
|
-
new_value:
|
|
12178
|
-
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
|
|
12179
12215
|
}, d);
|
|
12180
12216
|
} catch {}
|
|
12181
|
-
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);
|
|
12182
12232
|
}
|
|
12183
12233
|
function getTaskHistory(taskId, db) {
|
|
12184
12234
|
const d = db || getDatabase();
|
|
@@ -13181,6 +13231,145 @@ var init_task_graph = __esm(() => {
|
|
|
13181
13231
|
init_task_crud();
|
|
13182
13232
|
});
|
|
13183
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
|
+
|
|
13184
13373
|
// src/db/task-lifecycle.ts
|
|
13185
13374
|
var exports_task_lifecycle = {};
|
|
13186
13375
|
__export(exports_task_lifecycle, {
|
|
@@ -13189,6 +13378,7 @@ __export(exports_task_lifecycle, {
|
|
|
13189
13378
|
startTask: () => startTask,
|
|
13190
13379
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
13191
13380
|
lockTask: () => lockTask,
|
|
13381
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
13192
13382
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
13193
13383
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
13194
13384
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -13457,6 +13647,38 @@ function unlockTask(id, agentId, db) {
|
|
|
13457
13647
|
WHERE id = ?`, [timestamp2, id]);
|
|
13458
13648
|
return true;
|
|
13459
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
|
+
}
|
|
13460
13682
|
function getTaskLockStatus(id, db) {
|
|
13461
13683
|
const d = db || getDatabase();
|
|
13462
13684
|
const task = getTask(id, d);
|
|
@@ -13757,6 +13979,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
13757
13979
|
init_task_crud();
|
|
13758
13980
|
init_task_graph();
|
|
13759
13981
|
init_prewrite_secrets();
|
|
13982
|
+
init_stale_lock_handoff();
|
|
13760
13983
|
});
|
|
13761
13984
|
|
|
13762
13985
|
// src/db/task-crud.ts
|
|
@@ -15309,6 +15532,48 @@ function updatePlan(id, input, db) {
|
|
|
15309
15532
|
return updatePlanStored(id, input, d);
|
|
15310
15533
|
})();
|
|
15311
15534
|
}
|
|
15535
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
15536
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
15537
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
15538
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
15539
|
+
}
|
|
15540
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
15541
|
+
const d = db || getDatabase();
|
|
15542
|
+
return d.transaction(() => {
|
|
15543
|
+
guardPlanRowsSqlite([id], d);
|
|
15544
|
+
const plan = getPlan(id, d);
|
|
15545
|
+
if (!plan)
|
|
15546
|
+
throw new PlanNotFoundError(id);
|
|
15547
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
15548
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
15549
|
+
}
|
|
15550
|
+
if (plan.status === "completed")
|
|
15551
|
+
return { plan, applied: false };
|
|
15552
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
15553
|
+
const result = d.run(`UPDATE plans
|
|
15554
|
+
SET status = 'completed', updated_at = ?
|
|
15555
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
15556
|
+
if (result.changes !== 1) {
|
|
15557
|
+
const current = getPlan(id, d);
|
|
15558
|
+
if (!current)
|
|
15559
|
+
throw new PlanNotFoundError(id);
|
|
15560
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
15561
|
+
}
|
|
15562
|
+
const completed = getPlan(id, d);
|
|
15563
|
+
emitLocalEventHooksQuiet({
|
|
15564
|
+
type: "plan.updated",
|
|
15565
|
+
payload: {
|
|
15566
|
+
id,
|
|
15567
|
+
old_status: plan.status,
|
|
15568
|
+
new_status: completed.status,
|
|
15569
|
+
name: completed.name,
|
|
15570
|
+
project_id: completed.project_id
|
|
15571
|
+
},
|
|
15572
|
+
databasePath: databasePathFromDatabase(d)
|
|
15573
|
+
});
|
|
15574
|
+
return { plan: completed, applied: true };
|
|
15575
|
+
})();
|
|
15576
|
+
}
|
|
15312
15577
|
function deletePlan(id, db) {
|
|
15313
15578
|
const d = db || getDatabase();
|
|
15314
15579
|
const plan = getPlan(id, d);
|
|
@@ -17287,6 +17552,7 @@ __export(exports_tasks, {
|
|
|
17287
17552
|
insertTaskTags: () => insertTaskTags,
|
|
17288
17553
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
17289
17554
|
importCalendarIcs: () => importCalendarIcs,
|
|
17555
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
17290
17556
|
getTimeReport: () => getTimeReport,
|
|
17291
17557
|
getTimeLogs: () => getTimeLogs,
|
|
17292
17558
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -20744,6 +21010,7 @@ var init_http_client = __esm(() => {
|
|
|
20744
21010
|
// src/cli/cloud-router.ts
|
|
20745
21011
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
20746
21012
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
21013
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20747
21014
|
import { resolve as resolvePath } from "path";
|
|
20748
21015
|
function cleanMode(value) {
|
|
20749
21016
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -21146,7 +21413,10 @@ async function cloudGetTask(client, id) {
|
|
|
21146
21413
|
}
|
|
21147
21414
|
async function cloudCreateTask(client, input) {
|
|
21148
21415
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
21149
|
-
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"]));
|
|
21150
21420
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
21151
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");
|
|
21152
21422
|
}
|
|
@@ -21157,7 +21427,7 @@ async function cloudCreateTask(client, input) {
|
|
|
21157
21427
|
return persisted;
|
|
21158
21428
|
}
|
|
21159
21429
|
async function cloudUpdateTask(client, id, patch) {
|
|
21160
|
-
return unwrapTask(await client.
|
|
21430
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
21161
21431
|
}
|
|
21162
21432
|
async function cloudDeleteTask(client, id) {
|
|
21163
21433
|
try {
|
|
@@ -21368,7 +21638,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
21368
21638
|
return input.toLowerCase();
|
|
21369
21639
|
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
21370
21640
|
}
|
|
21371
|
-
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21641
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21372
21642
|
var init_cloud_router = __esm(() => {
|
|
21373
21643
|
init_types();
|
|
21374
21644
|
init_redaction();
|
|
@@ -21385,6 +21655,7 @@ var init_cloud_router = __esm(() => {
|
|
|
21385
21655
|
hybrid: "http"
|
|
21386
21656
|
};
|
|
21387
21657
|
completionCapabilityCache = new Map;
|
|
21658
|
+
retryCapabilityCache = new Map;
|
|
21388
21659
|
gitRefCapabilityCache = new Map;
|
|
21389
21660
|
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
21390
21661
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -35481,7 +35752,7 @@ var package_default;
|
|
|
35481
35752
|
var init_package = __esm(() => {
|
|
35482
35753
|
package_default = {
|
|
35483
35754
|
name: "@hasna/todos",
|
|
35484
|
-
version: "0.15.
|
|
35755
|
+
version: "0.15.20",
|
|
35485
35756
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35486
35757
|
type: "module",
|
|
35487
35758
|
main: "dist/index.js",
|
|
@@ -35552,6 +35823,8 @@ var init_package = __esm(() => {
|
|
|
35552
35823
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
35553
35824
|
"dev:serve": "bun run src/server/index.ts",
|
|
35554
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",
|
|
35555
35828
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
35556
35829
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
35557
35830
|
},
|
|
@@ -45042,6 +45315,58 @@ var init_plan_project_links = __esm(() => {
|
|
|
45042
45315
|
init_tasks();
|
|
45043
45316
|
});
|
|
45044
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
|
+
|
|
45045
45370
|
// src/storage/sqlite-snapshot.ts
|
|
45046
45371
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
45047
45372
|
const d = db ?? getDatabase();
|
|
@@ -45075,9 +45400,12 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45075
45400
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
45076
45401
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
45077
45402
|
}
|
|
45403
|
+
const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
45404
|
+
result.errors.push(...auditImport.errors);
|
|
45078
45405
|
if (result.errors.length > 0)
|
|
45079
45406
|
return result;
|
|
45080
|
-
|
|
45407
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
45408
|
+
const applyRows = (objectType2, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
45081
45409
|
for (const row of rows) {
|
|
45082
45410
|
try {
|
|
45083
45411
|
const record = asRecord2(row);
|
|
@@ -45086,7 +45414,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45086
45414
|
result.skipped += 1;
|
|
45087
45415
|
continue;
|
|
45088
45416
|
}
|
|
45089
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
45417
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
45090
45418
|
if (state === "inserted")
|
|
45091
45419
|
result.inserted += 1;
|
|
45092
45420
|
else if (state === "updated")
|
|
@@ -45103,19 +45431,81 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45103
45431
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
45104
45432
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
45105
45433
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
45106
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
45434
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
45107
45435
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
45108
45436
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
45109
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
45437
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
45110
45438
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
45111
45439
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
45112
45440
|
}
|
|
45113
45441
|
});
|
|
45114
|
-
|
|
45442
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
45115
45443
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
45116
45444
|
return result;
|
|
45117
45445
|
}
|
|
45118
|
-
function
|
|
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
|
+
}
|
|
45508
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
45119
45509
|
const id = row["id"];
|
|
45120
45510
|
if (typeof id !== "string" || !id)
|
|
45121
45511
|
throw new Error(`${table} row is missing id`);
|
|
@@ -45127,7 +45517,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
45127
45517
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
45128
45518
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
45129
45519
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
45130
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
45520
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
45131
45521
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})
|
|
45132
45522
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})`;
|
|
45133
45523
|
const changes = db.run(sql, values).changes;
|
|
@@ -45212,7 +45602,7 @@ function tableForTombstone(objectType2) {
|
|
|
45212
45602
|
return "task_templates";
|
|
45213
45603
|
if (objectType2 === "template_tasks")
|
|
45214
45604
|
return "template_tasks";
|
|
45215
|
-
|
|
45605
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType2)}`);
|
|
45216
45606
|
}
|
|
45217
45607
|
function listRows(db, table, columns) {
|
|
45218
45608
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -45259,6 +45649,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
45259
45649
|
init_tasks();
|
|
45260
45650
|
init_templates();
|
|
45261
45651
|
init_storage_tombstones();
|
|
45652
|
+
init_audit_history_import();
|
|
45262
45653
|
PROJECT_COLUMNS = [
|
|
45263
45654
|
"id",
|
|
45264
45655
|
"name",
|
|
@@ -45523,6 +45914,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45523
45914
|
unlockTask(id, agentId, database());
|
|
45524
45915
|
return true;
|
|
45525
45916
|
},
|
|
45917
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
45526
45918
|
delete: (id) => deleteTask(id, database()),
|
|
45527
45919
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
45528
45920
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -45546,6 +45938,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45546
45938
|
get: (id) => getPlan(id, database()),
|
|
45547
45939
|
list: (projectId) => listPlans(projectId, database()),
|
|
45548
45940
|
update: (id, input) => updatePlan(id, input, database()),
|
|
45941
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
45549
45942
|
delete: (id) => deletePlan(id, database())
|
|
45550
45943
|
},
|
|
45551
45944
|
planProjectLinks: {
|
|
@@ -46441,7 +46834,7 @@ var init_api_keys = __esm(() => {
|
|
|
46441
46834
|
});
|
|
46442
46835
|
|
|
46443
46836
|
// src/storage/postgres-adapter.ts
|
|
46444
|
-
import { randomUUID as
|
|
46837
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
46445
46838
|
function createPostgresTodosStorageAdapter(options) {
|
|
46446
46839
|
const store = new PostgresJsonRecordStore(options);
|
|
46447
46840
|
const adapter = {
|
|
@@ -46470,6 +46863,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46470
46863
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
46471
46864
|
lock: (id, agentId) => lockTask2(id, agentId, store),
|
|
46472
46865
|
unlock: (id, agentId) => unlockTask2(id, agentId, store),
|
|
46866
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
46473
46867
|
getByFingerprint: (fingerprint3) => store.getTaskByFingerprint(fingerprint3)
|
|
46474
46868
|
},
|
|
46475
46869
|
dependencies: {
|
|
@@ -46506,6 +46900,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46506
46900
|
get: (id) => store.get("plans", id),
|
|
46507
46901
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
46508
46902
|
update: (id, input) => updatePlan2(id, input, store),
|
|
46903
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
46509
46904
|
delete: (id, context) => store.deletePlan(id, context)
|
|
46510
46905
|
},
|
|
46511
46906
|
planProjectLinks: {
|
|
@@ -46615,6 +47010,91 @@ class PostgresJsonRecordStore {
|
|
|
46615
47010
|
LIMIT 1`, [this.service, type, id]);
|
|
46616
47011
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
46617
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
|
+
}
|
|
46618
47098
|
async list(type) {
|
|
46619
47099
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
46620
47100
|
}
|
|
@@ -46884,6 +47364,28 @@ class PostgresJsonRecordStore {
|
|
|
46884
47364
|
}
|
|
46885
47365
|
return value;
|
|
46886
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
|
+
}
|
|
46887
47389
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
46888
47390
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
46889
47391
|
if (planIds.length === 0)
|
|
@@ -46993,6 +47495,54 @@ class PostgresJsonRecordStore {
|
|
|
46993
47495
|
throw new PlanNotFoundError(value.id);
|
|
46994
47496
|
return payloadRecord2(row.payload);
|
|
46995
47497
|
}
|
|
47498
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
47499
|
+
await this.ensureSchema();
|
|
47500
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
47501
|
+
SELECT date_trunc(
|
|
47502
|
+
'milliseconds',
|
|
47503
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
47504
|
+
) AS completed_at
|
|
47505
|
+
), stored AS (
|
|
47506
|
+
UPDATE ${this.tableName} AS record SET
|
|
47507
|
+
payload = record.payload || jsonb_build_object(
|
|
47508
|
+
'status', 'completed',
|
|
47509
|
+
'updated_at', to_char(
|
|
47510
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
47511
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
47512
|
+
)
|
|
47513
|
+
),
|
|
47514
|
+
updated_at = next_clock.completed_at,
|
|
47515
|
+
deleted_at = NULL,
|
|
47516
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
47517
|
+
version = COALESCE(record.version, 0) + 1
|
|
47518
|
+
FROM next_clock
|
|
47519
|
+
WHERE record.service = $1
|
|
47520
|
+
AND record.object_type = 'plans'
|
|
47521
|
+
AND record.object_id = $2
|
|
47522
|
+
AND record.deleted_at IS NULL
|
|
47523
|
+
AND record.payload->>'updated_at' = $3::text
|
|
47524
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
47525
|
+
RETURNING record.payload
|
|
47526
|
+
)
|
|
47527
|
+
SELECT payload FROM stored`, [
|
|
47528
|
+
this.service,
|
|
47529
|
+
id,
|
|
47530
|
+
expectedUpdatedAt,
|
|
47531
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
47532
|
+
]);
|
|
47533
|
+
const payload = result.rows[0]?.payload;
|
|
47534
|
+
if (payload)
|
|
47535
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
47536
|
+
const current = await this.get("plans", id);
|
|
47537
|
+
if (!current)
|
|
47538
|
+
throw new PlanNotFoundError(id);
|
|
47539
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
47540
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
47541
|
+
}
|
|
47542
|
+
if (current.status === "completed")
|
|
47543
|
+
return { plan: current, applied: false };
|
|
47544
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
47545
|
+
}
|
|
46996
47546
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
46997
47547
|
await this.ensureSchema();
|
|
46998
47548
|
const records = [
|
|
@@ -47662,7 +48212,7 @@ async function createTask3(input, store, context) {
|
|
|
47662
48212
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
47663
48213
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
47664
48214
|
const task2 = {
|
|
47665
|
-
id:
|
|
48215
|
+
id: randomUUID4(),
|
|
47666
48216
|
short_id: shortId,
|
|
47667
48217
|
project_id: effectiveProjectId,
|
|
47668
48218
|
parent_id: input.parent_id ?? null,
|
|
@@ -47908,7 +48458,7 @@ async function addVerification(input, store, context) {
|
|
|
47908
48458
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
47909
48459
|
const timestamp4 = new Date().toISOString();
|
|
47910
48460
|
const verification = {
|
|
47911
|
-
id:
|
|
48461
|
+
id: randomUUID4(),
|
|
47912
48462
|
task_id: input.task_id,
|
|
47913
48463
|
command: input.command,
|
|
47914
48464
|
status: input.status ?? "unknown",
|
|
@@ -47929,7 +48479,7 @@ async function addCommit(input, store, context) {
|
|
|
47929
48479
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
47930
48480
|
const timestamp4 = new Date().toISOString();
|
|
47931
48481
|
const commit = {
|
|
47932
|
-
id:
|
|
48482
|
+
id: randomUUID4(),
|
|
47933
48483
|
task_id: input.task_id,
|
|
47934
48484
|
sha: input.sha,
|
|
47935
48485
|
message: input.message ?? null,
|
|
@@ -47952,7 +48502,7 @@ async function addGitRef(input, store, context) {
|
|
|
47952
48502
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
47953
48503
|
const timestamp4 = new Date().toISOString();
|
|
47954
48504
|
const gitRef = {
|
|
47955
|
-
id:
|
|
48505
|
+
id: randomUUID4(),
|
|
47956
48506
|
task_id: input.task_id,
|
|
47957
48507
|
ref_type: input.ref_type,
|
|
47958
48508
|
name: input.name,
|
|
@@ -48022,7 +48572,7 @@ async function createProject2(input, store, context) {
|
|
|
48022
48572
|
if (!derivedSlug || !taskListId)
|
|
48023
48573
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
48024
48574
|
const project = {
|
|
48025
|
-
id:
|
|
48575
|
+
id: randomUUID4(),
|
|
48026
48576
|
name: input.name,
|
|
48027
48577
|
path: input.path,
|
|
48028
48578
|
description: input.description ?? null,
|
|
@@ -48054,7 +48604,7 @@ async function createPlan2(input, store, context) {
|
|
|
48054
48604
|
store
|
|
48055
48605
|
});
|
|
48056
48606
|
return store.upsert("plans", {
|
|
48057
|
-
id:
|
|
48607
|
+
id: randomUUID4(),
|
|
48058
48608
|
slug,
|
|
48059
48609
|
project_id: projectId,
|
|
48060
48610
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
@@ -48106,7 +48656,7 @@ async function registerAgent2(input, store, context) {
|
|
|
48106
48656
|
}
|
|
48107
48657
|
const timestamp4 = new Date().toISOString();
|
|
48108
48658
|
const agent = {
|
|
48109
|
-
id: existing?.id ??
|
|
48659
|
+
id: existing?.id ?? randomUUID4().slice(0, 8),
|
|
48110
48660
|
name: canonicalName,
|
|
48111
48661
|
description: input.description ?? existing?.description ?? null,
|
|
48112
48662
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -48179,7 +48729,7 @@ async function createTaskList2(input, store, context) {
|
|
|
48179
48729
|
if (!slug)
|
|
48180
48730
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
48181
48731
|
return store.upsert("task_lists", {
|
|
48182
|
-
id:
|
|
48732
|
+
id: randomUUID4(),
|
|
48183
48733
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
48184
48734
|
slug,
|
|
48185
48735
|
name: input.name,
|
|
@@ -48214,7 +48764,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
48214
48764
|
async function createTemplate2(input, store, context) {
|
|
48215
48765
|
const timestamp4 = new Date().toISOString();
|
|
48216
48766
|
const template = {
|
|
48217
|
-
id:
|
|
48767
|
+
id: randomUUID4(),
|
|
48218
48768
|
name: input.name,
|
|
48219
48769
|
title_pattern: input.title_pattern,
|
|
48220
48770
|
description: input.description ?? null,
|
|
@@ -48235,7 +48785,7 @@ async function createTemplate2(input, store, context) {
|
|
|
48235
48785
|
}
|
|
48236
48786
|
function buildTemplateTasks(templateId, inputs, timestamp4) {
|
|
48237
48787
|
return inputs.map((input, position) => ({
|
|
48238
|
-
id:
|
|
48788
|
+
id: randomUUID4(),
|
|
48239
48789
|
template_id: templateId,
|
|
48240
48790
|
position,
|
|
48241
48791
|
title_pattern: input.title_pattern,
|
|
@@ -48268,7 +48818,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
48268
48818
|
}
|
|
48269
48819
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
48270
48820
|
const entry2 = {
|
|
48271
|
-
id:
|
|
48821
|
+
id: randomUUID4(),
|
|
48272
48822
|
task_id: taskId,
|
|
48273
48823
|
action,
|
|
48274
48824
|
field: field ?? null,
|
|
@@ -48282,7 +48832,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
48282
48832
|
}
|
|
48283
48833
|
async function addComment2(input, store, context) {
|
|
48284
48834
|
const comment = {
|
|
48285
|
-
id:
|
|
48835
|
+
id: randomUUID4(),
|
|
48286
48836
|
task_id: input.task_id,
|
|
48287
48837
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
48288
48838
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -48324,6 +48874,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48324
48874
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
48325
48875
|
if (result.errors.length > 0)
|
|
48326
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;
|
|
48327
48882
|
const entries = [
|
|
48328
48883
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
48329
48884
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -48332,9 +48887,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48332
48887
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
48333
48888
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
48334
48889
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
48335
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48336
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
48890
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48337
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
|
+
}
|
|
48338
48904
|
for (const [type, row] of entries) {
|
|
48339
48905
|
try {
|
|
48340
48906
|
const existing = await store.get(type, row.id);
|
|
@@ -48368,6 +48934,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48368
48934
|
}
|
|
48369
48935
|
return result;
|
|
48370
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
|
+
}
|
|
48371
48963
|
async function requireRecord(type, id, store) {
|
|
48372
48964
|
const record = await store.get(type, id);
|
|
48373
48965
|
if (!record)
|
|
@@ -48474,9 +49066,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
48474
49066
|
init_types();
|
|
48475
49067
|
init_creator_identity();
|
|
48476
49068
|
init_plan_project_link_contract();
|
|
49069
|
+
init_stale_lock_handoff();
|
|
48477
49070
|
init_postgres_sync();
|
|
48478
49071
|
init_integrity();
|
|
48479
49072
|
init_redaction();
|
|
49073
|
+
init_audit_history_import();
|
|
48480
49074
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
48481
49075
|
});
|
|
48482
49076
|
|
|
@@ -53821,6 +54415,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
53821
54415
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
53822
54416
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
53823
54417
|
TaskComment: taskCommentSchema,
|
|
54418
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
54419
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
53824
54420
|
TaskGitRef: taskGitRefSchema,
|
|
53825
54421
|
Plan: planSchema,
|
|
53826
54422
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -55028,6 +55624,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55028
55624
|
}
|
|
55029
55625
|
}
|
|
55030
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
|
+
},
|
|
55031
55672
|
"/v1/tasks/{id}/refs": {
|
|
55032
55673
|
get: {
|
|
55033
55674
|
operationId: "listTaskGitRefs",
|
|
@@ -55534,8 +56175,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55534
56175
|
"/v1/import": {
|
|
55535
56176
|
post: {
|
|
55536
56177
|
operationId: "importSnapshot",
|
|
55537
|
-
summary: "Bulk-ingest a
|
|
55538
|
-
description: "Upserts every record
|
|
56178
|
+
summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
|
|
56179
|
+
description: "Upserts every snapshot record by primary key, or accepts exactly one planCompletions operation that changes only plan status under an expected_updated_at CAS. Snapshot records and planCompletions are mutually exclusive. Requires the todos:write scope.",
|
|
55539
56180
|
requestBody: {
|
|
55540
56181
|
required: true,
|
|
55541
56182
|
content: {
|
|
@@ -55554,7 +56195,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55554
56195
|
templates: { type: "array", items: { type: "object" } },
|
|
55555
56196
|
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
55556
56197
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
55557
|
-
tombstones: { type: "array", items: { type: "object" } }
|
|
56198
|
+
tombstones: { type: "array", items: { type: "object" } },
|
|
56199
|
+
planCompletions: {
|
|
56200
|
+
type: "array",
|
|
56201
|
+
minItems: 1,
|
|
56202
|
+
maxItems: 1,
|
|
56203
|
+
items: {
|
|
56204
|
+
type: "object",
|
|
56205
|
+
additionalProperties: false,
|
|
56206
|
+
required: ["id", "expected_updated_at", "status"],
|
|
56207
|
+
properties: {
|
|
56208
|
+
id: { type: "string" },
|
|
56209
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
56210
|
+
status: { type: "string", enum: ["completed"] }
|
|
56211
|
+
}
|
|
56212
|
+
}
|
|
56213
|
+
}
|
|
55558
56214
|
}
|
|
55559
56215
|
}
|
|
55560
56216
|
}
|
|
@@ -55577,6 +56233,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55577
56233
|
skipped: { type: "number" },
|
|
55578
56234
|
errors: { type: "array", items: { type: "string" } }
|
|
55579
56235
|
}
|
|
56236
|
+
},
|
|
56237
|
+
planCompletions: {
|
|
56238
|
+
type: "array",
|
|
56239
|
+
items: {
|
|
56240
|
+
type: "object",
|
|
56241
|
+
required: [
|
|
56242
|
+
"id",
|
|
56243
|
+
"status",
|
|
56244
|
+
"expected_updated_at",
|
|
56245
|
+
"result_updated_at",
|
|
56246
|
+
"applied"
|
|
56247
|
+
],
|
|
56248
|
+
properties: {
|
|
56249
|
+
id: { type: "string" },
|
|
56250
|
+
status: { type: "string", enum: ["completed"] },
|
|
56251
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
56252
|
+
result_updated_at: { type: "string", format: "date-time" },
|
|
56253
|
+
applied: { type: "boolean" }
|
|
56254
|
+
}
|
|
56255
|
+
}
|
|
55580
56256
|
}
|
|
55581
56257
|
}
|
|
55582
56258
|
}
|
|
@@ -55589,7 +56265,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55589
56265
|
}
|
|
55590
56266
|
};
|
|
55591
56267
|
}
|
|
55592
|
-
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;
|
|
55593
56269
|
var init_openapi = __esm(() => {
|
|
55594
56270
|
init_package_version();
|
|
55595
56271
|
init_types();
|
|
@@ -55608,6 +56284,8 @@ var init_openapi = __esm(() => {
|
|
|
55608
56284
|
reason: { type: "string", nullable: true },
|
|
55609
56285
|
tags: { type: "array", items: { type: "string" } },
|
|
55610
56286
|
version: { type: "number" },
|
|
56287
|
+
locked_by: { type: "string", nullable: true },
|
|
56288
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
55611
56289
|
created_at: { type: "string" },
|
|
55612
56290
|
updated_at: { type: "string" }
|
|
55613
56291
|
}
|
|
@@ -55772,6 +56450,69 @@ var init_openapi = __esm(() => {
|
|
|
55772
56450
|
created_at: { type: "string", format: "date-time" }
|
|
55773
56451
|
}
|
|
55774
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
|
+
};
|
|
55775
56516
|
taskGitRefSchema = {
|
|
55776
56517
|
type: "object",
|
|
55777
56518
|
additionalProperties: false,
|
|
@@ -56773,6 +57514,65 @@ function normalizeImportSnapshot(raw) {
|
|
|
56773
57514
|
function countSnapshotRecords(s) {
|
|
56774
57515
|
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
56775
57516
|
}
|
|
57517
|
+
function validatePlanCompletionImports(raw) {
|
|
57518
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
57519
|
+
return { present: false, operations: [] };
|
|
57520
|
+
}
|
|
57521
|
+
const body2 = raw;
|
|
57522
|
+
if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
|
|
57523
|
+
return { present: false, operations: [] };
|
|
57524
|
+
}
|
|
57525
|
+
if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
|
|
57526
|
+
return {
|
|
57527
|
+
present: true,
|
|
57528
|
+
operations: [],
|
|
57529
|
+
error: "planCompletions must contain exactly one completion operation"
|
|
57530
|
+
};
|
|
57531
|
+
}
|
|
57532
|
+
const operation = body2["planCompletions"][0];
|
|
57533
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
57534
|
+
return { present: true, operations: [], error: "plan completion must be an object" };
|
|
57535
|
+
}
|
|
57536
|
+
const record = operation;
|
|
57537
|
+
const allowed = new Set(["id", "expected_updated_at", "status"]);
|
|
57538
|
+
const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
|
|
57539
|
+
if (unknown) {
|
|
57540
|
+
return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
|
|
57541
|
+
}
|
|
57542
|
+
if (typeof record["id"] !== "string" || !record["id"].trim()) {
|
|
57543
|
+
return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
|
|
57544
|
+
}
|
|
57545
|
+
if (record["status"] !== "completed") {
|
|
57546
|
+
return { present: true, operations: [], error: "plan completion status must be completed" };
|
|
57547
|
+
}
|
|
57548
|
+
const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
|
|
57549
|
+
const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
|
|
57550
|
+
const parsedTimestamp = Date.parse(expectedUpdatedAt);
|
|
57551
|
+
if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
|
|
57552
|
+
return {
|
|
57553
|
+
present: true,
|
|
57554
|
+
operations: [],
|
|
57555
|
+
error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
|
|
57556
|
+
};
|
|
57557
|
+
}
|
|
57558
|
+
const [, year, month, day] = timestampMatch;
|
|
57559
|
+
const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
57560
|
+
if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
|
|
57561
|
+
return {
|
|
57562
|
+
present: true,
|
|
57563
|
+
operations: [],
|
|
57564
|
+
error: "plan completion expected_updated_at names a date that does not exist"
|
|
57565
|
+
};
|
|
57566
|
+
}
|
|
57567
|
+
return {
|
|
57568
|
+
present: true,
|
|
57569
|
+
operations: [{
|
|
57570
|
+
id: record["id"],
|
|
57571
|
+
expected_updated_at: expectedUpdatedAt,
|
|
57572
|
+
status: "completed"
|
|
57573
|
+
}]
|
|
57574
|
+
};
|
|
57575
|
+
}
|
|
56776
57576
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
56777
57577
|
const path = url.pathname;
|
|
56778
57578
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
@@ -56954,6 +57754,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
56954
57754
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
56955
57755
|
}
|
|
56956
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
|
+
}
|
|
56957
57796
|
if (action === "comments") {
|
|
56958
57797
|
if (method === "GET") {
|
|
56959
57798
|
if (!await store.tasks.get(id))
|
|
@@ -57709,10 +58548,57 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57709
58548
|
return error(400, "invalid JSON body");
|
|
57710
58549
|
const snapshot = normalizeImportSnapshot(raw);
|
|
57711
58550
|
const received = countSnapshotRecords(snapshot);
|
|
58551
|
+
const completionImports = validatePlanCompletionImports(raw);
|
|
58552
|
+
if (completionImports.present) {
|
|
58553
|
+
if (completionImports.error)
|
|
58554
|
+
return error(400, completionImports.error);
|
|
58555
|
+
if (received !== 0) {
|
|
58556
|
+
return error(400, "planCompletions cannot be combined with snapshot record arrays");
|
|
58557
|
+
}
|
|
58558
|
+
if (typeof store.plans.completeAtRevision !== "function") {
|
|
58559
|
+
return error(501, "atomic plan completion is not supported by this storage backend");
|
|
58560
|
+
}
|
|
58561
|
+
const operation = completionImports.operations[0];
|
|
58562
|
+
const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
|
|
58563
|
+
return json5({
|
|
58564
|
+
result: {
|
|
58565
|
+
inserted: 0,
|
|
58566
|
+
updated: completed.applied ? 1 : 0,
|
|
58567
|
+
deleted: 0,
|
|
58568
|
+
skipped: completed.applied ? 0 : 1,
|
|
58569
|
+
errors: []
|
|
58570
|
+
},
|
|
58571
|
+
received: 1,
|
|
58572
|
+
planCompletions: [{
|
|
58573
|
+
id: operation.id,
|
|
58574
|
+
status: "completed",
|
|
58575
|
+
expected_updated_at: operation.expected_updated_at,
|
|
58576
|
+
result_updated_at: completed.plan.updated_at,
|
|
58577
|
+
applied: completed.applied
|
|
58578
|
+
}]
|
|
58579
|
+
});
|
|
58580
|
+
}
|
|
57712
58581
|
if (received === 0) {
|
|
57713
58582
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
57714
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
|
+
}
|
|
57715
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
|
+
}
|
|
57716
58602
|
return json5({ result, received });
|
|
57717
58603
|
}
|
|
57718
58604
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -57735,6 +58621,26 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57735
58621
|
if (e instanceof TaskNotFoundError) {
|
|
57736
58622
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
57737
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
|
+
}
|
|
58632
|
+
if (e instanceof PlanNotFoundError) {
|
|
58633
|
+
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
58634
|
+
}
|
|
58635
|
+
if (e instanceof PlanRevisionConflictError) {
|
|
58636
|
+
return error(409, e.message, {
|
|
58637
|
+
code: PlanRevisionConflictError.code,
|
|
58638
|
+
conflict: true,
|
|
58639
|
+
plan_id: e.planId,
|
|
58640
|
+
expected_updated_at: e.expectedUpdatedAt,
|
|
58641
|
+
current_updated_at: e.currentUpdatedAt
|
|
58642
|
+
});
|
|
58643
|
+
}
|
|
57738
58644
|
if (e instanceof LockError)
|
|
57739
58645
|
return error(409, e.message, { code: LockError.code });
|
|
57740
58646
|
if (e instanceof TaskNotStartableError) {
|
|
@@ -57757,6 +58663,8 @@ var init_v1 = __esm(() => {
|
|
|
57757
58663
|
init_redaction();
|
|
57758
58664
|
init_project_task_list_ensure();
|
|
57759
58665
|
init_plan_project_link();
|
|
58666
|
+
init_stale_lock_handoff();
|
|
58667
|
+
init_audit_history_import();
|
|
57760
58668
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
57761
58669
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
57762
58670
|
});
|