@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.
Files changed (45) hide show
  1. package/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
  2. package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/dist/cli/cloud-router.d.ts.map +1 -1
  5. package/dist/cli/components/Dashboard.d.ts +1 -1
  6. package/dist/cli/components/Dashboard.d.ts.map +1 -1
  7. package/dist/cli/components/Header.d.ts +1 -1
  8. package/dist/cli/components/Header.d.ts.map +1 -1
  9. package/dist/cli/components/ProjectList.d.ts +1 -1
  10. package/dist/cli/components/ProjectList.d.ts.map +1 -1
  11. package/dist/cli/components/SearchView.d.ts +1 -1
  12. package/dist/cli/components/SearchView.d.ts.map +1 -1
  13. package/dist/cli/components/TaskDetail.d.ts +1 -1
  14. package/dist/cli/components/TaskDetail.d.ts.map +1 -1
  15. package/dist/cli/components/TaskForm.d.ts +1 -1
  16. package/dist/cli/components/TaskForm.d.ts.map +1 -1
  17. package/dist/cli/components/TaskList.d.ts +1 -1
  18. package/dist/cli/components/TaskList.d.ts.map +1 -1
  19. package/dist/cli/index.js +521 -120
  20. package/dist/contracts.js +346 -83
  21. package/dist/db/task-graph.d.ts.map +1 -1
  22. package/dist/db/task-lifecycle.d.ts.map +1 -1
  23. package/dist/db/webhooks.d.ts.map +1 -1
  24. package/dist/index.js +499 -113
  25. package/dist/lib/instant-compare.d.ts +35 -0
  26. package/dist/lib/instant-compare.d.ts.map +1 -0
  27. package/dist/mcp/index.js +520 -119
  28. package/dist/mcp.js +5 -4
  29. package/dist/pr-groups/postgres.d.ts.map +1 -1
  30. package/dist/project-registration/postgres.d.ts.map +1 -1
  31. package/dist/project-registration.js +466 -100
  32. package/dist/registry.js +346 -83
  33. package/dist/release-provenance.json +5 -5
  34. package/dist/server/cloud.d.ts.map +1 -1
  35. package/dist/server/index.js +1179 -309
  36. package/dist/storage/local-sqlite.d.ts.map +1 -1
  37. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  38. package/dist/storage.js +447 -89
  39. package/dist/task-manifest/postgres.d.ts.map +1 -1
  40. package/dist/task-manifest.js +15 -6
  41. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -1
  42. package/dist/task-subtree-transfer.js +15 -6
  43. package/package.json +5 -4
  44. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  45. 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.11/node_modules/@hasna/events/dist/index.js
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 listEvents() {
11252
+ async appendEventOnce(event, options = {}) {
11250
11253
  await this.init();
11251
- return this.readJson(this.eventsPath, []);
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.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
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 = event.time;
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
- "X-Hasna-Timestamp": timestamp2,
11330
- ...channel.webhook.headers
11459
+ ...channel.webhook.headers,
11460
+ "X-Hasna-Timestamp": timestamp2
11331
11461
  };
11332
- if (channel.webhook.secret) {
11333
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
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
- const { body, headers } = buildWebhookRequest(event, channel);
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 = { fetchImpl: options.fetchImpl };
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.dedupe !== false) {
11503
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
11504
- if (existing) {
11505
- return { event: existing, deliveries: [], deduped: true };
11506
- }
11507
- }
11508
- await this.store.appendEvent(event);
11509
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
11510
- return { event, deliveries, deduped: false };
11511
- }
11512
- async listEvents() {
11513
- return this.store.listEvents();
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 events = (await this.store.listEvents()).filter((event) => {
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 redactPaths(event, paths, replacement = "[REDACTED]") {
11624
- if (paths.length === 0)
11625
- return event;
11626
- const copy = structuredClone(event);
11627
- for (const path of paths) {
11628
- setPath(copy, path, replacement);
11629
- }
11630
- return copy;
11631
- }
11632
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
11633
- return redactValue2(event, replacement);
11634
- }
11635
- function shouldRedactKey(key) {
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 !== "completed";
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 !== "completed")
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 != 'completed')");
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 != 'completed'`).all(task.id);
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.41",
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.3",
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: "^3.24.2"
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.every((tag) => taskTags.has(tag)))
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 ??= (async () => {
47437
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
47438
- await this.options.client.query(sql);
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
- if (typeof this.options.client.transaction !== "function") {
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 this.options.client.transaction(async (client) => {
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 > since);
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 ??= (async () => {
50192
- for (const statement of postgresPrGroupSchemaSql())
50193
- await this.client.query(statement);
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 ??= (async () => {
50613
- for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
50614
- await this.client.query(statement);
50615
- }
50616
- for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
50617
- await this.client.query(statement);
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 ??= (async () => {
53330
- for (const sql of postgresTodosSyncSchemaSql(this.tableName))
53331
- await this.client.query(sql);
53332
- for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
53333
- await this.client.query(sql);
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 ??= (async () => {
54635
- for (const sql of postgresTodosSyncSchemaSql(this.tableName))
54636
- await this.client.query(sql);
54637
- for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
54638
- await this.client.query(sql);
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() {