@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
|
@@ -6607,8 +6607,9 @@ var init_event_hooks = __esm(() => {
|
|
|
6607
6607
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
6608
6608
|
});
|
|
6609
6609
|
|
|
6610
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
6610
|
+
// node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
|
|
6611
6611
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6612
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
6612
6613
|
import { existsSync as existsSync6 } from "fs";
|
|
6613
6614
|
import { homedir as homedir2 } from "os";
|
|
6614
6615
|
import { join as join4 } from "path";
|
|
@@ -6717,11 +6718,13 @@ function getEventsDataDir(override) {
|
|
|
6717
6718
|
|
|
6718
6719
|
class JsonEventsStore {
|
|
6719
6720
|
dataDir;
|
|
6721
|
+
runtime;
|
|
6720
6722
|
channelsPath;
|
|
6721
6723
|
eventsPath;
|
|
6722
6724
|
deliveriesPath;
|
|
6723
6725
|
constructor(dataDir = getEventsDataDir()) {
|
|
6724
6726
|
this.dataDir = dataDir;
|
|
6727
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
6725
6728
|
this.channelsPath = join4(dataDir, "channels.json");
|
|
6726
6729
|
this.eventsPath = join4(dataDir, "events.json");
|
|
6727
6730
|
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
@@ -6769,13 +6772,58 @@ class JsonEventsStore {
|
|
|
6769
6772
|
await this.writeJson(this.eventsPath, events);
|
|
6770
6773
|
return event;
|
|
6771
6774
|
}
|
|
6772
|
-
async
|
|
6775
|
+
async appendEventOnce(event, options = {}) {
|
|
6773
6776
|
await this.init();
|
|
6774
|
-
|
|
6777
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6778
|
+
const dedupe = options.dedupe !== false;
|
|
6779
|
+
if (dedupe) {
|
|
6780
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
6781
|
+
if (existing) {
|
|
6782
|
+
return {
|
|
6783
|
+
event: existing,
|
|
6784
|
+
stored: false,
|
|
6785
|
+
deduped: true,
|
|
6786
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
6787
|
+
};
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
events.push(event);
|
|
6791
|
+
await this.writeJson(this.eventsPath, events);
|
|
6792
|
+
return {
|
|
6793
|
+
event,
|
|
6794
|
+
stored: true,
|
|
6795
|
+
deduped: false,
|
|
6796
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
6797
|
+
};
|
|
6798
|
+
}
|
|
6799
|
+
async listEvents(options = {}) {
|
|
6800
|
+
await this.init();
|
|
6801
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6802
|
+
return queryEvents(events, options);
|
|
6803
|
+
}
|
|
6804
|
+
async listEventsPage(options = {}) {
|
|
6805
|
+
await this.init();
|
|
6806
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6807
|
+
const queried = queryEvents(events, {
|
|
6808
|
+
eventId: options.eventId,
|
|
6809
|
+
source: options.source,
|
|
6810
|
+
type: options.type
|
|
6811
|
+
});
|
|
6812
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6813
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
6814
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
6815
|
+
const nextOffset = offset + pageEvents.length;
|
|
6816
|
+
const hasMore = nextOffset < queried.length;
|
|
6817
|
+
return {
|
|
6818
|
+
events: pageEvents,
|
|
6819
|
+
cursor: options.cursor,
|
|
6820
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
6821
|
+
hasMore
|
|
6822
|
+
};
|
|
6775
6823
|
}
|
|
6776
6824
|
async findEventByIdentity(identity) {
|
|
6777
6825
|
const events = await this.listEvents();
|
|
6778
|
-
return events
|
|
6826
|
+
return findEventByIdentity(events, identity);
|
|
6779
6827
|
}
|
|
6780
6828
|
async appendDelivery(result) {
|
|
6781
6829
|
await this.init();
|
|
@@ -6826,6 +6874,83 @@ class JsonEventsStore {
|
|
|
6826
6874
|
});
|
|
6827
6875
|
}
|
|
6828
6876
|
}
|
|
6877
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
6878
|
+
return {
|
|
6879
|
+
mode: "local-files",
|
|
6880
|
+
name: "json-events-store",
|
|
6881
|
+
remote: false,
|
|
6882
|
+
localFiles: true,
|
|
6883
|
+
localSqlite: false,
|
|
6884
|
+
postgres: false,
|
|
6885
|
+
s3: false,
|
|
6886
|
+
aws: false,
|
|
6887
|
+
durable: true,
|
|
6888
|
+
idempotency: "best-effort-local",
|
|
6889
|
+
replayCursors: true,
|
|
6890
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6893
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
6894
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6895
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
6896
|
+
const payload = {
|
|
6897
|
+
offset,
|
|
6898
|
+
eventId: options.eventId,
|
|
6899
|
+
source: options.source,
|
|
6900
|
+
type: options.type
|
|
6901
|
+
};
|
|
6902
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
6903
|
+
}
|
|
6904
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
6905
|
+
if (!cursor)
|
|
6906
|
+
return 0;
|
|
6907
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
6908
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6909
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
6910
|
+
let payload;
|
|
6911
|
+
try {
|
|
6912
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
6913
|
+
} catch {
|
|
6914
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6915
|
+
}
|
|
6916
|
+
const offset = payload.offset;
|
|
6917
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6918
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6919
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
6920
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
6921
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
6922
|
+
return offset;
|
|
6923
|
+
}
|
|
6924
|
+
function normalizeEventPageLimit(limit) {
|
|
6925
|
+
if (limit === undefined)
|
|
6926
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
6927
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
6928
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
6929
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
6930
|
+
}
|
|
6931
|
+
function queryEvents(events, options) {
|
|
6932
|
+
let rows = events;
|
|
6933
|
+
if (options.eventId)
|
|
6934
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
6935
|
+
if (options.source)
|
|
6936
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
6937
|
+
if (options.type)
|
|
6938
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
6939
|
+
if (options.cursor) {
|
|
6940
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6941
|
+
rows = rows.slice(offset);
|
|
6942
|
+
}
|
|
6943
|
+
if (options.limit !== undefined)
|
|
6944
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
6945
|
+
return rows;
|
|
6946
|
+
}
|
|
6947
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
6948
|
+
if (cursorValue !== optionValue)
|
|
6949
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
6950
|
+
}
|
|
6951
|
+
function findEventByIdentity(events, identity) {
|
|
6952
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
6953
|
+
}
|
|
6829
6954
|
function buildSignatureBase(timestamp2, body) {
|
|
6830
6955
|
return `${timestamp2}.${body}`;
|
|
6831
6956
|
}
|
|
@@ -6839,21 +6964,27 @@ function now2() {
|
|
|
6839
6964
|
function truncate(value, max = 4096) {
|
|
6840
6965
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
6841
6966
|
}
|
|
6842
|
-
function buildWebhookRequest(event, channel) {
|
|
6967
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
6843
6968
|
if (!channel.webhook)
|
|
6844
6969
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6970
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
6971
|
+
if (/^x-hasna-/i.test(name)) {
|
|
6972
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6845
6975
|
const body = JSON.stringify(event);
|
|
6846
|
-
const timestamp2 =
|
|
6976
|
+
const timestamp2 = options.timestamp ?? new Date().toISOString();
|
|
6847
6977
|
const headers = {
|
|
6848
6978
|
"Content-Type": "application/json",
|
|
6849
6979
|
"User-Agent": "@hasna/events",
|
|
6850
6980
|
"X-Hasna-Event-Id": event.id,
|
|
6851
6981
|
"X-Hasna-Event-Type": event.type,
|
|
6852
|
-
|
|
6853
|
-
|
|
6982
|
+
...channel.webhook.headers,
|
|
6983
|
+
"X-Hasna-Timestamp": timestamp2
|
|
6854
6984
|
};
|
|
6855
|
-
|
|
6856
|
-
|
|
6985
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
6986
|
+
if (secret) {
|
|
6987
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
|
|
6857
6988
|
}
|
|
6858
6989
|
return { body, headers };
|
|
6859
6990
|
}
|
|
@@ -6861,7 +6992,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6861
6992
|
if (!channel.webhook)
|
|
6862
6993
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6863
6994
|
const startedAt = now2();
|
|
6864
|
-
|
|
6995
|
+
let secret = channel.webhook.secret;
|
|
6996
|
+
if (channel.webhook.secretRef) {
|
|
6997
|
+
if (!options.secretResolver) {
|
|
6998
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
6999
|
+
}
|
|
7000
|
+
try {
|
|
7001
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
7002
|
+
} catch {
|
|
7003
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7004
|
+
}
|
|
7005
|
+
if (!secret)
|
|
7006
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
7007
|
+
}
|
|
7008
|
+
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
7009
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
6865
7010
|
const controller = new AbortController;
|
|
6866
7011
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6867
7012
|
try {
|
|
@@ -6893,6 +7038,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6893
7038
|
clearTimeout(timeout);
|
|
6894
7039
|
}
|
|
6895
7040
|
}
|
|
7041
|
+
function failedAttempt(startedAt, error) {
|
|
7042
|
+
return {
|
|
7043
|
+
attempt: 1,
|
|
7044
|
+
status: "failed",
|
|
7045
|
+
startedAt,
|
|
7046
|
+
completedAt: now2(),
|
|
7047
|
+
error
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
6896
7050
|
async function dispatchCommand(event, channel) {
|
|
6897
7051
|
if (!channel.command)
|
|
6898
7052
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -6981,6 +7135,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
6981
7135
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
6982
7136
|
};
|
|
6983
7137
|
}
|
|
7138
|
+
|
|
7139
|
+
class EventTypeCatalog {
|
|
7140
|
+
definitions = new Map;
|
|
7141
|
+
register(definition) {
|
|
7142
|
+
this.definitions.set(definition.type, definition);
|
|
7143
|
+
return this;
|
|
7144
|
+
}
|
|
7145
|
+
unregister(type) {
|
|
7146
|
+
return this.definitions.delete(type);
|
|
7147
|
+
}
|
|
7148
|
+
has(type) {
|
|
7149
|
+
return this.definitions.has(type);
|
|
7150
|
+
}
|
|
7151
|
+
get(type) {
|
|
7152
|
+
return this.definitions.get(type);
|
|
7153
|
+
}
|
|
7154
|
+
list() {
|
|
7155
|
+
return [...this.definitions.values()];
|
|
7156
|
+
}
|
|
7157
|
+
validateEvent(event) {
|
|
7158
|
+
const definition = this.definitions.get(event.type);
|
|
7159
|
+
if (!definition)
|
|
7160
|
+
return { ok: true };
|
|
7161
|
+
return definition.validate(event.data, event);
|
|
7162
|
+
}
|
|
7163
|
+
assertEventValid(event) {
|
|
7164
|
+
const result = this.validateEvent(event);
|
|
7165
|
+
if (!result.ok) {
|
|
7166
|
+
throw new EventValidationError(event.type, result.issues);
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
7171
|
+
if (paths.length === 0)
|
|
7172
|
+
return event;
|
|
7173
|
+
const copy = structuredClone(event);
|
|
7174
|
+
for (const path of paths) {
|
|
7175
|
+
setPath(copy, path, replacement);
|
|
7176
|
+
}
|
|
7177
|
+
return copy;
|
|
7178
|
+
}
|
|
7179
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
7180
|
+
return redactValue2(event, replacement);
|
|
7181
|
+
}
|
|
7182
|
+
function shouldRedactKey(key) {
|
|
7183
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
7184
|
+
}
|
|
7185
|
+
function redactValue2(value, replacement) {
|
|
7186
|
+
if (Array.isArray(value))
|
|
7187
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
7188
|
+
if (!value || typeof value !== "object")
|
|
7189
|
+
return value;
|
|
7190
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
7191
|
+
key,
|
|
7192
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
7193
|
+
]));
|
|
7194
|
+
}
|
|
7195
|
+
function setPath(input, path, replacement) {
|
|
7196
|
+
const parts = path.split(".");
|
|
7197
|
+
let cursor = input;
|
|
7198
|
+
for (const part of parts.slice(0, -1)) {
|
|
7199
|
+
const next = cursor[part];
|
|
7200
|
+
if (!next || typeof next !== "object")
|
|
7201
|
+
return;
|
|
7202
|
+
cursor = next;
|
|
7203
|
+
}
|
|
7204
|
+
const last = parts.at(-1);
|
|
7205
|
+
if (last && last in cursor)
|
|
7206
|
+
cursor[last] = replacement;
|
|
7207
|
+
}
|
|
6984
7208
|
function createEvent(input) {
|
|
6985
7209
|
return {
|
|
6986
7210
|
id: input.id ?? randomUUID22(),
|
|
@@ -7001,10 +7225,18 @@ class EventsClient {
|
|
|
7001
7225
|
store;
|
|
7002
7226
|
redactors;
|
|
7003
7227
|
transportOptions;
|
|
7228
|
+
catalog;
|
|
7229
|
+
validateCatalogTypes;
|
|
7004
7230
|
constructor(options = {}) {
|
|
7005
7231
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
7006
7232
|
this.redactors = options.redactors ?? [];
|
|
7007
|
-
this.transportOptions = {
|
|
7233
|
+
this.transportOptions = {
|
|
7234
|
+
fetchImpl: options.fetchImpl,
|
|
7235
|
+
secretResolver: options.secretResolver,
|
|
7236
|
+
now: options.now
|
|
7237
|
+
};
|
|
7238
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
7239
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
7008
7240
|
}
|
|
7009
7241
|
async addChannel(input) {
|
|
7010
7242
|
const timestamp2 = new Date().toISOString();
|
|
@@ -7022,18 +7254,40 @@ class EventsClient {
|
|
|
7022
7254
|
}
|
|
7023
7255
|
async emit(input, options = {}) {
|
|
7024
7256
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
7025
|
-
if (options.
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
7033
|
-
return { event, deliveries, deduped: false };
|
|
7034
|
-
}
|
|
7035
|
-
async listEvents() {
|
|
7036
|
-
|
|
7257
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
7258
|
+
this.catalog.assertEventValid(event);
|
|
7259
|
+
}
|
|
7260
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
7261
|
+
if (append.deduped) {
|
|
7262
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
7263
|
+
}
|
|
7264
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
7265
|
+
return { event: append.event, deliveries, deduped: false };
|
|
7266
|
+
}
|
|
7267
|
+
async listEvents(options = {}) {
|
|
7268
|
+
if (Object.keys(options).length === 0)
|
|
7269
|
+
return this.store.listEvents();
|
|
7270
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
7271
|
+
}
|
|
7272
|
+
async listEventsPage(options = {}) {
|
|
7273
|
+
if (this.store.listEventsPage)
|
|
7274
|
+
return this.store.listEventsPage(options);
|
|
7275
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
7276
|
+
eventId: options.eventId,
|
|
7277
|
+
source: options.source,
|
|
7278
|
+
type: options.type
|
|
7279
|
+
});
|
|
7280
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
7281
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
7282
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
7283
|
+
const nextOffset = offset + pageEvents.length;
|
|
7284
|
+
const hasMore = nextOffset < events.length;
|
|
7285
|
+
return {
|
|
7286
|
+
events: pageEvents,
|
|
7287
|
+
cursor: options.cursor,
|
|
7288
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
7289
|
+
hasMore
|
|
7290
|
+
};
|
|
7037
7291
|
}
|
|
7038
7292
|
async listDeliveries() {
|
|
7039
7293
|
return this.store.listDeliveries();
|
|
@@ -7101,22 +7355,37 @@ class EventsClient {
|
|
|
7101
7355
|
return result;
|
|
7102
7356
|
}
|
|
7103
7357
|
async replay(options = {}) {
|
|
7104
|
-
const
|
|
7105
|
-
if (options.eventId && event.id !== options.eventId)
|
|
7106
|
-
return false;
|
|
7107
|
-
if (options.source && event.source !== options.source)
|
|
7108
|
-
return false;
|
|
7109
|
-
if (options.type && event.type !== options.type)
|
|
7110
|
-
return false;
|
|
7111
|
-
return true;
|
|
7112
|
-
});
|
|
7358
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
7113
7359
|
if (options.dryRun)
|
|
7114
|
-
return { events, deliveries: [] };
|
|
7360
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
7115
7361
|
const deliveries = [];
|
|
7116
|
-
for (const event of events) {
|
|
7362
|
+
for (const event of page.events) {
|
|
7117
7363
|
deliveries.push(...await this.deliver(event));
|
|
7118
7364
|
}
|
|
7119
|
-
return { events, deliveries };
|
|
7365
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
7366
|
+
}
|
|
7367
|
+
async appendEvent(event, options) {
|
|
7368
|
+
if (this.store.appendEventOnce) {
|
|
7369
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
7370
|
+
}
|
|
7371
|
+
if (options.dedupe) {
|
|
7372
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
7373
|
+
if (existing) {
|
|
7374
|
+
return {
|
|
7375
|
+
event: existing,
|
|
7376
|
+
stored: false,
|
|
7377
|
+
deduped: true,
|
|
7378
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
7379
|
+
};
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
const stored = await this.store.appendEvent(event);
|
|
7383
|
+
return {
|
|
7384
|
+
event: stored,
|
|
7385
|
+
stored: true,
|
|
7386
|
+
deduped: false,
|
|
7387
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
7388
|
+
};
|
|
7120
7389
|
}
|
|
7121
7390
|
async applyRedaction(event, channel) {
|
|
7122
7391
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -7143,43 +7412,19 @@ class EventsClient {
|
|
|
7143
7412
|
return createDeliveryResult(event, channel, attempts);
|
|
7144
7413
|
}
|
|
7145
7414
|
}
|
|
7146
|
-
function
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
7160
|
-
}
|
|
7161
|
-
function redactValue2(value, replacement) {
|
|
7162
|
-
if (Array.isArray(value))
|
|
7163
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
7164
|
-
if (!value || typeof value !== "object")
|
|
7165
|
-
return value;
|
|
7166
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
7167
|
-
key,
|
|
7168
|
-
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
7169
|
-
]));
|
|
7170
|
-
}
|
|
7171
|
-
function setPath(input, path, replacement) {
|
|
7172
|
-
const parts = path.split(".");
|
|
7173
|
-
let cursor = input;
|
|
7174
|
-
for (const part of parts.slice(0, -1)) {
|
|
7175
|
-
const next = cursor[part];
|
|
7176
|
-
if (!next || typeof next !== "object")
|
|
7177
|
-
return;
|
|
7178
|
-
cursor = next;
|
|
7179
|
-
}
|
|
7180
|
-
const last = parts.at(-1);
|
|
7181
|
-
if (last && last in cursor)
|
|
7182
|
-
cursor[last] = replacement;
|
|
7415
|
+
function queryClientEvents(events, options) {
|
|
7416
|
+
let rows = events;
|
|
7417
|
+
if (options.eventId)
|
|
7418
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
7419
|
+
if (options.source)
|
|
7420
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
7421
|
+
if (options.type)
|
|
7422
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
7423
|
+
if (options.cursor)
|
|
7424
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
7425
|
+
if (options.limit !== undefined)
|
|
7426
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
7427
|
+
return rows;
|
|
7183
7428
|
}
|
|
7184
7429
|
function normalizeTime(value) {
|
|
7185
7430
|
if (!value)
|
|
@@ -7193,9 +7438,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
7193
7438
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
7194
7439
|
};
|
|
7195
7440
|
}
|
|
7196
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
7441
|
+
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;
|
|
7197
7442
|
var init_dist = __esm(() => {
|
|
7198
7443
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
7444
|
+
EventValidationError = class EventValidationError extends Error {
|
|
7445
|
+
eventType;
|
|
7446
|
+
issues;
|
|
7447
|
+
constructor(eventType, issues) {
|
|
7448
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
7449
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
7450
|
+
this.name = "EventValidationError";
|
|
7451
|
+
this.eventType = eventType;
|
|
7452
|
+
this.issues = issues;
|
|
7453
|
+
}
|
|
7454
|
+
};
|
|
7455
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
7456
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
7199
7457
|
});
|
|
7200
7458
|
|
|
7201
7459
|
// src/db/task-lists.ts
|
|
@@ -8070,8 +8328,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
8070
8328
|
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
8071
8329
|
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8072
8330
|
}
|
|
8073
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
8331
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
|
|
8074
8332
|
const respText = await resp.text().catch(() => "");
|
|
8333
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
8334
|
+
logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
|
|
8335
|
+
return;
|
|
8336
|
+
}
|
|
8075
8337
|
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
8076
8338
|
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
8077
8339
|
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
@@ -8787,7 +9049,7 @@ function getTaskGraph(taskId, direction = "both", db) {
|
|
|
8787
9049
|
const deps = getTaskDependencies(t.id, d);
|
|
8788
9050
|
const hasUnfinishedDeps = deps.some((dep) => {
|
|
8789
9051
|
const depTask = getTask(dep.depends_on, d);
|
|
8790
|
-
return depTask && depTask.status
|
|
9052
|
+
return depTask && isBlockingDependencyStatus(depTask.status);
|
|
8791
9053
|
});
|
|
8792
9054
|
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
8793
9055
|
}
|
|
@@ -8911,7 +9173,7 @@ function getBlockingDeps(id, db) {
|
|
|
8911
9173
|
const blocking = [];
|
|
8912
9174
|
for (const dep of deps) {
|
|
8913
9175
|
const task = getTask(dep.depends_on, d);
|
|
8914
|
-
if (task && task.status
|
|
9176
|
+
if (task && isBlockingDependencyStatus(task.status))
|
|
8915
9177
|
blocking.push(task);
|
|
8916
9178
|
}
|
|
8917
9179
|
return blocking;
|
|
@@ -9235,7 +9497,7 @@ function getNextTask2(agentId, filters, db) {
|
|
|
9235
9497
|
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
9236
9498
|
params.push(...filters.tags);
|
|
9237
9499
|
}
|
|
9238
|
-
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
|
|
9500
|
+
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'))");
|
|
9239
9501
|
const where = conditions.join(" AND ");
|
|
9240
9502
|
let recentProjectIds = [];
|
|
9241
9503
|
const assignedAliasParams = [];
|
|
@@ -9279,7 +9541,7 @@ function getActiveWork2(filters, db) {
|
|
|
9279
9541
|
}
|
|
9280
9542
|
function getTasksChangedSince(since, filters, db) {
|
|
9281
9543
|
const d = db || getDatabase();
|
|
9282
|
-
const conditions = ["updated_at > ?"];
|
|
9544
|
+
const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
|
|
9283
9545
|
const params = [since];
|
|
9284
9546
|
if (filters?.project_id) {
|
|
9285
9547
|
conditions.push("project_id = ?");
|
|
@@ -12812,7 +13074,7 @@ import { createHash as createHash7 } from "crypto";
|
|
|
12812
13074
|
// package.json
|
|
12813
13075
|
var package_default = {
|
|
12814
13076
|
name: "@hasna/todos",
|
|
12815
|
-
version: "0.15.
|
|
13077
|
+
version: "0.15.47",
|
|
12816
13078
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12817
13079
|
type: "module",
|
|
12818
13080
|
main: "dist/index.js",
|
|
@@ -12930,18 +13192,19 @@ var package_default = {
|
|
|
12930
13192
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
12931
13193
|
license: "Apache-2.0",
|
|
12932
13194
|
dependencies: {
|
|
12933
|
-
"@hasna/contracts": "0.13.
|
|
13195
|
+
"@hasna/contracts": "0.13.4",
|
|
12934
13196
|
"@hasna/events": "^0.1.11",
|
|
12935
13197
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
12936
13198
|
chalk: "^5.4.1",
|
|
12937
13199
|
commander: "^13.1.0",
|
|
12938
13200
|
ink: "^5.2.0",
|
|
12939
13201
|
react: "^18.3.1",
|
|
12940
|
-
zod: "
|
|
13202
|
+
zod: "3.25.76"
|
|
12941
13203
|
},
|
|
12942
13204
|
overrides: {
|
|
12943
13205
|
ajv: "8.20.0",
|
|
12944
|
-
"fast-uri": "3.1.2"
|
|
13206
|
+
"fast-uri": "3.1.2",
|
|
13207
|
+
zod: "3.25.76"
|
|
12945
13208
|
},
|
|
12946
13209
|
devDependencies: {
|
|
12947
13210
|
"@types/bun": "^1.2.4",
|
|
@@ -12968,6 +13231,57 @@ function normalizeAgentNameInput(name) {
|
|
|
12968
13231
|
// src/storage/postgres-adapter.ts
|
|
12969
13232
|
init_creator_identity();
|
|
12970
13233
|
|
|
13234
|
+
// src/lib/instant-compare.ts
|
|
13235
|
+
var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
|
|
13236
|
+
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
13237
|
+
function isLeapYear(year) {
|
|
13238
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
13239
|
+
}
|
|
13240
|
+
function sqliteJulianDay(value) {
|
|
13241
|
+
const m = SQLITE_STAMP.exec(value);
|
|
13242
|
+
if (!m)
|
|
13243
|
+
return null;
|
|
13244
|
+
const year = Number(m[1]);
|
|
13245
|
+
const month = Number(m[2]);
|
|
13246
|
+
const day = Number(m[3]);
|
|
13247
|
+
const hour = m[4] === undefined ? 0 : Number(m[4]);
|
|
13248
|
+
const minute = m[5] === undefined ? 0 : Number(m[5]);
|
|
13249
|
+
const second = m[6] === undefined ? 0 : Number(m[6]);
|
|
13250
|
+
const frac = m[7];
|
|
13251
|
+
const sign = m[8];
|
|
13252
|
+
const offsetHour = m[9];
|
|
13253
|
+
const offsetMinute = m[10];
|
|
13254
|
+
if (month < 1 || month > 12)
|
|
13255
|
+
return null;
|
|
13256
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
13257
|
+
return null;
|
|
13258
|
+
const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
|
|
13259
|
+
if (day < 1 || day > maxDay)
|
|
13260
|
+
return null;
|
|
13261
|
+
let offsetMinutes = 0;
|
|
13262
|
+
if (sign !== undefined) {
|
|
13263
|
+
const oh = Number(offsetHour ?? "0");
|
|
13264
|
+
const om = Number(offsetMinute ?? "0");
|
|
13265
|
+
if (oh > 23 || om > 59)
|
|
13266
|
+
return null;
|
|
13267
|
+
offsetMinutes = oh * 60 + om;
|
|
13268
|
+
if (sign === "-")
|
|
13269
|
+
offsetMinutes = -offsetMinutes;
|
|
13270
|
+
}
|
|
13271
|
+
const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
|
|
13272
|
+
const ms = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
13273
|
+
return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
|
|
13274
|
+
}
|
|
13275
|
+
function changedSinceStampNewer(stamp, since) {
|
|
13276
|
+
const stampJd = sqliteJulianDay(stamp);
|
|
13277
|
+
if (stampJd === null)
|
|
13278
|
+
return true;
|
|
13279
|
+
const sinceJd = sqliteJulianDay(since);
|
|
13280
|
+
if (sinceJd === null)
|
|
13281
|
+
return false;
|
|
13282
|
+
return stampJd > sinceJd;
|
|
13283
|
+
}
|
|
13284
|
+
|
|
12971
13285
|
// src/lib/plan-project-link-contract.ts
|
|
12972
13286
|
init_types();
|
|
12973
13287
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -13904,11 +14218,18 @@ class PostgresJsonRecordStore {
|
|
|
13904
14218
|
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
13905
14219
|
}
|
|
13906
14220
|
async ensureSchema() {
|
|
13907
|
-
this.schemaReady
|
|
13908
|
-
|
|
13909
|
-
await
|
|
13910
|
-
|
|
13911
|
-
|
|
14221
|
+
if (!this.schemaReady) {
|
|
14222
|
+
this.schemaReady = (async () => {
|
|
14223
|
+
await retryOnTransientPostgresError(async () => {
|
|
14224
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
14225
|
+
await this.options.client.query(sql);
|
|
14226
|
+
}
|
|
14227
|
+
});
|
|
14228
|
+
})().catch((error) => {
|
|
14229
|
+
this.schemaReady = null;
|
|
14230
|
+
throw error;
|
|
14231
|
+
});
|
|
14232
|
+
}
|
|
13912
14233
|
await this.schemaReady;
|
|
13913
14234
|
}
|
|
13914
14235
|
async get(type, id) {
|
|
@@ -14317,13 +14638,14 @@ class PostgresJsonRecordStore {
|
|
|
14317
14638
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
14318
14639
|
}
|
|
14319
14640
|
async withTaskParentIntegrityTransaction(fn) {
|
|
14320
|
-
|
|
14641
|
+
const transaction = this.options.client.transaction;
|
|
14642
|
+
if (typeof transaction !== "function") {
|
|
14321
14643
|
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
14322
14644
|
}
|
|
14323
|
-
return
|
|
14645
|
+
return retryOnTransientPostgresError(() => transaction(async (client) => {
|
|
14324
14646
|
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
14325
14647
|
return fn(client);
|
|
14326
|
-
});
|
|
14648
|
+
}));
|
|
14327
14649
|
}
|
|
14328
14650
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
14329
14651
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
@@ -15673,7 +15995,7 @@ async function getActiveWork(filters, store) {
|
|
|
15673
15995
|
}));
|
|
15674
15996
|
}
|
|
15675
15997
|
async function getChangedSince(since, filters, store) {
|
|
15676
|
-
return (await listTasks(filters ?? {}, store)).filter((task) => task.updated_at
|
|
15998
|
+
return (await listTasks(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
|
|
15677
15999
|
}
|
|
15678
16000
|
async function createProject(input, store, context) {
|
|
15679
16001
|
const timestamp = new Date().toISOString();
|
|
@@ -16209,6 +16531,41 @@ function compareClock(left, right) {
|
|
|
16209
16531
|
function numberValue2(value) {
|
|
16210
16532
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
16211
16533
|
}
|
|
16534
|
+
var TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
|
|
16535
|
+
var TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
|
|
16536
|
+
"canceling statement due to lock timeout",
|
|
16537
|
+
"deadlock detected",
|
|
16538
|
+
"could not serialize access"
|
|
16539
|
+
];
|
|
16540
|
+
function isTransientPostgresError(error) {
|
|
16541
|
+
if (typeof error !== "object" || error === null)
|
|
16542
|
+
return false;
|
|
16543
|
+
const candidate = error;
|
|
16544
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
16545
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
16546
|
+
const cause = candidate.cause;
|
|
16547
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
16548
|
+
}
|
|
16549
|
+
if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
|
|
16550
|
+
return true;
|
|
16551
|
+
}
|
|
16552
|
+
const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
|
|
16553
|
+
return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
|
|
16554
|
+
}
|
|
16555
|
+
async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
16556
|
+
let lastError;
|
|
16557
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
16558
|
+
try {
|
|
16559
|
+
return await fn();
|
|
16560
|
+
} catch (error) {
|
|
16561
|
+
lastError = error;
|
|
16562
|
+
if (!isTransientPostgresError(error) || attempt === attempts)
|
|
16563
|
+
throw error;
|
|
16564
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
|
|
16565
|
+
}
|
|
16566
|
+
}
|
|
16567
|
+
throw lastError;
|
|
16568
|
+
}
|
|
16212
16569
|
function isPostgresUniqueViolation(error) {
|
|
16213
16570
|
if (typeof error !== "object" || error === null)
|
|
16214
16571
|
return false;
|
|
@@ -16604,14 +16961,23 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
16604
16961
|
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
16605
16962
|
}
|
|
16606
16963
|
async ensureSchema() {
|
|
16607
|
-
this.schemaReady
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16964
|
+
if (this.schemaReady === null) {
|
|
16965
|
+
const attempt = (async () => {
|
|
16966
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
16967
|
+
await this.client.query(statement);
|
|
16968
|
+
}
|
|
16969
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
16970
|
+
await this.client.query(statement);
|
|
16971
|
+
}
|
|
16972
|
+
})();
|
|
16973
|
+
this.schemaReady = attempt;
|
|
16974
|
+
try {
|
|
16975
|
+
await attempt;
|
|
16976
|
+
} catch (error) {
|
|
16977
|
+
this.schemaReady = null;
|
|
16978
|
+
throw error;
|
|
16613
16979
|
}
|
|
16614
|
-
}
|
|
16980
|
+
}
|
|
16615
16981
|
await this.schemaReady;
|
|
16616
16982
|
}
|
|
16617
16983
|
async transaction(fn) {
|
|
@@ -18293,7 +18659,7 @@ function matchesExtraFilters(task, filter) {
|
|
|
18293
18659
|
}
|
|
18294
18660
|
if (filter.tags?.length) {
|
|
18295
18661
|
const taskTags = new Set(task.tags ?? []);
|
|
18296
|
-
if (!filter.tags.
|
|
18662
|
+
if (!filter.tags.some((tag) => taskTags.has(tag)))
|
|
18297
18663
|
return false;
|
|
18298
18664
|
}
|
|
18299
18665
|
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|