@hasna/todos 0.15.46 → 0.15.49

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 (58) 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/commands/query-commands.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 +671 -155
  20. package/dist/contracts.js +396 -82
  21. package/dist/db/task-crud.d.ts +6 -0
  22. package/dist/db/task-crud.d.ts.map +1 -1
  23. package/dist/db/tasks.d.ts +1 -1
  24. package/dist/db/tasks.d.ts.map +1 -1
  25. package/dist/db/webhooks.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +561 -143
  29. package/dist/lib/dedupe-projection.d.ts +48 -0
  30. package/dist/lib/dedupe-projection.d.ts.map +1 -0
  31. package/dist/lib/redaction.d.ts.map +1 -1
  32. package/dist/lib/task-dedupe.d.ts +5 -9
  33. package/dist/lib/task-dedupe.d.ts.map +1 -1
  34. package/dist/mcp/index.js +621 -154
  35. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  36. package/dist/mcp.js +6 -4
  37. package/dist/project-registration.js +502 -141
  38. package/dist/registry.js +396 -82
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/server/cloud.d.ts.map +1 -1
  41. package/dist/server/index.js +1278 -342
  42. package/dist/server/routes.d.ts.map +1 -1
  43. package/dist/server/v1.d.ts.map +1 -1
  44. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  45. package/dist/storage/postgres-errors.d.ts +15 -0
  46. package/dist/storage/postgres-errors.d.ts.map +1 -0
  47. package/dist/storage/postgres-sync.d.ts +7 -0
  48. package/dist/storage/postgres-sync.d.ts.map +1 -1
  49. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  50. package/dist/storage/shadow.d.ts.map +1 -1
  51. package/dist/storage.js +510 -137
  52. package/dist/task-manifest.js +118 -47
  53. package/dist/task-subtree-transfer.js +326 -44
  54. package/dist/types/index.d.ts +2 -2
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/package.json +6 -4
  57. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  58. package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.46",
2126
+ version: "0.15.49",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2187,6 +2187,7 @@ var init_package = __esm(() => {
2187
2187
  ],
2188
2188
  scripts: {
2189
2189
  build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
2190
+ "build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
2190
2191
  "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
2191
2192
  migrate: "bun run src/server/index.ts migrate",
2192
2193
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
@@ -2241,18 +2242,19 @@ var init_package = __esm(() => {
2241
2242
  author: "Andrei Hasna <andrei@hasna.com>",
2242
2243
  license: "Apache-2.0",
2243
2244
  dependencies: {
2244
- "@hasna/contracts": "0.13.4",
2245
+ "@hasna/contracts": "0.14.0",
2245
2246
  "@hasna/events": "^0.1.11",
2246
2247
  "@modelcontextprotocol/sdk": "^1.12.1",
2247
2248
  chalk: "^5.4.1",
2248
2249
  commander: "^13.1.0",
2249
2250
  ink: "^5.2.0",
2250
2251
  react: "^18.3.1",
2251
- zod: "^3.24.2"
2252
+ zod: "3.25.76"
2252
2253
  },
2253
2254
  overrides: {
2254
2255
  ajv: "8.20.0",
2255
- "fast-uri": "3.1.2"
2256
+ "fast-uri": "3.1.2",
2257
+ zod: "3.25.76"
2256
2258
  },
2257
2259
  devDependencies: {
2258
2260
  "@types/bun": "^1.2.4",
@@ -2850,13 +2852,15 @@ function upsertSecretSafetyConfig(input) {
2850
2852
  saveConfig({ ...config, secret_safety: next });
2851
2853
  return next;
2852
2854
  }
2853
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
2855
+ var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
2854
2856
  var init_redaction = __esm(() => {
2855
2857
  init_config();
2858
+ XAI_PREFIX = ["x", "ai", "-"].join("");
2856
2859
  DEFAULT_SECRET_PATTERNS = [
2857
2860
  { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
2858
2861
  { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
2859
2862
  { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
2863
+ { name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
2860
2864
  { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
2861
2865
  { name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
2862
2866
  { name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
@@ -14578,8 +14582,9 @@ var init_event_hooks = __esm(() => {
14578
14582
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
14579
14583
  });
14580
14584
 
14581
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
14585
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
14582
14586
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
14587
+ import { Buffer as Buffer2 } from "buffer";
14583
14588
  import { existsSync as existsSync6 } from "fs";
14584
14589
  import { homedir as homedir2 } from "os";
14585
14590
  import { join as join5 } from "path";
@@ -14688,11 +14693,13 @@ function getEventsDataDir(override) {
14688
14693
 
14689
14694
  class JsonEventsStore {
14690
14695
  dataDir;
14696
+ runtime;
14691
14697
  channelsPath;
14692
14698
  eventsPath;
14693
14699
  deliveriesPath;
14694
14700
  constructor(dataDir = getEventsDataDir()) {
14695
14701
  this.dataDir = dataDir;
14702
+ this.runtime = localJsonRuntime(dataDir);
14696
14703
  this.channelsPath = join5(dataDir, "channels.json");
14697
14704
  this.eventsPath = join5(dataDir, "events.json");
14698
14705
  this.deliveriesPath = join5(dataDir, "deliveries.json");
@@ -14740,13 +14747,58 @@ class JsonEventsStore {
14740
14747
  await this.writeJson(this.eventsPath, events);
14741
14748
  return event;
14742
14749
  }
14743
- async listEvents() {
14750
+ async appendEventOnce(event, options = {}) {
14744
14751
  await this.init();
14745
- return this.readJson(this.eventsPath, []);
14752
+ const events = await this.readJson(this.eventsPath, []);
14753
+ const dedupe2 = options.dedupe !== false;
14754
+ if (dedupe2) {
14755
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
14756
+ if (existing) {
14757
+ return {
14758
+ event: existing,
14759
+ stored: false,
14760
+ deduped: true,
14761
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
14762
+ };
14763
+ }
14764
+ }
14765
+ events.push(event);
14766
+ await this.writeJson(this.eventsPath, events);
14767
+ return {
14768
+ event,
14769
+ stored: true,
14770
+ deduped: false,
14771
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
14772
+ };
14773
+ }
14774
+ async listEvents(options = {}) {
14775
+ await this.init();
14776
+ const events = await this.readJson(this.eventsPath, []);
14777
+ return queryEvents(events, options);
14778
+ }
14779
+ async listEventsPage(options = {}) {
14780
+ await this.init();
14781
+ const events = await this.readJson(this.eventsPath, []);
14782
+ const queried = queryEvents(events, {
14783
+ eventId: options.eventId,
14784
+ source: options.source,
14785
+ type: options.type
14786
+ });
14787
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14788
+ const limit = normalizeEventPageLimit(options.limit);
14789
+ const pageEvents = queried.slice(offset, offset + limit);
14790
+ const nextOffset = offset + pageEvents.length;
14791
+ const hasMore = nextOffset < queried.length;
14792
+ return {
14793
+ events: pageEvents,
14794
+ cursor: options.cursor,
14795
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
14796
+ hasMore
14797
+ };
14746
14798
  }
14747
14799
  async findEventByIdentity(identity) {
14748
14800
  const events = await this.listEvents();
14749
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14801
+ return findEventByIdentity(events, identity);
14750
14802
  }
14751
14803
  async appendDelivery(result) {
14752
14804
  await this.init();
@@ -14797,6 +14849,83 @@ class JsonEventsStore {
14797
14849
  });
14798
14850
  }
14799
14851
  }
14852
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
14853
+ return {
14854
+ mode: "local-files",
14855
+ name: "json-events-store",
14856
+ remote: false,
14857
+ localFiles: true,
14858
+ localSqlite: false,
14859
+ postgres: false,
14860
+ s3: false,
14861
+ aws: false,
14862
+ durable: true,
14863
+ idempotency: "best-effort-local",
14864
+ replayCursors: true,
14865
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
14866
+ };
14867
+ }
14868
+ function encodeLocalJsonEventCursor(offset, options = {}) {
14869
+ if (!Number.isInteger(offset) || offset < 0)
14870
+ throw new Error(`Invalid event cursor offset: ${offset}`);
14871
+ const payload = {
14872
+ offset,
14873
+ eventId: options.eventId,
14874
+ source: options.source,
14875
+ type: options.type
14876
+ };
14877
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
14878
+ }
14879
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
14880
+ if (!cursor)
14881
+ return 0;
14882
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
14883
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14884
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
14885
+ let payload;
14886
+ try {
14887
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
14888
+ } catch {
14889
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14890
+ }
14891
+ const offset = payload.offset;
14892
+ if (!Number.isInteger(offset) || offset < 0)
14893
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14894
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
14895
+ assertCursorFilter("source", payload.source, options.source);
14896
+ assertCursorFilter("type", payload.type, options.type);
14897
+ return offset;
14898
+ }
14899
+ function normalizeEventPageLimit(limit) {
14900
+ if (limit === undefined)
14901
+ return DEFAULT_EVENT_PAGE_LIMIT;
14902
+ if (!Number.isInteger(limit) || limit < 1)
14903
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
14904
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
14905
+ }
14906
+ function queryEvents(events, options) {
14907
+ let rows = events;
14908
+ if (options.eventId)
14909
+ rows = rows.filter((event) => event.id === options.eventId);
14910
+ if (options.source)
14911
+ rows = rows.filter((event) => event.source === options.source);
14912
+ if (options.type)
14913
+ rows = rows.filter((event) => event.type === options.type);
14914
+ if (options.cursor) {
14915
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14916
+ rows = rows.slice(offset);
14917
+ }
14918
+ if (options.limit !== undefined)
14919
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
14920
+ return rows;
14921
+ }
14922
+ function assertCursorFilter(name, cursorValue, optionValue) {
14923
+ if (cursorValue !== optionValue)
14924
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
14925
+ }
14926
+ function findEventByIdentity(events, identity) {
14927
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14928
+ }
14800
14929
  function buildSignatureBase(timestamp2, body) {
14801
14930
  return `${timestamp2}.${body}`;
14802
14931
  }
@@ -14810,21 +14939,27 @@ function now2() {
14810
14939
  function truncate(value, max = 4096) {
14811
14940
  return value.length > max ? `${value.slice(0, max)}...` : value;
14812
14941
  }
14813
- function buildWebhookRequest(event, channel) {
14942
+ function buildWebhookRequest(event, channel, options = {}) {
14814
14943
  if (!channel.webhook)
14815
14944
  throw new Error(`Channel ${channel.id} has no webhook config`);
14945
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
14946
+ if (/^x-hasna-/i.test(name)) {
14947
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
14948
+ }
14949
+ }
14816
14950
  const body = JSON.stringify(event);
14817
- const timestamp2 = event.time;
14951
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
14818
14952
  const headers = {
14819
14953
  "Content-Type": "application/json",
14820
14954
  "User-Agent": "@hasna/events",
14821
14955
  "X-Hasna-Event-Id": event.id,
14822
14956
  "X-Hasna-Event-Type": event.type,
14823
- "X-Hasna-Timestamp": timestamp2,
14824
- ...channel.webhook.headers
14957
+ ...channel.webhook.headers,
14958
+ "X-Hasna-Timestamp": timestamp2
14825
14959
  };
14826
- if (channel.webhook.secret) {
14827
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
14960
+ const secret = options.secret ?? channel.webhook.secret;
14961
+ if (secret) {
14962
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
14828
14963
  }
14829
14964
  return { body, headers };
14830
14965
  }
@@ -14832,7 +14967,21 @@ async function dispatchWebhook(event, channel, options = {}) {
14832
14967
  if (!channel.webhook)
14833
14968
  throw new Error(`Channel ${channel.id} has no webhook config`);
14834
14969
  const startedAt = now2();
14835
- const { body, headers } = buildWebhookRequest(event, channel);
14970
+ let secret = channel.webhook.secret;
14971
+ if (channel.webhook.secretRef) {
14972
+ if (!options.secretResolver) {
14973
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
14974
+ }
14975
+ try {
14976
+ secret = await options.secretResolver(channel.webhook.secretRef);
14977
+ } catch {
14978
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14979
+ }
14980
+ if (!secret)
14981
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14982
+ }
14983
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
14984
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
14836
14985
  const controller = new AbortController;
14837
14986
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
14838
14987
  try {
@@ -14864,6 +15013,15 @@ async function dispatchWebhook(event, channel, options = {}) {
14864
15013
  clearTimeout(timeout);
14865
15014
  }
14866
15015
  }
15016
+ function failedAttempt(startedAt, error) {
15017
+ return {
15018
+ attempt: 1,
15019
+ status: "failed",
15020
+ startedAt,
15021
+ completedAt: now2(),
15022
+ error
15023
+ };
15024
+ }
14867
15025
  async function dispatchCommand(event, channel) {
14868
15026
  if (!channel.command)
14869
15027
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -14952,6 +15110,76 @@ function createDeliveryResult(event, channel, attempts) {
14952
15110
  completedAt: attempts.at(-1)?.completedAt ?? now2()
14953
15111
  };
14954
15112
  }
15113
+
15114
+ class EventTypeCatalog {
15115
+ definitions = new Map;
15116
+ register(definition) {
15117
+ this.definitions.set(definition.type, definition);
15118
+ return this;
15119
+ }
15120
+ unregister(type) {
15121
+ return this.definitions.delete(type);
15122
+ }
15123
+ has(type) {
15124
+ return this.definitions.has(type);
15125
+ }
15126
+ get(type) {
15127
+ return this.definitions.get(type);
15128
+ }
15129
+ list() {
15130
+ return [...this.definitions.values()];
15131
+ }
15132
+ validateEvent(event) {
15133
+ const definition = this.definitions.get(event.type);
15134
+ if (!definition)
15135
+ return { ok: true };
15136
+ return definition.validate(event.data, event);
15137
+ }
15138
+ assertEventValid(event) {
15139
+ const result = this.validateEvent(event);
15140
+ if (!result.ok) {
15141
+ throw new EventValidationError(event.type, result.issues);
15142
+ }
15143
+ }
15144
+ }
15145
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
15146
+ if (paths.length === 0)
15147
+ return event;
15148
+ const copy = structuredClone(event);
15149
+ for (const path of paths) {
15150
+ setPath(copy, path, replacement);
15151
+ }
15152
+ return copy;
15153
+ }
15154
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15155
+ return redactValue2(event, replacement);
15156
+ }
15157
+ function shouldRedactKey(key) {
15158
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
15159
+ }
15160
+ function redactValue2(value, replacement) {
15161
+ if (Array.isArray(value))
15162
+ return value.map((item) => redactValue2(item, replacement));
15163
+ if (!value || typeof value !== "object")
15164
+ return value;
15165
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15166
+ key,
15167
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15168
+ ]));
15169
+ }
15170
+ function setPath(input, path, replacement) {
15171
+ const parts = path.split(".");
15172
+ let cursor = input;
15173
+ for (const part of parts.slice(0, -1)) {
15174
+ const next = cursor[part];
15175
+ if (!next || typeof next !== "object")
15176
+ return;
15177
+ cursor = next;
15178
+ }
15179
+ const last = parts.at(-1);
15180
+ if (last && last in cursor)
15181
+ cursor[last] = replacement;
15182
+ }
14955
15183
  function createEvent(input) {
14956
15184
  return {
14957
15185
  id: input.id ?? randomUUID22(),
@@ -14972,10 +15200,18 @@ class EventsClient {
14972
15200
  store;
14973
15201
  redactors;
14974
15202
  transportOptions;
15203
+ catalog;
15204
+ validateCatalogTypes;
14975
15205
  constructor(options = {}) {
14976
15206
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
14977
15207
  this.redactors = options.redactors ?? [];
14978
- this.transportOptions = { fetchImpl: options.fetchImpl };
15208
+ this.transportOptions = {
15209
+ fetchImpl: options.fetchImpl,
15210
+ secretResolver: options.secretResolver,
15211
+ now: options.now
15212
+ };
15213
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
15214
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
14979
15215
  }
14980
15216
  async addChannel(input) {
14981
15217
  const timestamp2 = new Date().toISOString();
@@ -14993,18 +15229,40 @@ class EventsClient {
14993
15229
  }
14994
15230
  async emit(input, options = {}) {
14995
15231
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
14996
- if (options.dedupe !== false) {
14997
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
14998
- if (existing) {
14999
- return { event: existing, deliveries: [], deduped: true };
15000
- }
15001
- }
15002
- await this.store.appendEvent(event);
15003
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
15004
- return { event, deliveries, deduped: false };
15005
- }
15006
- async listEvents() {
15007
- return this.store.listEvents();
15232
+ if (options.validate ?? this.validateCatalogTypes) {
15233
+ this.catalog.assertEventValid(event);
15234
+ }
15235
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
15236
+ if (append.deduped) {
15237
+ return { event: append.event, deliveries: [], deduped: true };
15238
+ }
15239
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
15240
+ return { event: append.event, deliveries, deduped: false };
15241
+ }
15242
+ async listEvents(options = {}) {
15243
+ if (Object.keys(options).length === 0)
15244
+ return this.store.listEvents();
15245
+ return queryClientEvents(await this.store.listEvents(), options);
15246
+ }
15247
+ async listEventsPage(options = {}) {
15248
+ if (this.store.listEventsPage)
15249
+ return this.store.listEventsPage(options);
15250
+ const events = queryClientEvents(await this.store.listEvents(), {
15251
+ eventId: options.eventId,
15252
+ source: options.source,
15253
+ type: options.type
15254
+ });
15255
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15256
+ const limit = normalizeEventPageLimit(options.limit);
15257
+ const pageEvents = events.slice(offset, offset + limit);
15258
+ const nextOffset = offset + pageEvents.length;
15259
+ const hasMore = nextOffset < events.length;
15260
+ return {
15261
+ events: pageEvents,
15262
+ cursor: options.cursor,
15263
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
15264
+ hasMore
15265
+ };
15008
15266
  }
15009
15267
  async listDeliveries() {
15010
15268
  return this.store.listDeliveries();
@@ -15072,22 +15330,37 @@ class EventsClient {
15072
15330
  return result;
15073
15331
  }
15074
15332
  async replay(options = {}) {
15075
- const events = (await this.store.listEvents()).filter((event) => {
15076
- if (options.eventId && event.id !== options.eventId)
15077
- return false;
15078
- if (options.source && event.source !== options.source)
15079
- return false;
15080
- if (options.type && event.type !== options.type)
15081
- return false;
15082
- return true;
15083
- });
15333
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15084
15334
  if (options.dryRun)
15085
- return { events, deliveries: [] };
15335
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15086
15336
  const deliveries = [];
15087
- for (const event of events) {
15337
+ for (const event of page.events) {
15088
15338
  deliveries.push(...await this.deliver(event));
15089
15339
  }
15090
- return { events, deliveries };
15340
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15341
+ }
15342
+ async appendEvent(event, options) {
15343
+ if (this.store.appendEventOnce) {
15344
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
15345
+ }
15346
+ if (options.dedupe) {
15347
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
15348
+ if (existing) {
15349
+ return {
15350
+ event: existing,
15351
+ stored: false,
15352
+ deduped: true,
15353
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
15354
+ };
15355
+ }
15356
+ }
15357
+ const stored = await this.store.appendEvent(event);
15358
+ return {
15359
+ event: stored,
15360
+ stored: true,
15361
+ deduped: false,
15362
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
15363
+ };
15091
15364
  }
15092
15365
  async applyRedaction(event, channel) {
15093
15366
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -15114,43 +15387,19 @@ class EventsClient {
15114
15387
  return createDeliveryResult(event, channel, attempts);
15115
15388
  }
15116
15389
  }
15117
- function redactPaths(event, paths, replacement = "[REDACTED]") {
15118
- if (paths.length === 0)
15119
- return event;
15120
- const copy = structuredClone(event);
15121
- for (const path of paths) {
15122
- setPath(copy, path, replacement);
15123
- }
15124
- return copy;
15125
- }
15126
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15127
- return redactValue2(event, replacement);
15128
- }
15129
- function shouldRedactKey(key) {
15130
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
15131
- }
15132
- function redactValue2(value, replacement) {
15133
- if (Array.isArray(value))
15134
- return value.map((item) => redactValue2(item, replacement));
15135
- if (!value || typeof value !== "object")
15136
- return value;
15137
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15138
- key,
15139
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15140
- ]));
15141
- }
15142
- function setPath(input, path, replacement) {
15143
- const parts = path.split(".");
15144
- let cursor = input;
15145
- for (const part of parts.slice(0, -1)) {
15146
- const next = cursor[part];
15147
- if (!next || typeof next !== "object")
15148
- return;
15149
- cursor = next;
15150
- }
15151
- const last = parts.at(-1);
15152
- if (last && last in cursor)
15153
- cursor[last] = replacement;
15390
+ function queryClientEvents(events, options) {
15391
+ let rows = events;
15392
+ if (options.eventId)
15393
+ rows = rows.filter((event) => event.id === options.eventId);
15394
+ if (options.source)
15395
+ rows = rows.filter((event) => event.source === options.source);
15396
+ if (options.type)
15397
+ rows = rows.filter((event) => event.type === options.type);
15398
+ if (options.cursor)
15399
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
15400
+ if (options.limit !== undefined)
15401
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
15402
+ return rows;
15154
15403
  }
15155
15404
  function normalizeTime(value) {
15156
15405
  if (!value)
@@ -15164,9 +15413,22 @@ function normalizeRetryPolicy(policy) {
15164
15413
  multiplier: Math.max(1, policy?.multiplier ?? 2)
15165
15414
  };
15166
15415
  }
15167
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
15416
+ 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;
15168
15417
  var init_dist = __esm(() => {
15169
15418
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15419
+ EventValidationError = class EventValidationError extends Error {
15420
+ eventType;
15421
+ issues;
15422
+ constructor(eventType, issues) {
15423
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
15424
+ super(`Event validation failed for type "${eventType}": ${detail}`);
15425
+ this.name = "EventValidationError";
15426
+ this.eventType = eventType;
15427
+ this.issues = issues;
15428
+ }
15429
+ };
15430
+ defaultEventTypeCatalog = new EventTypeCatalog;
15431
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
15170
15432
  });
15171
15433
 
15172
15434
  // src/db/task-lists.ts
@@ -16221,8 +16483,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
16221
16483
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
16222
16484
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
16223
16485
  }
16224
- const resp = await fetch(wh.url, { method: "POST", headers, body });
16486
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
16225
16487
  const respText = await resp.text().catch(() => "");
16488
+ if (resp.status >= 300 && resp.status < 400) {
16489
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
16490
+ return;
16491
+ }
16226
16492
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
16227
16493
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
16228
16494
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -18051,6 +18317,12 @@ function listTasks(filter = {}, db) {
18051
18317
  const d = db || getDatabase();
18052
18318
  const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
18053
18319
  clearExpiredLocks2(d);
18320
+ if (filter.limit !== undefined) {
18321
+ const limit = filter.limit;
18322
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
18323
+ throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
18324
+ }
18325
+ }
18054
18326
  const conditions = [];
18055
18327
  const params = [];
18056
18328
  if (filter.project_id) {
@@ -18584,6 +18856,7 @@ function deleteTask(id, db) {
18584
18856
  }
18585
18857
  return result.changes > 0;
18586
18858
  }
18859
+ var MAX_TASK_LIST_LIMIT = 1e5;
18587
18860
  var init_task_crud = __esm(() => {
18588
18861
  init_types();
18589
18862
  init_database();
@@ -21535,7 +21808,8 @@ __export(exports_tasks, {
21535
21808
  buildTaskBoardSnapshot: () => buildTaskBoardSnapshot,
21536
21809
  archiveTasks: () => archiveTasks,
21537
21810
  archiveCompletedTasks: () => archiveCompletedTasks,
21538
- addDependency: () => addDependency
21811
+ addDependency: () => addDependency,
21812
+ MAX_TASK_LIST_LIMIT: () => MAX_TASK_LIST_LIMIT
21539
21813
  });
21540
21814
  var init_tasks = __esm(() => {
21541
21815
  init_task_crud();
@@ -31975,6 +32249,27 @@ var init_project_commands = __esm(() => {
31975
32249
  };
31976
32250
  });
31977
32251
 
32252
+ // src/storage/postgres-errors.ts
32253
+ function isPostgresUniqueViolation(error) {
32254
+ if (typeof error !== "object" || error === null)
32255
+ return false;
32256
+ const candidate = error;
32257
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
32258
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
32259
+ const cause = candidate.cause;
32260
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
32261
+ }
32262
+ return states.some((state) => String(state) === "23505");
32263
+ }
32264
+ function postgresConstraintName(error) {
32265
+ if (typeof error !== "object" || error === null)
32266
+ return "";
32267
+ const candidate = error;
32268
+ const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
32269
+ const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
32270
+ return typeof constraint === "string" ? constraint : "";
32271
+ }
32272
+
31978
32273
  // src/storage/postgres-sync.ts
31979
32274
  function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
31980
32275
  assertSafeIdentifier(tableName);
@@ -32169,53 +32464,98 @@ class PostgresTodosSyncStore {
32169
32464
  if (routingErrors.length > 0) {
32170
32465
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
32171
32466
  }
32172
- const existing = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
32173
- FROM ${this.tableName}
32174
- WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
32175
- const existingProjects = [];
32176
- const existingTaskLists = [];
32177
- for (const row of existing.rows) {
32178
- const payload = payloadRecord(row.payload);
32179
- if (row.object_type === "projects")
32180
- existingProjects.push(payload);
32181
- if (row.object_type === "task_lists")
32182
- existingTaskLists.push(payload);
32183
- }
32184
- const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
32185
- if (destinationErrors.length > 0) {
32186
- throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
32187
- }
32188
- const result = { records: 0, objectTypes: {} };
32189
- const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
32190
- for (const entry of snapshotEntries(snapshot)) {
32191
- if (entry.deletedAt === null)
32192
- assertCanonicalScopedSlugEntry(entry);
32193
- await this.client.query(`INSERT INTO ${this.tableName} (
32194
- service, object_type, object_id, payload, updated_at,
32195
- deleted_at, source_machine_id, version
32196
- ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
32197
- ON CONFLICT (service, object_type, object_id) DO UPDATE SET
32198
- payload = EXCLUDED.payload,
32199
- updated_at = EXCLUDED.updated_at,
32200
- deleted_at = EXCLUDED.deleted_at,
32201
- source_machine_id = EXCLUDED.source_machine_id,
32202
- version = EXCLUDED.version
32203
- WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
32204
- OR (${this.tableName}.updated_at = EXCLUDED.updated_at
32205
- AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
32206
- this.service,
32207
- entry.type,
32208
- entry.id,
32209
- entry.payload,
32210
- entry.updatedAt,
32211
- entry.deletedAt,
32212
- sourceMachineId,
32213
- entry.version
32214
- ]);
32215
- result.records += 1;
32216
- result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
32467
+ const push = async (client) => {
32468
+ const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
32469
+ FROM ${this.tableName}
32470
+ WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
32471
+ const existingProjects = [];
32472
+ const existingTaskLists = [];
32473
+ for (const row of existing.rows) {
32474
+ const payload = payloadRecord(row.payload);
32475
+ if (row.object_type === "projects")
32476
+ existingProjects.push(payload);
32477
+ if (row.object_type === "task_lists")
32478
+ existingTaskLists.push(payload);
32479
+ }
32480
+ const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
32481
+ if (destinationErrors.length > 0) {
32482
+ throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
32483
+ }
32484
+ const result = { records: 0, objectTypes: {} };
32485
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
32486
+ for (const entry of snapshotEntries(snapshot)) {
32487
+ if (entry.deletedAt === null)
32488
+ assertCanonicalScopedSlugEntry(entry);
32489
+ try {
32490
+ await client.query(`INSERT INTO ${this.tableName} (
32491
+ service, object_type, object_id, payload, updated_at,
32492
+ deleted_at, source_machine_id, version
32493
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
32494
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
32495
+ payload = EXCLUDED.payload,
32496
+ updated_at = EXCLUDED.updated_at,
32497
+ deleted_at = EXCLUDED.deleted_at,
32498
+ source_machine_id = EXCLUDED.source_machine_id,
32499
+ version = EXCLUDED.version
32500
+ WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
32501
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
32502
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
32503
+ this.service,
32504
+ entry.type,
32505
+ entry.id,
32506
+ entry.payload,
32507
+ entry.updatedAt,
32508
+ entry.deletedAt,
32509
+ sourceMachineId,
32510
+ entry.version
32511
+ ]);
32512
+ } catch (error) {
32513
+ await this.classifySyncInsertConflict(error, entry);
32514
+ }
32515
+ result.records += 1;
32516
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
32517
+ }
32518
+ return result;
32519
+ };
32520
+ if (typeof this.client.transaction === "function") {
32521
+ return this.client.transaction((client) => push(client));
32217
32522
  }
32218
- return result;
32523
+ return push(this.client);
32524
+ }
32525
+ async classifySyncInsertConflict(error, entry) {
32526
+ if (!isPostgresUniqueViolation(error))
32527
+ throw error;
32528
+ const payload = entry.payload;
32529
+ const taskListSlug = String(payload["slug"] ?? "");
32530
+ const projectSlug = String(payload["task_list_id"] ?? "");
32531
+ const constraintName = postgresConstraintName(error);
32532
+ if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
32533
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
32534
+ }
32535
+ if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
32536
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
32537
+ }
32538
+ if (entry.type === "task_lists") {
32539
+ const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
32540
+ const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
32541
+ SELECT 1 FROM ${this.tableName}
32542
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
32543
+ AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
32544
+ ) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
32545
+ if (conflict.rows[0]?.conflict) {
32546
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
32547
+ }
32548
+ } else if (entry.type === "projects") {
32549
+ const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
32550
+ SELECT 1 FROM ${this.tableName}
32551
+ WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
32552
+ AND deleted_at IS NULL AND payload->>'task_list_id' = $3
32553
+ ) AS conflict`, [this.service, entry.id, projectSlug]);
32554
+ if (conflict.rows[0]?.conflict) {
32555
+ throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
32556
+ }
32557
+ }
32558
+ throw error;
32219
32559
  }
32220
32560
  async pullSnapshot(options = {}) {
32221
32561
  const params = [this.service];
@@ -32372,6 +32712,7 @@ function assertSafeIdentifier(value) {
32372
32712
  }
32373
32713
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors", PostgresScopedSlugMigrationConflictError, PostgresScopedSlugIndexBuildError;
32374
32714
  var init_postgres_sync = __esm(() => {
32715
+ init_types();
32375
32716
  PostgresScopedSlugMigrationConflictError = class PostgresScopedSlugMigrationConflictError extends Error {
32376
32717
  conflicts;
32377
32718
  constructor(conflicts) {
@@ -32599,9 +32940,11 @@ class PostgresJsonRecordStore {
32599
32940
  async ensureSchema() {
32600
32941
  if (!this.schemaReady) {
32601
32942
  this.schemaReady = (async () => {
32602
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32603
- await this.options.client.query(sql);
32604
- }
32943
+ await retryOnTransientPostgresError(async () => {
32944
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32945
+ await this.options.client.query(sql);
32946
+ }
32947
+ });
32605
32948
  })().catch((error) => {
32606
32949
  this.schemaReady = null;
32607
32950
  throw error;
@@ -33015,13 +33358,14 @@ class PostgresJsonRecordStore {
33015
33358
  throw new Error(divergentAuditHistoryReplayError(value.id));
33016
33359
  }
33017
33360
  async withTaskParentIntegrityTransaction(fn) {
33018
- if (typeof this.options.client.transaction !== "function") {
33361
+ const transaction = this.options.client.transaction;
33362
+ if (typeof transaction !== "function") {
33019
33363
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
33020
33364
  }
33021
- return this.options.client.transaction(async (client) => {
33365
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
33022
33366
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
33023
33367
  return fn(client);
33024
- });
33368
+ }));
33025
33369
  }
33026
33370
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
33027
33371
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -34904,7 +35248,7 @@ function compareClock(left, right) {
34904
35248
  function numberValue2(value) {
34905
35249
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
34906
35250
  }
34907
- function isPostgresUniqueViolation(error) {
35251
+ function isTransientPostgresError(error) {
34908
35252
  if (typeof error !== "object" || error === null)
34909
35253
  return false;
34910
35254
  const candidate = error;
@@ -34913,17 +35257,27 @@ function isPostgresUniqueViolation(error) {
34913
35257
  const cause = candidate.cause;
34914
35258
  states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
34915
35259
  }
34916
- return states.some((state) => String(state) === "23505");
35260
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
35261
+ return true;
35262
+ }
35263
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
35264
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
34917
35265
  }
34918
- function postgresConstraintName(error) {
34919
- if (typeof error !== "object" || error === null)
34920
- return "";
34921
- const candidate = error;
34922
- const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
34923
- const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
34924
- return typeof constraint === "string" ? constraint : "";
35266
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
35267
+ let lastError;
35268
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
35269
+ try {
35270
+ return await fn();
35271
+ } catch (error) {
35272
+ lastError = error;
35273
+ if (!isTransientPostgresError(error) || attempt === attempts)
35274
+ throw error;
35275
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs * attempt));
35276
+ }
35277
+ }
35278
+ throw lastError;
34925
35279
  }
34926
- 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;
35280
+ 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;
34927
35281
  var init_postgres_adapter = __esm(() => {
34928
35282
  init_types();
34929
35283
  init_creator_identity();
@@ -34937,6 +35291,12 @@ var init_postgres_adapter = __esm(() => {
34937
35291
  init_audit_history_import();
34938
35292
  init_canonical();
34939
35293
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
35294
+ TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
35295
+ TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
35296
+ "canceling statement due to lock timeout",
35297
+ "deadlock detected",
35298
+ "could not serialize access"
35299
+ ];
34940
35300
  });
34941
35301
 
34942
35302
  // src/project-registration/postgres.ts
@@ -51893,6 +52253,13 @@ function resolveSigningSecret(env = process.env) {
51893
52253
  function isPostgresBackendConfigured(env = process.env) {
51894
52254
  return Boolean(resolveCloudDatabaseUrl(env));
51895
52255
  }
52256
+ function schemaRetryMinIntervalMs(env = process.env) {
52257
+ const raw = env.HASNA_TODOS_SCHEMA_RETRY_MIN_MS;
52258
+ if (!raw)
52259
+ return DEFAULT_SCHEMA_RETRY_MIN_MS;
52260
+ const parsed = Number(raw);
52261
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SCHEMA_RETRY_MIN_MS;
52262
+ }
51896
52263
  function getCloudTenantId() {
51897
52264
  return process.env.HASNA_TODOS_TENANT_ID ?? "default";
51898
52265
  }
@@ -51988,6 +52355,10 @@ function getCloudVerifier() {
51988
52355
  async function ensureCloudSchema() {
51989
52356
  if (schemaEnsured)
51990
52357
  return schemaEnsured;
52358
+ if (lastSchemaFailure !== null && Date.now() - lastSchemaAttemptAtMs < schemaRetryMinIntervalMs()) {
52359
+ throw lastSchemaFailure;
52360
+ }
52361
+ lastSchemaAttemptAtMs = Date.now();
51991
52362
  schemaEnsured = (async () => {
51992
52363
  const client = getClient();
51993
52364
  for (const sql of postgresTodosSyncSchemaSql()) {
@@ -52008,6 +52379,7 @@ async function ensureCloudSchema() {
52008
52379
  await getApiKeyStore().ensureSchema();
52009
52380
  })().catch((error) => {
52010
52381
  schemaEnsured = null;
52382
+ lastSchemaFailure = error;
52011
52383
  throw error;
52012
52384
  });
52013
52385
  return schemaEnsured;
@@ -52055,8 +52427,10 @@ async function closeCloud() {
52055
52427
  cachedTaskManifestAuthority = null;
52056
52428
  cachedTaskSubtreeTransferAuthority = null;
52057
52429
  schemaEnsured = null;
52430
+ lastSchemaAttemptAtMs = 0;
52431
+ lastSchemaFailure = null;
52058
52432
  }
52059
- var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
52433
+ var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null, lastSchemaAttemptAtMs = 0, lastSchemaFailure = null, DEFAULT_SCHEMA_RETRY_MIN_MS = 1e4;
52060
52434
  var init_cloud = __esm(() => {
52061
52435
  init_cloud_client();
52062
52436
  init_postgres_adapter();
@@ -52922,6 +53296,19 @@ function taskStatusQueryParam(url) {
52922
53296
  return { ok: false, message: result.message };
52923
53297
  return { ok: true, value: collapseEnumValues(result.values) };
52924
53298
  }
53299
+ function parsePaginationQueryParam(url, name) {
53300
+ const raw = url.searchParams.get(name);
53301
+ if (raw === null)
53302
+ return { ok: true, value: undefined };
53303
+ const min = name === "limit" ? 1 : 0;
53304
+ const message = name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer";
53305
+ if (!/^\d+$/.test(raw))
53306
+ return { ok: false, message };
53307
+ const value = Number(raw);
53308
+ if (!Number.isSafeInteger(value) || value < min)
53309
+ return { ok: false, message };
53310
+ return { ok: true, value };
53311
+ }
52925
53312
  async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
52926
53313
  const statusParam = taskStatusQueryParam(url);
52927
53314
  if (!statusParam.ok)
@@ -52929,16 +53316,20 @@ async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
52929
53316
  const projectId = url.searchParams.get("project_id") || undefined;
52930
53317
  const sessionId = url.searchParams.get("session_id") || undefined;
52931
53318
  const agentId = url.searchParams.get("agent_id") || undefined;
52932
- const limitParam = url.searchParams.get("limit");
52933
- const offsetParam = url.searchParams.get("offset");
53319
+ const limitParam = parsePaginationQueryParam(url, "limit");
53320
+ if (!limitParam.ok)
53321
+ return json5({ error: limitParam.message }, 400);
53322
+ const offsetParam = parsePaginationQueryParam(url, "offset");
53323
+ if (!offsetParam.ok)
53324
+ return json5({ error: offsetParam.message }, 400);
52934
53325
  const fields = parseFieldsParam(url);
52935
53326
  const tasks = listTasks({
52936
53327
  status: statusParam.value,
52937
53328
  project_id: projectId,
52938
53329
  session_id: sessionId,
52939
53330
  agent_id: agentId,
52940
- limit: limitParam ? parseInt(limitParam, 10) : undefined,
52941
- offset: offsetParam ? parseInt(offsetParam, 10) : undefined
53331
+ limit: limitParam.value,
53332
+ offset: offsetParam.value
52942
53333
  });
52943
53334
  return json5(tasks.map((t) => taskToSummary2(t, fields)));
52944
53335
  }
@@ -53730,6 +54121,12 @@ class TodosShadowOutbox {
53730
54121
  this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
53731
54122
  } catch (error) {
53732
54123
  const message = error instanceof Error ? error.message : String(error);
54124
+ if (error instanceof ResourceConflictError) {
54125
+ this.metrics.lastError = message;
54126
+ this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
54127
+ this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
54128
+ return;
54129
+ }
53733
54130
  this.metrics.retries += 1;
53734
54131
  this.metrics.lastError = message;
53735
54132
  const attempts = row.attempts + 1;
@@ -53857,6 +54254,7 @@ function createTodosShadowOutbox(options) {
53857
54254
  }
53858
54255
  var MAX_BACKOFF_MS;
53859
54256
  var init_shadow_outbox = __esm(() => {
54257
+ init_types();
53860
54258
  init_local_sqlite();
53861
54259
  init_postgres_sync();
53862
54260
  init_shadow_outbox_schema();
@@ -57831,6 +58229,26 @@ function parseSinceCursor(raw) {
57831
58229
  }
57832
58230
  return { ok: true, value: new Date(parsed).toISOString() };
57833
58231
  }
58232
+ function paginationQueryParam(url, name) {
58233
+ const raw = url.searchParams.get(name);
58234
+ if (raw === null)
58235
+ return { ok: true, value: undefined };
58236
+ const min = name === "limit" ? 1 : 0;
58237
+ if (!/^\d+$/.test(raw)) {
58238
+ return {
58239
+ ok: false,
58240
+ response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
58241
+ };
58242
+ }
58243
+ const value = Number(raw);
58244
+ if (!Number.isSafeInteger(value) || value < min) {
58245
+ return {
58246
+ ok: false,
58247
+ response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
58248
+ };
58249
+ }
58250
+ return { ok: true, value };
58251
+ }
57834
58252
  function validateTaskCompletion(value) {
57835
58253
  if (!value || typeof value !== "object" || Array.isArray(value))
57836
58254
  return { ok: false, message: "completion body must be an object" };
@@ -58359,6 +58777,12 @@ async function handleV1Request(req, url, dependencies = {}) {
58359
58777
  if (updatedAfter !== null && !updatedAfter.ok) {
58360
58778
  return error(400, updatedAfter.message);
58361
58779
  }
58780
+ const limitParam = paginationQueryParam(url, "limit");
58781
+ if (!limitParam.ok)
58782
+ return limitParam.response;
58783
+ const offsetParam = paginationQueryParam(url, "offset");
58784
+ if (!offsetParam.ok)
58785
+ return offsetParam.response;
58362
58786
  const filter = {
58363
58787
  ...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
58364
58788
  ...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
@@ -58375,8 +58799,8 @@ async function handleV1Request(req, url, dependencies = {}) {
58375
58799
  ...url.searchParams.get("tags") ? {
58376
58800
  tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
58377
58801
  } : {},
58378
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
58379
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
58802
+ ...limitParam.value !== undefined ? { limit: limitParam.value } : {},
58803
+ ...offsetParam.value !== undefined ? { offset: offsetParam.value } : {}
58380
58804
  };
58381
58805
  const tasks = await store.tasks.list(filter);
58382
58806
  const { limit: _l, offset: _o, ...countFilter } = filter;
@@ -60136,8 +60560,8 @@ function registerTaskCrudTools(server, ctx) {
60136
60560
  tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (AND logic)"),
60137
60561
  created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
60138
60562
  created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
60139
- limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
60140
- offset: exports_external.number().optional().describe("Pagination offset"),
60563
+ limit: exports_external.number().int().min(1).optional().describe("Max results (default: 50, max 500)"),
60564
+ offset: exports_external.number().int().min(0).optional().describe("Pagination offset"),
60141
60565
  metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
60142
60566
  }, async (params) => {
60143
60567
  try {
@@ -62099,6 +62523,48 @@ var init_task_relationships = __esm(() => {
62099
62523
  ];
62100
62524
  });
62101
62525
 
62526
+ // src/lib/dedupe-projection.ts
62527
+ function projectTasksForDedupe(tasks) {
62528
+ return tasks.map((task2) => {
62529
+ const metadata = {};
62530
+ for (const key2 of DEDUPE_SOURCE_KEY_ALLOWLIST) {
62531
+ if (key2 in task2.metadata) {
62532
+ metadata[key2] = redactValue(task2.metadata[key2]);
62533
+ }
62534
+ }
62535
+ return {
62536
+ id: task2.id,
62537
+ short_id: task2.short_id,
62538
+ title: redactEvidenceText(task2.title),
62539
+ description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
62540
+ status: task2.status,
62541
+ created_at: task2.created_at,
62542
+ updated_at: task2.updated_at,
62543
+ project_id: task2.project_id,
62544
+ task_list_id: task2.task_list_id,
62545
+ assigned_to: task2.assigned_to,
62546
+ priority: task2.priority,
62547
+ metadata
62548
+ };
62549
+ });
62550
+ }
62551
+ var DEDUPE_SOURCE_KEY_ALLOWLIST;
62552
+ var init_dedupe_projection = __esm(() => {
62553
+ init_redaction();
62554
+ DEDUPE_SOURCE_KEY_ALLOWLIST = [
62555
+ "github_url",
62556
+ "github_issue_url",
62557
+ "github_pr_url",
62558
+ "source_url",
62559
+ "url",
62560
+ "external_url",
62561
+ "issue_url",
62562
+ "github_owner",
62563
+ "github_repo",
62564
+ "github_number"
62565
+ ];
62566
+ });
62567
+
62102
62568
  // src/lib/task-dedupe.ts
62103
62569
  function asObject(value) {
62104
62570
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -62263,10 +62729,10 @@ function olderFirst(left, right) {
62263
62729
  function findDuplicateTasks(options = {}, db) {
62264
62730
  const d = db || getDatabase();
62265
62731
  const threshold = options.threshold ?? DEFAULT_THRESHOLD;
62266
- const fingerprints = listTasks({
62732
+ const fingerprints = projectTasksForDedupe(listTasks({
62267
62733
  include_archived: Boolean(options.include_archived),
62268
62734
  limit: options.limit ?? 1000
62269
- }, d).map(fingerprint);
62735
+ }, d)).map(fingerprint);
62270
62736
  const candidates = [];
62271
62737
  for (let i = 0;i < fingerprints.length; i++) {
62272
62738
  for (let j = i + 1;j < fingerprints.length; j++) {
@@ -62501,6 +62967,7 @@ var init_task_dedupe = __esm(() => {
62501
62967
  init_database();
62502
62968
  init_task_relationships();
62503
62969
  init_tasks();
62970
+ init_dedupe_projection();
62504
62971
  STOP_WORDS = new Set([
62505
62972
  "a",
62506
62973
  "an",
@@ -86972,13 +87439,25 @@ Findings`));
86972
87439
  handleError(e);
86973
87440
  }
86974
87441
  });
87442
+ function parseDedupeLimit(raw) {
87443
+ const value = Number(raw);
87444
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TASK_LIST_LIMIT) {
87445
+ return null;
87446
+ }
87447
+ return value;
87448
+ }
86975
87449
  const dedupe2 = program2.command("dedupe").description("Find and merge likely duplicate local tasks");
86976
87450
  dedupe2.command("scan").description("Scan local tasks for likely duplicates").option("--threshold <n>", "Minimum duplicate score from 0 to 1", "0.74").option("--limit <n>", "Maximum tasks to compare", "1000").option("--include-archived", "Include archived tasks").option("-j, --json", "Output as JSON").action((opts) => {
86977
87451
  const globalOpts = program2.opts();
86978
87452
  try {
87453
+ const limit = parseDedupeLimit(opts.limit);
87454
+ if (limit === null) {
87455
+ handleError(new Error(`--limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT} (got: ${opts.limit})`));
87456
+ return;
87457
+ }
86979
87458
  const candidates = findDuplicateTasks({
86980
87459
  threshold: Number(opts.threshold),
86981
- limit: Number(opts.limit),
87460
+ limit,
86982
87461
  include_archived: Boolean(opts.includeArchived)
86983
87462
  });
86984
87463
  if (opts.json || globalOpts.json) {
@@ -86998,6 +87477,35 @@ Findings`));
86998
87477
  handleError(e);
86999
87478
  }
87000
87479
  });
87480
+ dedupe2.command("project <ref>").description("Project a project's tasks into the bounded dedup projection (safe to emit: no free-form metadata, tags, comments, or run fields)").option("--limit <n>", "Maximum tasks to project", "1000").option("--include-archived", "Include archived tasks").option("-j, --json", "Output as JSON").action((ref, opts) => {
87481
+ const globalOpts = program2.opts();
87482
+ try {
87483
+ const d = getDatabase();
87484
+ const projectId = resolvePartialId(d, "projects", ref) || d.query("SELECT id FROM projects WHERE path = ? OR name = ? OR task_list_id = ?").get(ref, ref, ref)?.id;
87485
+ if (!projectId) {
87486
+ handleError(new Error(`Could not resolve project: ${ref}`));
87487
+ return;
87488
+ }
87489
+ const limit = parseDedupeLimit(opts.limit);
87490
+ if (limit === null) {
87491
+ handleError(new Error(`--limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT} (got: ${opts.limit})`));
87492
+ return;
87493
+ }
87494
+ const tasks = listTasks({
87495
+ project_id: projectId,
87496
+ include_archived: Boolean(opts.includeArchived),
87497
+ limit
87498
+ }, d);
87499
+ const projected = projectTasksForDedupe(tasks);
87500
+ if (opts.json || globalOpts.json) {
87501
+ output({ project_id: projectId, count: projected.length, tasks: projected }, true);
87502
+ return;
87503
+ }
87504
+ console.log(chalk9.dim(`${projected.length} task(s) projected for dedupe (bounded \u2014 no free-form metadata, tags, comments, or run fields).`));
87505
+ } catch (e) {
87506
+ handleError(e);
87507
+ }
87508
+ });
87001
87509
  dedupe2.command("merge <primary-task-id> <duplicate-task-id>").description("Merge a duplicate task into a primary task and archive the duplicate").option("--agent <agent>", "Agent ID recording the merge").option("--reason <reason>", "Human-readable merge reason").option("-j, --json", "Output as JSON").action((primaryTaskId, duplicateTaskId, opts) => {
87002
87510
  const globalOpts = program2.opts();
87003
87511
  try {
@@ -87302,6 +87810,7 @@ var init_query_commands = __esm(() => {
87302
87810
  init_audit();
87303
87811
  init_handoffs();
87304
87812
  init_task_dedupe();
87813
+ init_dedupe_projection();
87305
87814
  init_local_fields();
87306
87815
  init_workflow_states();
87307
87816
  init_local_reports();
@@ -95207,6 +95716,12 @@ class TodosShadowMirror {
95207
95716
  const message = error2 instanceof Error ? error2.message : String(error2);
95208
95717
  const objectType2 = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
95209
95718
  const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
95719
+ if (error2 instanceof ResourceConflictError) {
95720
+ this.metrics.failed += 1;
95721
+ this.metrics.lastError = message;
95722
+ this.onEvent?.({ type: "dropped", objectType: objectType2, id, error: message });
95723
+ return;
95724
+ }
95210
95725
  this.metrics.retries += 1;
95211
95726
  this.metrics.lastError = message;
95212
95727
  op.attempts += 1;
@@ -95454,6 +95969,7 @@ function isAgent(value) {
95454
95969
  }
95455
95970
  var SNAPSHOT_TO_OBJECT_TYPE;
95456
95971
  var init_shadow = __esm(() => {
95972
+ init_types();
95457
95973
  init_local_sqlite();
95458
95974
  init_postgres_sync();
95459
95975
  SNAPSHOT_TO_OBJECT_TYPE = {