@hasna/todos 0.15.41 → 0.15.47
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/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
- package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/components/Dashboard.d.ts +1 -1
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/ProjectList.d.ts +1 -1
- package/dist/cli/components/ProjectList.d.ts.map +1 -1
- package/dist/cli/components/SearchView.d.ts +1 -1
- package/dist/cli/components/SearchView.d.ts.map +1 -1
- package/dist/cli/components/TaskDetail.d.ts +1 -1
- package/dist/cli/components/TaskDetail.d.ts.map +1 -1
- package/dist/cli/components/TaskForm.d.ts +1 -1
- package/dist/cli/components/TaskForm.d.ts.map +1 -1
- package/dist/cli/components/TaskList.d.ts +1 -1
- package/dist/cli/components/TaskList.d.ts.map +1 -1
- package/dist/cli/index.js +521 -120
- package/dist/contracts.js +346 -83
- package/dist/db/task-graph.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.js +499 -113
- package/dist/lib/instant-compare.d.ts +35 -0
- package/dist/lib/instant-compare.d.ts.map +1 -0
- package/dist/mcp/index.js +520 -119
- package/dist/mcp.js +5 -4
- package/dist/pr-groups/postgres.d.ts.map +1 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration.js +466 -100
- package/dist/registry.js +346 -83
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1179 -309
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.js +447 -89
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest.js +15 -6
- package/dist/task-subtree-transfer/postgres.d.ts.map +1 -1
- package/dist/task-subtree-transfer.js +15 -6
- package/package.json +5 -4
- package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
- package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/mcp/index.js
CHANGED
|
@@ -11084,8 +11084,9 @@ var init_event_hooks = __esm(() => {
|
|
|
11084
11084
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
11085
11085
|
});
|
|
11086
11086
|
|
|
11087
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
11087
|
+
// node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
|
|
11088
11088
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
11089
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
11089
11090
|
import { existsSync as existsSync5 } from "fs";
|
|
11090
11091
|
import { homedir as homedir2 } from "os";
|
|
11091
11092
|
import { join as join4 } from "path";
|
|
@@ -11194,11 +11195,13 @@ function getEventsDataDir(override) {
|
|
|
11194
11195
|
|
|
11195
11196
|
class JsonEventsStore {
|
|
11196
11197
|
dataDir;
|
|
11198
|
+
runtime;
|
|
11197
11199
|
channelsPath;
|
|
11198
11200
|
eventsPath;
|
|
11199
11201
|
deliveriesPath;
|
|
11200
11202
|
constructor(dataDir = getEventsDataDir()) {
|
|
11201
11203
|
this.dataDir = dataDir;
|
|
11204
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
11202
11205
|
this.channelsPath = join4(dataDir, "channels.json");
|
|
11203
11206
|
this.eventsPath = join4(dataDir, "events.json");
|
|
11204
11207
|
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
@@ -11246,13 +11249,58 @@ class JsonEventsStore {
|
|
|
11246
11249
|
await this.writeJson(this.eventsPath, events);
|
|
11247
11250
|
return event;
|
|
11248
11251
|
}
|
|
11249
|
-
async
|
|
11252
|
+
async appendEventOnce(event, options = {}) {
|
|
11250
11253
|
await this.init();
|
|
11251
|
-
|
|
11254
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11255
|
+
const dedupe = options.dedupe !== false;
|
|
11256
|
+
if (dedupe) {
|
|
11257
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
11258
|
+
if (existing) {
|
|
11259
|
+
return {
|
|
11260
|
+
event: existing,
|
|
11261
|
+
stored: false,
|
|
11262
|
+
deduped: true,
|
|
11263
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
}
|
|
11267
|
+
events.push(event);
|
|
11268
|
+
await this.writeJson(this.eventsPath, events);
|
|
11269
|
+
return {
|
|
11270
|
+
event,
|
|
11271
|
+
stored: true,
|
|
11272
|
+
deduped: false,
|
|
11273
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
11274
|
+
};
|
|
11275
|
+
}
|
|
11276
|
+
async listEvents(options = {}) {
|
|
11277
|
+
await this.init();
|
|
11278
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11279
|
+
return queryEvents(events, options);
|
|
11280
|
+
}
|
|
11281
|
+
async listEventsPage(options = {}) {
|
|
11282
|
+
await this.init();
|
|
11283
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11284
|
+
const queried = queryEvents(events, {
|
|
11285
|
+
eventId: options.eventId,
|
|
11286
|
+
source: options.source,
|
|
11287
|
+
type: options.type
|
|
11288
|
+
});
|
|
11289
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11290
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
11291
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
11292
|
+
const nextOffset = offset + pageEvents.length;
|
|
11293
|
+
const hasMore = nextOffset < queried.length;
|
|
11294
|
+
return {
|
|
11295
|
+
events: pageEvents,
|
|
11296
|
+
cursor: options.cursor,
|
|
11297
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
11298
|
+
hasMore
|
|
11299
|
+
};
|
|
11252
11300
|
}
|
|
11253
11301
|
async findEventByIdentity(identity) {
|
|
11254
11302
|
const events = await this.listEvents();
|
|
11255
|
-
return events
|
|
11303
|
+
return findEventByIdentity(events, identity);
|
|
11256
11304
|
}
|
|
11257
11305
|
async appendDelivery(result) {
|
|
11258
11306
|
await this.init();
|
|
@@ -11303,6 +11351,83 @@ class JsonEventsStore {
|
|
|
11303
11351
|
});
|
|
11304
11352
|
}
|
|
11305
11353
|
}
|
|
11354
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
11355
|
+
return {
|
|
11356
|
+
mode: "local-files",
|
|
11357
|
+
name: "json-events-store",
|
|
11358
|
+
remote: false,
|
|
11359
|
+
localFiles: true,
|
|
11360
|
+
localSqlite: false,
|
|
11361
|
+
postgres: false,
|
|
11362
|
+
s3: false,
|
|
11363
|
+
aws: false,
|
|
11364
|
+
durable: true,
|
|
11365
|
+
idempotency: "best-effort-local",
|
|
11366
|
+
replayCursors: true,
|
|
11367
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
11368
|
+
};
|
|
11369
|
+
}
|
|
11370
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
11371
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
11372
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
11373
|
+
const payload = {
|
|
11374
|
+
offset,
|
|
11375
|
+
eventId: options.eventId,
|
|
11376
|
+
source: options.source,
|
|
11377
|
+
type: options.type
|
|
11378
|
+
};
|
|
11379
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
11380
|
+
}
|
|
11381
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
11382
|
+
if (!cursor)
|
|
11383
|
+
return 0;
|
|
11384
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
11385
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11386
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
11387
|
+
let payload;
|
|
11388
|
+
try {
|
|
11389
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
11390
|
+
} catch {
|
|
11391
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11392
|
+
}
|
|
11393
|
+
const offset = payload.offset;
|
|
11394
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
11395
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11396
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
11397
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
11398
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
11399
|
+
return offset;
|
|
11400
|
+
}
|
|
11401
|
+
function normalizeEventPageLimit(limit) {
|
|
11402
|
+
if (limit === undefined)
|
|
11403
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
11404
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
11405
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
11406
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
11407
|
+
}
|
|
11408
|
+
function queryEvents(events, options) {
|
|
11409
|
+
let rows = events;
|
|
11410
|
+
if (options.eventId)
|
|
11411
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
11412
|
+
if (options.source)
|
|
11413
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
11414
|
+
if (options.type)
|
|
11415
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
11416
|
+
if (options.cursor) {
|
|
11417
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11418
|
+
rows = rows.slice(offset);
|
|
11419
|
+
}
|
|
11420
|
+
if (options.limit !== undefined)
|
|
11421
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
11422
|
+
return rows;
|
|
11423
|
+
}
|
|
11424
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
11425
|
+
if (cursorValue !== optionValue)
|
|
11426
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
11427
|
+
}
|
|
11428
|
+
function findEventByIdentity(events, identity) {
|
|
11429
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
11430
|
+
}
|
|
11306
11431
|
function buildSignatureBase(timestamp2, body) {
|
|
11307
11432
|
return `${timestamp2}.${body}`;
|
|
11308
11433
|
}
|
|
@@ -11316,21 +11441,27 @@ function now2() {
|
|
|
11316
11441
|
function truncate(value, max = 4096) {
|
|
11317
11442
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
11318
11443
|
}
|
|
11319
|
-
function buildWebhookRequest(event, channel) {
|
|
11444
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
11320
11445
|
if (!channel.webhook)
|
|
11321
11446
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
11447
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
11448
|
+
if (/^x-hasna-/i.test(name)) {
|
|
11449
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11322
11452
|
const body = JSON.stringify(event);
|
|
11323
|
-
const timestamp2 =
|
|
11453
|
+
const timestamp2 = options.timestamp ?? new Date().toISOString();
|
|
11324
11454
|
const headers = {
|
|
11325
11455
|
"Content-Type": "application/json",
|
|
11326
11456
|
"User-Agent": "@hasna/events",
|
|
11327
11457
|
"X-Hasna-Event-Id": event.id,
|
|
11328
11458
|
"X-Hasna-Event-Type": event.type,
|
|
11329
|
-
|
|
11330
|
-
|
|
11459
|
+
...channel.webhook.headers,
|
|
11460
|
+
"X-Hasna-Timestamp": timestamp2
|
|
11331
11461
|
};
|
|
11332
|
-
|
|
11333
|
-
|
|
11462
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
11463
|
+
if (secret) {
|
|
11464
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
|
|
11334
11465
|
}
|
|
11335
11466
|
return { body, headers };
|
|
11336
11467
|
}
|
|
@@ -11338,7 +11469,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
11338
11469
|
if (!channel.webhook)
|
|
11339
11470
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
11340
11471
|
const startedAt = now2();
|
|
11341
|
-
|
|
11472
|
+
let secret = channel.webhook.secret;
|
|
11473
|
+
if (channel.webhook.secretRef) {
|
|
11474
|
+
if (!options.secretResolver) {
|
|
11475
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
11476
|
+
}
|
|
11477
|
+
try {
|
|
11478
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
11479
|
+
} catch {
|
|
11480
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
11481
|
+
}
|
|
11482
|
+
if (!secret)
|
|
11483
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
11484
|
+
}
|
|
11485
|
+
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
11486
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
11342
11487
|
const controller = new AbortController;
|
|
11343
11488
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
11344
11489
|
try {
|
|
@@ -11370,6 +11515,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
11370
11515
|
clearTimeout(timeout);
|
|
11371
11516
|
}
|
|
11372
11517
|
}
|
|
11518
|
+
function failedAttempt(startedAt, error) {
|
|
11519
|
+
return {
|
|
11520
|
+
attempt: 1,
|
|
11521
|
+
status: "failed",
|
|
11522
|
+
startedAt,
|
|
11523
|
+
completedAt: now2(),
|
|
11524
|
+
error
|
|
11525
|
+
};
|
|
11526
|
+
}
|
|
11373
11527
|
async function dispatchCommand(event, channel) {
|
|
11374
11528
|
if (!channel.command)
|
|
11375
11529
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -11458,6 +11612,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
11458
11612
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
11459
11613
|
};
|
|
11460
11614
|
}
|
|
11615
|
+
|
|
11616
|
+
class EventTypeCatalog {
|
|
11617
|
+
definitions = new Map;
|
|
11618
|
+
register(definition) {
|
|
11619
|
+
this.definitions.set(definition.type, definition);
|
|
11620
|
+
return this;
|
|
11621
|
+
}
|
|
11622
|
+
unregister(type) {
|
|
11623
|
+
return this.definitions.delete(type);
|
|
11624
|
+
}
|
|
11625
|
+
has(type) {
|
|
11626
|
+
return this.definitions.has(type);
|
|
11627
|
+
}
|
|
11628
|
+
get(type) {
|
|
11629
|
+
return this.definitions.get(type);
|
|
11630
|
+
}
|
|
11631
|
+
list() {
|
|
11632
|
+
return [...this.definitions.values()];
|
|
11633
|
+
}
|
|
11634
|
+
validateEvent(event) {
|
|
11635
|
+
const definition = this.definitions.get(event.type);
|
|
11636
|
+
if (!definition)
|
|
11637
|
+
return { ok: true };
|
|
11638
|
+
return definition.validate(event.data, event);
|
|
11639
|
+
}
|
|
11640
|
+
assertEventValid(event) {
|
|
11641
|
+
const result = this.validateEvent(event);
|
|
11642
|
+
if (!result.ok) {
|
|
11643
|
+
throw new EventValidationError(event.type, result.issues);
|
|
11644
|
+
}
|
|
11645
|
+
}
|
|
11646
|
+
}
|
|
11647
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
11648
|
+
if (paths.length === 0)
|
|
11649
|
+
return event;
|
|
11650
|
+
const copy = structuredClone(event);
|
|
11651
|
+
for (const path of paths) {
|
|
11652
|
+
setPath(copy, path, replacement);
|
|
11653
|
+
}
|
|
11654
|
+
return copy;
|
|
11655
|
+
}
|
|
11656
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
11657
|
+
return redactValue2(event, replacement);
|
|
11658
|
+
}
|
|
11659
|
+
function shouldRedactKey(key) {
|
|
11660
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
11661
|
+
}
|
|
11662
|
+
function redactValue2(value, replacement) {
|
|
11663
|
+
if (Array.isArray(value))
|
|
11664
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
11665
|
+
if (!value || typeof value !== "object")
|
|
11666
|
+
return value;
|
|
11667
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
11668
|
+
key,
|
|
11669
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
11670
|
+
]));
|
|
11671
|
+
}
|
|
11672
|
+
function setPath(input, path, replacement) {
|
|
11673
|
+
const parts = path.split(".");
|
|
11674
|
+
let cursor = input;
|
|
11675
|
+
for (const part of parts.slice(0, -1)) {
|
|
11676
|
+
const next = cursor[part];
|
|
11677
|
+
if (!next || typeof next !== "object")
|
|
11678
|
+
return;
|
|
11679
|
+
cursor = next;
|
|
11680
|
+
}
|
|
11681
|
+
const last = parts.at(-1);
|
|
11682
|
+
if (last && last in cursor)
|
|
11683
|
+
cursor[last] = replacement;
|
|
11684
|
+
}
|
|
11461
11685
|
function createEvent(input) {
|
|
11462
11686
|
return {
|
|
11463
11687
|
id: input.id ?? randomUUID22(),
|
|
@@ -11478,10 +11702,18 @@ class EventsClient {
|
|
|
11478
11702
|
store;
|
|
11479
11703
|
redactors;
|
|
11480
11704
|
transportOptions;
|
|
11705
|
+
catalog;
|
|
11706
|
+
validateCatalogTypes;
|
|
11481
11707
|
constructor(options = {}) {
|
|
11482
11708
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
11483
11709
|
this.redactors = options.redactors ?? [];
|
|
11484
|
-
this.transportOptions = {
|
|
11710
|
+
this.transportOptions = {
|
|
11711
|
+
fetchImpl: options.fetchImpl,
|
|
11712
|
+
secretResolver: options.secretResolver,
|
|
11713
|
+
now: options.now
|
|
11714
|
+
};
|
|
11715
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
11716
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
11485
11717
|
}
|
|
11486
11718
|
async addChannel(input) {
|
|
11487
11719
|
const timestamp2 = new Date().toISOString();
|
|
@@ -11499,18 +11731,40 @@ class EventsClient {
|
|
|
11499
11731
|
}
|
|
11500
11732
|
async emit(input, options = {}) {
|
|
11501
11733
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
11502
|
-
if (options.
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
11510
|
-
return { event, deliveries, deduped: false };
|
|
11511
|
-
}
|
|
11512
|
-
async listEvents() {
|
|
11513
|
-
|
|
11734
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
11735
|
+
this.catalog.assertEventValid(event);
|
|
11736
|
+
}
|
|
11737
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
11738
|
+
if (append.deduped) {
|
|
11739
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
11740
|
+
}
|
|
11741
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
11742
|
+
return { event: append.event, deliveries, deduped: false };
|
|
11743
|
+
}
|
|
11744
|
+
async listEvents(options = {}) {
|
|
11745
|
+
if (Object.keys(options).length === 0)
|
|
11746
|
+
return this.store.listEvents();
|
|
11747
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
11748
|
+
}
|
|
11749
|
+
async listEventsPage(options = {}) {
|
|
11750
|
+
if (this.store.listEventsPage)
|
|
11751
|
+
return this.store.listEventsPage(options);
|
|
11752
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
11753
|
+
eventId: options.eventId,
|
|
11754
|
+
source: options.source,
|
|
11755
|
+
type: options.type
|
|
11756
|
+
});
|
|
11757
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11758
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
11759
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
11760
|
+
const nextOffset = offset + pageEvents.length;
|
|
11761
|
+
const hasMore = nextOffset < events.length;
|
|
11762
|
+
return {
|
|
11763
|
+
events: pageEvents,
|
|
11764
|
+
cursor: options.cursor,
|
|
11765
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
11766
|
+
hasMore
|
|
11767
|
+
};
|
|
11514
11768
|
}
|
|
11515
11769
|
async listDeliveries() {
|
|
11516
11770
|
return this.store.listDeliveries();
|
|
@@ -11578,22 +11832,37 @@ class EventsClient {
|
|
|
11578
11832
|
return result;
|
|
11579
11833
|
}
|
|
11580
11834
|
async replay(options = {}) {
|
|
11581
|
-
const
|
|
11582
|
-
if (options.eventId && event.id !== options.eventId)
|
|
11583
|
-
return false;
|
|
11584
|
-
if (options.source && event.source !== options.source)
|
|
11585
|
-
return false;
|
|
11586
|
-
if (options.type && event.type !== options.type)
|
|
11587
|
-
return false;
|
|
11588
|
-
return true;
|
|
11589
|
-
});
|
|
11835
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
11590
11836
|
if (options.dryRun)
|
|
11591
|
-
return { events, deliveries: [] };
|
|
11837
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
11592
11838
|
const deliveries = [];
|
|
11593
|
-
for (const event of events) {
|
|
11839
|
+
for (const event of page.events) {
|
|
11594
11840
|
deliveries.push(...await this.deliver(event));
|
|
11595
11841
|
}
|
|
11596
|
-
return { events, deliveries };
|
|
11842
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
11843
|
+
}
|
|
11844
|
+
async appendEvent(event, options) {
|
|
11845
|
+
if (this.store.appendEventOnce) {
|
|
11846
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
11847
|
+
}
|
|
11848
|
+
if (options.dedupe) {
|
|
11849
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
11850
|
+
if (existing) {
|
|
11851
|
+
return {
|
|
11852
|
+
event: existing,
|
|
11853
|
+
stored: false,
|
|
11854
|
+
deduped: true,
|
|
11855
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
11856
|
+
};
|
|
11857
|
+
}
|
|
11858
|
+
}
|
|
11859
|
+
const stored = await this.store.appendEvent(event);
|
|
11860
|
+
return {
|
|
11861
|
+
event: stored,
|
|
11862
|
+
stored: true,
|
|
11863
|
+
deduped: false,
|
|
11864
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
11865
|
+
};
|
|
11597
11866
|
}
|
|
11598
11867
|
async applyRedaction(event, channel) {
|
|
11599
11868
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -11620,43 +11889,19 @@ class EventsClient {
|
|
|
11620
11889
|
return createDeliveryResult(event, channel, attempts);
|
|
11621
11890
|
}
|
|
11622
11891
|
}
|
|
11623
|
-
function
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
11637
|
-
}
|
|
11638
|
-
function redactValue2(value, replacement) {
|
|
11639
|
-
if (Array.isArray(value))
|
|
11640
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
11641
|
-
if (!value || typeof value !== "object")
|
|
11642
|
-
return value;
|
|
11643
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
11644
|
-
key,
|
|
11645
|
-
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
11646
|
-
]));
|
|
11647
|
-
}
|
|
11648
|
-
function setPath(input, path, replacement) {
|
|
11649
|
-
const parts = path.split(".");
|
|
11650
|
-
let cursor = input;
|
|
11651
|
-
for (const part of parts.slice(0, -1)) {
|
|
11652
|
-
const next = cursor[part];
|
|
11653
|
-
if (!next || typeof next !== "object")
|
|
11654
|
-
return;
|
|
11655
|
-
cursor = next;
|
|
11656
|
-
}
|
|
11657
|
-
const last = parts.at(-1);
|
|
11658
|
-
if (last && last in cursor)
|
|
11659
|
-
cursor[last] = replacement;
|
|
11892
|
+
function queryClientEvents(events, options) {
|
|
11893
|
+
let rows = events;
|
|
11894
|
+
if (options.eventId)
|
|
11895
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
11896
|
+
if (options.source)
|
|
11897
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
11898
|
+
if (options.type)
|
|
11899
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
11900
|
+
if (options.cursor)
|
|
11901
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
11902
|
+
if (options.limit !== undefined)
|
|
11903
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
11904
|
+
return rows;
|
|
11660
11905
|
}
|
|
11661
11906
|
function normalizeTime(value) {
|
|
11662
11907
|
if (!value)
|
|
@@ -11670,9 +11915,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
11670
11915
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
11671
11916
|
};
|
|
11672
11917
|
}
|
|
11673
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
11918
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
11674
11919
|
var init_dist = __esm(() => {
|
|
11675
11920
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
11921
|
+
EventValidationError = class EventValidationError extends Error {
|
|
11922
|
+
eventType;
|
|
11923
|
+
issues;
|
|
11924
|
+
constructor(eventType, issues) {
|
|
11925
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
11926
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
11927
|
+
this.name = "EventValidationError";
|
|
11928
|
+
this.eventType = eventType;
|
|
11929
|
+
this.issues = issues;
|
|
11930
|
+
}
|
|
11931
|
+
};
|
|
11932
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
11933
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
11676
11934
|
});
|
|
11677
11935
|
|
|
11678
11936
|
// src/db/task-lists.ts
|
|
@@ -12511,8 +12769,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
12511
12769
|
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
12512
12770
|
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12513
12771
|
}
|
|
12514
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
12772
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
|
|
12515
12773
|
const respText = await resp.text().catch(() => "");
|
|
12774
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
12775
|
+
logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
|
|
12776
|
+
return;
|
|
12777
|
+
}
|
|
12516
12778
|
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
12517
12779
|
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
12518
12780
|
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
@@ -13289,7 +13551,7 @@ function getTaskGraph(taskId, direction = "both", db) {
|
|
|
13289
13551
|
const deps = getTaskDependencies(t.id, d);
|
|
13290
13552
|
const hasUnfinishedDeps = deps.some((dep) => {
|
|
13291
13553
|
const depTask = getTask(dep.depends_on, d);
|
|
13292
|
-
return depTask && depTask.status
|
|
13554
|
+
return depTask && isBlockingDependencyStatus(depTask.status);
|
|
13293
13555
|
});
|
|
13294
13556
|
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
13295
13557
|
}
|
|
@@ -13549,7 +13811,7 @@ function getBlockingDeps(id, db) {
|
|
|
13549
13811
|
const blocking = [];
|
|
13550
13812
|
for (const dep of deps) {
|
|
13551
13813
|
const task = getTask(dep.depends_on, d);
|
|
13552
|
-
if (task && task.status
|
|
13814
|
+
if (task && isBlockingDependencyStatus(task.status))
|
|
13553
13815
|
blocking.push(task);
|
|
13554
13816
|
}
|
|
13555
13817
|
return blocking;
|
|
@@ -13873,7 +14135,7 @@ function getNextTask(agentId, filters, db) {
|
|
|
13873
14135
|
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
13874
14136
|
params.push(...filters.tags);
|
|
13875
14137
|
}
|
|
13876
|
-
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status
|
|
14138
|
+
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
|
|
13877
14139
|
const where = conditions.join(" AND ");
|
|
13878
14140
|
let recentProjectIds = [];
|
|
13879
14141
|
const assignedAliasParams = [];
|
|
@@ -13917,7 +14179,7 @@ function getActiveWork(filters, db) {
|
|
|
13917
14179
|
}
|
|
13918
14180
|
function getTasksChangedSince(since, filters, db) {
|
|
13919
14181
|
const d = db || getDatabase();
|
|
13920
|
-
const conditions = ["updated_at > ?"];
|
|
14182
|
+
const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
|
|
13921
14183
|
const params = [since];
|
|
13922
14184
|
if (filters?.project_id) {
|
|
13923
14185
|
conditions.push("project_id = ?");
|
|
@@ -18867,6 +19129,60 @@ var init_assignee_context = __esm(() => {
|
|
|
18867
19129
|
init_assignee_validation();
|
|
18868
19130
|
});
|
|
18869
19131
|
|
|
19132
|
+
// src/lib/instant-compare.ts
|
|
19133
|
+
function isLeapYear(year) {
|
|
19134
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
19135
|
+
}
|
|
19136
|
+
function sqliteJulianDay(value) {
|
|
19137
|
+
const m = SQLITE_STAMP.exec(value);
|
|
19138
|
+
if (!m)
|
|
19139
|
+
return null;
|
|
19140
|
+
const year = Number(m[1]);
|
|
19141
|
+
const month = Number(m[2]);
|
|
19142
|
+
const day = Number(m[3]);
|
|
19143
|
+
const hour = m[4] === undefined ? 0 : Number(m[4]);
|
|
19144
|
+
const minute = m[5] === undefined ? 0 : Number(m[5]);
|
|
19145
|
+
const second = m[6] === undefined ? 0 : Number(m[6]);
|
|
19146
|
+
const frac = m[7];
|
|
19147
|
+
const sign = m[8];
|
|
19148
|
+
const offsetHour = m[9];
|
|
19149
|
+
const offsetMinute = m[10];
|
|
19150
|
+
if (month < 1 || month > 12)
|
|
19151
|
+
return null;
|
|
19152
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
19153
|
+
return null;
|
|
19154
|
+
const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
|
|
19155
|
+
if (day < 1 || day > maxDay)
|
|
19156
|
+
return null;
|
|
19157
|
+
let offsetMinutes = 0;
|
|
19158
|
+
if (sign !== undefined) {
|
|
19159
|
+
const oh = Number(offsetHour ?? "0");
|
|
19160
|
+
const om = Number(offsetMinute ?? "0");
|
|
19161
|
+
if (oh > 23 || om > 59)
|
|
19162
|
+
return null;
|
|
19163
|
+
offsetMinutes = oh * 60 + om;
|
|
19164
|
+
if (sign === "-")
|
|
19165
|
+
offsetMinutes = -offsetMinutes;
|
|
19166
|
+
}
|
|
19167
|
+
const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
|
|
19168
|
+
const ms = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
19169
|
+
return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
|
|
19170
|
+
}
|
|
19171
|
+
function changedSinceStampNewer(stamp, since) {
|
|
19172
|
+
const stampJd = sqliteJulianDay(stamp);
|
|
19173
|
+
if (stampJd === null)
|
|
19174
|
+
return true;
|
|
19175
|
+
const sinceJd = sqliteJulianDay(since);
|
|
19176
|
+
if (sinceJd === null)
|
|
19177
|
+
return false;
|
|
19178
|
+
return stampJd > sinceJd;
|
|
19179
|
+
}
|
|
19180
|
+
var SQLITE_STAMP, DAYS_IN_MONTH;
|
|
19181
|
+
var init_instant_compare = __esm(() => {
|
|
19182
|
+
SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
|
|
19183
|
+
DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
19184
|
+
});
|
|
19185
|
+
|
|
18870
19186
|
// src/lib/plan-project-link-contract.ts
|
|
18871
19187
|
import { createHash as createHash3 } from "crypto";
|
|
18872
19188
|
function canonicalPlanProjectLinkJson(value) {
|
|
@@ -21920,6 +22236,7 @@ var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabil
|
|
|
21920
22236
|
var init_cloud_router = __esm(() => {
|
|
21921
22237
|
init_types();
|
|
21922
22238
|
init_redaction();
|
|
22239
|
+
init_instant_compare();
|
|
21923
22240
|
init_plan_project_link_contract();
|
|
21924
22241
|
init_http_client();
|
|
21925
22242
|
init_adoption_validation();
|
|
@@ -34724,7 +35041,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
34724
35041
|
const blockers = d.query(`SELECT dep.id, dep.short_id, dep.title, dep.status
|
|
34725
35042
|
FROM task_dependencies td
|
|
34726
35043
|
JOIN tasks dep ON dep.id = td.depends_on
|
|
34727
|
-
WHERE td.task_id = ? AND dep.status
|
|
35044
|
+
WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).all(task.id);
|
|
34728
35045
|
return { id: task.id, short_id: task.short_id, title: redactEvidenceText(task.title), blockers };
|
|
34729
35046
|
}).filter((entry) => entry.blockers.length > 0);
|
|
34730
35047
|
const overdue = tasks.filter((task) => activeTaskIds.has(task.id) && Boolean(task.due_at && task.due_at < generatedAt)).map((task) => ({ id: task.id, short_id: task.short_id, title: redactEvidenceText(task.title), due_at: task.due_at }));
|
|
@@ -36047,7 +36364,7 @@ var package_default;
|
|
|
36047
36364
|
var init_package = __esm(() => {
|
|
36048
36365
|
package_default = {
|
|
36049
36366
|
name: "@hasna/todos",
|
|
36050
|
-
version: "0.15.
|
|
36367
|
+
version: "0.15.47",
|
|
36051
36368
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
36052
36369
|
type: "module",
|
|
36053
36370
|
main: "dist/index.js",
|
|
@@ -36165,18 +36482,19 @@ var init_package = __esm(() => {
|
|
|
36165
36482
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
36166
36483
|
license: "Apache-2.0",
|
|
36167
36484
|
dependencies: {
|
|
36168
|
-
"@hasna/contracts": "0.13.
|
|
36485
|
+
"@hasna/contracts": "0.13.4",
|
|
36169
36486
|
"@hasna/events": "^0.1.11",
|
|
36170
36487
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
36171
36488
|
chalk: "^5.4.1",
|
|
36172
36489
|
commander: "^13.1.0",
|
|
36173
36490
|
ink: "^5.2.0",
|
|
36174
36491
|
react: "^18.3.1",
|
|
36175
|
-
zod: "
|
|
36492
|
+
zod: "3.25.76"
|
|
36176
36493
|
},
|
|
36177
36494
|
overrides: {
|
|
36178
36495
|
ajv: "8.20.0",
|
|
36179
|
-
"fast-uri": "3.1.2"
|
|
36496
|
+
"fast-uri": "3.1.2",
|
|
36497
|
+
zod: "3.25.76"
|
|
36180
36498
|
},
|
|
36181
36499
|
devDependencies: {
|
|
36182
36500
|
"@types/bun": "^1.2.4",
|
|
@@ -46241,7 +46559,7 @@ function matchesExtraFilters(task2, filter) {
|
|
|
46241
46559
|
}
|
|
46242
46560
|
if (filter.tags?.length) {
|
|
46243
46561
|
const taskTags = new Set(task2.tags ?? []);
|
|
46244
|
-
if (!filter.tags.
|
|
46562
|
+
if (!filter.tags.some((tag) => taskTags.has(tag)))
|
|
46245
46563
|
return false;
|
|
46246
46564
|
}
|
|
46247
46565
|
if (filter.include_subtasks !== true && filter.parent_id === undefined && task2.parent_id)
|
|
@@ -47433,11 +47751,18 @@ class PostgresJsonRecordStore {
|
|
|
47433
47751
|
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
47434
47752
|
}
|
|
47435
47753
|
async ensureSchema() {
|
|
47436
|
-
this.schemaReady
|
|
47437
|
-
|
|
47438
|
-
await
|
|
47439
|
-
|
|
47440
|
-
|
|
47754
|
+
if (!this.schemaReady) {
|
|
47755
|
+
this.schemaReady = (async () => {
|
|
47756
|
+
await retryOnTransientPostgresError(async () => {
|
|
47757
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
47758
|
+
await this.options.client.query(sql);
|
|
47759
|
+
}
|
|
47760
|
+
});
|
|
47761
|
+
})().catch((error) => {
|
|
47762
|
+
this.schemaReady = null;
|
|
47763
|
+
throw error;
|
|
47764
|
+
});
|
|
47765
|
+
}
|
|
47441
47766
|
await this.schemaReady;
|
|
47442
47767
|
}
|
|
47443
47768
|
async get(type, id) {
|
|
@@ -47846,13 +48171,14 @@ class PostgresJsonRecordStore {
|
|
|
47846
48171
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47847
48172
|
}
|
|
47848
48173
|
async withTaskParentIntegrityTransaction(fn) {
|
|
47849
|
-
|
|
48174
|
+
const transaction = this.options.client.transaction;
|
|
48175
|
+
if (typeof transaction !== "function") {
|
|
47850
48176
|
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
47851
48177
|
}
|
|
47852
|
-
return
|
|
48178
|
+
return retryOnTransientPostgresError(() => transaction(async (client) => {
|
|
47853
48179
|
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
47854
48180
|
return fn(client);
|
|
47855
|
-
});
|
|
48181
|
+
}));
|
|
47856
48182
|
}
|
|
47857
48183
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
47858
48184
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
@@ -49199,7 +49525,7 @@ async function getActiveWork2(filters, store) {
|
|
|
49199
49525
|
}));
|
|
49200
49526
|
}
|
|
49201
49527
|
async function getChangedSince(since, filters, store) {
|
|
49202
|
-
return (await listTasks3(filters ?? {}, store)).filter((task2) => task2.updated_at
|
|
49528
|
+
return (await listTasks3(filters ?? {}, store)).filter((task2) => changedSinceStampNewer(task2.updated_at ?? "", since));
|
|
49203
49529
|
}
|
|
49204
49530
|
async function createProject2(input, store, context) {
|
|
49205
49531
|
const timestamp4 = new Date().toISOString();
|
|
@@ -49735,6 +50061,35 @@ function compareClock(left, right) {
|
|
|
49735
50061
|
function numberValue3(value) {
|
|
49736
50062
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
49737
50063
|
}
|
|
50064
|
+
function isTransientPostgresError(error) {
|
|
50065
|
+
if (typeof error !== "object" || error === null)
|
|
50066
|
+
return false;
|
|
50067
|
+
const candidate = error;
|
|
50068
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
50069
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
50070
|
+
const cause = candidate.cause;
|
|
50071
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
50072
|
+
}
|
|
50073
|
+
if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
|
|
50074
|
+
return true;
|
|
50075
|
+
}
|
|
50076
|
+
const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
|
|
50077
|
+
return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
|
|
50078
|
+
}
|
|
50079
|
+
async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
50080
|
+
let lastError;
|
|
50081
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
50082
|
+
try {
|
|
50083
|
+
return await fn();
|
|
50084
|
+
} catch (error) {
|
|
50085
|
+
lastError = error;
|
|
50086
|
+
if (!isTransientPostgresError(error) || attempt === attempts)
|
|
50087
|
+
throw error;
|
|
50088
|
+
await new Promise((resolve16) => setTimeout(resolve16, delayMs * attempt));
|
|
50089
|
+
}
|
|
50090
|
+
}
|
|
50091
|
+
throw lastError;
|
|
50092
|
+
}
|
|
49738
50093
|
function isPostgresUniqueViolation(error) {
|
|
49739
50094
|
if (typeof error !== "object" || error === null)
|
|
49740
50095
|
return false;
|
|
@@ -49754,10 +50109,11 @@ function postgresConstraintName(error) {
|
|
|
49754
50109
|
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
49755
50110
|
return typeof constraint === "string" ? constraint : "";
|
|
49756
50111
|
}
|
|
49757
|
-
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY;
|
|
50112
|
+
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY, TRANSIENT_POSTGRES_SQLSTATES, TRANSIENT_POSTGRES_MESSAGE_MARKERS;
|
|
49758
50113
|
var init_postgres_adapter = __esm(() => {
|
|
49759
50114
|
init_types();
|
|
49760
50115
|
init_creator_identity();
|
|
50116
|
+
init_instant_compare();
|
|
49761
50117
|
init_plan_project_link_contract();
|
|
49762
50118
|
init_stale_lock_handoff();
|
|
49763
50119
|
init_postgres_sync();
|
|
@@ -49767,6 +50123,12 @@ var init_postgres_adapter = __esm(() => {
|
|
|
49767
50123
|
init_audit_history_import();
|
|
49768
50124
|
init_canonical();
|
|
49769
50125
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
50126
|
+
TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
|
|
50127
|
+
TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
|
|
50128
|
+
"canceling statement due to lock timeout",
|
|
50129
|
+
"deadlock detected",
|
|
50130
|
+
"could not serialize access"
|
|
50131
|
+
];
|
|
49770
50132
|
});
|
|
49771
50133
|
|
|
49772
50134
|
// src/pr-groups/postgres.ts
|
|
@@ -50188,10 +50550,19 @@ class PostgresPrGroupLedgerPersistence {
|
|
|
50188
50550
|
this.client = client;
|
|
50189
50551
|
}
|
|
50190
50552
|
async ensureSchema() {
|
|
50191
|
-
this.schemaReady
|
|
50192
|
-
|
|
50193
|
-
|
|
50194
|
-
|
|
50553
|
+
if (this.schemaReady === null) {
|
|
50554
|
+
const attempt = (async () => {
|
|
50555
|
+
for (const statement of postgresPrGroupSchemaSql())
|
|
50556
|
+
await this.client.query(statement);
|
|
50557
|
+
})();
|
|
50558
|
+
this.schemaReady = attempt;
|
|
50559
|
+
try {
|
|
50560
|
+
await attempt;
|
|
50561
|
+
} catch (error) {
|
|
50562
|
+
this.schemaReady = null;
|
|
50563
|
+
throw error;
|
|
50564
|
+
}
|
|
50565
|
+
}
|
|
50195
50566
|
return this.schemaReady;
|
|
50196
50567
|
}
|
|
50197
50568
|
async transaction(fn) {
|
|
@@ -50609,14 +50980,23 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
50609
50980
|
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
50610
50981
|
}
|
|
50611
50982
|
async ensureSchema() {
|
|
50612
|
-
this.schemaReady
|
|
50613
|
-
|
|
50614
|
-
|
|
50615
|
-
|
|
50616
|
-
|
|
50617
|
-
|
|
50983
|
+
if (this.schemaReady === null) {
|
|
50984
|
+
const attempt = (async () => {
|
|
50985
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
50986
|
+
await this.client.query(statement);
|
|
50987
|
+
}
|
|
50988
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
50989
|
+
await this.client.query(statement);
|
|
50990
|
+
}
|
|
50991
|
+
})();
|
|
50992
|
+
this.schemaReady = attempt;
|
|
50993
|
+
try {
|
|
50994
|
+
await attempt;
|
|
50995
|
+
} catch (error) {
|
|
50996
|
+
this.schemaReady = null;
|
|
50997
|
+
throw error;
|
|
50618
50998
|
}
|
|
50619
|
-
}
|
|
50999
|
+
}
|
|
50620
51000
|
await this.schemaReady;
|
|
50621
51001
|
}
|
|
50622
51002
|
async transaction(fn) {
|
|
@@ -53326,12 +53706,21 @@ class PostgresTodosTaskManifestBackend {
|
|
|
53326
53706
|
this.tenantId = options.tenantId ?? "default";
|
|
53327
53707
|
}
|
|
53328
53708
|
async ensureSchema() {
|
|
53329
|
-
this.schemaReady
|
|
53330
|
-
|
|
53331
|
-
|
|
53332
|
-
|
|
53333
|
-
|
|
53334
|
-
|
|
53709
|
+
if (this.schemaReady === null) {
|
|
53710
|
+
const attempt = (async () => {
|
|
53711
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
53712
|
+
await this.client.query(sql);
|
|
53713
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
53714
|
+
await this.client.query(sql);
|
|
53715
|
+
})();
|
|
53716
|
+
this.schemaReady = attempt;
|
|
53717
|
+
try {
|
|
53718
|
+
await attempt;
|
|
53719
|
+
} catch (error) {
|
|
53720
|
+
this.schemaReady = null;
|
|
53721
|
+
throw error;
|
|
53722
|
+
}
|
|
53723
|
+
}
|
|
53335
53724
|
await this.schemaReady;
|
|
53336
53725
|
}
|
|
53337
53726
|
async insertSync(tx, objectType2, objectId, payload, now4) {
|
|
@@ -54631,12 +55020,21 @@ class PostgresTodosTaskSubtreeTransferBackend {
|
|
|
54631
55020
|
this.tenantId = options.tenantId ?? "default";
|
|
54632
55021
|
}
|
|
54633
55022
|
async ensureSchema() {
|
|
54634
|
-
this.schemaReady
|
|
54635
|
-
|
|
54636
|
-
|
|
54637
|
-
|
|
54638
|
-
|
|
54639
|
-
|
|
55023
|
+
if (this.schemaReady === null) {
|
|
55024
|
+
const attempt = (async () => {
|
|
55025
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
55026
|
+
await this.client.query(sql);
|
|
55027
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
55028
|
+
await this.client.query(sql);
|
|
55029
|
+
})();
|
|
55030
|
+
this.schemaReady = attempt;
|
|
55031
|
+
try {
|
|
55032
|
+
await attempt;
|
|
55033
|
+
} catch (error) {
|
|
55034
|
+
this.schemaReady = null;
|
|
55035
|
+
throw error;
|
|
55036
|
+
}
|
|
55037
|
+
}
|
|
54640
55038
|
await this.schemaReady;
|
|
54641
55039
|
}
|
|
54642
55040
|
async snapshot(client, input, forUpdate = false) {
|
|
@@ -55723,7 +56121,10 @@ async function ensureCloudSchema() {
|
|
|
55723
56121
|
await client.query(sql);
|
|
55724
56122
|
}
|
|
55725
56123
|
await getApiKeyStore().ensureSchema();
|
|
55726
|
-
})()
|
|
56124
|
+
})().catch((error) => {
|
|
56125
|
+
schemaEnsured = null;
|
|
56126
|
+
throw error;
|
|
56127
|
+
});
|
|
55727
56128
|
return schemaEnsured;
|
|
55728
56129
|
}
|
|
55729
56130
|
async function ensureCloudCommentCursorIndex() {
|