@morlay/session-rdb 0.1.9 → 0.1.10-alpha.2
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/README.md +24 -3
- package/dist/artifact.d.mts +118 -3
- package/dist/artifact.mjs +5 -3
- package/dist/{log-C0q5svm7.d.mts → backend-BKtJ5sO5.d.mts} +189 -291
- package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
- package/dist/branch-CG1PUlO6.mjs +833 -0
- package/dist/deletion-SSX7IqU8.mjs +35 -0
- package/dist/deletion.d.mts +1 -3
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -3
- package/dist/export.mjs +32 -57
- package/dist/import.d.mts +1 -3
- package/dist/import.mjs +32 -78
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1562 -5
- package/dist/open-YoyD_kah.mjs +561 -0
- package/dist/organize.d.mts +9 -0
- package/dist/organize.mjs +30 -0
- package/dist/route-DpfY0zq-.mjs +114 -0
- package/dist/storage.d.mts +41 -50
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -5
- package/dist/testing.mjs +27 -8
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +8 -42
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +32 -36
- package/src/artifact.ts +4 -1
- package/src/backend-rdb.ts +331 -0
- package/src/backend.ts +32 -15
- package/src/{branch.ts → branch/index.ts} +125 -112
- package/src/branch/port.ts +62 -0
- package/src/branch/semantics.ts +69 -0
- package/src/db/open.ts +180 -0
- package/src/db/stats.ts +449 -0
- package/src/deletion-error.ts +17 -0
- package/src/deletion.ts +19 -72
- package/src/entities/index.ts +16 -1
- package/src/entities/v3/event-usage.ts +3 -3
- package/src/entities/v3/events.ts +3 -3
- package/src/entities/v3/index.ts +4 -16
- package/src/entities/v3/persistence-state.ts +3 -3
- package/src/entities/v3/schema-meta.ts +3 -3
- package/src/entities/v3/session-counts.ts +3 -3
- package/src/entities/v3/session-events.ts +3 -3
- package/src/entities/v3/session-projcache-rows.ts +3 -3
- package/src/entities/v3/session-usage.ts +3 -3
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-records.ts +17 -0
- package/src/entities/v3/storage-units.ts +3 -3
- package/src/{schema.ts → event-shape.ts} +0 -46
- package/src/export.ts +32 -78
- package/src/handles.ts +349 -0
- package/src/http/route.ts +178 -0
- package/src/import-storages.ts +5 -2
- package/src/import.ts +63 -125
- package/src/index.ts +15 -1630
- package/src/legacy.ts +3 -1
- package/src/live-routing.ts +205 -0
- package/src/log/export.ts +84 -0
- package/src/{log.ts → log/repair.ts} +1 -272
- package/src/log/rows.ts +107 -0
- package/src/log/usage-row.ts +64 -0
- package/src/migrations.ts +196 -0
- package/src/organize.ts +38 -0
- package/src/read-path.ts +190 -0
- package/src/routes.ts +2 -1
- package/src/rows.ts +25 -77
- package/src/session-ops.ts +444 -0
- package/src/session-query.ts +17 -4
- package/src/session-store.ts +675 -0
- package/src/stats-fold.ts +44 -0
- package/src/storage-takeover/index.ts +2 -5
- package/src/storage-takeover/legacy-domain-migration.ts +118 -0
- package/src/storage-takeover/projection-cache.ts +139 -77
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +238 -425
- package/src/storage-takeover/storage-backend.ts +47 -149
- package/src/storage-takeover/types.ts +45 -14
- package/src/storage-takeover/workspace-documents.ts +65 -0
- package/src/storage.ts +4 -2
- package/src/testing.ts +21 -1
- package/src/usage.ts +11 -65
- package/src/version.ts +13 -0
- package/src/write-path.ts +286 -0
- package/dist/branch-DbFW3rJX.mjs +0 -391
- package/dist/deletion-ODZFcbZm.mjs +0 -2218
- package/dist/gc.d.mts +0 -6
- package/dist/gc.mjs +0 -49
- package/dist/log-DYxdjJ-l.mjs +0 -562
- package/dist/routes-DGBlBE_I.mjs +0 -10
- package/dist/sqlite-DjOnMRh2.mjs +0 -1777
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
- package/src/adapters/index.ts +0 -12
- package/src/adapters/to-postgres.ts +0 -93
- package/src/adapters/to-sqlite.ts +0 -83
- package/src/adapters/types.ts +0 -44
- package/src/drizzle/postgres-v2.ts +0 -10
- package/src/drizzle/postgres-v3.ts +0 -18
- package/src/drizzle/sqlite-v2.ts +0 -9
- package/src/drizzle/sqlite-v3.ts +0 -18
- package/src/entities/v2/index.ts +0 -20
- package/src/entities/v2/session-events.ts +0 -10
- package/src/entities/v3/schedule-tasks.ts +0 -16
- package/src/entities/v3/workspace-sessions.ts +0 -19
- package/src/entities/v3/workspace-state.ts +0 -12
- package/src/entities/v3/workspaces.ts +0 -14
- package/src/gc.ts +0 -77
- package/src/postgres.ts +0 -1015
- package/src/sqlite.ts +0 -1086
|
@@ -1,2218 +0,0 @@
|
|
|
1
|
-
import { a as SESSION_ROWS_PATH, t as SESSION_DELETE_PATH } from "./routes-DGBlBE_I.mjs";
|
|
2
|
-
import { COUNTED_EVENT_TYPES, addActivityCount, addActivityTotals, addTokenTotals, addTotals, emptyTotals, localDayKey, registerSessionUsage } from "./usage.mjs";
|
|
3
|
-
import { S as usageRowOf, _ as sessionConflictRow, b as titleOfEventData, g as sealOwnEvents, h as scanRows, m as rowToMeta, r as needsShapeAdoption, u as repairReadView, v as sessionInsertRow, x as toJsonlArtifact } from "./log-DYxdjJ-l.mjs";
|
|
4
|
-
import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-DbFW3rJX.mjs";
|
|
5
|
-
import { registerSessionImport } from "./import.mjs";
|
|
6
|
-
import { E as WriteGuard, T as toPostgresSchema, a as EVENT_ENCODING, c as eventDimensions, i as DEFAULT_BUSY_TIMEOUT_MS, r as createStorageRepository, t as SqliteBackend, w as postgresTableDefs } from "./sqlite-DjOnMRh2.mjs";
|
|
7
|
-
import { registerSessionExport } from "./export.mjs";
|
|
8
|
-
import { registerSessionGc } from "./gc.mjs";
|
|
9
|
-
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
12
|
-
import { balanceRewindPrefix } from "@morlay/session-branch";
|
|
13
|
-
import z from "@deepseek-ai/schemastery";
|
|
14
|
-
import { Pool } from "pg";
|
|
15
|
-
import { drizzle } from "drizzle-orm/node-postgres";
|
|
16
|
-
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
|
|
17
|
-
import { and, desc, eq, gte, inArray, lt, notInArray, sql } from "drizzle-orm";
|
|
18
|
-
import { readdir } from "node:fs/promises";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
21
|
-
import { SessionQueryEngine, SessionQueryError } from "@deepseek-ai/dsh-session-query";
|
|
22
|
-
import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
|
|
23
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
24
|
-
//#region src/postgres.ts
|
|
25
|
-
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
26
|
-
const pgWriteQueues = /* @__PURE__ */ new Map();
|
|
27
|
-
const REFRESH_FLAG_CHUNK = 400;
|
|
28
|
-
function enqueuePgWrite(key, fn) {
|
|
29
|
-
const run = (pgWriteQueues.get(key) ?? Promise.resolve()).then(fn);
|
|
30
|
-
pgWriteQueues.set(key, run.then(() => void 0, () => void 0));
|
|
31
|
-
return run;
|
|
32
|
-
}
|
|
33
|
-
function text(value) {
|
|
34
|
-
return typeof value === "string" ? value : null;
|
|
35
|
-
}
|
|
36
|
-
function numeric(value) {
|
|
37
|
-
const parsed = typeof value === "number" ? value : Number(value ?? 0);
|
|
38
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
39
|
-
}
|
|
40
|
-
function tokenTotalsOf(row) {
|
|
41
|
-
return {
|
|
42
|
-
inputTokens: numeric(row["input_tokens"]),
|
|
43
|
-
outputTokens: numeric(row["output_tokens"]),
|
|
44
|
-
cacheReadTokens: numeric(row["cache_read_tokens"]),
|
|
45
|
-
reasoningTokens: numeric(row["reasoning_tokens"]),
|
|
46
|
-
totalTokens: numeric(row["total_tokens"])
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function emptyActivity() {
|
|
50
|
-
return {
|
|
51
|
-
turns: 0,
|
|
52
|
-
steps: 0,
|
|
53
|
-
userInputs: 0,
|
|
54
|
-
toolCalls: 0
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function activityTotalsOf(row) {
|
|
58
|
-
return {
|
|
59
|
-
turns: numeric(row["turns"]),
|
|
60
|
-
steps: numeric(row["steps"]),
|
|
61
|
-
userInputs: numeric(row["user_inputs"]),
|
|
62
|
-
toolCalls: numeric(row["tool_calls"])
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
66
|
-
var PostgresBackend = class PostgresBackend {
|
|
67
|
-
db;
|
|
68
|
-
options;
|
|
69
|
-
kind = "postgres";
|
|
70
|
-
storeIdentity;
|
|
71
|
-
tables;
|
|
72
|
-
storage;
|
|
73
|
-
opened;
|
|
74
|
-
resolveOpened;
|
|
75
|
-
rejectOpened;
|
|
76
|
-
txOverride;
|
|
77
|
-
constructor(db, options) {
|
|
78
|
-
this.db = db;
|
|
79
|
-
this.options = options;
|
|
80
|
-
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
81
|
-
this.opened = new Promise((resolve, reject) => {
|
|
82
|
-
this.resolveOpened = resolve;
|
|
83
|
-
this.rejectOpened = reject;
|
|
84
|
-
});
|
|
85
|
-
this.opened.catch(() => {});
|
|
86
|
-
const writeQueueKey = options.identityBase;
|
|
87
|
-
this.storage = createStorageRepository({
|
|
88
|
-
db: () => this.opened.then(() => this.txOverride ?? this.db),
|
|
89
|
-
writeAtomically: (fn) => enqueuePgWrite(writeQueueKey, () => this.db.transaction(async (tx) => {
|
|
90
|
-
const previous = this.txOverride;
|
|
91
|
-
this.txOverride = tx;
|
|
92
|
-
try {
|
|
93
|
-
return await fn();
|
|
94
|
-
} finally {
|
|
95
|
-
this.txOverride = previous;
|
|
96
|
-
}
|
|
97
|
-
}, { isolationLevel: "serializable" })),
|
|
98
|
-
tables: this.tables
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
async open() {
|
|
102
|
-
try {
|
|
103
|
-
await this.doOpen();
|
|
104
|
-
this.resolveOpened();
|
|
105
|
-
} catch (error) {
|
|
106
|
-
this.rejectOpened(error);
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async doOpen() {
|
|
111
|
-
const schema = this.options.schema ?? "public";
|
|
112
|
-
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
113
|
-
if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
|
|
114
|
-
if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
|
|
115
|
-
}
|
|
116
|
-
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
117
|
-
await this.backfillUsageTables();
|
|
118
|
-
const storeId = await this.db.transaction(async (tx) => {
|
|
119
|
-
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
120
|
-
fSingleton: 1,
|
|
121
|
-
fStoreId: randomUUID()
|
|
122
|
-
}).onConflictDoNothing().execute();
|
|
123
|
-
const id = (await tx.select({ fStoreId: this.tables["t_persistence_state"].fStoreId }).from(this.tables["t_persistence_state"]).where(eq(this.tables["t_persistence_state"].fSingleton, 1)).execute())[0]?.fStoreId;
|
|
124
|
-
if (id === void 0 || id.length === 0) throw new Error("session database has no valid store identity");
|
|
125
|
-
return id;
|
|
126
|
-
});
|
|
127
|
-
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
128
|
-
}
|
|
129
|
-
async baselineV2() {
|
|
130
|
-
const dir = (await readdir(postgresMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
|
|
131
|
-
if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/postgres");
|
|
132
|
-
await this.db.execute(sql`
|
|
133
|
-
CREATE SCHEMA IF NOT EXISTS drizzle
|
|
134
|
-
`);
|
|
135
|
-
await this.db.execute(sql`
|
|
136
|
-
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
137
|
-
id SERIAL PRIMARY KEY,
|
|
138
|
-
hash text NOT NULL,
|
|
139
|
-
created_at bigint,
|
|
140
|
-
name text,
|
|
141
|
-
applied_at timestamp with time zone DEFAULT now()
|
|
142
|
-
)
|
|
143
|
-
`);
|
|
144
|
-
await this.db.execute(sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`);
|
|
145
|
-
}
|
|
146
|
-
async close() {
|
|
147
|
-
await this.options.close();
|
|
148
|
-
}
|
|
149
|
-
async getSession(id) {
|
|
150
|
-
return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
151
|
-
}
|
|
152
|
-
async getEventRows(id, fromSequence) {
|
|
153
|
-
return (fromSequence === void 0 ? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id)) : this.eventRows(this.db).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence)))).orderBy(this.tables["t_session_events"].fSequence).execute();
|
|
154
|
-
}
|
|
155
|
-
async getEventTypeAt(id, sequence) {
|
|
156
|
-
const table = this.tables["t_session_events"];
|
|
157
|
-
return (await this.db.select({ fType: this.tables["t_events"].fType }).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
|
|
158
|
-
}
|
|
159
|
-
async getEventTypesBefore(id, beforeSequence, limit) {
|
|
160
|
-
const table = this.tables["t_session_events"];
|
|
161
|
-
return await this.db.select({
|
|
162
|
-
fSequence: table.fSequence,
|
|
163
|
-
fType: this.tables["t_events"].fType
|
|
164
|
-
}).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
|
|
165
|
-
}
|
|
166
|
-
async listSessions() {
|
|
167
|
-
return this.db.select().from(this.tables["t_sessions"]).execute();
|
|
168
|
-
}
|
|
169
|
-
async transaction(fn) {
|
|
170
|
-
return this.db.transaction(async (tx) => fn(this.txFor(tx)));
|
|
171
|
-
}
|
|
172
|
-
txFor(tx) {
|
|
173
|
-
return {
|
|
174
|
-
upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
|
|
175
|
-
getHead: (id) => this.getHead(tx, id),
|
|
176
|
-
getSeedLength: (id) => this.getSeedLength(tx, id),
|
|
177
|
-
updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
|
|
178
|
-
insertEvents: (events, usageRows) => this.insertEvents(tx, events, usageRows),
|
|
179
|
-
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
180
|
-
updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
|
|
181
|
-
bumpRevision: (id, lastEventAt) => this.bumpRevision(tx, id, lastEventAt),
|
|
182
|
-
refreshTitle: (id) => this.refreshTitle(tx, id),
|
|
183
|
-
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
184
|
-
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
|
|
185
|
-
deleteSession: (id) => this.deleteSession(tx, id),
|
|
186
|
-
deleteSessions: (ids) => this.deleteSessions(tx, ids)
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
async readMeta(exec, key) {
|
|
190
|
-
return (await exec.select({ fValue: this.tables["t_schema_meta"].fValue }).from(this.tables["t_schema_meta"]).where(eq(this.tables["t_schema_meta"].fKey, key)).execute())[0]?.fValue;
|
|
191
|
-
}
|
|
192
|
-
async upsertSession(exec, storage, incarnation) {
|
|
193
|
-
await exec.insert(this.tables["t_sessions"]).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
|
|
194
|
-
target: this.tables["t_sessions"].fSessionId,
|
|
195
|
-
set: sessionConflictRow(storage)
|
|
196
|
-
}).execute();
|
|
197
|
-
}
|
|
198
|
-
async getHead(exec, id) {
|
|
199
|
-
const head = (await exec.select({
|
|
200
|
-
fHeadEventId: this.tables["t_sessions"].fHeadEventId,
|
|
201
|
-
fHeadSequence: this.tables["t_sessions"].fHeadSequence
|
|
202
|
-
}).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
203
|
-
if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
204
|
-
return head;
|
|
205
|
-
}
|
|
206
|
-
async getSeedLength(exec, id) {
|
|
207
|
-
const row = (await exec.select({ fSeedLength: this.tables["t_sessions"].fSeedLength }).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
208
|
-
if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
209
|
-
return row.fSeedLength;
|
|
210
|
-
}
|
|
211
|
-
async updateSeedLength(exec, id, seedLength) {
|
|
212
|
-
await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
213
|
-
}
|
|
214
|
-
static INSERT_BATCH_ROWS = 1e3;
|
|
215
|
-
async insertEvents(exec, events, usageRows) {
|
|
216
|
-
if (events.length === 0) return;
|
|
217
|
-
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_events"]).values(events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).execute();
|
|
218
|
-
if (usageRows.length > 0) for (let i = 0; i < usageRows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_event_usage"]).values(usageRows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).onConflictDoNothing().execute();
|
|
219
|
-
}
|
|
220
|
-
async insertBridges(exec, rows) {
|
|
221
|
-
if (rows.length === 0) return;
|
|
222
|
-
for (let i = 0; i < rows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_session_events"]).values(rows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).execute();
|
|
223
|
-
}
|
|
224
|
-
async updateHead(exec, id, headEventId, headSequence) {
|
|
225
|
-
await exec.update(this.tables["t_sessions"]).set({
|
|
226
|
-
fHeadEventId: headEventId,
|
|
227
|
-
fHeadSequence: headSequence
|
|
228
|
-
}).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
229
|
-
}
|
|
230
|
-
async bumpRevision(exec, id, lastEventAt) {
|
|
231
|
-
const sessions = this.tables["t_sessions"];
|
|
232
|
-
await exec.update(sessions).set({
|
|
233
|
-
fRevision: sql`${sessions.fRevision} + 1`,
|
|
234
|
-
...lastEventAt === void 0 ? {} : { fLastEventAt: sql`GREATEST(COALESCE(${sessions.fLastEventAt}, 0), ${lastEventAt})` }
|
|
235
|
-
}).where(eq(sessions.fSessionId, id)).execute();
|
|
236
|
-
}
|
|
237
|
-
async refreshTitle(exec, id) {
|
|
238
|
-
const bridges = this.tables["t_session_events"];
|
|
239
|
-
const entities = this.tables["t_events"];
|
|
240
|
-
const row = (await exec.select({
|
|
241
|
-
fSequence: bridges.fSequence,
|
|
242
|
-
fData: entities.fData
|
|
243
|
-
}).from(bridges).innerJoin(entities, eq(entities.fEventId, bridges.fEventId)).where(and(eq(bridges.fSessionId, id), eq(entities.fType, "session/title"))).orderBy(desc(bridges.fSequence)).limit(1).execute())[0];
|
|
244
|
-
const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
|
|
245
|
-
await exec.update(this.tables["t_sessions"]).set({
|
|
246
|
-
fTitle: title ?? null,
|
|
247
|
-
fTitleSeq: title === void 0 ? null : row.fSequence
|
|
248
|
-
}).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
249
|
-
}
|
|
250
|
-
async deleteBridgeTail(exec, id, fromSequence) {
|
|
251
|
-
const table = this.tables["t_session_events"];
|
|
252
|
-
return (await exec.execute(sql`
|
|
253
|
-
DELETE FROM ${table} WHERE f_session_id = ${id} AND f_sequence >= ${fromSequence}
|
|
254
|
-
RETURNING f_event_id
|
|
255
|
-
`)).rows.map((row) => row.f_event_id);
|
|
256
|
-
}
|
|
257
|
-
async deleteSession(exec, id) {
|
|
258
|
-
const result = await exec.execute(sql`
|
|
259
|
-
DELETE FROM ${this.tables["t_session_events"]} WHERE f_session_id = ${id} RETURNING f_event_id
|
|
260
|
-
`);
|
|
261
|
-
await exec.delete(this.tables["t_workspace_sessions"]).where(eq(this.tables["t_workspace_sessions"].fSessionId, id)).execute();
|
|
262
|
-
await exec.delete(this.tables["t_session_projcache_row"]).where(eq(this.tables["t_session_projcache_row"].fSessionId, id)).execute();
|
|
263
|
-
await exec.delete(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
264
|
-
return result.rows.map((row) => row.f_event_id);
|
|
265
|
-
}
|
|
266
|
-
async collectOrphans() {
|
|
267
|
-
const tEvents = this.tables["t_events"];
|
|
268
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
269
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
270
|
-
const result = await this.db.delete(tEvents).where(notInArray(tEvents.fEventId, this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents))).execute();
|
|
271
|
-
await this.db.delete(tEventUsage).where(notInArray(tEventUsage.fEventId, this.db.select({ fEventId: tEvents.fEventId }).from(tEvents))).execute();
|
|
272
|
-
return result.rowCount ?? 0;
|
|
273
|
-
}
|
|
274
|
-
async listOrphanSubagentSessions() {
|
|
275
|
-
const tSessions = this.tables["t_sessions"];
|
|
276
|
-
return (await this.db.execute(sql`
|
|
277
|
-
SELECT s.f_session_id AS id FROM ${tSessions} AS s
|
|
278
|
-
WHERE s.f_origin = 'subagent'
|
|
279
|
-
AND (s.f_parent_session IS NULL
|
|
280
|
-
OR s.f_parent_session NOT IN (SELECT p.f_session_id FROM ${tSessions} AS p))
|
|
281
|
-
`)).rows.map((row) => row.id);
|
|
282
|
-
}
|
|
283
|
-
async deleteSessions(exec, ids) {
|
|
284
|
-
if (ids.length === 0) return {
|
|
285
|
-
deleted: 0,
|
|
286
|
-
eventIds: []
|
|
287
|
-
};
|
|
288
|
-
const removed = await exec.execute(sql`
|
|
289
|
-
DELETE FROM ${this.tables["t_session_events"]}
|
|
290
|
-
WHERE f_session_id IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})
|
|
291
|
-
RETURNING f_event_id
|
|
292
|
-
`);
|
|
293
|
-
await exec.delete(this.tables["t_workspace_sessions"]).where(inArray(this.tables["t_workspace_sessions"].fSessionId, ids)).execute();
|
|
294
|
-
await exec.delete(this.tables["t_session_projcache_row"]).where(inArray(this.tables["t_session_projcache_row"].fSessionId, ids)).execute();
|
|
295
|
-
const result = await exec.delete(this.tables["t_sessions"]).where(inArray(this.tables["t_sessions"].fSessionId, ids)).execute();
|
|
296
|
-
return {
|
|
297
|
-
deleted: Number(result.rowCount ?? 0),
|
|
298
|
-
eventIds: removed.rows.map((row) => row.f_event_id)
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
async vacuum() {
|
|
302
|
-
await this.db.execute(sql`VACUUM ANALYZE`);
|
|
303
|
-
}
|
|
304
|
-
async backfillUsageTables() {
|
|
305
|
-
await this.backfillEventUsage();
|
|
306
|
-
await this.backfillSessionUsage();
|
|
307
|
-
await this.backfillSessionCounts();
|
|
308
|
-
}
|
|
309
|
-
async tableIsEmpty(table) {
|
|
310
|
-
const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
|
|
311
|
-
return Number(existing.rows[0]?.["n"] ?? 0) === 0;
|
|
312
|
-
}
|
|
313
|
-
async backfillEventUsage() {
|
|
314
|
-
if (!await this.tableIsEmpty(this.qualifiedTable("t_event_usage"))) return;
|
|
315
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
316
|
-
const tEvents = this.tables["t_events"];
|
|
317
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
318
|
-
const tSessions = this.tables["t_sessions"];
|
|
319
|
-
await this.db.execute(sql`
|
|
320
|
-
INSERT INTO ${tEventUsage} (
|
|
321
|
-
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
322
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
323
|
-
SELECT e.f_event_id, e.f_created_at,
|
|
324
|
-
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
325
|
-
coalesce(e.f_data::json #>> '{data,message,source,provider}',
|
|
326
|
-
e.f_data::json #>> '{message,source,provider}'),
|
|
327
|
-
coalesce(e.f_data::json #>> '{data,message,source,model}',
|
|
328
|
-
e.f_data::json #>> '{message,source,model}'),
|
|
329
|
-
(EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
330
|
-
WHERE rb.f_event_id = e.f_event_id))::int,
|
|
331
|
-
(EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
332
|
-
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
333
|
-
WHERE sb.f_event_id = e.f_event_id AND ss.f_origin = 'subagent'))::int,
|
|
334
|
-
coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
|
|
335
|
-
(e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
|
|
336
|
-
coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
|
|
337
|
-
(e.f_data::json #>> '{usage,outputTokens}')::integer, 0),
|
|
338
|
-
coalesce((e.f_data::json #>> '{data,usage,cacheReadTokens}')::integer,
|
|
339
|
-
(e.f_data::json #>> '{usage,cacheReadTokens}')::integer, 0),
|
|
340
|
-
coalesce((e.f_data::json #>> '{data,usage,reasoningTokens}')::integer,
|
|
341
|
-
(e.f_data::json #>> '{usage,reasoningTokens}')::integer, 0),
|
|
342
|
-
coalesce((e.f_data::json #>> '{data,usage,totalTokens}')::integer,
|
|
343
|
-
(e.f_data::json #>> '{usage,totalTokens}')::integer, 0)
|
|
344
|
-
FROM ${tEvents} e
|
|
345
|
-
WHERE e.f_type = 'assistant/message'
|
|
346
|
-
AND (e.f_data::json #> '{data,usage}' IS NOT NULL
|
|
347
|
-
OR e.f_data::json #> '{usage}' IS NOT NULL)
|
|
348
|
-
ON CONFLICT DO NOTHING
|
|
349
|
-
`);
|
|
350
|
-
}
|
|
351
|
-
async backfillSessionUsage() {
|
|
352
|
-
if (!await this.tableIsEmpty(this.qualifiedTable("t_session_usage"))) return;
|
|
353
|
-
const tSessionUsage = this.tables["t_session_usage"];
|
|
354
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
355
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
356
|
-
await this.db.execute(sql`
|
|
357
|
-
INSERT INTO ${tSessionUsage} (
|
|
358
|
-
f_session_id, f_day, f_provider, f_model,
|
|
359
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
360
|
-
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
361
|
-
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
362
|
-
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
363
|
-
FROM ${tSessionEvents} b
|
|
364
|
-
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
365
|
-
GROUP BY 1, 2, 3, 4
|
|
366
|
-
ON CONFLICT DO NOTHING
|
|
367
|
-
`);
|
|
368
|
-
}
|
|
369
|
-
async backfillSessionCounts() {
|
|
370
|
-
if (!await this.tableIsEmpty(this.qualifiedTable("t_session_counts"))) return;
|
|
371
|
-
const tSessionCounts = this.tables["t_session_counts"];
|
|
372
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
373
|
-
const tEvents = this.tables["t_events"];
|
|
374
|
-
await this.db.execute(sql`
|
|
375
|
-
INSERT INTO ${tSessionCounts} (
|
|
376
|
-
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
377
|
-
SELECT b.f_session_id,
|
|
378
|
-
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
379
|
-
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
380
|
-
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
381
|
-
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
382
|
-
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
383
|
-
FROM ${tSessionEvents} b
|
|
384
|
-
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
385
|
-
WHERE e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
386
|
-
GROUP BY 1, 2
|
|
387
|
-
ON CONFLICT DO NOTHING
|
|
388
|
-
`);
|
|
389
|
-
}
|
|
390
|
-
async listSessionRows(query = {}) {
|
|
391
|
-
const needle = query.query?.trim().toLowerCase() ?? "";
|
|
392
|
-
const like = `%${needle}%`;
|
|
393
|
-
const includeSubagents = query.includeSubagents === true;
|
|
394
|
-
const limit = query.limit ?? 20;
|
|
395
|
-
const offset = query.offset ?? 0;
|
|
396
|
-
const tSessions = this.tables["t_sessions"];
|
|
397
|
-
const tWorkspaces = this.tables["t_workspaces"];
|
|
398
|
-
const tWorkspaceSessions = this.tables["t_workspace_sessions"];
|
|
399
|
-
const workspaceTitle = sql`(SELECT w.f_title FROM ${tWorkspaceSessions} b
|
|
400
|
-
JOIN ${tWorkspaces} w ON w.f_workspace_id = b.f_workspace_id
|
|
401
|
-
WHERE b.f_session_id = s.f_session_id
|
|
402
|
-
ORDER BY b.f_position LIMIT 1)`;
|
|
403
|
-
const where = sql`WHERE (${needle} = ''
|
|
404
|
-
OR lower(COALESCE(s.f_title, '')) LIKE ${like}
|
|
405
|
-
OR lower(COALESCE(${workspaceTitle}, '')) LIKE ${like})
|
|
406
|
-
AND (${includeSubagents} OR s.f_origin IS DISTINCT FROM 'subagent')`;
|
|
407
|
-
const rows = await this.db.execute(sql`
|
|
408
|
-
SELECT s.f_session_id AS session_id,
|
|
409
|
-
s.f_title AS title,
|
|
410
|
-
s.f_origin AS origin,
|
|
411
|
-
s.f_cwd AS cwd,
|
|
412
|
-
s.f_created_at AS created_at,
|
|
413
|
-
(s.f_archived_at IS NOT NULL) AS archived,
|
|
414
|
-
COALESCE(s.f_last_event_at, s.f_created_at) AS updated_at,
|
|
415
|
-
${workspaceTitle} AS workspace
|
|
416
|
-
FROM ${tSessions} s
|
|
417
|
-
${where}
|
|
418
|
-
ORDER BY updated_at DESC, s.f_session_id
|
|
419
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
420
|
-
`);
|
|
421
|
-
const counted = await this.db.execute(sql`
|
|
422
|
-
SELECT count(*) AS n FROM ${tSessions} s ${where}
|
|
423
|
-
`);
|
|
424
|
-
return {
|
|
425
|
-
items: rows.rows.map((row) => ({
|
|
426
|
-
sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
427
|
-
title: typeof row["title"] === "string" ? row["title"] : null,
|
|
428
|
-
origin: typeof row["origin"] === "string" ? row["origin"] : null,
|
|
429
|
-
cwd: typeof row["cwd"] === "string" ? row["cwd"] : null,
|
|
430
|
-
createdAt: numeric(row["created_at"]),
|
|
431
|
-
updatedAt: numeric(row["updated_at"]),
|
|
432
|
-
archived: row["archived"] === true,
|
|
433
|
-
subagent: row["origin"] === "subagent",
|
|
434
|
-
workspace: typeof row["workspace"] === "string" ? row["workspace"] : null
|
|
435
|
-
})),
|
|
436
|
-
total: numeric(counted.rows[0]?.["n"] ?? 0)
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
async usageReport(sinceMs) {
|
|
440
|
-
const usageSince = sinceMs === void 0 ? sql`` : sql` AND f_created_at >= ${sinceMs}`;
|
|
441
|
-
const dayFilter = (column) => sinceMs === void 0 ? sql`` : sql` AND ${sql.raw(column)} >= ${localDayKey(sinceMs)}`;
|
|
442
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
443
|
-
const tSessionUsage = this.tables["t_session_usage"];
|
|
444
|
-
const tSessionCounts = this.tables["t_session_counts"];
|
|
445
|
-
const tSessions = this.tables["t_sessions"];
|
|
446
|
-
const tokenColumns = (alias) => sql.raw(`sum(${alias}.f_input_tokens) AS input_tokens,
|
|
447
|
-
sum(${alias}.f_output_tokens) AS output_tokens,
|
|
448
|
-
sum(${alias}.f_cache_read_tokens) AS cache_read_tokens,
|
|
449
|
-
sum(${alias}.f_reasoning_tokens) AS reasoning_tokens,
|
|
450
|
-
sum(${alias}.f_total_tokens) AS total_tokens`);
|
|
451
|
-
const buckets = await this.db.execute(sql`
|
|
452
|
-
SELECT a.f_day AS day,
|
|
453
|
-
a.f_provider AS provider,
|
|
454
|
-
a.f_model AS model,
|
|
455
|
-
(a.f_subagent = 1) AS subagent,
|
|
456
|
-
${tokenColumns("a")}
|
|
457
|
-
FROM ${tEventUsage} a
|
|
458
|
-
WHERE a.f_referenced = 1${usageSince}
|
|
459
|
-
GROUP BY 1, 2, 3, 4
|
|
460
|
-
`);
|
|
461
|
-
const tokenScope = await this.db.execute(sql`
|
|
462
|
-
SELECT (a.f_subagent = 1) AS subagent,
|
|
463
|
-
${tokenColumns("a")}
|
|
464
|
-
FROM ${tEventUsage} a
|
|
465
|
-
WHERE a.f_referenced = 1${usageSince}
|
|
466
|
-
GROUP BY 1
|
|
467
|
-
`);
|
|
468
|
-
const activityScope = await this.db.execute(sql`
|
|
469
|
-
SELECT (s.f_origin = 'subagent') AS subagent,
|
|
470
|
-
sum(c.f_turns) AS turns,
|
|
471
|
-
sum(c.f_steps) AS steps,
|
|
472
|
-
sum(c.f_user_inputs) AS user_inputs,
|
|
473
|
-
sum(c.f_tool_calls) AS tool_calls
|
|
474
|
-
FROM ${tSessionCounts} c
|
|
475
|
-
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
476
|
-
WHERE EXISTS (SELECT 1 FROM ${tSessionUsage} ub WHERE ub.f_session_id = c.f_session_id)${dayFilter("c.f_day")}
|
|
477
|
-
GROUP BY 1
|
|
478
|
-
`);
|
|
479
|
-
const tokenSessions = await this.db.execute(sql`
|
|
480
|
-
SELECT u.f_session_id AS session_id,
|
|
481
|
-
s.f_title AS title,
|
|
482
|
-
(s.f_origin = 'subagent') AS subagent,
|
|
483
|
-
(s.f_archived_at IS NOT NULL) AS archived,
|
|
484
|
-
${tokenColumns("u")}
|
|
485
|
-
FROM ${tSessionUsage} u
|
|
486
|
-
JOIN ${tSessions} s ON s.f_session_id = u.f_session_id
|
|
487
|
-
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
488
|
-
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
489
|
-
`);
|
|
490
|
-
const activitySessions = await this.db.execute(sql`
|
|
491
|
-
SELECT c.f_session_id AS session_id,
|
|
492
|
-
sum(c.f_turns) AS turns,
|
|
493
|
-
sum(c.f_steps) AS steps,
|
|
494
|
-
sum(c.f_user_inputs) AS user_inputs,
|
|
495
|
-
sum(c.f_tool_calls) AS tool_calls
|
|
496
|
-
FROM ${tSessionCounts} c
|
|
497
|
-
WHERE 1 = 1${dayFilter("c.f_day")}
|
|
498
|
-
GROUP BY c.f_session_id
|
|
499
|
-
`);
|
|
500
|
-
const human = emptyTotals();
|
|
501
|
-
const subagent = emptyTotals();
|
|
502
|
-
for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
503
|
-
for (const row of activityScope.rows) addActivityTotals(row["subagent"] === true ? subagent : human, activityTotalsOf(row));
|
|
504
|
-
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
505
|
-
const activityBySession = /* @__PURE__ */ new Map();
|
|
506
|
-
for (const row of activitySessions.rows) activityBySession.set(String(row["session_id"]), activityTotalsOf(row));
|
|
507
|
-
return {
|
|
508
|
-
totals,
|
|
509
|
-
subagent,
|
|
510
|
-
human,
|
|
511
|
-
buckets: buckets.rows.map((row) => ({
|
|
512
|
-
day: String(row["day"]),
|
|
513
|
-
provider: text(row["provider"]),
|
|
514
|
-
model: text(row["model"]),
|
|
515
|
-
subagent: row["subagent"] === true,
|
|
516
|
-
...tokenTotalsOf(row)
|
|
517
|
-
})),
|
|
518
|
-
sessions: tokenSessions.rows.map((row) => ({
|
|
519
|
-
sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
520
|
-
title: text(row["title"]),
|
|
521
|
-
subagent: row["subagent"] === true,
|
|
522
|
-
archived: row["archived"] === true,
|
|
523
|
-
...tokenTotalsOf(row),
|
|
524
|
-
...activityBySession.get(String(row["session_id"])) ?? emptyActivity()
|
|
525
|
-
}))
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
qualifiedTable(name) {
|
|
529
|
-
const schema = this.options.schema ?? "public";
|
|
530
|
-
return schema === "public" ? name : `"${schema}".${name}`;
|
|
531
|
-
}
|
|
532
|
-
async incrementSessionUsage(id, buckets) {
|
|
533
|
-
if (buckets.length === 0) return;
|
|
534
|
-
const tSessionUsage = this.tables["t_session_usage"];
|
|
535
|
-
for (const bucket of buckets) await this.db.execute(sql`
|
|
536
|
-
INSERT INTO ${tSessionUsage} (
|
|
537
|
-
f_session_id, f_day, f_provider, f_model,
|
|
538
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
539
|
-
VALUES (${id}, ${bucket.day}, ${bucket.provider ?? ""}, ${bucket.model ?? ""},
|
|
540
|
-
${bucket.inputTokens}, ${bucket.outputTokens}, ${bucket.cacheReadTokens},
|
|
541
|
-
${bucket.reasoningTokens}, ${bucket.totalTokens})
|
|
542
|
-
ON CONFLICT (f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
543
|
-
f_input_tokens = ${tSessionUsage}.f_input_tokens + EXCLUDED.f_input_tokens,
|
|
544
|
-
f_output_tokens = ${tSessionUsage}.f_output_tokens + EXCLUDED.f_output_tokens,
|
|
545
|
-
f_cache_read_tokens = ${tSessionUsage}.f_cache_read_tokens + EXCLUDED.f_cache_read_tokens,
|
|
546
|
-
f_reasoning_tokens = ${tSessionUsage}.f_reasoning_tokens + EXCLUDED.f_reasoning_tokens,
|
|
547
|
-
f_total_tokens = ${tSessionUsage}.f_total_tokens + EXCLUDED.f_total_tokens
|
|
548
|
-
`);
|
|
549
|
-
}
|
|
550
|
-
async incrementSessionCounts(id, buckets) {
|
|
551
|
-
if (buckets.length === 0) return;
|
|
552
|
-
const tSessionCounts = this.tables["t_session_counts"];
|
|
553
|
-
for (const bucket of buckets) await this.db.execute(sql`
|
|
554
|
-
INSERT INTO ${tSessionCounts} (
|
|
555
|
-
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
556
|
-
VALUES (${id}, ${bucket.day}, ${bucket.turns}, ${bucket.steps},
|
|
557
|
-
${bucket.userInputs}, ${bucket.toolCalls})
|
|
558
|
-
ON CONFLICT (f_session_id, f_day) DO UPDATE SET
|
|
559
|
-
f_turns = ${tSessionCounts}.f_turns + EXCLUDED.f_turns,
|
|
560
|
-
f_steps = ${tSessionCounts}.f_steps + EXCLUDED.f_steps,
|
|
561
|
-
f_user_inputs = ${tSessionCounts}.f_user_inputs + EXCLUDED.f_user_inputs,
|
|
562
|
-
f_tool_calls = ${tSessionCounts}.f_tool_calls + EXCLUDED.f_tool_calls
|
|
563
|
-
`);
|
|
564
|
-
}
|
|
565
|
-
async rebuildSessionStats(id) {
|
|
566
|
-
const tSessionUsage = this.tables["t_session_usage"];
|
|
567
|
-
const tSessionCounts = this.tables["t_session_counts"];
|
|
568
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
569
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
570
|
-
const tEvents = this.tables["t_events"];
|
|
571
|
-
await this.db.execute(sql`DELETE FROM ${tSessionUsage} WHERE f_session_id = ${id}`);
|
|
572
|
-
await this.db.execute(sql`
|
|
573
|
-
INSERT INTO ${tSessionUsage} (
|
|
574
|
-
f_session_id, f_day, f_provider, f_model,
|
|
575
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
576
|
-
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
577
|
-
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
578
|
-
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
579
|
-
FROM ${tSessionEvents} b
|
|
580
|
-
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
581
|
-
WHERE b.f_session_id = ${id}
|
|
582
|
-
GROUP BY 1, 2, 3, 4
|
|
583
|
-
`);
|
|
584
|
-
await this.db.execute(sql`DELETE FROM ${tSessionCounts} WHERE f_session_id = ${id}`);
|
|
585
|
-
await this.db.execute(sql`
|
|
586
|
-
INSERT INTO ${tSessionCounts} (
|
|
587
|
-
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
588
|
-
SELECT b.f_session_id,
|
|
589
|
-
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
590
|
-
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
591
|
-
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
592
|
-
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
593
|
-
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
594
|
-
FROM ${tSessionEvents} b
|
|
595
|
-
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
596
|
-
WHERE b.f_session_id = ${id} AND e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
597
|
-
GROUP BY 1, 2
|
|
598
|
-
`);
|
|
599
|
-
}
|
|
600
|
-
async refreshEventUsageFlags(eventIds) {
|
|
601
|
-
const tEventUsage = this.tables["t_event_usage"];
|
|
602
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
603
|
-
const tSessions = this.tables["t_sessions"];
|
|
604
|
-
for (let at = 0; at < eventIds.length; at += REFRESH_FLAG_CHUNK) {
|
|
605
|
-
const chunk = eventIds.slice(at, at + REFRESH_FLAG_CHUNK);
|
|
606
|
-
await this.db.execute(sql`
|
|
607
|
-
UPDATE ${tEventUsage} SET
|
|
608
|
-
f_referenced = (EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
609
|
-
WHERE rb.f_event_id = ${tEventUsage}.f_event_id))::int,
|
|
610
|
-
f_subagent = (EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
611
|
-
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
612
|
-
WHERE sb.f_event_id = ${tEventUsage}.f_event_id
|
|
613
|
-
AND ss.f_origin = 'subagent'))::int
|
|
614
|
-
WHERE f_event_id IN (${sql.join(chunk.map((id) => sql`${id}`), sql`, `)})
|
|
615
|
-
`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
async deleteSessionStats(id) {
|
|
619
|
-
await this.db.execute(sql`DELETE FROM ${this.tables["t_session_usage"]} WHERE f_session_id = ${id}`);
|
|
620
|
-
await this.db.execute(sql`DELETE FROM ${this.tables["t_session_counts"]} WHERE f_session_id = ${id}`);
|
|
621
|
-
}
|
|
622
|
-
async getPrevBridge(exec, id, sequence) {
|
|
623
|
-
return (await exec.select({
|
|
624
|
-
fEventId: this.tables["t_session_events"].fEventId,
|
|
625
|
-
fSequence: this.tables["t_session_events"].fSequence
|
|
626
|
-
}).from(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), eq(this.tables["t_session_events"].fSequence, sequence))).execute())[0];
|
|
627
|
-
}
|
|
628
|
-
eventRows(exec) {
|
|
629
|
-
return exec.select({
|
|
630
|
-
fEventId: this.tables["t_session_events"].fEventId,
|
|
631
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
632
|
-
fType: this.tables["t_events"].fType,
|
|
633
|
-
fKind: this.tables["t_events"].fKind,
|
|
634
|
-
fRole: this.tables["t_events"].fRole,
|
|
635
|
-
fName: this.tables["t_events"].fName,
|
|
636
|
-
fActionId: this.tables["t_events"].fActionId,
|
|
637
|
-
fCreatedAt: this.tables["t_events"].fCreatedAt,
|
|
638
|
-
fData: this.tables["t_events"].fData,
|
|
639
|
-
fSurfaceOp: this.tables["t_session_events"].fSurfaceOp
|
|
640
|
-
}).from(this.tables["t_session_events"]).innerJoin(this.tables["t_events"], eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId));
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
//#endregion
|
|
644
|
-
//#region src/rows.ts
|
|
645
|
-
const MAX_PAGE_SIZE = 200;
|
|
646
|
-
const DEFAULT_PAGE_SIZE = 20;
|
|
647
|
-
function registerSessionRows(ctx, backend) {
|
|
648
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
649
|
-
const webServer = webCtx.webServer;
|
|
650
|
-
const connection = webCtx.get("connection");
|
|
651
|
-
return webCtx.effect(() => webServer.register({
|
|
652
|
-
kind: "exact",
|
|
653
|
-
path: SESSION_ROWS_PATH,
|
|
654
|
-
handler: async (req, res) => {
|
|
655
|
-
const rejection = connection.requestRejection(req);
|
|
656
|
-
if (rejection !== void 0) {
|
|
657
|
-
res.writeHead(rejection);
|
|
658
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (req.method !== "POST") {
|
|
662
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
663
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const chunks = [];
|
|
667
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
668
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
669
|
-
let envelope = {};
|
|
670
|
-
if (raw !== "") try {
|
|
671
|
-
envelope = JSON.parse(raw);
|
|
672
|
-
} catch {
|
|
673
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
674
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const page = Number.isSafeInteger(envelope.page) && (envelope.page ?? 0) > 0 ? envelope.page : 1;
|
|
678
|
-
const requested = envelope.pageSize;
|
|
679
|
-
const pageSize = Number.isSafeInteger(requested) && (requested ?? 0) > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
|
|
680
|
-
try {
|
|
681
|
-
const { items, total } = await backend.listSessionRows({
|
|
682
|
-
...envelope.query === void 0 ? {} : { query: envelope.query },
|
|
683
|
-
...envelope.includeSubagents === void 0 ? {} : { includeSubagents: envelope.includeSubagents },
|
|
684
|
-
limit: pageSize,
|
|
685
|
-
offset: (page - 1) * pageSize
|
|
686
|
-
});
|
|
687
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
688
|
-
res.end(JSON.stringify({
|
|
689
|
-
items,
|
|
690
|
-
total,
|
|
691
|
-
page,
|
|
692
|
-
pageSize
|
|
693
|
-
}));
|
|
694
|
-
} catch (error) {
|
|
695
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
696
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session rows failed" }));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}), "session-rdb: session rows route");
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
//#endregion
|
|
703
|
-
//#region src/session-query.ts
|
|
704
|
-
var SessionQueryRdb = class extends SessionQueryEngine {
|
|
705
|
-
constructor(ctx, config = {}) {
|
|
706
|
-
super(ctx, config);
|
|
707
|
-
}
|
|
708
|
-
async listSessions(signal) {
|
|
709
|
-
const records = await super.listSessions(signal);
|
|
710
|
-
const archived = this.archivedSessionIds();
|
|
711
|
-
if (archived.size === 0) return records;
|
|
712
|
-
return records.filter((record) => !archived.has(String(record.header.id)));
|
|
713
|
-
}
|
|
714
|
-
archivedSessionIds() {
|
|
715
|
-
const registry = this.ctx.get("workspaceRegistry");
|
|
716
|
-
return new Set(registry?.archivedSessionIds ?? []);
|
|
717
|
-
}
|
|
718
|
-
async searchSessions() {
|
|
719
|
-
throw searchDisabled();
|
|
720
|
-
}
|
|
721
|
-
async searchEvents() {
|
|
722
|
-
throw searchDisabled();
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
function searchDisabled() {
|
|
726
|
-
return new SessionQueryError("session search is disabled: this deployment serves session queries from the rdb backend without a full-text index", "SESSION_QUERY_SEARCH_DISABLED");
|
|
727
|
-
}
|
|
728
|
-
//#endregion
|
|
729
|
-
//#region src/storage-takeover/projection-cache.ts
|
|
730
|
-
const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
|
|
731
|
-
const PREDECESSOR_TITLE_KEY = "title";
|
|
732
|
-
var SessionProjectionCacheRdb = class extends Service {
|
|
733
|
-
static inject = ["sessionProjections", "sessions"];
|
|
734
|
-
records = /* @__PURE__ */ new Map();
|
|
735
|
-
dirty = /* @__PURE__ */ new Map();
|
|
736
|
-
directReads;
|
|
737
|
-
config;
|
|
738
|
-
repository;
|
|
739
|
-
ready;
|
|
740
|
-
constructor(ctx, options) {
|
|
741
|
-
super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
|
|
742
|
-
this.config = options;
|
|
743
|
-
this.repository = options.repository;
|
|
744
|
-
this.ready = options.ready;
|
|
745
|
-
this.directReads = options.repository.readProjcacheDirect !== void 0;
|
|
746
|
-
}
|
|
747
|
-
async [Service.init]() {
|
|
748
|
-
this.installWritePath();
|
|
749
|
-
await this.ready;
|
|
750
|
-
const pruned = await this.repository.pruneStaleProjcache();
|
|
751
|
-
if (pruned > 0) this.ctx.logger.info(`session projection cache: pruned stale snapshot(s) for ${String(pruned)} session(s)`);
|
|
752
|
-
if (!this.directReads) for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
|
|
753
|
-
}
|
|
754
|
-
lookup(id) {
|
|
755
|
-
if (this.directReads) return this.repository.readProjcacheDirect?.(id);
|
|
756
|
-
return this.records.get(id);
|
|
757
|
-
}
|
|
758
|
-
recordFor(id, expected) {
|
|
759
|
-
const record = this.lookup(id);
|
|
760
|
-
if (record === void 0) return void 0;
|
|
761
|
-
return identityMatches(record.identity, expected) ? record : void 0;
|
|
762
|
-
}
|
|
763
|
-
cachedSnapshot(meta, keys) {
|
|
764
|
-
const record = this.recordForLifecycle(meta);
|
|
765
|
-
const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
|
|
766
|
-
return this.withDirectTitle(meta.id, keys, snapshot);
|
|
767
|
-
}
|
|
768
|
-
recordForLifecycle(meta) {
|
|
769
|
-
const record = this.lookup(meta.id);
|
|
770
|
-
return record === void 0 || !identityMatches(record.identity, lifecycleIdentityOf(meta)) ? void 0 : record;
|
|
771
|
-
}
|
|
772
|
-
withDirectTitle(id, keys, snapshot) {
|
|
773
|
-
if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
|
|
774
|
-
if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
|
|
775
|
-
const direct = this.repository.readSessionTitleDirect?.(id);
|
|
776
|
-
if (direct === void 0) return snapshot;
|
|
777
|
-
const values = {
|
|
778
|
-
...snapshot?.values,
|
|
779
|
-
[PREDECESSOR_TITLE_KEY]: direct.title
|
|
780
|
-
};
|
|
781
|
-
return {
|
|
782
|
-
asOfSeq: snapshot === void 0 ? direct.seq : Math.min(snapshot.asOfSeq, direct.seq),
|
|
783
|
-
values
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
cachedPredecessorTitle(meta) {
|
|
787
|
-
const record = this.lookup(meta.id);
|
|
788
|
-
if (record === void 0 || !predecessorIdentityMatches(record.identity, lifecycleIdentityOf(meta))) return;
|
|
789
|
-
return this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
|
|
790
|
-
}
|
|
791
|
-
viewRecord(record, keys) {
|
|
792
|
-
const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
|
|
793
|
-
const servedKeys = Object.keys(values);
|
|
794
|
-
if (servedKeys.length === 0) return void 0;
|
|
795
|
-
const firstKey = servedKeys[0];
|
|
796
|
-
let asOfSeq = record.rows[firstKey].seq;
|
|
797
|
-
for (const key of servedKeys.slice(1)) {
|
|
798
|
-
const row = record.rows[key];
|
|
799
|
-
if (row.seq < asOfSeq) asOfSeq = row.seq;
|
|
800
|
-
}
|
|
801
|
-
return {
|
|
802
|
-
asOfSeq,
|
|
803
|
-
values
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
hydratePrepared(session, events) {
|
|
807
|
-
const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
|
|
808
|
-
if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
|
|
809
|
-
try {
|
|
810
|
-
return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
|
|
811
|
-
} catch {
|
|
812
|
-
return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
async write(session) {
|
|
816
|
-
const rows = this.ctx.sessionProjections.checkpoint(session);
|
|
817
|
-
this.markClean(session);
|
|
818
|
-
if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
|
|
819
|
-
await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
|
|
820
|
-
}
|
|
821
|
-
async truncateTo(meta, inheritedEventCount, headSeq) {
|
|
822
|
-
const identity = identityOf(meta, inheritedEventCount);
|
|
823
|
-
const record = this.recordFor(meta.id, identity);
|
|
824
|
-
if (record === void 0) return;
|
|
825
|
-
const rows = Object.fromEntries(Object.entries(record.rows).filter(([, row]) => row.seq <= headSeq));
|
|
826
|
-
await this.put(meta.id, identity, rows);
|
|
827
|
-
}
|
|
828
|
-
coldSnapshot(meta, inheritedEventCount, events) {
|
|
829
|
-
const identity = identityOf(meta, inheritedEventCount);
|
|
830
|
-
const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
|
|
831
|
-
this.put(meta.id, identity, restored.checkpoint).catch((error) => {
|
|
832
|
-
this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
|
|
833
|
-
});
|
|
834
|
-
return restored.snapshot;
|
|
835
|
-
}
|
|
836
|
-
installWritePath() {
|
|
837
|
-
this.ctx.on("session/event", (session, event) => {
|
|
838
|
-
if (event.type === "turn/end") {
|
|
839
|
-
this.flushSoft(session, "turn/end");
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
const state = this.dirty.get(session) ?? {
|
|
843
|
-
pending: 0,
|
|
844
|
-
timer: void 0
|
|
845
|
-
};
|
|
846
|
-
this.dirty.set(session, state);
|
|
847
|
-
state.pending += 1;
|
|
848
|
-
if (state.pending >= this.config.writeEveryEvents) {
|
|
849
|
-
this.flushSoft(session, "count threshold");
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
state.timer ??= setTimeout(() => {
|
|
853
|
-
this.flushSoft(session, "interval");
|
|
854
|
-
}, this.config.writeIntervalMs);
|
|
855
|
-
});
|
|
856
|
-
this.ctx.on("session/created", (session) => {
|
|
857
|
-
this.flushSoft(session, "create");
|
|
858
|
-
});
|
|
859
|
-
this.ctx.on("session/disposed", (session) => {
|
|
860
|
-
this.flushSoft(session, "detach");
|
|
861
|
-
this.markClean(session);
|
|
862
|
-
this.dirty.delete(session);
|
|
863
|
-
});
|
|
864
|
-
this.ctx.effect(() => () => {
|
|
865
|
-
for (const state of this.dirty.values()) if (state.timer !== void 0) clearTimeout(state.timer);
|
|
866
|
-
this.dirty.clear();
|
|
867
|
-
}, "sessionProjectionCacheRdb.timers");
|
|
868
|
-
}
|
|
869
|
-
async flushSoft(session, trigger) {
|
|
870
|
-
try {
|
|
871
|
-
await this.write(session);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
markClean(session) {
|
|
877
|
-
const state = this.dirty.get(session);
|
|
878
|
-
if (state === void 0) return;
|
|
879
|
-
state.pending = 0;
|
|
880
|
-
if (state.timer !== void 0) {
|
|
881
|
-
clearTimeout(state.timer);
|
|
882
|
-
state.timer = void 0;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
async put(id, identity, rows) {
|
|
886
|
-
const detached = detachJson(rows);
|
|
887
|
-
await this.repository.putProjcache(id, detached);
|
|
888
|
-
if (!this.directReads) this.records.set(id, {
|
|
889
|
-
sessionId: id,
|
|
890
|
-
identity,
|
|
891
|
-
rows: detached
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
function detachJson(rows) {
|
|
896
|
-
let text;
|
|
897
|
-
try {
|
|
898
|
-
text = JSON.stringify(rows);
|
|
899
|
-
} catch (error) {
|
|
900
|
-
throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
|
|
901
|
-
}
|
|
902
|
-
if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
|
|
903
|
-
return JSON.parse(text);
|
|
904
|
-
}
|
|
905
|
-
function lifecycleIdentityOf(header) {
|
|
906
|
-
return {
|
|
907
|
-
formatVersion: header.version,
|
|
908
|
-
createdAt: header.createdAt,
|
|
909
|
-
...header.cwd === void 0 ? {} : { cwd: header.cwd },
|
|
910
|
-
isSeeded: header.isSeeded
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function identityOf(header, inheritedEventCount) {
|
|
914
|
-
const cut = SessionLogOffset(inheritedEventCount);
|
|
915
|
-
if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
|
|
916
|
-
return {
|
|
917
|
-
formatVersion: header.version,
|
|
918
|
-
createdAt: header.createdAt,
|
|
919
|
-
...header.cwd === void 0 ? {} : { cwd: header.cwd },
|
|
920
|
-
isSeeded: header.isSeeded,
|
|
921
|
-
inheritedEventCount: cut
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
function identityMatches(stored, expected) {
|
|
925
|
-
return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
|
|
926
|
-
}
|
|
927
|
-
function predecessorIdentityMatches(stored, expected) {
|
|
928
|
-
return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
|
|
929
|
-
}
|
|
930
|
-
function lifecycleIdentityMatches(stored, expected) {
|
|
931
|
-
return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded;
|
|
932
|
-
}
|
|
933
|
-
//#endregion
|
|
934
|
-
//#region src/storage-takeover/storage-backend.ts
|
|
935
|
-
const WORKSPACE_UNIT = "workspace";
|
|
936
|
-
const WORKSPACE_TABLE = "workspaces";
|
|
937
|
-
const SCHEDULE_UNIT = "schedule";
|
|
938
|
-
const SCHEDULE_TABLE = "tasks";
|
|
939
|
-
var RdbStorageBackend = class {
|
|
940
|
-
repository;
|
|
941
|
-
kv = { open: (descriptor) => this.openUnit(descriptor) };
|
|
942
|
-
open = /* @__PURE__ */ new Map();
|
|
943
|
-
closed = false;
|
|
944
|
-
constructor(repository) {
|
|
945
|
-
this.repository = repository;
|
|
946
|
-
}
|
|
947
|
-
async openUnit(descriptor) {
|
|
948
|
-
if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
|
|
949
|
-
if (descriptor.layout !== void 0 && descriptor.layout !== "single") throw new Error(`rdb storage backend serves only the 'single' layout (domain '${descriptor.name}' declares '${descriptor.layout}')`);
|
|
950
|
-
if (this.open.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
|
|
951
|
-
const unit = this.unitFor(descriptor);
|
|
952
|
-
this.open.set(descriptor.name, unit);
|
|
953
|
-
try {
|
|
954
|
-
const stored = await this.repository.readUnitVersion(descriptor.name);
|
|
955
|
-
if (stored === void 0) await this.repository.insertUnitVersion(descriptor.name, descriptor.version);
|
|
956
|
-
else if (stored !== descriptor.version) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${stored} on the medium, incompatible with descriptor version ${descriptor.version}`);
|
|
957
|
-
} catch (error) {
|
|
958
|
-
this.open.delete(descriptor.name);
|
|
959
|
-
throw error;
|
|
960
|
-
}
|
|
961
|
-
unit.onClose(() => {
|
|
962
|
-
this.open.delete(descriptor.name);
|
|
963
|
-
});
|
|
964
|
-
return unit;
|
|
965
|
-
}
|
|
966
|
-
unitFor(descriptor) {
|
|
967
|
-
if (descriptor.name === WORKSPACE_UNIT) {
|
|
968
|
-
if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) throw new Error(`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape (table '${WORKSPACE_TABLE}' plus a global slot)`);
|
|
969
|
-
return new WorkspaceKvUnit(this.repository, descriptor);
|
|
970
|
-
}
|
|
971
|
-
if (descriptor.name === SCHEDULE_UNIT) {
|
|
972
|
-
if (!descriptor.tables.includes(SCHEDULE_TABLE) || descriptor.hasGlobal !== false) throw new Error(`rdb storage backend expects the '${SCHEDULE_UNIT}' domain shape (table '${SCHEDULE_TABLE}' without a global slot)`);
|
|
973
|
-
return new ScheduleKvUnit(this.repository, descriptor);
|
|
974
|
-
}
|
|
975
|
-
throw new Error(`rdb storage backend serves only the '${WORKSPACE_UNIT}' and '${SCHEDULE_UNIT}' domains (requested '${descriptor.name}')`);
|
|
976
|
-
}
|
|
977
|
-
async close() {
|
|
978
|
-
this.closed = true;
|
|
979
|
-
const units = [...this.open.values()];
|
|
980
|
-
this.open.clear();
|
|
981
|
-
await Promise.all(units.map((unit) => unit.close()));
|
|
982
|
-
}
|
|
983
|
-
};
|
|
984
|
-
var TrackedKvUnit = class {
|
|
985
|
-
repository;
|
|
986
|
-
descriptor;
|
|
987
|
-
closed = false;
|
|
988
|
-
inflight = /* @__PURE__ */ new Set();
|
|
989
|
-
onClosed;
|
|
990
|
-
constructor(repository, descriptor) {
|
|
991
|
-
this.repository = repository;
|
|
992
|
-
this.descriptor = descriptor;
|
|
993
|
-
}
|
|
994
|
-
onClose(release) {
|
|
995
|
-
this.onClosed = release;
|
|
996
|
-
}
|
|
997
|
-
async close() {
|
|
998
|
-
if (this.closed) return;
|
|
999
|
-
this.closed = true;
|
|
1000
|
-
while (this.inflight.size > 0) await Promise.allSettled(this.inflight);
|
|
1001
|
-
this.onClosed?.();
|
|
1002
|
-
}
|
|
1003
|
-
async track(operation) {
|
|
1004
|
-
const settled = operation.then(() => void 0, () => void 0);
|
|
1005
|
-
this.inflight.add(settled);
|
|
1006
|
-
try {
|
|
1007
|
-
await operation;
|
|
1008
|
-
} finally {
|
|
1009
|
-
this.inflight.delete(settled);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
assertOpen() {
|
|
1013
|
-
if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
|
|
1014
|
-
}
|
|
1015
|
-
assertTable(table) {
|
|
1016
|
-
if (!this.tables.includes(table)) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
var WorkspaceKvUnit = class extends TrackedKvUnit {
|
|
1020
|
-
tables = [WORKSPACE_TABLE];
|
|
1021
|
-
async loadAll() {
|
|
1022
|
-
this.assertOpen();
|
|
1023
|
-
const records = Object.create(null);
|
|
1024
|
-
for (const { id, record } of await this.repository.listWorkspaces()) records[id] = record;
|
|
1025
|
-
const state = await this.repository.readWorkspaceState();
|
|
1026
|
-
return {
|
|
1027
|
-
tables: { [WORKSPACE_TABLE]: records },
|
|
1028
|
-
global: state
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
async putRecord(table, key, value) {
|
|
1032
|
-
this.assertOpen();
|
|
1033
|
-
this.assertTable(table);
|
|
1034
|
-
await this.track(this.repository.putWorkspace(key, workspaceRecordOf(value)));
|
|
1035
|
-
}
|
|
1036
|
-
async deleteRecord(table, key) {
|
|
1037
|
-
this.assertOpen();
|
|
1038
|
-
this.assertTable(table);
|
|
1039
|
-
await this.track(this.repository.deleteWorkspace(key));
|
|
1040
|
-
}
|
|
1041
|
-
async setGlobal(value) {
|
|
1042
|
-
this.assertOpen();
|
|
1043
|
-
await this.track(this.repository.writeWorkspaceState(workspaceStateOf(value)));
|
|
1044
|
-
}
|
|
1045
|
-
};
|
|
1046
|
-
var ScheduleKvUnit = class extends TrackedKvUnit {
|
|
1047
|
-
tables = [SCHEDULE_TABLE];
|
|
1048
|
-
async loadAll() {
|
|
1049
|
-
this.assertOpen();
|
|
1050
|
-
const records = Object.create(null);
|
|
1051
|
-
for (const { id, task } of await this.repository.listScheduleTasks()) records[id] = task;
|
|
1052
|
-
return {
|
|
1053
|
-
tables: { [SCHEDULE_TABLE]: records },
|
|
1054
|
-
global: null
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
async putRecord(table, key, value) {
|
|
1058
|
-
this.assertOpen();
|
|
1059
|
-
this.assertTable(table);
|
|
1060
|
-
await this.track(this.repository.putScheduleTask(key, scheduleTaskOf(value)));
|
|
1061
|
-
}
|
|
1062
|
-
async deleteRecord(table, key) {
|
|
1063
|
-
this.assertOpen();
|
|
1064
|
-
this.assertTable(table);
|
|
1065
|
-
await this.track(this.repository.deleteScheduleTask(key));
|
|
1066
|
-
}
|
|
1067
|
-
setGlobal(_value) {
|
|
1068
|
-
throw new Error(`unit '${this.descriptor.name}' declares no global slot`);
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
function workspaceRecordOf(value) {
|
|
1072
|
-
const record = value;
|
|
1073
|
-
if (typeof record !== "object" || record === null || typeof record.path !== "string" || typeof record.title !== "string" || !Array.isArray(record.sessionIds) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string") throw new TypeError("workspace record does not match the stored shape");
|
|
1074
|
-
return {
|
|
1075
|
-
path: record.path,
|
|
1076
|
-
title: record.title,
|
|
1077
|
-
sessionIds: record.sessionIds.map((id) => id),
|
|
1078
|
-
createdAt: record.createdAt,
|
|
1079
|
-
updatedAt: record.updatedAt
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
function workspaceStateOf(value) {
|
|
1083
|
-
const state = value;
|
|
1084
|
-
if (typeof state !== "object" || state === null || typeof state.initialized !== "boolean" || !Array.isArray(state.workspaceIds) || !Array.isArray(state.archivedSessionIds) || !Array.isArray(state.pinnedSessionIds)) throw new TypeError("workspace registry state does not match the stored shape");
|
|
1085
|
-
return {
|
|
1086
|
-
initialized: state.initialized,
|
|
1087
|
-
workspaceIds: state.workspaceIds.map((id) => id),
|
|
1088
|
-
archivedSessionIds: state.archivedSessionIds.map((id) => id),
|
|
1089
|
-
pinnedSessionIds: state.pinnedSessionIds.map((id) => id),
|
|
1090
|
-
...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
function scheduleTaskOf(value) {
|
|
1094
|
-
const task = value;
|
|
1095
|
-
if (typeof task !== "object" || task === null || typeof task.sessionId !== "string" || typeof task.record !== "object" || task.record === null) throw new TypeError("schedule task does not match the stored shape");
|
|
1096
|
-
return task;
|
|
1097
|
-
}
|
|
1098
|
-
//#endregion
|
|
1099
|
-
//#region src/storage-takeover/index.ts
|
|
1100
|
-
function installStorageTakeover(ctx, options) {
|
|
1101
|
-
ctx.inject(["storage"], (storageCtx) => {
|
|
1102
|
-
const backend = new RdbStorageBackend(options.repository);
|
|
1103
|
-
storageCtx.effect(() => {
|
|
1104
|
-
const unregister = storageCtx.storage.backend.register("rdb", backend);
|
|
1105
|
-
return async () => {
|
|
1106
|
-
unregister();
|
|
1107
|
-
await backend.close();
|
|
1108
|
-
};
|
|
1109
|
-
}, "session-rdb.storageBackend");
|
|
1110
|
-
storageCtx.provide(storageBackendServiceKey("rdb"), backend);
|
|
1111
|
-
});
|
|
1112
|
-
ctx.plugin(SessionProjectionCacheRdb, {
|
|
1113
|
-
...options.projectionCache,
|
|
1114
|
-
repository: options.repository,
|
|
1115
|
-
ready: options.ready
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
//#endregion
|
|
1119
|
-
//#region src/index.ts
|
|
1120
|
-
function maxEventTime(events) {
|
|
1121
|
-
let max;
|
|
1122
|
-
for (const event of events) {
|
|
1123
|
-
const time = event.time;
|
|
1124
|
-
if (typeof time !== "number") continue;
|
|
1125
|
-
if (max === void 0 || time > max) max = time;
|
|
1126
|
-
}
|
|
1127
|
-
return max;
|
|
1128
|
-
}
|
|
1129
|
-
const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
|
|
1130
|
-
const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
|
|
1131
|
-
var SessionDeletionError = class extends Error {
|
|
1132
|
-
code;
|
|
1133
|
-
constructor(message, code) {
|
|
1134
|
-
super(message);
|
|
1135
|
-
this.code = code;
|
|
1136
|
-
this.name = "SessionDeletionError";
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
var RdbBackendTracker = class {
|
|
1140
|
-
name;
|
|
1141
|
-
writers = /* @__PURE__ */ new Map();
|
|
1142
|
-
pending = /* @__PURE__ */ new Map();
|
|
1143
|
-
openHandles = /* @__PURE__ */ new Set();
|
|
1144
|
-
counter = 0;
|
|
1145
|
-
constructor(name) {
|
|
1146
|
-
this.name = name;
|
|
1147
|
-
}
|
|
1148
|
-
registerCreated(header, inheritedEventCount) {
|
|
1149
|
-
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
1150
|
-
this.writers.set(header.id, null);
|
|
1151
|
-
this.pending.set(header.id, {
|
|
1152
|
-
header,
|
|
1153
|
-
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
1154
|
-
inheritedEventCount,
|
|
1155
|
-
cursor: 0,
|
|
1156
|
-
everAppended: false
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
updatePending(id, cursor, everAppended) {
|
|
1160
|
-
const entry = this.pending.get(id);
|
|
1161
|
-
if (entry === void 0) return;
|
|
1162
|
-
this.pending.set(id, {
|
|
1163
|
-
...entry,
|
|
1164
|
-
cursor,
|
|
1165
|
-
everAppended
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
claimWrite(id) {
|
|
1169
|
-
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
1170
|
-
this.writers.set(id, null);
|
|
1171
|
-
}
|
|
1172
|
-
releaseClaim(id) {
|
|
1173
|
-
this.writers.delete(id);
|
|
1174
|
-
}
|
|
1175
|
-
pendingOf(id) {
|
|
1176
|
-
return this.pending.get(id);
|
|
1177
|
-
}
|
|
1178
|
-
hasPending(id) {
|
|
1179
|
-
return this.pending.has(id);
|
|
1180
|
-
}
|
|
1181
|
-
pendingEntries() {
|
|
1182
|
-
return this.pending.entries();
|
|
1183
|
-
}
|
|
1184
|
-
materialized(id) {
|
|
1185
|
-
this.pending.delete(id);
|
|
1186
|
-
}
|
|
1187
|
-
adopt(handle) {
|
|
1188
|
-
this.openHandles.add(handle);
|
|
1189
|
-
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
1190
|
-
return handle;
|
|
1191
|
-
}
|
|
1192
|
-
release(handle, materialized) {
|
|
1193
|
-
this.openHandles.delete(handle);
|
|
1194
|
-
if (handle.access !== "write") return;
|
|
1195
|
-
this.writers.delete(handle.id);
|
|
1196
|
-
if (!materialized) this.pending.delete(handle.id);
|
|
1197
|
-
}
|
|
1198
|
-
writerOf(id) {
|
|
1199
|
-
const writer = this.writers.get(id);
|
|
1200
|
-
return writer === null ? void 0 : writer;
|
|
1201
|
-
}
|
|
1202
|
-
hasOpenHandle(id) {
|
|
1203
|
-
for (const handle of this.openHandles) if (handle.id === id) return true;
|
|
1204
|
-
return false;
|
|
1205
|
-
}
|
|
1206
|
-
async flushAll() {
|
|
1207
|
-
const errors = [];
|
|
1208
|
-
for (const writer of this.writers.values()) {
|
|
1209
|
-
if (writer === null) continue;
|
|
1210
|
-
try {
|
|
1211
|
-
await writer.drainLive();
|
|
1212
|
-
await writer.flush();
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
if (error instanceof SessionHandleClosedError) continue;
|
|
1215
|
-
errors.push(error);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
1219
|
-
}
|
|
1220
|
-
async closeAll() {
|
|
1221
|
-
const errors = [];
|
|
1222
|
-
for (const handle of this.openHandles) try {
|
|
1223
|
-
await handle.close();
|
|
1224
|
-
} catch (error) {
|
|
1225
|
-
errors.push(error);
|
|
1226
|
-
}
|
|
1227
|
-
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
1228
|
-
}
|
|
1229
|
-
};
|
|
1230
|
-
var RdbSessionHandle = class RdbSessionHandle {
|
|
1231
|
-
persistence;
|
|
1232
|
-
id;
|
|
1233
|
-
header;
|
|
1234
|
-
access;
|
|
1235
|
-
state;
|
|
1236
|
-
chain = Promise.resolve();
|
|
1237
|
-
closing;
|
|
1238
|
-
cursor;
|
|
1239
|
-
materialized;
|
|
1240
|
-
buffered = [];
|
|
1241
|
-
batchTimer;
|
|
1242
|
-
drainPaused = false;
|
|
1243
|
-
draining;
|
|
1244
|
-
tornTruncateTo;
|
|
1245
|
-
everAppended = false;
|
|
1246
|
-
constructor(persistence, id, header, access, state) {
|
|
1247
|
-
this.persistence = persistence;
|
|
1248
|
-
this.id = id;
|
|
1249
|
-
this.header = header;
|
|
1250
|
-
this.access = access;
|
|
1251
|
-
this.state = state;
|
|
1252
|
-
this.cursor = state.cursor;
|
|
1253
|
-
this.materialized = state.materialized;
|
|
1254
|
-
this.tornTruncateTo = state.tornTruncateTo;
|
|
1255
|
-
}
|
|
1256
|
-
get inheritedEventCount() {
|
|
1257
|
-
return this.state.inheritedEventCount;
|
|
1258
|
-
}
|
|
1259
|
-
get cursorValue() {
|
|
1260
|
-
return this.cursor;
|
|
1261
|
-
}
|
|
1262
|
-
async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
|
|
1263
|
-
this.assertOpen("read");
|
|
1264
|
-
if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
1265
|
-
if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
1266
|
-
options?.signal?.throwIfAborted();
|
|
1267
|
-
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
1268
|
-
if (log === void 0) {
|
|
1269
|
-
if (this.persistence.tracker.hasPending(this.id)) return {
|
|
1270
|
-
eventState: "detached",
|
|
1271
|
-
events: []
|
|
1272
|
-
};
|
|
1273
|
-
throw new SessionPersistenceNotFoundError(this.id);
|
|
1274
|
-
}
|
|
1275
|
-
repairReadView(log.events);
|
|
1276
|
-
return {
|
|
1277
|
-
eventState: "detached",
|
|
1278
|
-
events: log.events.slice(offset, offset + length)
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
async append(events, options) {
|
|
1282
|
-
this.assertOpen("append");
|
|
1283
|
-
const batch = materializeAppendBatch(events);
|
|
1284
|
-
return this.run("append", async () => {
|
|
1285
|
-
options?.signal?.throwIfAborted();
|
|
1286
|
-
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
1287
|
-
if (batch.length === 0) return;
|
|
1288
|
-
this.everAppended = true;
|
|
1289
|
-
assertContiguous(this.id, batch, this.cursor);
|
|
1290
|
-
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
|
|
1291
|
-
this.tornTruncateTo = void 0;
|
|
1292
|
-
this.cursor += batch.length;
|
|
1293
|
-
this.materialized = true;
|
|
1294
|
-
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
async flush(options) {
|
|
1298
|
-
return this.run("flush", async () => {
|
|
1299
|
-
options?.signal?.throwIfAborted();
|
|
1300
|
-
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
1301
|
-
if (this.materialized) return;
|
|
1302
|
-
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
1303
|
-
this.materialized = true;
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
close() {
|
|
1307
|
-
return this.closing ??= (async () => {
|
|
1308
|
-
let drainFailure;
|
|
1309
|
-
for (;;) {
|
|
1310
|
-
try {
|
|
1311
|
-
await this.drainLive();
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
drainFailure = error;
|
|
1314
|
-
break;
|
|
1315
|
-
}
|
|
1316
|
-
await this.chain;
|
|
1317
|
-
if (this.buffered.length === 0) break;
|
|
1318
|
-
}
|
|
1319
|
-
await this.chain;
|
|
1320
|
-
const failures = [];
|
|
1321
|
-
if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
|
|
1322
|
-
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
1323
|
-
if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
1324
|
-
if (failures[0] !== void 0) throw failures[0];
|
|
1325
|
-
})();
|
|
1326
|
-
}
|
|
1327
|
-
[Symbol.asyncDispose]() {
|
|
1328
|
-
return this.close();
|
|
1329
|
-
}
|
|
1330
|
-
enqueueLive(event, reportBackgroundFailure) {
|
|
1331
|
-
this.buffered.push(structuredClone(event));
|
|
1332
|
-
if (this.batchTimer !== void 0 || this.drainPaused) return;
|
|
1333
|
-
this.batchTimer = setTimeout(() => {
|
|
1334
|
-
this.batchTimer = void 0;
|
|
1335
|
-
this.drainLive().catch(reportBackgroundFailure);
|
|
1336
|
-
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
1337
|
-
}
|
|
1338
|
-
resetAfterRewind(cursor, inheritedEventCount) {
|
|
1339
|
-
this.cursor = cursor;
|
|
1340
|
-
if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
|
|
1341
|
-
}
|
|
1342
|
-
drainLive() {
|
|
1343
|
-
return this.draining ??= this.drainBuffered().finally(() => {
|
|
1344
|
-
this.draining = void 0;
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
async drainBuffered() {
|
|
1348
|
-
if (this.batchTimer !== void 0) {
|
|
1349
|
-
clearTimeout(this.batchTimer);
|
|
1350
|
-
this.batchTimer = void 0;
|
|
1351
|
-
}
|
|
1352
|
-
this.drainPaused = false;
|
|
1353
|
-
while (this.buffered.length > 0) await this.enqueueChain(async () => {
|
|
1354
|
-
const batch = this.buffered.splice(0);
|
|
1355
|
-
try {
|
|
1356
|
-
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
1357
|
-
if (fresh.length === 0) return;
|
|
1358
|
-
for (const [index, event] of fresh.entries()) if (event.seq !== this.cursor + index) throw new Error(`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`);
|
|
1359
|
-
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
|
|
1360
|
-
this.tornTruncateTo = void 0;
|
|
1361
|
-
this.cursor += fresh.length;
|
|
1362
|
-
this.materialized = true;
|
|
1363
|
-
} catch (error) {
|
|
1364
|
-
this.buffered = batch.concat(this.buffered);
|
|
1365
|
-
this.drainPaused = true;
|
|
1366
|
-
throw error;
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
enqueueChain(op) {
|
|
1371
|
-
const next = this.chain.then(op);
|
|
1372
|
-
this.chain = next.catch(() => {});
|
|
1373
|
-
return next;
|
|
1374
|
-
}
|
|
1375
|
-
async run(operation, op) {
|
|
1376
|
-
this.assertOpen(operation);
|
|
1377
|
-
return this.enqueueChain(async () => {
|
|
1378
|
-
this.assertOpen(operation);
|
|
1379
|
-
return op();
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
assertOpen(operation) {
|
|
1383
|
-
if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
|
|
1384
|
-
}
|
|
1385
|
-
static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
1386
|
-
};
|
|
1387
|
-
const localized = (text) => text;
|
|
1388
|
-
const projectionCacheSchema = z.object({
|
|
1389
|
-
writeEveryEvents: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS).description(localized({
|
|
1390
|
-
zh: "每累计这么多事件写回一次投影 checkpoint。",
|
|
1391
|
-
en: "Write the projection checkpoint back after this many events."
|
|
1392
|
-
})),
|
|
1393
|
-
writeIntervalMs: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS).description(localized({
|
|
1394
|
-
zh: "距离上次写回超过这个间隔(毫秒)时也写一次。",
|
|
1395
|
-
en: "Also write back when this many ms passed since the last one."
|
|
1396
|
-
}))
|
|
1397
|
-
}).default({
|
|
1398
|
-
writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
1399
|
-
writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
1400
|
-
});
|
|
1401
|
-
var SessionPersistenceRdb = class extends SessionPersistence {
|
|
1402
|
-
static inject = ["sessions"];
|
|
1403
|
-
static Config = z.union([z.object({
|
|
1404
|
-
type: z.const("sqlite"),
|
|
1405
|
-
path: z.string().required().description(localized({
|
|
1406
|
-
zh: "SQLite 数据库文件路径;`:memory:` 是内存库(进程结束即丢)。",
|
|
1407
|
-
en: "SQLite database file path; `:memory:` keeps it in memory and loses it with the process."
|
|
1408
|
-
})),
|
|
1409
|
-
journalMode: z.union([
|
|
1410
|
-
"wal",
|
|
1411
|
-
"delete",
|
|
1412
|
-
"truncate",
|
|
1413
|
-
"persist"
|
|
1414
|
-
]).default("wal").description(localized({
|
|
1415
|
-
zh: "SQLite 日志模式;默认 wal(并发读最好)。",
|
|
1416
|
-
en: "SQLite journal mode; `wal` by default (best concurrent reads)."
|
|
1417
|
-
})),
|
|
1418
|
-
busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS).description(localized({
|
|
1419
|
-
zh: "写锁冲突时的忙等超时(毫秒)。",
|
|
1420
|
-
en: "Busy timeout in ms while a write lock is contended."
|
|
1421
|
-
})),
|
|
1422
|
-
projectionCache: projectionCacheSchema
|
|
1423
|
-
}), z.object({
|
|
1424
|
-
type: z.const("postgres"),
|
|
1425
|
-
connectionString: z.string().required().description(localized({
|
|
1426
|
-
zh: "PostgreSQL 连接串(含库名与凭证)。",
|
|
1427
|
-
en: "PostgreSQL connection string (database and credentials)."
|
|
1428
|
-
})),
|
|
1429
|
-
schema: z.string().default("public").description(localized({
|
|
1430
|
-
zh: "库内的 schema 名;默认 public。",
|
|
1431
|
-
en: "Schema name inside the database; `public` by default."
|
|
1432
|
-
})),
|
|
1433
|
-
projectionCache: projectionCacheSchema
|
|
1434
|
-
})]).description(localized({
|
|
1435
|
-
zh: "会话持久化后端:改这里的连接参数会在这一行重挂时生效(Loader 会重装被改的行)。",
|
|
1436
|
-
en: "Session persistence backend: edits apply when this row is remounted (the Loader re-installs changed rows)."
|
|
1437
|
-
})).volatile();
|
|
1438
|
-
name = "session-rdb";
|
|
1439
|
-
tracker = new RdbBackendTracker(this.name);
|
|
1440
|
-
backend;
|
|
1441
|
-
storeIdentity;
|
|
1442
|
-
ready;
|
|
1443
|
-
writeGuard = new WriteGuard();
|
|
1444
|
-
reuseEventIds = /* @__PURE__ */ new Map();
|
|
1445
|
-
liveBuffers = /* @__PURE__ */ new Map();
|
|
1446
|
-
liveReady = /* @__PURE__ */ new Map();
|
|
1447
|
-
config;
|
|
1448
|
-
constructor(ctx, config, injectedBackend) {
|
|
1449
|
-
super(ctx);
|
|
1450
|
-
this.config = config.get();
|
|
1451
|
-
this.backend = injectedBackend ?? createBackend(this.config);
|
|
1452
|
-
this.ready = this.init();
|
|
1453
|
-
this.installLiveRouting(ctx);
|
|
1454
|
-
new SessionBranchRdb(this.ctx);
|
|
1455
|
-
this.ctx.plugin(SessionQueryRdb, {});
|
|
1456
|
-
registerSessionImport(this.ctx, this);
|
|
1457
|
-
registerSessionDeletion(this.ctx, this);
|
|
1458
|
-
registerSessionExport(this.ctx, this);
|
|
1459
|
-
registerSessionGc(this.ctx, this);
|
|
1460
|
-
registerSessionUsage(this.ctx, this);
|
|
1461
|
-
registerSessionRows(this.ctx, this.backend);
|
|
1462
|
-
installStorageTakeover(this.ctx, {
|
|
1463
|
-
repository: this.backend.storage,
|
|
1464
|
-
ready: this.ready,
|
|
1465
|
-
projectionCache: {
|
|
1466
|
-
writeEveryEvents: this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
1467
|
-
writeIntervalMs: this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
1468
|
-
}
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
async init() {
|
|
1472
|
-
await this.backend.open();
|
|
1473
|
-
this.storeIdentity = this.backend.storeIdentity;
|
|
1474
|
-
}
|
|
1475
|
-
async create(header, options) {
|
|
1476
|
-
options?.signal?.throwIfAborted();
|
|
1477
|
-
const snapshot = materializeCreateHeader(header);
|
|
1478
|
-
if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
|
|
1479
|
-
const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
|
|
1480
|
-
if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
|
|
1481
|
-
await this.ready;
|
|
1482
|
-
options?.signal?.throwIfAborted();
|
|
1483
|
-
if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
|
|
1484
|
-
this.tracker.registerCreated(snapshot, inheritedEventCount);
|
|
1485
|
-
return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
|
|
1486
|
-
cursor: 0,
|
|
1487
|
-
materialized: false,
|
|
1488
|
-
inheritedEventCount
|
|
1489
|
-
}));
|
|
1490
|
-
}
|
|
1491
|
-
async open(id, access, options) {
|
|
1492
|
-
options?.signal?.throwIfAborted();
|
|
1493
|
-
await this.ready;
|
|
1494
|
-
options?.signal?.throwIfAborted();
|
|
1495
|
-
const pending = this.tracker.pendingOf(id);
|
|
1496
|
-
if (access === "read") {
|
|
1497
|
-
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
|
|
1498
|
-
cursor: 0,
|
|
1499
|
-
materialized: false,
|
|
1500
|
-
inheritedEventCount: pending.inheritedEventCount
|
|
1501
|
-
}));
|
|
1502
|
-
const log = await this.readLog(id, {}, options?.signal);
|
|
1503
|
-
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
1504
|
-
repairReadView(log.events);
|
|
1505
|
-
validateStoredEvents(log.meta, log.events);
|
|
1506
|
-
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
|
|
1507
|
-
cursor: log.events.length,
|
|
1508
|
-
materialized: true,
|
|
1509
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
1510
|
-
}));
|
|
1511
|
-
}
|
|
1512
|
-
this.tracker.claimWrite(id);
|
|
1513
|
-
try {
|
|
1514
|
-
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
|
|
1515
|
-
cursor: pending.cursor,
|
|
1516
|
-
materialized: false,
|
|
1517
|
-
inheritedEventCount: pending.inheritedEventCount
|
|
1518
|
-
}));
|
|
1519
|
-
const log = await this.readLog(id, {}, options?.signal);
|
|
1520
|
-
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
1521
|
-
repairReadView(log.events);
|
|
1522
|
-
validateStoredEvents(log.meta, log.events);
|
|
1523
|
-
if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
|
|
1524
|
-
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
1525
|
-
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
|
|
1526
|
-
cursor: log.events.length,
|
|
1527
|
-
materialized: true,
|
|
1528
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1529
|
-
...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
|
|
1530
|
-
}));
|
|
1531
|
-
} catch (error) {
|
|
1532
|
-
this.tracker.releaseClaim(id);
|
|
1533
|
-
throw error;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
flush() {
|
|
1537
|
-
return this.tracker.flushAll();
|
|
1538
|
-
}
|
|
1539
|
-
async stat(id, options) {
|
|
1540
|
-
options?.signal?.throwIfAborted();
|
|
1541
|
-
await this.ready;
|
|
1542
|
-
options?.signal?.throwIfAborted();
|
|
1543
|
-
const pending = this.tracker.pendingOf(id);
|
|
1544
|
-
if (pending !== void 0) return {
|
|
1545
|
-
header: pending.header,
|
|
1546
|
-
revision: pending.revision
|
|
1547
|
-
};
|
|
1548
|
-
const row = await this.backend.getSession(id);
|
|
1549
|
-
if (row === void 0) return void 0;
|
|
1550
|
-
return {
|
|
1551
|
-
header: rowToMeta(row),
|
|
1552
|
-
revision: this.rowRevision(row)
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
async list(options) {
|
|
1556
|
-
const signal = options?.signal;
|
|
1557
|
-
const snapshots = [];
|
|
1558
|
-
const listed = /* @__PURE__ */ new Set();
|
|
1559
|
-
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
1560
|
-
snapshots.push({
|
|
1561
|
-
header: pending.header,
|
|
1562
|
-
revision: pending.revision
|
|
1563
|
-
});
|
|
1564
|
-
listed.add(id);
|
|
1565
|
-
}
|
|
1566
|
-
signal?.throwIfAborted();
|
|
1567
|
-
await this.ready;
|
|
1568
|
-
signal?.throwIfAborted();
|
|
1569
|
-
const rows = await this.backend.listSessions();
|
|
1570
|
-
signal?.throwIfAborted();
|
|
1571
|
-
for (const row of rows) {
|
|
1572
|
-
if (listed.has(row.fSessionId)) continue;
|
|
1573
|
-
snapshots.push({
|
|
1574
|
-
header: rowToMeta(row),
|
|
1575
|
-
revision: this.rowRevision(row)
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
return snapshots;
|
|
1579
|
-
}
|
|
1580
|
-
async deleteSession(id, signal) {
|
|
1581
|
-
signal?.throwIfAborted();
|
|
1582
|
-
await this.ready;
|
|
1583
|
-
signal?.throwIfAborted();
|
|
1584
|
-
const row = await this.backend.getSession(id);
|
|
1585
|
-
if (row === void 0) throw new SessionDeletionError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
1586
|
-
if (row.fArchivedAt === null) throw new SessionDeletionError(`session "${id}" is not archived; only archived sessions can be deleted`, "SESSION_NOT_ARCHIVED");
|
|
1587
|
-
if (this.tracker.hasPending(id) || this.tracker.hasOpenHandle(id)) throw new SessionDeletionError(`session "${id}" is live; stop it before deleting`, "SESSION_LIVE");
|
|
1588
|
-
await this.unarchiveBeforeDeletion(id);
|
|
1589
|
-
const referenced = await this.backend.transaction((tx) => tx.deleteSession(id));
|
|
1590
|
-
this.liveBuffers.delete(id);
|
|
1591
|
-
this.liveReady.delete(id);
|
|
1592
|
-
this.liveFailures.delete(id);
|
|
1593
|
-
this.liveDropWarned.delete(id);
|
|
1594
|
-
this.reuseEventIds.delete(id);
|
|
1595
|
-
await this.dropSessionStats(id);
|
|
1596
|
-
await this.refreshEventUsageFlags(referenced);
|
|
1597
|
-
}
|
|
1598
|
-
async collectOrphans() {
|
|
1599
|
-
await this.ready;
|
|
1600
|
-
return this.backend.collectOrphans();
|
|
1601
|
-
}
|
|
1602
|
-
async collectOrphanSessions() {
|
|
1603
|
-
await this.ready;
|
|
1604
|
-
const deletable = (await this.backend.listOrphanSubagentSessions()).filter((id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id));
|
|
1605
|
-
if (deletable.length === 0) return 0;
|
|
1606
|
-
const { deleted, eventIds } = await this.backend.transaction((tx) => tx.deleteSessions(deletable));
|
|
1607
|
-
for (const id of deletable) {
|
|
1608
|
-
this.liveBuffers.delete(id);
|
|
1609
|
-
this.liveReady.delete(id);
|
|
1610
|
-
this.liveFailures.delete(id);
|
|
1611
|
-
this.liveDropWarned.delete(id);
|
|
1612
|
-
this.reuseEventIds.delete(id);
|
|
1613
|
-
await this.dropSessionStats(id);
|
|
1614
|
-
}
|
|
1615
|
-
await this.refreshEventUsageFlags(eventIds);
|
|
1616
|
-
return deleted;
|
|
1617
|
-
}
|
|
1618
|
-
async usageReport(sinceMs) {
|
|
1619
|
-
await this.ready;
|
|
1620
|
-
return this.backend.usageReport(sinceMs);
|
|
1621
|
-
}
|
|
1622
|
-
async vacuum() {
|
|
1623
|
-
await this.ready;
|
|
1624
|
-
await this.backend.vacuum();
|
|
1625
|
-
}
|
|
1626
|
-
async unarchiveBeforeDeletion(id) {
|
|
1627
|
-
const registry = this.ctx.get("workspaceRegistry");
|
|
1628
|
-
if (registry === void 0) return;
|
|
1629
|
-
if (!registry.archivedSessionIds.includes(id)) return;
|
|
1630
|
-
await registry.unarchiveSession(id);
|
|
1631
|
-
}
|
|
1632
|
-
async readRaw(id, signal) {
|
|
1633
|
-
signal?.throwIfAborted();
|
|
1634
|
-
await this.ready;
|
|
1635
|
-
signal?.throwIfAborted();
|
|
1636
|
-
const log = await this.readLog(id, {}, signal);
|
|
1637
|
-
if (log === void 0) return void 0;
|
|
1638
|
-
repairReadView(log.events);
|
|
1639
|
-
const events = balanceRewindPrefix(log.events, { keepOpenTail: true });
|
|
1640
|
-
const inheritedEventCount = Math.min(log.inheritedEventCount, events.length);
|
|
1641
|
-
return {
|
|
1642
|
-
meta: log.meta,
|
|
1643
|
-
inheritedEventCount,
|
|
1644
|
-
filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
|
|
1645
|
-
content: toJsonlArtifact(log.meta, inheritedEventCount, events)
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
async materializeEmpty(meta, inheritedEventCount) {
|
|
1649
|
-
await this.ready;
|
|
1650
|
-
await this.backend.transaction(async (tx) => {
|
|
1651
|
-
await tx.upsertSession({
|
|
1652
|
-
meta,
|
|
1653
|
-
inheritedEventCount
|
|
1654
|
-
}, randomUUID());
|
|
1655
|
-
await tx.bumpRevision(meta.id);
|
|
1656
|
-
});
|
|
1657
|
-
this.tracker.materialized(meta.id);
|
|
1658
|
-
this.writeGuard.confirmHead(meta.id, -1);
|
|
1659
|
-
}
|
|
1660
|
-
async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
|
|
1661
|
-
await this.ready;
|
|
1662
|
-
if (events.length === 0) return false;
|
|
1663
|
-
sealOwnEvents(events);
|
|
1664
|
-
validateStoredEvents(meta, [...events]);
|
|
1665
|
-
const reuse = this.reuseEventIds.get(meta.id);
|
|
1666
|
-
let confirmedHead = -1;
|
|
1667
|
-
let usageRows = [];
|
|
1668
|
-
await this.backend.transaction(async (tx) => {
|
|
1669
|
-
if (tornTruncateTo !== void 0) {
|
|
1670
|
-
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
1671
|
-
const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
|
|
1672
|
-
if (prev === void 0) await tx.updateHead(meta.id, "", -1);
|
|
1673
|
-
else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
1674
|
-
}
|
|
1675
|
-
await tx.upsertSession({
|
|
1676
|
-
meta,
|
|
1677
|
-
inheritedEventCount
|
|
1678
|
-
}, randomUUID());
|
|
1679
|
-
const head = await tx.getHead(meta.id);
|
|
1680
|
-
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
1681
|
-
const appended = await appendEventTail(tx, meta, events, {
|
|
1682
|
-
parentId: head.fHeadEventId,
|
|
1683
|
-
nextSeq: head.fHeadSequence + 1
|
|
1684
|
-
}, reuse);
|
|
1685
|
-
usageRows = appended.usageRows;
|
|
1686
|
-
await tx.updateHead(meta.id, appended.headEventId, appended.headSequence);
|
|
1687
|
-
await tx.bumpRevision(meta.id, maxEventTime(events));
|
|
1688
|
-
confirmedHead = appended.headSequence;
|
|
1689
|
-
});
|
|
1690
|
-
if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
|
|
1691
|
-
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
1692
|
-
this.tracker.materialized(meta.id);
|
|
1693
|
-
await this.recordSessionStats(meta.id, events, usageRows);
|
|
1694
|
-
return true;
|
|
1695
|
-
}
|
|
1696
|
-
async readLog(id, options = {}, signal) {
|
|
1697
|
-
signal?.throwIfAborted();
|
|
1698
|
-
await this.ready;
|
|
1699
|
-
signal?.throwIfAborted();
|
|
1700
|
-
const row = await this.backend.getSession(id);
|
|
1701
|
-
if (row === void 0) return void 0;
|
|
1702
|
-
const meta = rowToMeta(row);
|
|
1703
|
-
const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
|
|
1704
|
-
signal?.throwIfAborted();
|
|
1705
|
-
if (isLegacyVersion(row.fVersion)) try {
|
|
1706
|
-
const converted = convertLegacyRows(row, eventRows);
|
|
1707
|
-
return {
|
|
1708
|
-
meta: converted.meta,
|
|
1709
|
-
inheritedEventCount: converted.inheritedEventCount,
|
|
1710
|
-
events: converted.events,
|
|
1711
|
-
incarnation: row.fIncarnation,
|
|
1712
|
-
revision: row.fRevision,
|
|
1713
|
-
storedCount: eventRows.length,
|
|
1714
|
-
migrated: true
|
|
1715
|
-
};
|
|
1716
|
-
} catch (error) {
|
|
1717
|
-
this.ctx.logger.warn(`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`);
|
|
1718
|
-
const adopted = adoptLegacyRows(row, eventRows);
|
|
1719
|
-
return {
|
|
1720
|
-
meta: adopted.meta,
|
|
1721
|
-
inheritedEventCount: adopted.inheritedEventCount,
|
|
1722
|
-
events: adopted.events,
|
|
1723
|
-
incarnation: row.fIncarnation,
|
|
1724
|
-
revision: row.fRevision,
|
|
1725
|
-
storedCount: eventRows.length,
|
|
1726
|
-
migrated: false,
|
|
1727
|
-
...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
1731
|
-
if (needsShapeAdoption(preserved)) {
|
|
1732
|
-
const adopted = adoptLegacyRows(row, eventRows);
|
|
1733
|
-
return {
|
|
1734
|
-
meta: adopted.meta,
|
|
1735
|
-
inheritedEventCount: adopted.inheritedEventCount,
|
|
1736
|
-
events: adopted.events,
|
|
1737
|
-
incarnation: row.fIncarnation,
|
|
1738
|
-
revision: row.fRevision,
|
|
1739
|
-
storedCount: eventRows.length,
|
|
1740
|
-
migrated: false,
|
|
1741
|
-
...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
return {
|
|
1745
|
-
meta,
|
|
1746
|
-
inheritedEventCount: row.fSeedLength ?? 0,
|
|
1747
|
-
events: preserved,
|
|
1748
|
-
incarnation: row.fIncarnation,
|
|
1749
|
-
revision: row.fRevision,
|
|
1750
|
-
storedCount: eventRows.length,
|
|
1751
|
-
migrated: false,
|
|
1752
|
-
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
async recordSessionStats(id, events, usageRows) {
|
|
1756
|
-
const usageBuckets = sessionUsageBuckets(usageRows);
|
|
1757
|
-
if (usageBuckets.length > 0) try {
|
|
1758
|
-
await this.backend.incrementSessionUsage(id, usageBuckets);
|
|
1759
|
-
} catch (error) {
|
|
1760
|
-
this.ctx.logger.warn(`session-rdb: token usage for "${id}" not updated (${describeError(error)})`);
|
|
1761
|
-
}
|
|
1762
|
-
const countBuckets = sessionCountBuckets(events);
|
|
1763
|
-
if (countBuckets.length > 0) try {
|
|
1764
|
-
await this.backend.incrementSessionCounts(id, countBuckets);
|
|
1765
|
-
} catch (error) {
|
|
1766
|
-
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
async rebuildSessionStats(id) {
|
|
1770
|
-
try {
|
|
1771
|
-
await this.backend.rebuildSessionStats(id);
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
this.ctx.logger.warn(`session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
async refreshEventUsageFlags(eventIds) {
|
|
1777
|
-
const unique = [...new Set(eventIds)];
|
|
1778
|
-
if (unique.length === 0) return;
|
|
1779
|
-
try {
|
|
1780
|
-
await this.backend.refreshEventUsageFlags(unique);
|
|
1781
|
-
} catch (error) {
|
|
1782
|
-
this.ctx.logger.warn(`session-rdb: usage reference flags not refreshed (${describeError(error)})`);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
async dropSessionStats(id) {
|
|
1786
|
-
try {
|
|
1787
|
-
await this.backend.deleteSessionStats(id);
|
|
1788
|
-
} catch (error) {
|
|
1789
|
-
this.ctx.logger.warn(`session-rdb: session stats for "${id}" not deleted (${describeError(error)})`);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
async rewriteMigratedLog(id, log) {
|
|
1793
|
-
let removed = [];
|
|
1794
|
-
await this.backend.transaction(async (tx) => {
|
|
1795
|
-
const head = await tx.getHead(id);
|
|
1796
|
-
if (!this.writeGuard.has(id)) this.writeGuard.confirmHead(id, head.fHeadSequence);
|
|
1797
|
-
this.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
|
|
1798
|
-
removed = await tx.deleteBridgeTail(id, 0);
|
|
1799
|
-
await tx.upsertSession({
|
|
1800
|
-
meta: log.meta,
|
|
1801
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
1802
|
-
}, randomUUID());
|
|
1803
|
-
const appended = await appendEventTail(tx, log.meta, log.events, {
|
|
1804
|
-
parentId: "",
|
|
1805
|
-
nextSeq: 0
|
|
1806
|
-
});
|
|
1807
|
-
await tx.updateHead(id, appended.headEventId, appended.headSequence);
|
|
1808
|
-
await tx.bumpRevision(id, maxEventTime(log.events));
|
|
1809
|
-
});
|
|
1810
|
-
await this.rebuildSessionStats(id);
|
|
1811
|
-
await this.refreshEventUsageFlags(removed);
|
|
1812
|
-
}
|
|
1813
|
-
async listSnapshots(signal) {
|
|
1814
|
-
signal?.throwIfAborted();
|
|
1815
|
-
await this.ready;
|
|
1816
|
-
signal?.throwIfAborted();
|
|
1817
|
-
const snapshots = [];
|
|
1818
|
-
const listed = /* @__PURE__ */ new Set();
|
|
1819
|
-
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
1820
|
-
snapshots.push({
|
|
1821
|
-
header: pending.header,
|
|
1822
|
-
revision: pending.revision,
|
|
1823
|
-
inheritedEventCount: pending.inheritedEventCount
|
|
1824
|
-
});
|
|
1825
|
-
listed.add(id);
|
|
1826
|
-
}
|
|
1827
|
-
const rows = await this.backend.listSessions();
|
|
1828
|
-
signal?.throwIfAborted();
|
|
1829
|
-
for (const row of rows) {
|
|
1830
|
-
if (listed.has(row.fSessionId)) continue;
|
|
1831
|
-
snapshots.push({
|
|
1832
|
-
header: rowToMeta(row),
|
|
1833
|
-
revision: this.rowRevision(row),
|
|
1834
|
-
inheritedEventCount: row.fSeedLength ?? 0
|
|
1835
|
-
});
|
|
1836
|
-
}
|
|
1837
|
-
return snapshots;
|
|
1838
|
-
}
|
|
1839
|
-
async readStoredRevision(id, signal) {
|
|
1840
|
-
signal?.throwIfAborted();
|
|
1841
|
-
await this.ready;
|
|
1842
|
-
signal?.throwIfAborted();
|
|
1843
|
-
const row = await this.backend.getSession(id);
|
|
1844
|
-
if (row === void 0) return void 0;
|
|
1845
|
-
return this.rowRevision(row);
|
|
1846
|
-
}
|
|
1847
|
-
async createAndAppend(header, events, inheritedEventCount) {
|
|
1848
|
-
const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
|
|
1849
|
-
try {
|
|
1850
|
-
if (events.length > 0) await handle.append(events);
|
|
1851
|
-
} finally {
|
|
1852
|
-
await handle.close();
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
async load(id, signal) {
|
|
1856
|
-
signal?.throwIfAborted();
|
|
1857
|
-
await this.ready;
|
|
1858
|
-
signal?.throwIfAborted();
|
|
1859
|
-
const log = await this.readLog(id, {}, signal);
|
|
1860
|
-
if (log === void 0) {
|
|
1861
|
-
const pending = this.tracker.pendingOf(id);
|
|
1862
|
-
if (pending !== void 0) return {
|
|
1863
|
-
meta: pending.header,
|
|
1864
|
-
inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
|
|
1865
|
-
events: []
|
|
1866
|
-
};
|
|
1867
|
-
throw new SessionPersistenceNotFoundError(id);
|
|
1868
|
-
}
|
|
1869
|
-
repairReadView(log.events);
|
|
1870
|
-
validateStoredEvents(log.meta, log.events);
|
|
1871
|
-
return {
|
|
1872
|
-
meta: log.meta,
|
|
1873
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1874
|
-
events: log.events
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
async append(id, events) {
|
|
1878
|
-
const handle = await this.open(id, "write");
|
|
1879
|
-
try {
|
|
1880
|
-
await handle.append(events);
|
|
1881
|
-
} finally {
|
|
1882
|
-
await handle.close();
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
async readFrom(id, fromSeq, signal) {
|
|
1886
|
-
const log = await this.readLog(id, { fromSeq }, signal);
|
|
1887
|
-
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
1888
|
-
return {
|
|
1889
|
-
meta: log.meta,
|
|
1890
|
-
inheritedEventCount: log.inheritedEventCount,
|
|
1891
|
-
events: log.events
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
async close() {
|
|
1895
|
-
await this.ready;
|
|
1896
|
-
await this.backend.close();
|
|
1897
|
-
}
|
|
1898
|
-
registerReuseEventIds(childId, map) {
|
|
1899
|
-
this.reuseEventIds.set(childId, new Map(map));
|
|
1900
|
-
}
|
|
1901
|
-
dropReuseEventIds(childId) {
|
|
1902
|
-
this.reuseEventIds.delete(childId);
|
|
1903
|
-
}
|
|
1904
|
-
internals() {
|
|
1905
|
-
return {
|
|
1906
|
-
backend: this.backend,
|
|
1907
|
-
writeGuard: this.writeGuard,
|
|
1908
|
-
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
|
|
1909
|
-
append: (id, events) => this.append(id, events),
|
|
1910
|
-
load: (id) => this.load(id),
|
|
1911
|
-
inspect: (id, signal) => this.load(id, signal),
|
|
1912
|
-
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
1913
|
-
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
1914
|
-
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
1915
|
-
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
|
|
1916
|
-
dropReuseEventIds: (childId) => this.dropReuseEventIds(childId)
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
rowRevision(row) {
|
|
1920
|
-
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
1921
|
-
}
|
|
1922
|
-
liveFailures = /* @__PURE__ */ new Map();
|
|
1923
|
-
liveDropWarned = /* @__PURE__ */ new Set();
|
|
1924
|
-
watchLiveReady(ctx, session, ready) {
|
|
1925
|
-
ready.catch((error) => {
|
|
1926
|
-
const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
|
|
1927
|
-
this.liveFailures.set(session.id, failure);
|
|
1928
|
-
this.liveBuffers.delete(session.id);
|
|
1929
|
-
ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
installLiveRouting(ctx) {
|
|
1933
|
-
ctx.on("session/created", (session) => {
|
|
1934
|
-
this.liveBuffers.set(session.id, []);
|
|
1935
|
-
const ready = this.ensureLiveHandle(session);
|
|
1936
|
-
this.liveReady.set(session.id, ready);
|
|
1937
|
-
this.watchLiveReady(ctx, session, ready);
|
|
1938
|
-
});
|
|
1939
|
-
for (const session of ctx.sessions.list()) {
|
|
1940
|
-
this.liveBuffers.set(session.id, []);
|
|
1941
|
-
const ready = this.ensureLiveHandle(session);
|
|
1942
|
-
this.liveReady.set(session.id, ready);
|
|
1943
|
-
this.watchLiveReady(ctx, session, ready);
|
|
1944
|
-
}
|
|
1945
|
-
ctx.on("session/event", (session, event) => {
|
|
1946
|
-
const handle = this.tracker.writerOf(session.id);
|
|
1947
|
-
if (handle !== void 0) {
|
|
1948
|
-
handle.enqueueLive(event, (error) => {
|
|
1949
|
-
ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
|
|
1950
|
-
});
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
if (this.liveFailures.has(session.id)) {
|
|
1954
|
-
if (!this.liveDropWarned.has(session.id)) {
|
|
1955
|
-
this.liveDropWarned.add(session.id);
|
|
1956
|
-
ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
|
|
1957
|
-
}
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
1961
|
-
});
|
|
1962
|
-
ctx.on("session/flush", (session) => {
|
|
1963
|
-
const failure = this.liveFailures.get(session.id);
|
|
1964
|
-
if (failure !== void 0) return Promise.reject(failure);
|
|
1965
|
-
const handle = this.tracker.writerOf(session.id);
|
|
1966
|
-
if (handle === void 0) {
|
|
1967
|
-
const ready = this.liveReady.get(session.id);
|
|
1968
|
-
if (ready === void 0) return void 0;
|
|
1969
|
-
return ready.then(() => {
|
|
1970
|
-
const settled = this.tracker.writerOf(session.id);
|
|
1971
|
-
if (settled === void 0) return void 0;
|
|
1972
|
-
return settled.drainLive().then(() => settled.flush());
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
return handle.drainLive().then(() => handle.flush());
|
|
1976
|
-
});
|
|
1977
|
-
ctx.on("session/disposed", (session) => {
|
|
1978
|
-
const ready = this.liveReady.get(session.id);
|
|
1979
|
-
this.liveBuffers.delete(session.id);
|
|
1980
|
-
this.liveReady.delete(session.id);
|
|
1981
|
-
this.liveFailures.delete(session.id);
|
|
1982
|
-
this.liveDropWarned.delete(session.id);
|
|
1983
|
-
const closeHandle = () => {
|
|
1984
|
-
const handle = this.tracker.writerOf(session.id);
|
|
1985
|
-
if (handle === void 0) return;
|
|
1986
|
-
handle.close().catch((error) => {
|
|
1987
|
-
ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
|
|
1988
|
-
});
|
|
1989
|
-
};
|
|
1990
|
-
if (ready === void 0) {
|
|
1991
|
-
closeHandle();
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
ready.then(closeHandle, closeHandle);
|
|
1995
|
-
});
|
|
1996
|
-
ctx.effect(() => async () => {
|
|
1997
|
-
await Promise.allSettled(this.liveReady.values());
|
|
1998
|
-
this.liveBuffers.clear();
|
|
1999
|
-
this.liveReady.clear();
|
|
2000
|
-
this.liveFailures.clear();
|
|
2001
|
-
this.liveDropWarned.clear();
|
|
2002
|
-
await this.tracker.closeAll();
|
|
2003
|
-
await this.close();
|
|
2004
|
-
}, `${this.name} open handles`);
|
|
2005
|
-
}
|
|
2006
|
-
async ensureLiveHandle(session) {
|
|
2007
|
-
const id = session.header.id;
|
|
2008
|
-
if (this.tracker.writerOf(id) !== void 0) return;
|
|
2009
|
-
await this.ready;
|
|
2010
|
-
const stored = await this.readLog(id, {});
|
|
2011
|
-
let handle;
|
|
2012
|
-
if (stored === void 0) {
|
|
2013
|
-
handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
|
|
2014
|
-
const seed = session.snapshotEvents();
|
|
2015
|
-
if (seed.length > 0) await handle.append(seed);
|
|
2016
|
-
} else {
|
|
2017
|
-
if (stored.meta.cwd !== session.header.cwd) throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
|
|
2018
|
-
if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
|
|
2019
|
-
assertVersion(stored.meta);
|
|
2020
|
-
repairReadView(stored.events);
|
|
2021
|
-
const seed = session.snapshotEvents();
|
|
2022
|
-
if (!seedCoversPrefix(seed, stored.events)) throw new Error(`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`);
|
|
2023
|
-
handle = await this.open(id, "write");
|
|
2024
|
-
const suffix = seed.slice(stored.events.length);
|
|
2025
|
-
if (suffix.length > 0) await handle.append(suffix);
|
|
2026
|
-
}
|
|
2027
|
-
const buffered = this.liveBuffers.get(id);
|
|
2028
|
-
if (buffered !== void 0 && buffered.length > 0) {
|
|
2029
|
-
this.liveBuffers.set(id, []);
|
|
2030
|
-
for (const event of buffered) handle.enqueueLive(event, () => {});
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
};
|
|
2034
|
-
function seedCoversPrefix(seed, prefix) {
|
|
2035
|
-
return prefix.length <= seed.length && prefix.every((event, index) => {
|
|
2036
|
-
const seedEvent = seed[index];
|
|
2037
|
-
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
function sessionCountBuckets(events) {
|
|
2041
|
-
const counted = /* @__PURE__ */ new Map();
|
|
2042
|
-
for (const event of events) {
|
|
2043
|
-
if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
|
|
2044
|
-
const day = localDayKey(event.time);
|
|
2045
|
-
const bucket = counted.get(day) ?? {
|
|
2046
|
-
day,
|
|
2047
|
-
turns: 0,
|
|
2048
|
-
steps: 0,
|
|
2049
|
-
userInputs: 0,
|
|
2050
|
-
toolCalls: 0
|
|
2051
|
-
};
|
|
2052
|
-
addActivityCount(bucket, event.type, 1);
|
|
2053
|
-
counted.set(day, bucket);
|
|
2054
|
-
}
|
|
2055
|
-
return [...counted.values()];
|
|
2056
|
-
}
|
|
2057
|
-
function sessionUsageBuckets(usageRows) {
|
|
2058
|
-
const counted = /* @__PURE__ */ new Map();
|
|
2059
|
-
for (const row of usageRows) {
|
|
2060
|
-
const provider = row.fProvider ?? "";
|
|
2061
|
-
const model = row.fModel ?? "";
|
|
2062
|
-
const key = `${row.fDay}#${provider}#${model}`;
|
|
2063
|
-
const bucket = counted.get(key) ?? {
|
|
2064
|
-
day: row.fDay,
|
|
2065
|
-
provider,
|
|
2066
|
-
model,
|
|
2067
|
-
inputTokens: 0,
|
|
2068
|
-
outputTokens: 0,
|
|
2069
|
-
cacheReadTokens: 0,
|
|
2070
|
-
reasoningTokens: 0,
|
|
2071
|
-
totalTokens: 0
|
|
2072
|
-
};
|
|
2073
|
-
bucket.inputTokens += row.fInputTokens;
|
|
2074
|
-
bucket.outputTokens += row.fOutputTokens;
|
|
2075
|
-
bucket.cacheReadTokens += row.fCacheReadTokens;
|
|
2076
|
-
bucket.reasoningTokens += row.fReasoningTokens;
|
|
2077
|
-
bucket.totalTokens += row.fTotalTokens;
|
|
2078
|
-
counted.set(key, bucket);
|
|
2079
|
-
}
|
|
2080
|
-
return [...counted.values()];
|
|
2081
|
-
}
|
|
2082
|
-
function describeError(error) {
|
|
2083
|
-
return error instanceof Error ? error.message : "unknown error";
|
|
2084
|
-
}
|
|
2085
|
-
function createBackend(config) {
|
|
2086
|
-
if (config.type === "sqlite") return new SqliteBackend({
|
|
2087
|
-
path: config.path,
|
|
2088
|
-
journalMode: config.journalMode ?? "wal",
|
|
2089
|
-
busyTimeout: config.busyTimeout ?? 5e3
|
|
2090
|
-
});
|
|
2091
|
-
const pool = new Pool({ connectionString: config.connectionString });
|
|
2092
|
-
pool.on("error", () => {});
|
|
2093
|
-
return new PostgresBackend(drizzle({ client: pool }), {
|
|
2094
|
-
identityBase: [
|
|
2095
|
-
"postgres",
|
|
2096
|
-
pool.options.host ?? "localhost",
|
|
2097
|
-
String(pool.options.port ?? 5432),
|
|
2098
|
-
pool.options.database ?? "",
|
|
2099
|
-
config.schema ?? "public"
|
|
2100
|
-
].join(":"),
|
|
2101
|
-
schema: config.schema ?? "public",
|
|
2102
|
-
close: () => pool.end()
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
2106
|
-
let parentId = anchor.parentId;
|
|
2107
|
-
let nextSeq = anchor.nextSeq;
|
|
2108
|
-
const eventRows = [];
|
|
2109
|
-
const usageRows = [];
|
|
2110
|
-
const bridgeRows = [];
|
|
2111
|
-
for (const event of events) {
|
|
2112
|
-
const reusedId = reuse?.get(event.seq);
|
|
2113
|
-
const eventId = reusedId ?? randomUUID();
|
|
2114
|
-
if (reusedId === void 0) {
|
|
2115
|
-
const { kind, role, name, actionId } = eventDimensions(event);
|
|
2116
|
-
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
|
|
2117
|
-
const row = {
|
|
2118
|
-
fEventId: eventId,
|
|
2119
|
-
fParentId: parentId,
|
|
2120
|
-
fType: event.type,
|
|
2121
|
-
fKind: kind,
|
|
2122
|
-
fRole: role,
|
|
2123
|
-
fName: name,
|
|
2124
|
-
fActionId: actionId,
|
|
2125
|
-
fEncoding: EVENT_ENCODING,
|
|
2126
|
-
fData: JSON.stringify({
|
|
2127
|
-
...envelope,
|
|
2128
|
-
data
|
|
2129
|
-
}),
|
|
2130
|
-
fCreatedAt: event.time
|
|
2131
|
-
};
|
|
2132
|
-
eventRows.push(row);
|
|
2133
|
-
const usageRow = usageRowOf(row, meta.origin === "subagent");
|
|
2134
|
-
if (usageRow !== void 0) usageRows.push(usageRow);
|
|
2135
|
-
}
|
|
2136
|
-
const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
|
|
2137
|
-
bridgeRows.push({
|
|
2138
|
-
fSessionId: meta.id,
|
|
2139
|
-
fEventId: eventId,
|
|
2140
|
-
fSequence: nextSeq,
|
|
2141
|
-
fSurfaceOp: surfaceOp
|
|
2142
|
-
});
|
|
2143
|
-
parentId = eventId;
|
|
2144
|
-
nextSeq++;
|
|
2145
|
-
}
|
|
2146
|
-
if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
|
|
2147
|
-
await tx.insertBridges(bridgeRows);
|
|
2148
|
-
if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
|
|
2149
|
-
return {
|
|
2150
|
-
headEventId: parentId,
|
|
2151
|
-
headSequence: nextSeq - 1,
|
|
2152
|
-
usageRows
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
//#endregion
|
|
2156
|
-
//#region src/deletion.ts
|
|
2157
|
-
const STATUS_OF_DELETION_ERROR = {
|
|
2158
|
-
SESSION_NOT_FOUND: 404,
|
|
2159
|
-
SESSION_NOT_ARCHIVED: 409,
|
|
2160
|
-
SESSION_LIVE: 409
|
|
2161
|
-
};
|
|
2162
|
-
function registerSessionDeletion(ctx, persistence) {
|
|
2163
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
2164
|
-
const webServer = webCtx.webServer;
|
|
2165
|
-
const connection = webCtx.get("connection");
|
|
2166
|
-
return webCtx.effect(() => webServer.register({
|
|
2167
|
-
kind: "exact",
|
|
2168
|
-
path: SESSION_DELETE_PATH,
|
|
2169
|
-
handler: async (req, res) => {
|
|
2170
|
-
const rejection = connection.requestRejection(req);
|
|
2171
|
-
if (rejection !== void 0) {
|
|
2172
|
-
res.writeHead(rejection);
|
|
2173
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
2174
|
-
return;
|
|
2175
|
-
}
|
|
2176
|
-
if (req.method !== "POST") {
|
|
2177
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
2178
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
2179
|
-
return;
|
|
2180
|
-
}
|
|
2181
|
-
const chunks = [];
|
|
2182
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
2183
|
-
let envelope;
|
|
2184
|
-
try {
|
|
2185
|
-
envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2186
|
-
} catch {
|
|
2187
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
2188
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
if (typeof envelope.sessionId !== "string" || envelope.sessionId === "") {
|
|
2192
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
2193
|
-
res.end(JSON.stringify({ error: "missing sessionId field" }));
|
|
2194
|
-
return;
|
|
2195
|
-
}
|
|
2196
|
-
try {
|
|
2197
|
-
await persistence.deleteSession(envelope.sessionId);
|
|
2198
|
-
} catch (error) {
|
|
2199
|
-
if (error instanceof SessionDeletionError) {
|
|
2200
|
-
res.writeHead(STATUS_OF_DELETION_ERROR[error.code], { "content-type": "application/json" });
|
|
2201
|
-
res.end(JSON.stringify({
|
|
2202
|
-
error: error.message,
|
|
2203
|
-
code: error.code
|
|
2204
|
-
}));
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
2208
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session deletion failed" }));
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
2212
|
-
res.end(JSON.stringify({ deleted: envelope.sessionId }));
|
|
2213
|
-
}
|
|
2214
|
-
}));
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
//#endregion
|
|
2218
|
-
export { SessionDeletionError as n, SessionPersistenceRdb as r, registerSessionDeletion as t };
|