@hasna/todos 0.15.41 → 0.15.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
- package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/components/Dashboard.d.ts +1 -1
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/ProjectList.d.ts +1 -1
- package/dist/cli/components/ProjectList.d.ts.map +1 -1
- package/dist/cli/components/SearchView.d.ts +1 -1
- package/dist/cli/components/SearchView.d.ts.map +1 -1
- package/dist/cli/components/TaskDetail.d.ts +1 -1
- package/dist/cli/components/TaskDetail.d.ts.map +1 -1
- package/dist/cli/components/TaskForm.d.ts +1 -1
- package/dist/cli/components/TaskForm.d.ts.map +1 -1
- package/dist/cli/components/TaskList.d.ts +1 -1
- package/dist/cli/components/TaskList.d.ts.map +1 -1
- package/dist/cli/index.js +521 -120
- package/dist/contracts.js +346 -83
- package/dist/db/task-graph.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.js +499 -113
- package/dist/lib/instant-compare.d.ts +35 -0
- package/dist/lib/instant-compare.d.ts.map +1 -0
- package/dist/mcp/index.js +520 -119
- package/dist/mcp.js +5 -4
- package/dist/pr-groups/postgres.d.ts.map +1 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration.js +466 -100
- package/dist/registry.js +346 -83
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1179 -309
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.js +447 -89
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest.js +15 -6
- package/dist/task-subtree-transfer/postgres.d.ts.map +1 -1
- package/dist/task-subtree-transfer.js +15 -6
- package/package.json +5 -4
- package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
- package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/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.
|
|
2126
|
+
version: "0.15.47",
|
|
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",
|
|
@@ -2241,18 +2241,19 @@ var init_package = __esm(() => {
|
|
|
2241
2241
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
2242
2242
|
license: "Apache-2.0",
|
|
2243
2243
|
dependencies: {
|
|
2244
|
-
"@hasna/contracts": "0.13.
|
|
2244
|
+
"@hasna/contracts": "0.13.4",
|
|
2245
2245
|
"@hasna/events": "^0.1.11",
|
|
2246
2246
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2247
2247
|
chalk: "^5.4.1",
|
|
2248
2248
|
commander: "^13.1.0",
|
|
2249
2249
|
ink: "^5.2.0",
|
|
2250
2250
|
react: "^18.3.1",
|
|
2251
|
-
zod: "
|
|
2251
|
+
zod: "3.25.76"
|
|
2252
2252
|
},
|
|
2253
2253
|
overrides: {
|
|
2254
2254
|
ajv: "8.20.0",
|
|
2255
|
-
"fast-uri": "3.1.2"
|
|
2255
|
+
"fast-uri": "3.1.2",
|
|
2256
|
+
zod: "3.25.76"
|
|
2256
2257
|
},
|
|
2257
2258
|
devDependencies: {
|
|
2258
2259
|
"@types/bun": "^1.2.4",
|
|
@@ -2877,6 +2878,60 @@ var init_redaction = __esm(() => {
|
|
|
2877
2878
|
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
2878
2879
|
});
|
|
2879
2880
|
|
|
2881
|
+
// src/lib/instant-compare.ts
|
|
2882
|
+
function isLeapYear(year) {
|
|
2883
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
2884
|
+
}
|
|
2885
|
+
function sqliteJulianDay(value) {
|
|
2886
|
+
const m = SQLITE_STAMP.exec(value);
|
|
2887
|
+
if (!m)
|
|
2888
|
+
return null;
|
|
2889
|
+
const year = Number(m[1]);
|
|
2890
|
+
const month = Number(m[2]);
|
|
2891
|
+
const day = Number(m[3]);
|
|
2892
|
+
const hour = m[4] === undefined ? 0 : Number(m[4]);
|
|
2893
|
+
const minute = m[5] === undefined ? 0 : Number(m[5]);
|
|
2894
|
+
const second = m[6] === undefined ? 0 : Number(m[6]);
|
|
2895
|
+
const frac = m[7];
|
|
2896
|
+
const sign = m[8];
|
|
2897
|
+
const offsetHour = m[9];
|
|
2898
|
+
const offsetMinute = m[10];
|
|
2899
|
+
if (month < 1 || month > 12)
|
|
2900
|
+
return null;
|
|
2901
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
2902
|
+
return null;
|
|
2903
|
+
const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
|
|
2904
|
+
if (day < 1 || day > maxDay)
|
|
2905
|
+
return null;
|
|
2906
|
+
let offsetMinutes = 0;
|
|
2907
|
+
if (sign !== undefined) {
|
|
2908
|
+
const oh = Number(offsetHour ?? "0");
|
|
2909
|
+
const om = Number(offsetMinute ?? "0");
|
|
2910
|
+
if (oh > 23 || om > 59)
|
|
2911
|
+
return null;
|
|
2912
|
+
offsetMinutes = oh * 60 + om;
|
|
2913
|
+
if (sign === "-")
|
|
2914
|
+
offsetMinutes = -offsetMinutes;
|
|
2915
|
+
}
|
|
2916
|
+
const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
|
|
2917
|
+
const ms = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
2918
|
+
return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
|
|
2919
|
+
}
|
|
2920
|
+
function changedSinceStampNewer(stamp, since) {
|
|
2921
|
+
const stampJd = sqliteJulianDay(stamp);
|
|
2922
|
+
if (stampJd === null)
|
|
2923
|
+
return true;
|
|
2924
|
+
const sinceJd = sqliteJulianDay(since);
|
|
2925
|
+
if (sinceJd === null)
|
|
2926
|
+
return false;
|
|
2927
|
+
return stampJd > sinceJd;
|
|
2928
|
+
}
|
|
2929
|
+
var SQLITE_STAMP, DAYS_IN_MONTH;
|
|
2930
|
+
var init_instant_compare = __esm(() => {
|
|
2931
|
+
SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
|
|
2932
|
+
DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2880
2935
|
// src/lib/plan-project-link-contract.ts
|
|
2881
2936
|
import { createHash as createHash2 } from "crypto";
|
|
2882
2937
|
function canonicalPlanProjectLinkJson(value) {
|
|
@@ -7387,7 +7442,7 @@ async function cloudEscalatedTasks(client, opts = {}, at = new Date) {
|
|
|
7387
7442
|
}
|
|
7388
7443
|
async function cloudChangedSince(client, since, filter = {}) {
|
|
7389
7444
|
const tasks = await cloudListTasks(client, filter);
|
|
7390
|
-
return tasks.filter((t) => (t.updated_at ?? ""
|
|
7445
|
+
return tasks.filter((t) => changedSinceStampNewer(t.updated_at ?? "", since)).sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
|
|
7391
7446
|
}
|
|
7392
7447
|
async function cloudTaskStats(client, filter = {}) {
|
|
7393
7448
|
const tasks = await cloudListTasks(client, filter);
|
|
@@ -7672,6 +7727,7 @@ var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabil
|
|
|
7672
7727
|
var init_cloud_router = __esm(() => {
|
|
7673
7728
|
init_types();
|
|
7674
7729
|
init_redaction();
|
|
7730
|
+
init_instant_compare();
|
|
7675
7731
|
init_plan_project_link_contract();
|
|
7676
7732
|
init_http_client();
|
|
7677
7733
|
init_adoption_validation();
|
|
@@ -14523,8 +14579,9 @@ var init_event_hooks = __esm(() => {
|
|
|
14523
14579
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
14524
14580
|
});
|
|
14525
14581
|
|
|
14526
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
14582
|
+
// node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
|
|
14527
14583
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
14584
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
14528
14585
|
import { existsSync as existsSync6 } from "fs";
|
|
14529
14586
|
import { homedir as homedir2 } from "os";
|
|
14530
14587
|
import { join as join5 } from "path";
|
|
@@ -14633,11 +14690,13 @@ function getEventsDataDir(override) {
|
|
|
14633
14690
|
|
|
14634
14691
|
class JsonEventsStore {
|
|
14635
14692
|
dataDir;
|
|
14693
|
+
runtime;
|
|
14636
14694
|
channelsPath;
|
|
14637
14695
|
eventsPath;
|
|
14638
14696
|
deliveriesPath;
|
|
14639
14697
|
constructor(dataDir = getEventsDataDir()) {
|
|
14640
14698
|
this.dataDir = dataDir;
|
|
14699
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
14641
14700
|
this.channelsPath = join5(dataDir, "channels.json");
|
|
14642
14701
|
this.eventsPath = join5(dataDir, "events.json");
|
|
14643
14702
|
this.deliveriesPath = join5(dataDir, "deliveries.json");
|
|
@@ -14685,13 +14744,58 @@ class JsonEventsStore {
|
|
|
14685
14744
|
await this.writeJson(this.eventsPath, events);
|
|
14686
14745
|
return event;
|
|
14687
14746
|
}
|
|
14688
|
-
async
|
|
14747
|
+
async appendEventOnce(event, options = {}) {
|
|
14748
|
+
await this.init();
|
|
14749
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
14750
|
+
const dedupe2 = options.dedupe !== false;
|
|
14751
|
+
if (dedupe2) {
|
|
14752
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
14753
|
+
if (existing) {
|
|
14754
|
+
return {
|
|
14755
|
+
event: existing,
|
|
14756
|
+
stored: false,
|
|
14757
|
+
deduped: true,
|
|
14758
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
14759
|
+
};
|
|
14760
|
+
}
|
|
14761
|
+
}
|
|
14762
|
+
events.push(event);
|
|
14763
|
+
await this.writeJson(this.eventsPath, events);
|
|
14764
|
+
return {
|
|
14765
|
+
event,
|
|
14766
|
+
stored: true,
|
|
14767
|
+
deduped: false,
|
|
14768
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
14769
|
+
};
|
|
14770
|
+
}
|
|
14771
|
+
async listEvents(options = {}) {
|
|
14689
14772
|
await this.init();
|
|
14690
|
-
|
|
14773
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
14774
|
+
return queryEvents(events, options);
|
|
14775
|
+
}
|
|
14776
|
+
async listEventsPage(options = {}) {
|
|
14777
|
+
await this.init();
|
|
14778
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
14779
|
+
const queried = queryEvents(events, {
|
|
14780
|
+
eventId: options.eventId,
|
|
14781
|
+
source: options.source,
|
|
14782
|
+
type: options.type
|
|
14783
|
+
});
|
|
14784
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
14785
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
14786
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
14787
|
+
const nextOffset = offset + pageEvents.length;
|
|
14788
|
+
const hasMore = nextOffset < queried.length;
|
|
14789
|
+
return {
|
|
14790
|
+
events: pageEvents,
|
|
14791
|
+
cursor: options.cursor,
|
|
14792
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
14793
|
+
hasMore
|
|
14794
|
+
};
|
|
14691
14795
|
}
|
|
14692
14796
|
async findEventByIdentity(identity) {
|
|
14693
14797
|
const events = await this.listEvents();
|
|
14694
|
-
return events
|
|
14798
|
+
return findEventByIdentity(events, identity);
|
|
14695
14799
|
}
|
|
14696
14800
|
async appendDelivery(result) {
|
|
14697
14801
|
await this.init();
|
|
@@ -14742,6 +14846,83 @@ class JsonEventsStore {
|
|
|
14742
14846
|
});
|
|
14743
14847
|
}
|
|
14744
14848
|
}
|
|
14849
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
14850
|
+
return {
|
|
14851
|
+
mode: "local-files",
|
|
14852
|
+
name: "json-events-store",
|
|
14853
|
+
remote: false,
|
|
14854
|
+
localFiles: true,
|
|
14855
|
+
localSqlite: false,
|
|
14856
|
+
postgres: false,
|
|
14857
|
+
s3: false,
|
|
14858
|
+
aws: false,
|
|
14859
|
+
durable: true,
|
|
14860
|
+
idempotency: "best-effort-local",
|
|
14861
|
+
replayCursors: true,
|
|
14862
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
14863
|
+
};
|
|
14864
|
+
}
|
|
14865
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
14866
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
14867
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
14868
|
+
const payload = {
|
|
14869
|
+
offset,
|
|
14870
|
+
eventId: options.eventId,
|
|
14871
|
+
source: options.source,
|
|
14872
|
+
type: options.type
|
|
14873
|
+
};
|
|
14874
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
14875
|
+
}
|
|
14876
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
14877
|
+
if (!cursor)
|
|
14878
|
+
return 0;
|
|
14879
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
14880
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
14881
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
14882
|
+
let payload;
|
|
14883
|
+
try {
|
|
14884
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
14885
|
+
} catch {
|
|
14886
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
14887
|
+
}
|
|
14888
|
+
const offset = payload.offset;
|
|
14889
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
14890
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
14891
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
14892
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
14893
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
14894
|
+
return offset;
|
|
14895
|
+
}
|
|
14896
|
+
function normalizeEventPageLimit(limit) {
|
|
14897
|
+
if (limit === undefined)
|
|
14898
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
14899
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
14900
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
14901
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
14902
|
+
}
|
|
14903
|
+
function queryEvents(events, options) {
|
|
14904
|
+
let rows = events;
|
|
14905
|
+
if (options.eventId)
|
|
14906
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
14907
|
+
if (options.source)
|
|
14908
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
14909
|
+
if (options.type)
|
|
14910
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
14911
|
+
if (options.cursor) {
|
|
14912
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
14913
|
+
rows = rows.slice(offset);
|
|
14914
|
+
}
|
|
14915
|
+
if (options.limit !== undefined)
|
|
14916
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
14917
|
+
return rows;
|
|
14918
|
+
}
|
|
14919
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
14920
|
+
if (cursorValue !== optionValue)
|
|
14921
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
14922
|
+
}
|
|
14923
|
+
function findEventByIdentity(events, identity) {
|
|
14924
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
14925
|
+
}
|
|
14745
14926
|
function buildSignatureBase(timestamp2, body) {
|
|
14746
14927
|
return `${timestamp2}.${body}`;
|
|
14747
14928
|
}
|
|
@@ -14755,21 +14936,27 @@ function now2() {
|
|
|
14755
14936
|
function truncate(value, max = 4096) {
|
|
14756
14937
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
14757
14938
|
}
|
|
14758
|
-
function buildWebhookRequest(event, channel) {
|
|
14939
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
14759
14940
|
if (!channel.webhook)
|
|
14760
14941
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
14942
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
14943
|
+
if (/^x-hasna-/i.test(name)) {
|
|
14944
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
14945
|
+
}
|
|
14946
|
+
}
|
|
14761
14947
|
const body = JSON.stringify(event);
|
|
14762
|
-
const timestamp2 =
|
|
14948
|
+
const timestamp2 = options.timestamp ?? new Date().toISOString();
|
|
14763
14949
|
const headers = {
|
|
14764
14950
|
"Content-Type": "application/json",
|
|
14765
14951
|
"User-Agent": "@hasna/events",
|
|
14766
14952
|
"X-Hasna-Event-Id": event.id,
|
|
14767
14953
|
"X-Hasna-Event-Type": event.type,
|
|
14768
|
-
|
|
14769
|
-
|
|
14954
|
+
...channel.webhook.headers,
|
|
14955
|
+
"X-Hasna-Timestamp": timestamp2
|
|
14770
14956
|
};
|
|
14771
|
-
|
|
14772
|
-
|
|
14957
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
14958
|
+
if (secret) {
|
|
14959
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
|
|
14773
14960
|
}
|
|
14774
14961
|
return { body, headers };
|
|
14775
14962
|
}
|
|
@@ -14777,7 +14964,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
14777
14964
|
if (!channel.webhook)
|
|
14778
14965
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
14779
14966
|
const startedAt = now2();
|
|
14780
|
-
|
|
14967
|
+
let secret = channel.webhook.secret;
|
|
14968
|
+
if (channel.webhook.secretRef) {
|
|
14969
|
+
if (!options.secretResolver) {
|
|
14970
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
14971
|
+
}
|
|
14972
|
+
try {
|
|
14973
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
14974
|
+
} catch {
|
|
14975
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
14976
|
+
}
|
|
14977
|
+
if (!secret)
|
|
14978
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
14979
|
+
}
|
|
14980
|
+
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
14981
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
14781
14982
|
const controller = new AbortController;
|
|
14782
14983
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
14783
14984
|
try {
|
|
@@ -14809,6 +15010,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
14809
15010
|
clearTimeout(timeout);
|
|
14810
15011
|
}
|
|
14811
15012
|
}
|
|
15013
|
+
function failedAttempt(startedAt, error) {
|
|
15014
|
+
return {
|
|
15015
|
+
attempt: 1,
|
|
15016
|
+
status: "failed",
|
|
15017
|
+
startedAt,
|
|
15018
|
+
completedAt: now2(),
|
|
15019
|
+
error
|
|
15020
|
+
};
|
|
15021
|
+
}
|
|
14812
15022
|
async function dispatchCommand(event, channel) {
|
|
14813
15023
|
if (!channel.command)
|
|
14814
15024
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -14897,6 +15107,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
14897
15107
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
14898
15108
|
};
|
|
14899
15109
|
}
|
|
15110
|
+
|
|
15111
|
+
class EventTypeCatalog {
|
|
15112
|
+
definitions = new Map;
|
|
15113
|
+
register(definition) {
|
|
15114
|
+
this.definitions.set(definition.type, definition);
|
|
15115
|
+
return this;
|
|
15116
|
+
}
|
|
15117
|
+
unregister(type) {
|
|
15118
|
+
return this.definitions.delete(type);
|
|
15119
|
+
}
|
|
15120
|
+
has(type) {
|
|
15121
|
+
return this.definitions.has(type);
|
|
15122
|
+
}
|
|
15123
|
+
get(type) {
|
|
15124
|
+
return this.definitions.get(type);
|
|
15125
|
+
}
|
|
15126
|
+
list() {
|
|
15127
|
+
return [...this.definitions.values()];
|
|
15128
|
+
}
|
|
15129
|
+
validateEvent(event) {
|
|
15130
|
+
const definition = this.definitions.get(event.type);
|
|
15131
|
+
if (!definition)
|
|
15132
|
+
return { ok: true };
|
|
15133
|
+
return definition.validate(event.data, event);
|
|
15134
|
+
}
|
|
15135
|
+
assertEventValid(event) {
|
|
15136
|
+
const result = this.validateEvent(event);
|
|
15137
|
+
if (!result.ok) {
|
|
15138
|
+
throw new EventValidationError(event.type, result.issues);
|
|
15139
|
+
}
|
|
15140
|
+
}
|
|
15141
|
+
}
|
|
15142
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
15143
|
+
if (paths.length === 0)
|
|
15144
|
+
return event;
|
|
15145
|
+
const copy = structuredClone(event);
|
|
15146
|
+
for (const path of paths) {
|
|
15147
|
+
setPath(copy, path, replacement);
|
|
15148
|
+
}
|
|
15149
|
+
return copy;
|
|
15150
|
+
}
|
|
15151
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
15152
|
+
return redactValue2(event, replacement);
|
|
15153
|
+
}
|
|
15154
|
+
function shouldRedactKey(key) {
|
|
15155
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
15156
|
+
}
|
|
15157
|
+
function redactValue2(value, replacement) {
|
|
15158
|
+
if (Array.isArray(value))
|
|
15159
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
15160
|
+
if (!value || typeof value !== "object")
|
|
15161
|
+
return value;
|
|
15162
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
15163
|
+
key,
|
|
15164
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
15165
|
+
]));
|
|
15166
|
+
}
|
|
15167
|
+
function setPath(input, path, replacement) {
|
|
15168
|
+
const parts = path.split(".");
|
|
15169
|
+
let cursor = input;
|
|
15170
|
+
for (const part of parts.slice(0, -1)) {
|
|
15171
|
+
const next = cursor[part];
|
|
15172
|
+
if (!next || typeof next !== "object")
|
|
15173
|
+
return;
|
|
15174
|
+
cursor = next;
|
|
15175
|
+
}
|
|
15176
|
+
const last = parts.at(-1);
|
|
15177
|
+
if (last && last in cursor)
|
|
15178
|
+
cursor[last] = replacement;
|
|
15179
|
+
}
|
|
14900
15180
|
function createEvent(input) {
|
|
14901
15181
|
return {
|
|
14902
15182
|
id: input.id ?? randomUUID22(),
|
|
@@ -14917,10 +15197,18 @@ class EventsClient {
|
|
|
14917
15197
|
store;
|
|
14918
15198
|
redactors;
|
|
14919
15199
|
transportOptions;
|
|
15200
|
+
catalog;
|
|
15201
|
+
validateCatalogTypes;
|
|
14920
15202
|
constructor(options = {}) {
|
|
14921
15203
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
14922
15204
|
this.redactors = options.redactors ?? [];
|
|
14923
|
-
this.transportOptions = {
|
|
15205
|
+
this.transportOptions = {
|
|
15206
|
+
fetchImpl: options.fetchImpl,
|
|
15207
|
+
secretResolver: options.secretResolver,
|
|
15208
|
+
now: options.now
|
|
15209
|
+
};
|
|
15210
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
15211
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
14924
15212
|
}
|
|
14925
15213
|
async addChannel(input) {
|
|
14926
15214
|
const timestamp2 = new Date().toISOString();
|
|
@@ -14938,18 +15226,40 @@ class EventsClient {
|
|
|
14938
15226
|
}
|
|
14939
15227
|
async emit(input, options = {}) {
|
|
14940
15228
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
14941
|
-
if (options.
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
14949
|
-
return { event, deliveries, deduped: false };
|
|
14950
|
-
}
|
|
14951
|
-
async listEvents() {
|
|
14952
|
-
|
|
15229
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
15230
|
+
this.catalog.assertEventValid(event);
|
|
15231
|
+
}
|
|
15232
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
15233
|
+
if (append.deduped) {
|
|
15234
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
15235
|
+
}
|
|
15236
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
15237
|
+
return { event: append.event, deliveries, deduped: false };
|
|
15238
|
+
}
|
|
15239
|
+
async listEvents(options = {}) {
|
|
15240
|
+
if (Object.keys(options).length === 0)
|
|
15241
|
+
return this.store.listEvents();
|
|
15242
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
15243
|
+
}
|
|
15244
|
+
async listEventsPage(options = {}) {
|
|
15245
|
+
if (this.store.listEventsPage)
|
|
15246
|
+
return this.store.listEventsPage(options);
|
|
15247
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
15248
|
+
eventId: options.eventId,
|
|
15249
|
+
source: options.source,
|
|
15250
|
+
type: options.type
|
|
15251
|
+
});
|
|
15252
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
15253
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
15254
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
15255
|
+
const nextOffset = offset + pageEvents.length;
|
|
15256
|
+
const hasMore = nextOffset < events.length;
|
|
15257
|
+
return {
|
|
15258
|
+
events: pageEvents,
|
|
15259
|
+
cursor: options.cursor,
|
|
15260
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
15261
|
+
hasMore
|
|
15262
|
+
};
|
|
14953
15263
|
}
|
|
14954
15264
|
async listDeliveries() {
|
|
14955
15265
|
return this.store.listDeliveries();
|
|
@@ -15017,22 +15327,37 @@ class EventsClient {
|
|
|
15017
15327
|
return result;
|
|
15018
15328
|
}
|
|
15019
15329
|
async replay(options = {}) {
|
|
15020
|
-
const
|
|
15021
|
-
if (options.eventId && event.id !== options.eventId)
|
|
15022
|
-
return false;
|
|
15023
|
-
if (options.source && event.source !== options.source)
|
|
15024
|
-
return false;
|
|
15025
|
-
if (options.type && event.type !== options.type)
|
|
15026
|
-
return false;
|
|
15027
|
-
return true;
|
|
15028
|
-
});
|
|
15330
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
15029
15331
|
if (options.dryRun)
|
|
15030
|
-
return { events, deliveries: [] };
|
|
15332
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
15031
15333
|
const deliveries = [];
|
|
15032
|
-
for (const event of events) {
|
|
15334
|
+
for (const event of page.events) {
|
|
15033
15335
|
deliveries.push(...await this.deliver(event));
|
|
15034
15336
|
}
|
|
15035
|
-
return { events, deliveries };
|
|
15337
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
15338
|
+
}
|
|
15339
|
+
async appendEvent(event, options) {
|
|
15340
|
+
if (this.store.appendEventOnce) {
|
|
15341
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
15342
|
+
}
|
|
15343
|
+
if (options.dedupe) {
|
|
15344
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
15345
|
+
if (existing) {
|
|
15346
|
+
return {
|
|
15347
|
+
event: existing,
|
|
15348
|
+
stored: false,
|
|
15349
|
+
deduped: true,
|
|
15350
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
15351
|
+
};
|
|
15352
|
+
}
|
|
15353
|
+
}
|
|
15354
|
+
const stored = await this.store.appendEvent(event);
|
|
15355
|
+
return {
|
|
15356
|
+
event: stored,
|
|
15357
|
+
stored: true,
|
|
15358
|
+
deduped: false,
|
|
15359
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
15360
|
+
};
|
|
15036
15361
|
}
|
|
15037
15362
|
async applyRedaction(event, channel) {
|
|
15038
15363
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -15059,43 +15384,19 @@ class EventsClient {
|
|
|
15059
15384
|
return createDeliveryResult(event, channel, attempts);
|
|
15060
15385
|
}
|
|
15061
15386
|
}
|
|
15062
|
-
function
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
15076
|
-
}
|
|
15077
|
-
function redactValue2(value, replacement) {
|
|
15078
|
-
if (Array.isArray(value))
|
|
15079
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
15080
|
-
if (!value || typeof value !== "object")
|
|
15081
|
-
return value;
|
|
15082
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
15083
|
-
key,
|
|
15084
|
-
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
15085
|
-
]));
|
|
15086
|
-
}
|
|
15087
|
-
function setPath(input, path, replacement) {
|
|
15088
|
-
const parts = path.split(".");
|
|
15089
|
-
let cursor = input;
|
|
15090
|
-
for (const part of parts.slice(0, -1)) {
|
|
15091
|
-
const next = cursor[part];
|
|
15092
|
-
if (!next || typeof next !== "object")
|
|
15093
|
-
return;
|
|
15094
|
-
cursor = next;
|
|
15095
|
-
}
|
|
15096
|
-
const last = parts.at(-1);
|
|
15097
|
-
if (last && last in cursor)
|
|
15098
|
-
cursor[last] = replacement;
|
|
15387
|
+
function queryClientEvents(events, options) {
|
|
15388
|
+
let rows = events;
|
|
15389
|
+
if (options.eventId)
|
|
15390
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
15391
|
+
if (options.source)
|
|
15392
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
15393
|
+
if (options.type)
|
|
15394
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
15395
|
+
if (options.cursor)
|
|
15396
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
15397
|
+
if (options.limit !== undefined)
|
|
15398
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
15399
|
+
return rows;
|
|
15099
15400
|
}
|
|
15100
15401
|
function normalizeTime(value) {
|
|
15101
15402
|
if (!value)
|
|
@@ -15109,9 +15410,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
15109
15410
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
15110
15411
|
};
|
|
15111
15412
|
}
|
|
15112
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
15413
|
+
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;
|
|
15113
15414
|
var init_dist = __esm(() => {
|
|
15114
15415
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
15416
|
+
EventValidationError = class EventValidationError extends Error {
|
|
15417
|
+
eventType;
|
|
15418
|
+
issues;
|
|
15419
|
+
constructor(eventType, issues) {
|
|
15420
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
15421
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
15422
|
+
this.name = "EventValidationError";
|
|
15423
|
+
this.eventType = eventType;
|
|
15424
|
+
this.issues = issues;
|
|
15425
|
+
}
|
|
15426
|
+
};
|
|
15427
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
15428
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
15115
15429
|
});
|
|
15116
15430
|
|
|
15117
15431
|
// src/db/task-lists.ts
|
|
@@ -16166,8 +16480,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
16166
16480
|
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
16167
16481
|
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
16168
16482
|
}
|
|
16169
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
16483
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
|
|
16170
16484
|
const respText = await resp.text().catch(() => "");
|
|
16485
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
16486
|
+
logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
|
|
16487
|
+
return;
|
|
16488
|
+
}
|
|
16171
16489
|
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
16172
16490
|
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
16173
16491
|
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
@@ -16976,7 +17294,7 @@ function getTaskGraph(taskId, direction = "both", db) {
|
|
|
16976
17294
|
const deps = getTaskDependencies(t.id, d);
|
|
16977
17295
|
const hasUnfinishedDeps = deps.some((dep) => {
|
|
16978
17296
|
const depTask = getTask(dep.depends_on, d);
|
|
16979
|
-
return depTask && depTask.status
|
|
17297
|
+
return depTask && isBlockingDependencyStatus(depTask.status);
|
|
16980
17298
|
});
|
|
16981
17299
|
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
16982
17300
|
}
|
|
@@ -17236,7 +17554,7 @@ function getBlockingDeps(id, db) {
|
|
|
17236
17554
|
const blocking = [];
|
|
17237
17555
|
for (const dep of deps) {
|
|
17238
17556
|
const task = getTask(dep.depends_on, d);
|
|
17239
|
-
if (task && task.status
|
|
17557
|
+
if (task && isBlockingDependencyStatus(task.status))
|
|
17240
17558
|
blocking.push(task);
|
|
17241
17559
|
}
|
|
17242
17560
|
return blocking;
|
|
@@ -17560,7 +17878,7 @@ function getNextTask(agentId, filters, db) {
|
|
|
17560
17878
|
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
17561
17879
|
params.push(...filters.tags);
|
|
17562
17880
|
}
|
|
17563
|
-
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
|
|
17881
|
+
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'))");
|
|
17564
17882
|
const where = conditions.join(" AND ");
|
|
17565
17883
|
let recentProjectIds = [];
|
|
17566
17884
|
const assignedAliasParams = [];
|
|
@@ -17604,7 +17922,7 @@ function getActiveWork(filters, db) {
|
|
|
17604
17922
|
}
|
|
17605
17923
|
function getTasksChangedSince(since, filters, db) {
|
|
17606
17924
|
const d = db || getDatabase();
|
|
17607
|
-
const conditions = ["updated_at > ?"];
|
|
17925
|
+
const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
|
|
17608
17926
|
const params = [since];
|
|
17609
17927
|
if (filters?.project_id) {
|
|
17610
17928
|
conditions.push("project_id = ?");
|
|
@@ -26031,7 +26349,7 @@ function matchesExtraFilters(task, filter) {
|
|
|
26031
26349
|
}
|
|
26032
26350
|
if (filter.tags?.length) {
|
|
26033
26351
|
const taskTags = new Set(task.tags ?? []);
|
|
26034
|
-
if (!filter.tags.
|
|
26352
|
+
if (!filter.tags.some((tag) => taskTags.has(tag)))
|
|
26035
26353
|
return false;
|
|
26036
26354
|
}
|
|
26037
26355
|
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|
|
@@ -32542,11 +32860,18 @@ class PostgresJsonRecordStore {
|
|
|
32542
32860
|
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
32543
32861
|
}
|
|
32544
32862
|
async ensureSchema() {
|
|
32545
|
-
this.schemaReady
|
|
32546
|
-
|
|
32547
|
-
await
|
|
32548
|
-
|
|
32549
|
-
|
|
32863
|
+
if (!this.schemaReady) {
|
|
32864
|
+
this.schemaReady = (async () => {
|
|
32865
|
+
await retryOnTransientPostgresError(async () => {
|
|
32866
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
32867
|
+
await this.options.client.query(sql);
|
|
32868
|
+
}
|
|
32869
|
+
});
|
|
32870
|
+
})().catch((error) => {
|
|
32871
|
+
this.schemaReady = null;
|
|
32872
|
+
throw error;
|
|
32873
|
+
});
|
|
32874
|
+
}
|
|
32550
32875
|
await this.schemaReady;
|
|
32551
32876
|
}
|
|
32552
32877
|
async get(type, id) {
|
|
@@ -32955,13 +33280,14 @@ class PostgresJsonRecordStore {
|
|
|
32955
33280
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
32956
33281
|
}
|
|
32957
33282
|
async withTaskParentIntegrityTransaction(fn) {
|
|
32958
|
-
|
|
33283
|
+
const transaction = this.options.client.transaction;
|
|
33284
|
+
if (typeof transaction !== "function") {
|
|
32959
33285
|
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
32960
33286
|
}
|
|
32961
|
-
return
|
|
33287
|
+
return retryOnTransientPostgresError(() => transaction(async (client) => {
|
|
32962
33288
|
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
32963
33289
|
return fn(client);
|
|
32964
|
-
});
|
|
33290
|
+
}));
|
|
32965
33291
|
}
|
|
32966
33292
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
32967
33293
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
@@ -34308,7 +34634,7 @@ async function getActiveWork2(filters, store) {
|
|
|
34308
34634
|
}));
|
|
34309
34635
|
}
|
|
34310
34636
|
async function getChangedSince(since, filters, store) {
|
|
34311
|
-
return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at
|
|
34637
|
+
return (await listTasks2(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
|
|
34312
34638
|
}
|
|
34313
34639
|
async function createProject2(input, store, context) {
|
|
34314
34640
|
const timestamp2 = new Date().toISOString();
|
|
@@ -34844,6 +35170,35 @@ function compareClock(left, right) {
|
|
|
34844
35170
|
function numberValue2(value) {
|
|
34845
35171
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
34846
35172
|
}
|
|
35173
|
+
function isTransientPostgresError(error) {
|
|
35174
|
+
if (typeof error !== "object" || error === null)
|
|
35175
|
+
return false;
|
|
35176
|
+
const candidate = error;
|
|
35177
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
35178
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
35179
|
+
const cause = candidate.cause;
|
|
35180
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
35181
|
+
}
|
|
35182
|
+
if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
|
|
35183
|
+
return true;
|
|
35184
|
+
}
|
|
35185
|
+
const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
|
|
35186
|
+
return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
|
|
35187
|
+
}
|
|
35188
|
+
async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
35189
|
+
let lastError;
|
|
35190
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
35191
|
+
try {
|
|
35192
|
+
return await fn();
|
|
35193
|
+
} catch (error) {
|
|
35194
|
+
lastError = error;
|
|
35195
|
+
if (!isTransientPostgresError(error) || attempt === attempts)
|
|
35196
|
+
throw error;
|
|
35197
|
+
await new Promise((resolve14) => setTimeout(resolve14, delayMs * attempt));
|
|
35198
|
+
}
|
|
35199
|
+
}
|
|
35200
|
+
throw lastError;
|
|
35201
|
+
}
|
|
34847
35202
|
function isPostgresUniqueViolation(error) {
|
|
34848
35203
|
if (typeof error !== "object" || error === null)
|
|
34849
35204
|
return false;
|
|
@@ -34863,10 +35218,11 @@ function postgresConstraintName(error) {
|
|
|
34863
35218
|
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
34864
35219
|
return typeof constraint === "string" ? constraint : "";
|
|
34865
35220
|
}
|
|
34866
|
-
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;
|
|
35221
|
+
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;
|
|
34867
35222
|
var init_postgres_adapter = __esm(() => {
|
|
34868
35223
|
init_types();
|
|
34869
35224
|
init_creator_identity();
|
|
35225
|
+
init_instant_compare();
|
|
34870
35226
|
init_plan_project_link_contract();
|
|
34871
35227
|
init_stale_lock_handoff();
|
|
34872
35228
|
init_postgres_sync();
|
|
@@ -34876,6 +35232,12 @@ var init_postgres_adapter = __esm(() => {
|
|
|
34876
35232
|
init_audit_history_import();
|
|
34877
35233
|
init_canonical();
|
|
34878
35234
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
35235
|
+
TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
|
|
35236
|
+
TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
|
|
35237
|
+
"canceling statement due to lock timeout",
|
|
35238
|
+
"deadlock detected",
|
|
35239
|
+
"could not serialize access"
|
|
35240
|
+
];
|
|
34879
35241
|
});
|
|
34880
35242
|
|
|
34881
35243
|
// src/project-registration/postgres.ts
|
|
@@ -35238,14 +35600,23 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
35238
35600
|
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
35239
35601
|
}
|
|
35240
35602
|
async ensureSchema() {
|
|
35241
|
-
this.schemaReady
|
|
35242
|
-
|
|
35243
|
-
|
|
35244
|
-
|
|
35245
|
-
|
|
35246
|
-
|
|
35603
|
+
if (this.schemaReady === null) {
|
|
35604
|
+
const attempt = (async () => {
|
|
35605
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
35606
|
+
await this.client.query(statement);
|
|
35607
|
+
}
|
|
35608
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
35609
|
+
await this.client.query(statement);
|
|
35610
|
+
}
|
|
35611
|
+
})();
|
|
35612
|
+
this.schemaReady = attempt;
|
|
35613
|
+
try {
|
|
35614
|
+
await attempt;
|
|
35615
|
+
} catch (error) {
|
|
35616
|
+
this.schemaReady = null;
|
|
35617
|
+
throw error;
|
|
35247
35618
|
}
|
|
35248
|
-
}
|
|
35619
|
+
}
|
|
35249
35620
|
await this.schemaReady;
|
|
35250
35621
|
}
|
|
35251
35622
|
async transaction(fn) {
|
|
@@ -43754,10 +44125,19 @@ class PostgresPrGroupLedgerPersistence {
|
|
|
43754
44125
|
this.client = client;
|
|
43755
44126
|
}
|
|
43756
44127
|
async ensureSchema() {
|
|
43757
|
-
this.schemaReady
|
|
43758
|
-
|
|
43759
|
-
|
|
43760
|
-
|
|
44128
|
+
if (this.schemaReady === null) {
|
|
44129
|
+
const attempt = (async () => {
|
|
44130
|
+
for (const statement of postgresPrGroupSchemaSql())
|
|
44131
|
+
await this.client.query(statement);
|
|
44132
|
+
})();
|
|
44133
|
+
this.schemaReady = attempt;
|
|
44134
|
+
try {
|
|
44135
|
+
await attempt;
|
|
44136
|
+
} catch (error) {
|
|
44137
|
+
this.schemaReady = null;
|
|
44138
|
+
throw error;
|
|
44139
|
+
}
|
|
44140
|
+
}
|
|
43761
44141
|
return this.schemaReady;
|
|
43762
44142
|
}
|
|
43763
44143
|
async transaction(fn) {
|
|
@@ -49131,12 +49511,21 @@ class PostgresTodosTaskManifestBackend {
|
|
|
49131
49511
|
this.tenantId = options.tenantId ?? "default";
|
|
49132
49512
|
}
|
|
49133
49513
|
async ensureSchema() {
|
|
49134
|
-
this.schemaReady
|
|
49135
|
-
|
|
49136
|
-
|
|
49137
|
-
|
|
49138
|
-
|
|
49139
|
-
|
|
49514
|
+
if (this.schemaReady === null) {
|
|
49515
|
+
const attempt = (async () => {
|
|
49516
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
49517
|
+
await this.client.query(sql);
|
|
49518
|
+
for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
|
|
49519
|
+
await this.client.query(sql);
|
|
49520
|
+
})();
|
|
49521
|
+
this.schemaReady = attempt;
|
|
49522
|
+
try {
|
|
49523
|
+
await attempt;
|
|
49524
|
+
} catch (error) {
|
|
49525
|
+
this.schemaReady = null;
|
|
49526
|
+
throw error;
|
|
49527
|
+
}
|
|
49528
|
+
}
|
|
49140
49529
|
await this.schemaReady;
|
|
49141
49530
|
}
|
|
49142
49531
|
async insertSync(tx, objectType2, objectId, payload, now4) {
|
|
@@ -50475,12 +50864,21 @@ class PostgresTodosTaskSubtreeTransferBackend {
|
|
|
50475
50864
|
this.tenantId = options.tenantId ?? "default";
|
|
50476
50865
|
}
|
|
50477
50866
|
async ensureSchema() {
|
|
50478
|
-
this.schemaReady
|
|
50479
|
-
|
|
50480
|
-
|
|
50481
|
-
|
|
50482
|
-
|
|
50483
|
-
|
|
50867
|
+
if (this.schemaReady === null) {
|
|
50868
|
+
const attempt = (async () => {
|
|
50869
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
50870
|
+
await this.client.query(sql);
|
|
50871
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
50872
|
+
await this.client.query(sql);
|
|
50873
|
+
})();
|
|
50874
|
+
this.schemaReady = attempt;
|
|
50875
|
+
try {
|
|
50876
|
+
await attempt;
|
|
50877
|
+
} catch (error) {
|
|
50878
|
+
this.schemaReady = null;
|
|
50879
|
+
throw error;
|
|
50880
|
+
}
|
|
50881
|
+
}
|
|
50484
50882
|
await this.schemaReady;
|
|
50485
50883
|
}
|
|
50486
50884
|
async snapshot(client, input, forUpdate = false) {
|
|
@@ -51909,7 +52307,10 @@ async function ensureCloudSchema() {
|
|
|
51909
52307
|
await client.query(sql);
|
|
51910
52308
|
}
|
|
51911
52309
|
await getApiKeyStore().ensureSchema();
|
|
51912
|
-
})()
|
|
52310
|
+
})().catch((error) => {
|
|
52311
|
+
schemaEnsured = null;
|
|
52312
|
+
throw error;
|
|
52313
|
+
});
|
|
51913
52314
|
return schemaEnsured;
|
|
51914
52315
|
}
|
|
51915
52316
|
async function ensureCloudCommentCursorIndex() {
|
|
@@ -70451,7 +70852,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
70451
70852
|
const blockers = d.query(`SELECT dep.id, dep.short_id, dep.title, dep.status
|
|
70452
70853
|
FROM task_dependencies td
|
|
70453
70854
|
JOIN tasks dep ON dep.id = td.depends_on
|
|
70454
|
-
WHERE td.task_id = ? AND dep.status
|
|
70855
|
+
WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).all(task2.id);
|
|
70455
70856
|
return { id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), blockers };
|
|
70456
70857
|
}).filter((entry2) => entry2.blockers.length > 0);
|
|
70457
70858
|
const overdue = tasks.filter((task2) => activeTaskIds.has(task2.id) && Boolean(task2.due_at && task2.due_at < generatedAt)).map((task2) => ({ id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), due_at: task2.due_at }));
|