@mquan/portal 0.0.1
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 +168 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +357 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.js +618 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/daemon-lifecycle.d.ts +24 -0
- package/dist/src/daemon-lifecycle.js +107 -0
- package/dist/src/daemon-lifecycle.js.map +1 -0
- package/dist/src/daemon.d.ts +17 -0
- package/dist/src/daemon.js +90 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/doctor.d.ts +14 -0
- package/dist/src/doctor.js +51 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/entrypoint.d.ts +2 -0
- package/dist/src/entrypoint.js +14 -0
- package/dist/src/entrypoint.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/launch.d.ts +41 -0
- package/dist/src/launch.js +186 -0
- package/dist/src/launch.js.map +1 -0
- package/dist/src/mcp.d.ts +9 -0
- package/dist/src/mcp.js +132 -0
- package/dist/src/mcp.js.map +1 -0
- package/dist/src/network/client.d.ts +67 -0
- package/dist/src/network/client.js +540 -0
- package/dist/src/network/client.js.map +1 -0
- package/dist/src/network/coordinator.d.ts +83 -0
- package/dist/src/network/coordinator.js +225 -0
- package/dist/src/network/coordinator.js.map +1 -0
- package/dist/src/network/protocol.d.ts +1352 -0
- package/dist/src/network/protocol.js +360 -0
- package/dist/src/network/protocol.js.map +1 -0
- package/dist/src/network/server.d.ts +28 -0
- package/dist/src/network/server.js +474 -0
- package/dist/src/network/server.js.map +1 -0
- package/dist/src/network/tasks.d.ts +65 -0
- package/dist/src/network/tasks.js +276 -0
- package/dist/src/network/tasks.js.map +1 -0
- package/dist/src/portal-index.d.ts +21 -0
- package/dist/src/portal-index.js +140 -0
- package/dist/src/portal-index.js.map +1 -0
- package/dist/src/provider/claude-managed.d.ts +34 -0
- package/dist/src/provider/claude-managed.js +125 -0
- package/dist/src/provider/claude-managed.js.map +1 -0
- package/dist/src/provider/claude.d.ts +24 -0
- package/dist/src/provider/claude.js +114 -0
- package/dist/src/provider/claude.js.map +1 -0
- package/dist/src/provider/codex/app-server-client.d.ts +43 -0
- package/dist/src/provider/codex/app-server-client.js +143 -0
- package/dist/src/provider/codex/app-server-client.js.map +1 -0
- package/dist/src/provider/codex/index.d.ts +3 -0
- package/dist/src/provider/codex/index.js +4 -0
- package/dist/src/provider/codex/index.js.map +1 -0
- package/dist/src/provider/codex/managed-runner.d.ts +12 -0
- package/dist/src/provider/codex/managed-runner.js +162 -0
- package/dist/src/provider/codex/managed-runner.js.map +1 -0
- package/dist/src/provider/codex/probe.d.ts +2 -0
- package/dist/src/provider/codex/probe.js +33 -0
- package/dist/src/provider/codex/probe.js.map +1 -0
- package/dist/src/provider/codex/protocol.d.ts +193 -0
- package/dist/src/provider/codex/protocol.js +10 -0
- package/dist/src/provider/codex/protocol.js.map +1 -0
- package/dist/src/provider/codex/provider.d.ts +35 -0
- package/dist/src/provider/codex/provider.js +289 -0
- package/dist/src/provider/codex/provider.js.map +1 -0
- package/dist/src/provider/codex/remote-app-server.d.ts +16 -0
- package/dist/src/provider/codex/remote-app-server.js +80 -0
- package/dist/src/provider/codex/remote-app-server.js.map +1 -0
- package/dist/src/provider/codex/websocket-client.d.ts +40 -0
- package/dist/src/provider/codex/websocket-client.js +174 -0
- package/dist/src/provider/codex/websocket-client.js.map +1 -0
- package/dist/src/provider/types.d.ts +35 -0
- package/dist/src/provider/types.js +2 -0
- package/dist/src/provider/types.js.map +1 -0
- package/dist/src/recall/embedding.d.ts +28 -0
- package/dist/src/recall/embedding.js +142 -0
- package/dist/src/recall/embedding.js.map +1 -0
- package/dist/src/recall/types.d.ts +35 -0
- package/dist/src/recall/types.js +2 -0
- package/dist/src/recall/types.js.map +1 -0
- package/dist/src/rpc/client.d.ts +6 -0
- package/dist/src/rpc/client.js +58 -0
- package/dist/src/rpc/client.js.map +1 -0
- package/dist/src/rpc/protocol.d.ts +21 -0
- package/dist/src/rpc/protocol.js +11 -0
- package/dist/src/rpc/protocol.js.map +1 -0
- package/dist/src/rpc/server.d.ts +12 -0
- package/dist/src/rpc/server.js +154 -0
- package/dist/src/rpc/server.js.map +1 -0
- package/dist/src/runtime/paths.d.ts +8 -0
- package/dist/src/runtime/paths.js +18 -0
- package/dist/src/runtime/paths.js.map +1 -0
- package/dist/src/service.d.ts +127 -0
- package/dist/src/service.js +637 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/session-profile.d.ts +39 -0
- package/dist/src/session-profile.js +186 -0
- package/dist/src/session-profile.js.map +1 -0
- package/dist/src/storage/database.d.ts +9 -0
- package/dist/src/storage/database.js +354 -0
- package/dist/src/storage/database.js.map +1 -0
- package/dist/src/storage/repository.d.ts +275 -0
- package/dist/src/storage/repository.js +1291 -0
- package/dist/src/storage/repository.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/dist/src/version.js.map +1 -0
- package/docs/architecture-plan.md +191 -0
- package/docs/mvp-plan.md +111 -0
- package/docs/remote-demo.md +346 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { cosineSimilarity, embeddingBuckets } from "../recall/embedding.js";
|
|
3
|
+
const DEFAULT_INSTALLATION = "local";
|
|
4
|
+
const DEFAULT_ACCOUNT = "default";
|
|
5
|
+
export class PortalRepository {
|
|
6
|
+
database;
|
|
7
|
+
constructor(database) {
|
|
8
|
+
this.database = database;
|
|
9
|
+
}
|
|
10
|
+
upsertSession(input) {
|
|
11
|
+
const installation = input.installation ?? DEFAULT_INSTALLATION;
|
|
12
|
+
const accountFingerprint = input.accountFingerprint ?? DEFAULT_ACCOUNT;
|
|
13
|
+
const portalSessionId = stableId("ps", input.provider, installation, accountFingerprint, input.vendorSessionId);
|
|
14
|
+
this.database.connection
|
|
15
|
+
.prepare(`
|
|
16
|
+
INSERT INTO sessions (
|
|
17
|
+
portal_session_id, provider, installation, account_fingerprint,
|
|
18
|
+
vendor_session_id, workspace, title, managed_name, observed_at, updated_at, state,
|
|
19
|
+
state_confidence, indexed, connected, messageable
|
|
20
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
21
|
+
ON CONFLICT(provider, installation, account_fingerprint, vendor_session_id)
|
|
22
|
+
DO UPDATE SET
|
|
23
|
+
workspace = excluded.workspace,
|
|
24
|
+
title = excluded.title,
|
|
25
|
+
managed_name = coalesce(excluded.managed_name, sessions.managed_name),
|
|
26
|
+
observed_at = excluded.observed_at,
|
|
27
|
+
updated_at = excluded.updated_at,
|
|
28
|
+
state = excluded.state,
|
|
29
|
+
state_confidence = excluded.state_confidence,
|
|
30
|
+
indexed = max(sessions.indexed, excluded.indexed),
|
|
31
|
+
connected = excluded.connected,
|
|
32
|
+
messageable = excluded.messageable
|
|
33
|
+
`)
|
|
34
|
+
.run(portalSessionId, input.provider, installation, accountFingerprint, input.vendorSessionId, input.workspace, input.title, input.managedName ?? null, (input.observedAt ?? new Date()).getTime(), toEpoch(input.updatedAt), input.state, input.stateConfidence, Number(input.indexed), Number(input.connected), Number(input.messageable));
|
|
35
|
+
const stored = this.findSession({
|
|
36
|
+
provider: input.provider,
|
|
37
|
+
installation,
|
|
38
|
+
accountFingerprint,
|
|
39
|
+
vendorSessionId: input.vendorSessionId,
|
|
40
|
+
});
|
|
41
|
+
if (stored === null)
|
|
42
|
+
throw new Error(`failed to load upserted session ${portalSessionId}`);
|
|
43
|
+
return stored;
|
|
44
|
+
}
|
|
45
|
+
getSession(portalSessionId) {
|
|
46
|
+
const row = this.database.connection
|
|
47
|
+
.prepare("SELECT * FROM sessions WHERE portal_session_id = ?")
|
|
48
|
+
.get(portalSessionId);
|
|
49
|
+
return row === undefined ? null : mapSession(row);
|
|
50
|
+
}
|
|
51
|
+
bindSessionVendorId(portalSessionId, vendorSessionId) {
|
|
52
|
+
if (vendorSessionId.trim().length === 0)
|
|
53
|
+
throw new TypeError("vendor session ID cannot be empty");
|
|
54
|
+
try {
|
|
55
|
+
const changed = this.database.connection
|
|
56
|
+
.prepare("UPDATE sessions SET vendor_session_id = ?, observed_at = ? WHERE portal_session_id = ?")
|
|
57
|
+
.run(vendorSessionId, Date.now(), portalSessionId);
|
|
58
|
+
if (Number(changed.changes) !== 1)
|
|
59
|
+
throw new Error(`unknown Portal session: ${portalSessionId}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (String(error).includes("UNIQUE constraint failed")) {
|
|
63
|
+
throw new Error(`Codex session is already bound in Portal: ${vendorSessionId}`);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const session = this.getSession(portalSessionId);
|
|
68
|
+
if (session === null)
|
|
69
|
+
throw new Error(`failed to bind Portal session: ${portalSessionId}`);
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
findSession(identity) {
|
|
73
|
+
const row = this.database.connection
|
|
74
|
+
.prepare(`
|
|
75
|
+
SELECT * FROM sessions
|
|
76
|
+
WHERE provider = ? AND installation = ? AND account_fingerprint = ?
|
|
77
|
+
AND vendor_session_id = ?
|
|
78
|
+
`)
|
|
79
|
+
.get(identity.provider, identity.installation ?? DEFAULT_INSTALLATION, identity.accountFingerprint ?? DEFAULT_ACCOUNT, identity.vendorSessionId);
|
|
80
|
+
return row === undefined ? null : mapSession(row);
|
|
81
|
+
}
|
|
82
|
+
listSessions(workspace) {
|
|
83
|
+
const rows = (workspace === undefined
|
|
84
|
+
? this.database.connection.prepare("SELECT * FROM sessions ORDER BY updated_at DESC").all()
|
|
85
|
+
: this.database.connection
|
|
86
|
+
.prepare("SELECT * FROM sessions WHERE workspace = ? ORDER BY updated_at DESC")
|
|
87
|
+
.all(workspace));
|
|
88
|
+
return rows.map(mapSession);
|
|
89
|
+
}
|
|
90
|
+
upsertSessionProfile(input) {
|
|
91
|
+
if (this.getSession(input.sessionId) === null) {
|
|
92
|
+
throw new Error(`unknown Portal session: ${input.sessionId}`);
|
|
93
|
+
}
|
|
94
|
+
const displayName = input.displayName.trim();
|
|
95
|
+
if (displayName.length === 0)
|
|
96
|
+
throw new TypeError("session profile display name cannot be empty");
|
|
97
|
+
const topics = input.topics.map((topic) => topic.trim()).filter((topic) => topic.length > 0);
|
|
98
|
+
const updatedAt = input.updatedAt ?? new Date();
|
|
99
|
+
if (!Number.isFinite(updatedAt.getTime()))
|
|
100
|
+
throw new TypeError("session profile updatedAt is invalid");
|
|
101
|
+
this.database.connection.prepare(`
|
|
102
|
+
INSERT INTO session_profiles (
|
|
103
|
+
session_id, display_name, description, topics_json, source,
|
|
104
|
+
transcript_watermark, updated_at
|
|
105
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
106
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
107
|
+
display_name = excluded.display_name,
|
|
108
|
+
description = excluded.description,
|
|
109
|
+
topics_json = excluded.topics_json,
|
|
110
|
+
source = excluded.source,
|
|
111
|
+
transcript_watermark = excluded.transcript_watermark,
|
|
112
|
+
updated_at = excluded.updated_at
|
|
113
|
+
`).run(input.sessionId, displayName, input.description.trim(), JSON.stringify(topics), input.source, input.transcriptWatermark, updatedAt.getTime());
|
|
114
|
+
const profile = this.getSessionProfile(input.sessionId);
|
|
115
|
+
if (profile === null)
|
|
116
|
+
throw new Error(`failed to upsert session profile: ${input.sessionId}`);
|
|
117
|
+
return profile;
|
|
118
|
+
}
|
|
119
|
+
getSessionProfile(sessionId) {
|
|
120
|
+
const row = this.database.connection
|
|
121
|
+
.prepare("SELECT * FROM session_profiles WHERE session_id = ?")
|
|
122
|
+
.get(sessionId);
|
|
123
|
+
return row === undefined ? null : mapSessionProfile(row);
|
|
124
|
+
}
|
|
125
|
+
listSessionProfiles(workspace) {
|
|
126
|
+
const rows = (workspace === undefined
|
|
127
|
+
? this.database.connection
|
|
128
|
+
.prepare("SELECT * FROM session_profiles ORDER BY updated_at DESC, session_id")
|
|
129
|
+
.all()
|
|
130
|
+
: this.database.connection.prepare(`
|
|
131
|
+
SELECT p.* FROM session_profiles p
|
|
132
|
+
JOIN sessions s ON s.portal_session_id = p.session_id
|
|
133
|
+
WHERE s.workspace = ?
|
|
134
|
+
ORDER BY p.updated_at DESC, p.session_id
|
|
135
|
+
`).all(workspace));
|
|
136
|
+
return rows.map(mapSessionProfile);
|
|
137
|
+
}
|
|
138
|
+
ingestTranscript(portalSessionId, entries) {
|
|
139
|
+
const owningSession = this.getSession(portalSessionId);
|
|
140
|
+
if (owningSession === null) {
|
|
141
|
+
throw new Error(`unknown Portal session: ${portalSessionId}`);
|
|
142
|
+
}
|
|
143
|
+
const result = { inserted: 0, updated: 0, unchanged: 0, removed: 0, chunkIds: [] };
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
this.database.transaction(() => {
|
|
146
|
+
const findEntry = this.database.connection.prepare("SELECT entry_id, content_hash FROM transcript_entries WHERE session_id = ? AND source_id = ?");
|
|
147
|
+
const insertEntry = this.database.connection.prepare(`
|
|
148
|
+
INSERT INTO transcript_entries (
|
|
149
|
+
entry_id, session_id, source_id, role, text, occurred_at,
|
|
150
|
+
content_hash, created_at, updated_at
|
|
151
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
152
|
+
`);
|
|
153
|
+
const updateEntry = this.database.connection.prepare(`
|
|
154
|
+
UPDATE transcript_entries
|
|
155
|
+
SET role = ?, text = ?, occurred_at = ?, content_hash = ?, updated_at = ?
|
|
156
|
+
WHERE entry_id = ?
|
|
157
|
+
`);
|
|
158
|
+
const deleteChunks = this.database.connection.prepare("DELETE FROM chunks WHERE first_entry_id = ?");
|
|
159
|
+
const insertChunk = this.database.connection.prepare(`
|
|
160
|
+
INSERT INTO chunks (
|
|
161
|
+
chunk_id, session_id, first_entry_id, last_entry_id, ordinal,
|
|
162
|
+
text, content_hash, occurred_at
|
|
163
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
+
`);
|
|
165
|
+
this.database.connection.exec(`
|
|
166
|
+
CREATE TEMP TABLE IF NOT EXISTS portal_current_transcript_sources (
|
|
167
|
+
source_id TEXT PRIMARY KEY
|
|
168
|
+
) WITHOUT ROWID;
|
|
169
|
+
DELETE FROM portal_current_transcript_sources;
|
|
170
|
+
`);
|
|
171
|
+
const markSourcePresent = this.database.connection.prepare("INSERT OR IGNORE INTO portal_current_transcript_sources(source_id) VALUES (?)");
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
markSourcePresent.run(entry.sourceId);
|
|
174
|
+
if (entry.vendorSessionId.length > 0) {
|
|
175
|
+
if (owningSession.vendorSessionId !== entry.vendorSessionId) {
|
|
176
|
+
throw new Error(`transcript entry ${entry.sourceId} belongs to ${entry.vendorSessionId}, expected ${owningSession.vendorSessionId}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const entryId = stableId("te", portalSessionId, entry.sourceId);
|
|
180
|
+
const contentHash = digest(entry.role, entry.text, entry.occurredAt?.toISOString() ?? "");
|
|
181
|
+
const existing = findEntry.get(portalSessionId, entry.sourceId);
|
|
182
|
+
if (existing?.content_hash === contentHash) {
|
|
183
|
+
result.unchanged += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (existing === undefined) {
|
|
187
|
+
insertEntry.run(entryId, portalSessionId, entry.sourceId, entry.role, entry.text, toEpoch(entry.occurredAt), contentHash, now, now);
|
|
188
|
+
result.inserted += 1;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
updateEntry.run(entry.role, entry.text, toEpoch(entry.occurredAt), contentHash, now, existing.entry_id);
|
|
192
|
+
deleteChunks.run(existing.entry_id);
|
|
193
|
+
result.updated += 1;
|
|
194
|
+
}
|
|
195
|
+
const chunks = splitIntoChunks(entry.text);
|
|
196
|
+
for (let ordinal = 0; ordinal < chunks.length; ordinal += 1) {
|
|
197
|
+
const text = chunks[ordinal];
|
|
198
|
+
if (text === undefined)
|
|
199
|
+
continue;
|
|
200
|
+
const chunkHash = digest(text);
|
|
201
|
+
const chunkId = stableId("ch", entryId, String(ordinal), chunkHash);
|
|
202
|
+
insertChunk.run(chunkId, portalSessionId, entryId, entryId, ordinal, text, chunkHash, toEpoch(entry.occurredAt));
|
|
203
|
+
result.chunkIds.push(chunkId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const removed = this.database.connection.prepare(`
|
|
207
|
+
DELETE FROM transcript_entries
|
|
208
|
+
WHERE session_id = ?
|
|
209
|
+
AND NOT EXISTS (
|
|
210
|
+
SELECT 1 FROM portal_current_transcript_sources current
|
|
211
|
+
WHERE current.source_id = transcript_entries.source_id
|
|
212
|
+
)
|
|
213
|
+
`).run(portalSessionId);
|
|
214
|
+
result.removed = Number(removed.changes);
|
|
215
|
+
});
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
markSessionIndexed(portalSessionId) {
|
|
219
|
+
const changed = this.database.connection
|
|
220
|
+
.prepare("UPDATE sessions SET indexed = 1 WHERE portal_session_id = ?")
|
|
221
|
+
.run(portalSessionId);
|
|
222
|
+
if (Number(changed.changes) !== 1)
|
|
223
|
+
throw new Error(`unknown Portal session: ${portalSessionId}`);
|
|
224
|
+
}
|
|
225
|
+
chunksNeedingEmbedding(portalSessionId, model) {
|
|
226
|
+
const rows = this.database.connection
|
|
227
|
+
.prepare(`
|
|
228
|
+
SELECT chunk_id, session_id, text, embedding_model, embedding_json
|
|
229
|
+
FROM chunks
|
|
230
|
+
WHERE session_id = ? AND (embedding_model IS NULL OR embedding_model != ?)
|
|
231
|
+
ORDER BY occurred_at, chunk_id
|
|
232
|
+
`)
|
|
233
|
+
.all(portalSessionId, model);
|
|
234
|
+
return rows.map(mapChunk);
|
|
235
|
+
}
|
|
236
|
+
saveEmbedding(chunkId, model, vector) {
|
|
237
|
+
this.saveEmbeddings(model, [{ chunkId, vector }]);
|
|
238
|
+
}
|
|
239
|
+
saveEmbeddings(model, embeddings) {
|
|
240
|
+
for (const { vector } of embeddings) {
|
|
241
|
+
if (vector.some((value) => !Number.isFinite(value))) {
|
|
242
|
+
throw new TypeError("embedding contains a non-finite value");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (embeddings.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
this.database.transaction(() => {
|
|
248
|
+
const update = this.database.connection.prepare("UPDATE chunks SET embedding_model = ?, embedding_json = ? WHERE chunk_id = ?");
|
|
249
|
+
const removeBuckets = this.database.connection.prepare("DELETE FROM embedding_buckets WHERE chunk_id = ?");
|
|
250
|
+
const insert = this.database.connection.prepare("INSERT INTO embedding_buckets(model, bucket, chunk_id) VALUES (?, ?, ?)");
|
|
251
|
+
for (const { chunkId, vector } of embeddings) {
|
|
252
|
+
const changed = update.run(model, JSON.stringify(Array.from(vector)), chunkId);
|
|
253
|
+
if (Number(changed.changes) !== 1)
|
|
254
|
+
throw new Error(`unknown chunk: ${chunkId}`);
|
|
255
|
+
removeBuckets.run(chunkId);
|
|
256
|
+
for (const bucket of embeddingBuckets(vector))
|
|
257
|
+
insert.run(model, bucket, chunkId);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
lexicalCandidates(ftsQuery, query) {
|
|
262
|
+
if (ftsQuery.length === 0)
|
|
263
|
+
return [];
|
|
264
|
+
const { sql, parameters } = candidateSql(query, true);
|
|
265
|
+
const rows = this.database.connection
|
|
266
|
+
.prepare(sql)
|
|
267
|
+
.all(ftsQuery, query.workspace, ...parameters, query.limit);
|
|
268
|
+
return rows.map((row) => ({
|
|
269
|
+
chunkId: row.chunk_id,
|
|
270
|
+
text: row.text,
|
|
271
|
+
lexicalRank: row.lexical_rank,
|
|
272
|
+
citation: mapCitation(row),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
semanticCandidates(model, vector, query) {
|
|
276
|
+
const buckets = embeddingBuckets(vector);
|
|
277
|
+
if (buckets.length === 0)
|
|
278
|
+
return [];
|
|
279
|
+
const providerClause = providerFilter(query.providers);
|
|
280
|
+
const timeClause = timeFilter(query);
|
|
281
|
+
const placeholders = buckets.map(() => "?").join(", ");
|
|
282
|
+
const sql = `
|
|
283
|
+
SELECT
|
|
284
|
+
c.chunk_id, c.text, c.embedding_json,
|
|
285
|
+
s.portal_session_id, s.vendor_session_id, s.provider, s.workspace,
|
|
286
|
+
e.source_id, e.entry_id, c.occurred_at,
|
|
287
|
+
count(*) AS bucket_hits
|
|
288
|
+
FROM embedding_buckets b
|
|
289
|
+
JOIN chunks c ON c.chunk_id = b.chunk_id
|
|
290
|
+
JOIN sessions s ON s.portal_session_id = c.session_id
|
|
291
|
+
JOIN transcript_entries e ON e.entry_id = c.first_entry_id
|
|
292
|
+
WHERE b.model = ? AND b.bucket IN (${placeholders})
|
|
293
|
+
AND s.workspace = ?
|
|
294
|
+
${providerClause.sql}
|
|
295
|
+
${timeClause.sql}
|
|
296
|
+
GROUP BY c.chunk_id
|
|
297
|
+
ORDER BY bucket_hits DESC
|
|
298
|
+
LIMIT ?
|
|
299
|
+
`;
|
|
300
|
+
const rows = this.database.connection
|
|
301
|
+
.prepare(sql)
|
|
302
|
+
.all(model, ...buckets, query.workspace, ...providerClause.parameters, ...timeClause.parameters, query.limit);
|
|
303
|
+
return rows
|
|
304
|
+
.map((row) => ({
|
|
305
|
+
chunkId: row.chunk_id,
|
|
306
|
+
text: row.text,
|
|
307
|
+
similarity: cosineSimilarity(vector, JSON.parse(row.embedding_json)),
|
|
308
|
+
citation: mapCitation(row),
|
|
309
|
+
}))
|
|
310
|
+
.filter(({ similarity }) => similarity > 0.01)
|
|
311
|
+
.sort((left, right) => right.similarity - left.similarity || left.chunkId.localeCompare(right.chunkId));
|
|
312
|
+
}
|
|
313
|
+
explainLexicalQuery(ftsQuery, query) {
|
|
314
|
+
const { sql, parameters } = candidateSql(query, true);
|
|
315
|
+
const rows = this.database.connection
|
|
316
|
+
.prepare(`EXPLAIN QUERY PLAN ${sql}`)
|
|
317
|
+
.all(ftsQuery, query.workspace, ...parameters, query.limit);
|
|
318
|
+
return rows.map(({ detail }) => detail);
|
|
319
|
+
}
|
|
320
|
+
workspaceFreshness(workspace) {
|
|
321
|
+
const row = this.database.connection
|
|
322
|
+
.prepare(`
|
|
323
|
+
SELECT max(e.updated_at) AS freshness
|
|
324
|
+
FROM transcript_entries e
|
|
325
|
+
JOIN sessions s ON s.portal_session_id = e.session_id
|
|
326
|
+
WHERE s.workspace = ?
|
|
327
|
+
`)
|
|
328
|
+
.get(workspace);
|
|
329
|
+
return fromEpoch(row?.freshness ?? null);
|
|
330
|
+
}
|
|
331
|
+
upsertCursor(input) {
|
|
332
|
+
const updatedAt = Date.now();
|
|
333
|
+
this.database.connection
|
|
334
|
+
.prepare(`
|
|
335
|
+
INSERT INTO ingestion_cursors (
|
|
336
|
+
provider, source_scope, cursor, watermark, last_success, last_error, updated_at
|
|
337
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
338
|
+
ON CONFLICT(provider, source_scope) DO UPDATE SET
|
|
339
|
+
cursor = excluded.cursor,
|
|
340
|
+
watermark = excluded.watermark,
|
|
341
|
+
last_success = excluded.last_success,
|
|
342
|
+
last_error = excluded.last_error,
|
|
343
|
+
updated_at = excluded.updated_at
|
|
344
|
+
`)
|
|
345
|
+
.run(input.provider, input.sourceScope, input.cursor, input.watermark, toEpoch(input.lastSuccess), input.lastError, updatedAt);
|
|
346
|
+
const cursor = this.getCursor(input.provider, input.sourceScope);
|
|
347
|
+
if (cursor === null)
|
|
348
|
+
throw new Error("failed to upsert ingestion cursor");
|
|
349
|
+
return cursor;
|
|
350
|
+
}
|
|
351
|
+
getCursor(provider, sourceScope) {
|
|
352
|
+
const row = this.database.connection
|
|
353
|
+
.prepare("SELECT * FROM ingestion_cursors WHERE provider = ? AND source_scope = ?")
|
|
354
|
+
.get(provider, sourceScope);
|
|
355
|
+
if (row === undefined)
|
|
356
|
+
return null;
|
|
357
|
+
return {
|
|
358
|
+
provider: row.provider,
|
|
359
|
+
sourceScope: row.source_scope,
|
|
360
|
+
cursor: row.cursor,
|
|
361
|
+
watermark: row.watermark,
|
|
362
|
+
lastSuccess: fromEpoch(row.last_success),
|
|
363
|
+
lastError: row.last_error,
|
|
364
|
+
updatedAt: new Date(row.updated_at),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
countTranscriptEntries(sessionId) {
|
|
368
|
+
const row = this.database.connection
|
|
369
|
+
.prepare("SELECT count(*) AS count FROM transcript_entries WHERE session_id = ?")
|
|
370
|
+
.get(sessionId);
|
|
371
|
+
return row.count;
|
|
372
|
+
}
|
|
373
|
+
enqueueMessage(input) {
|
|
374
|
+
const sender = this.getSession(input.senderSessionId);
|
|
375
|
+
const target = this.getSession(input.targetSessionId);
|
|
376
|
+
if (sender === null || target === null)
|
|
377
|
+
throw new Error("message sender and target must exist");
|
|
378
|
+
if (sender.workspace === null || sender.workspace !== target.workspace) {
|
|
379
|
+
throw new Error("cross-workspace messages are not allowed");
|
|
380
|
+
}
|
|
381
|
+
if (input.idempotencyKey.length > 200)
|
|
382
|
+
throw new Error("message idempotency key is too long");
|
|
383
|
+
if (input.replyTo !== undefined && input.replyTo !== null) {
|
|
384
|
+
this.validateReply(input.replyTo, sender.workspace, input.senderSessionId, input.targetSessionId, 4);
|
|
385
|
+
}
|
|
386
|
+
const prior = this.getMessageByIdempotency(input.senderSessionId, input.idempotencyKey);
|
|
387
|
+
if (prior !== null) {
|
|
388
|
+
if (prior.targetSessionId !== input.targetSessionId ||
|
|
389
|
+
prior.body !== input.body ||
|
|
390
|
+
prior.replyTo !== (input.replyTo ?? null)) {
|
|
391
|
+
throw new Error("message idempotency key was reused with a different payload");
|
|
392
|
+
}
|
|
393
|
+
return prior;
|
|
394
|
+
}
|
|
395
|
+
const messageId = stableId("msg", input.senderSessionId, input.idempotencyKey);
|
|
396
|
+
this.database.connection
|
|
397
|
+
.prepare(`
|
|
398
|
+
INSERT INTO messages (
|
|
399
|
+
message_id, sender_session_id, target_session_id, body, state,
|
|
400
|
+
created_at, reply_to, idempotency_key
|
|
401
|
+
) VALUES (?, ?, ?, ?, 'queued', ?, ?, ?)
|
|
402
|
+
ON CONFLICT(sender_session_id, idempotency_key) DO NOTHING
|
|
403
|
+
`)
|
|
404
|
+
.run(messageId, input.senderSessionId, input.targetSessionId, input.body, Date.now(), input.replyTo ?? null, input.idempotencyKey);
|
|
405
|
+
const stored = this.getMessage(messageId);
|
|
406
|
+
if (stored === null)
|
|
407
|
+
throw new Error("failed to enqueue message");
|
|
408
|
+
return stored;
|
|
409
|
+
}
|
|
410
|
+
createMessageIdempotencyKey() {
|
|
411
|
+
return randomUUID();
|
|
412
|
+
}
|
|
413
|
+
countMessagesSince(senderSessionId, since) {
|
|
414
|
+
const row = this.database.connection
|
|
415
|
+
.prepare("SELECT count(*) AS count FROM messages WHERE sender_session_id = ? AND created_at >= ?")
|
|
416
|
+
.get(senderSessionId, since.getTime());
|
|
417
|
+
return row.count;
|
|
418
|
+
}
|
|
419
|
+
hasRunningDelivery(senderSessionId, targetSessionId) {
|
|
420
|
+
const row = this.database.connection.prepare(`
|
|
421
|
+
SELECT 1 AS running FROM messages m
|
|
422
|
+
JOIN message_receipts r ON r.message_id = m.message_id
|
|
423
|
+
WHERE m.sender_session_id = ? AND m.target_session_id = ?
|
|
424
|
+
AND m.state = 'delivering' AND r.status = 'running'
|
|
425
|
+
LIMIT 1
|
|
426
|
+
`).get(senderSessionId, targetSessionId);
|
|
427
|
+
return row?.running === 1;
|
|
428
|
+
}
|
|
429
|
+
getMessage(messageId) {
|
|
430
|
+
const row = this.database.connection
|
|
431
|
+
.prepare("SELECT * FROM messages WHERE message_id = ?")
|
|
432
|
+
.get(messageId);
|
|
433
|
+
return row === undefined ? null : mapMessage(row);
|
|
434
|
+
}
|
|
435
|
+
getMessageByIdempotency(senderSessionId, idempotencyKey) {
|
|
436
|
+
const row = this.database.connection
|
|
437
|
+
.prepare("SELECT * FROM messages WHERE sender_session_id = ? AND idempotency_key = ?")
|
|
438
|
+
.get(senderSessionId, idempotencyKey);
|
|
439
|
+
return row === undefined ? null : mapMessage(row);
|
|
440
|
+
}
|
|
441
|
+
updateMessageState(messageId, state, options = {}) {
|
|
442
|
+
const change = this.database.connection
|
|
443
|
+
.prepare("UPDATE messages SET state = ?, delivered_at = ?, error = ? WHERE message_id = ?")
|
|
444
|
+
.run(state, toEpoch(options.deliveredAt ?? null), options.error ?? null, messageId);
|
|
445
|
+
if (Number(change.changes) !== 1)
|
|
446
|
+
throw new Error(`unknown message: ${messageId}`);
|
|
447
|
+
const message = this.getMessage(messageId);
|
|
448
|
+
if (message === null)
|
|
449
|
+
throw new Error(`failed to load message: ${messageId}`);
|
|
450
|
+
return message;
|
|
451
|
+
}
|
|
452
|
+
listInbox(targetSessionId, state) {
|
|
453
|
+
const rows = (state === undefined
|
|
454
|
+
? this.database.connection
|
|
455
|
+
.prepare("SELECT * FROM messages WHERE target_session_id = ? ORDER BY created_at, message_id")
|
|
456
|
+
.all(targetSessionId)
|
|
457
|
+
: this.database.connection
|
|
458
|
+
.prepare("SELECT * FROM messages WHERE target_session_id = ? AND state = ? ORDER BY created_at, message_id")
|
|
459
|
+
.all(targetSessionId, state));
|
|
460
|
+
return rows.map(mapMessage);
|
|
461
|
+
}
|
|
462
|
+
receiveQueuedMessages(targetSessionId, acknowledgements = [], claimTtlMs = 60_000) {
|
|
463
|
+
if (acknowledgements.length > 100)
|
|
464
|
+
throw new Error("at most 100 inbox messages can be acknowledged at once");
|
|
465
|
+
const claimedAt = Date.now();
|
|
466
|
+
const staleBefore = claimedAt - claimTtlMs;
|
|
467
|
+
return this.database.transaction(() => {
|
|
468
|
+
const acknowledge = this.database.connection.prepare(`
|
|
469
|
+
UPDATE messages
|
|
470
|
+
SET state = 'delivered', delivered_at = ?, delivery_claimed_at = NULL, error = NULL
|
|
471
|
+
WHERE message_id = ? AND target_session_id = ? AND state = 'delivering'
|
|
472
|
+
`);
|
|
473
|
+
for (const messageId of new Set(acknowledgements)) {
|
|
474
|
+
acknowledge.run(claimedAt, messageId, targetSessionId);
|
|
475
|
+
}
|
|
476
|
+
const rows = this.database.connection
|
|
477
|
+
.prepare(`
|
|
478
|
+
SELECT * FROM messages
|
|
479
|
+
WHERE target_session_id = ? AND (
|
|
480
|
+
state = 'queued' OR
|
|
481
|
+
(state = 'delivering' AND delivery_claimed_at IS NOT NULL AND delivery_claimed_at <= ?)
|
|
482
|
+
)
|
|
483
|
+
ORDER BY created_at, message_id
|
|
484
|
+
`)
|
|
485
|
+
.all(targetSessionId, staleBefore);
|
|
486
|
+
const claim = this.database.connection.prepare(`
|
|
487
|
+
UPDATE messages
|
|
488
|
+
SET state = 'delivering', delivery_claimed_at = ?, delivery_attempts = delivery_attempts + 1, error = NULL
|
|
489
|
+
WHERE message_id = ? AND target_session_id = ? AND (
|
|
490
|
+
state = 'queued' OR
|
|
491
|
+
(state = 'delivering' AND delivery_claimed_at IS NOT NULL AND delivery_claimed_at <= ?)
|
|
492
|
+
)
|
|
493
|
+
`);
|
|
494
|
+
const received = [];
|
|
495
|
+
for (const row of rows) {
|
|
496
|
+
const changed = claim.run(claimedAt, row.message_id, targetSessionId, staleBefore);
|
|
497
|
+
if (Number(changed.changes) === 1) {
|
|
498
|
+
received.push(mapMessage({ ...row, state: "delivering", delivered_at: null }));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return received;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
touchRunnerLease(sessionId, ttlMs = 45_000) {
|
|
505
|
+
if (this.getSession(sessionId) === null)
|
|
506
|
+
throw new Error(`unknown Portal session: ${sessionId}`);
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
const expiresAt = new Date(now + ttlMs);
|
|
509
|
+
this.database.connection.prepare(`
|
|
510
|
+
INSERT INTO runner_leases (session_id, last_seen, expires_at) VALUES (?, ?, ?)
|
|
511
|
+
ON CONFLICT(session_id) DO UPDATE SET last_seen = excluded.last_seen, expires_at = excluded.expires_at
|
|
512
|
+
`).run(sessionId, now, expiresAt.getTime());
|
|
513
|
+
return expiresAt;
|
|
514
|
+
}
|
|
515
|
+
hasActiveRunnerLease(sessionId, now = new Date()) {
|
|
516
|
+
const row = this.database.connection
|
|
517
|
+
.prepare("SELECT 1 AS active FROM runner_leases WHERE session_id = ? AND expires_at > ?")
|
|
518
|
+
.get(sessionId, now.getTime());
|
|
519
|
+
return row?.active === 1;
|
|
520
|
+
}
|
|
521
|
+
clearRunnerLease(sessionId) {
|
|
522
|
+
this.database.connection.prepare("DELETE FROM runner_leases WHERE session_id = ?").run(sessionId);
|
|
523
|
+
}
|
|
524
|
+
claimNextRunnerMessage(targetSessionId, claimTtlMs = 60_000) {
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
const staleBefore = now - claimTtlMs;
|
|
527
|
+
return this.database.transaction(() => {
|
|
528
|
+
const row = this.database.connection.prepare(`
|
|
529
|
+
SELECT * FROM messages
|
|
530
|
+
WHERE target_session_id = ? AND (
|
|
531
|
+
state = 'queued' OR (state = 'delivering' AND delivery_claimed_at <= ?
|
|
532
|
+
AND NOT EXISTS (SELECT 1 FROM message_receipts r WHERE r.message_id = messages.message_id AND r.status = 'running'))
|
|
533
|
+
)
|
|
534
|
+
ORDER BY created_at, message_id LIMIT 1
|
|
535
|
+
`).get(targetSessionId, staleBefore);
|
|
536
|
+
if (row === undefined)
|
|
537
|
+
return null;
|
|
538
|
+
const changed = this.database.connection.prepare(`
|
|
539
|
+
UPDATE messages SET state = 'delivering', delivery_claimed_at = ?,
|
|
540
|
+
delivery_attempts = delivery_attempts + 1, error = NULL
|
|
541
|
+
WHERE message_id = ? AND target_session_id = ? AND (
|
|
542
|
+
state = 'queued' OR (state = 'delivering' AND delivery_claimed_at <= ?)
|
|
543
|
+
)
|
|
544
|
+
`).run(now, row.message_id, targetSessionId, staleBefore);
|
|
545
|
+
if (Number(changed.changes) !== 1)
|
|
546
|
+
return null;
|
|
547
|
+
this.database.connection.prepare(`
|
|
548
|
+
INSERT INTO message_receipts(message_id, status) VALUES (?, 'claimed')
|
|
549
|
+
ON CONFLICT(message_id) DO UPDATE SET status = 'claimed', error = NULL
|
|
550
|
+
`).run(row.message_id);
|
|
551
|
+
return mapMessage({ ...row, state: "delivering", delivered_at: null });
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
markRunnerMessageReceived(messageId, targetSessionId, turnId) {
|
|
555
|
+
const now = Date.now();
|
|
556
|
+
const changed = this.database.connection.prepare(`
|
|
557
|
+
UPDATE message_receipts SET target_turn_id = ?, received_at = ?, status = 'running', error = NULL
|
|
558
|
+
WHERE message_id = ? AND EXISTS (
|
|
559
|
+
SELECT 1 FROM messages WHERE message_id = ? AND target_session_id = ? AND state = 'delivering'
|
|
560
|
+
)
|
|
561
|
+
`).run(turnId, now, messageId, messageId, targetSessionId);
|
|
562
|
+
if (Number(changed.changes) !== 1)
|
|
563
|
+
throw new Error(`message is not claimed by this runner: ${messageId}`);
|
|
564
|
+
return this.requireMessageReceipt(messageId);
|
|
565
|
+
}
|
|
566
|
+
markRunnerMessageStarting(messageId, targetSessionId) {
|
|
567
|
+
const changed = this.database.connection.prepare(`
|
|
568
|
+
UPDATE message_receipts SET status = 'running', error = NULL
|
|
569
|
+
WHERE message_id = ? AND status = 'claimed' AND EXISTS (
|
|
570
|
+
SELECT 1 FROM messages WHERE message_id = ? AND target_session_id = ? AND state = 'delivering'
|
|
571
|
+
)
|
|
572
|
+
`).run(messageId, messageId, targetSessionId);
|
|
573
|
+
if (Number(changed.changes) !== 1)
|
|
574
|
+
throw new Error(`message is not claimed by this runner: ${messageId}`);
|
|
575
|
+
return this.requireMessageReceipt(messageId);
|
|
576
|
+
}
|
|
577
|
+
completeRunnerMessage(messageId, targetSessionId, input) {
|
|
578
|
+
const now = Date.now();
|
|
579
|
+
return this.database.transaction(() => {
|
|
580
|
+
const changed = this.database.connection.prepare(`
|
|
581
|
+
UPDATE messages SET state = 'delivered', delivered_at = ?, delivery_claimed_at = NULL, error = NULL
|
|
582
|
+
WHERE message_id = ? AND target_session_id = ? AND state = 'delivering'
|
|
583
|
+
`).run(now, messageId, targetSessionId);
|
|
584
|
+
if (Number(changed.changes) !== 1)
|
|
585
|
+
throw new Error(`message is not running for this target: ${messageId}`);
|
|
586
|
+
this.database.connection.prepare(`
|
|
587
|
+
UPDATE message_receipts SET target_turn_id = ?, completed_at = ?, status = 'completed',
|
|
588
|
+
answer = ?, tool_results_json = ?, error = NULL WHERE message_id = ?
|
|
589
|
+
`).run(input.turnId, now, input.answer?.slice(0, 65_536) ?? null, JSON.stringify(input.toolResults).slice(0, 65_536), messageId);
|
|
590
|
+
return this.requireMessageReceipt(messageId);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
failRunnerMessage(messageId, targetSessionId, error) {
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
return this.database.transaction(() => {
|
|
596
|
+
const receipt = this.getMessageReceipt(messageId);
|
|
597
|
+
if (receipt === null)
|
|
598
|
+
throw new Error(`unknown message receipt: ${messageId}`);
|
|
599
|
+
const started = receipt.status === "running";
|
|
600
|
+
this.database.connection.prepare(`
|
|
601
|
+
UPDATE messages SET state = ?, delivery_claimed_at = NULL, error = ?
|
|
602
|
+
WHERE message_id = ? AND target_session_id = ? AND state = 'delivering'
|
|
603
|
+
`).run(started ? "failed" : "queued", error.slice(0, 4096), messageId, targetSessionId);
|
|
604
|
+
this.database.connection.prepare(`
|
|
605
|
+
UPDATE message_receipts SET completed_at = ?, status = 'failed', error = ? WHERE message_id = ?
|
|
606
|
+
`).run(now, error.slice(0, 4096), messageId);
|
|
607
|
+
return this.requireMessageReceipt(messageId);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
getMessageReceipt(messageId) {
|
|
611
|
+
const row = this.database.connection.prepare("SELECT * FROM message_receipts WHERE message_id = ?")
|
|
612
|
+
.get(messageId);
|
|
613
|
+
return row === undefined ? null : mapMessageReceipt(row);
|
|
614
|
+
}
|
|
615
|
+
requireMessageReceipt(messageId) {
|
|
616
|
+
const receipt = this.getMessageReceipt(messageId);
|
|
617
|
+
if (receipt === null)
|
|
618
|
+
throw new Error(`unknown message receipt: ${messageId}`);
|
|
619
|
+
return receipt;
|
|
620
|
+
}
|
|
621
|
+
recordRemoteTaskEvent(input) {
|
|
622
|
+
const serverUrl = requireText(input.serverUrl, "remote server URL");
|
|
623
|
+
const taskId = requireText(input.taskId, "remote task ID");
|
|
624
|
+
const eventId = requireText(input.eventId, "remote event ID");
|
|
625
|
+
const envelopeId = requireText(input.envelopeId, "remote envelope ID");
|
|
626
|
+
const localRouteId = requireText(input.localRouteId, "local route ID");
|
|
627
|
+
const remoteRouteId = requireText(input.remoteRouteId, "remote route ID");
|
|
628
|
+
const senderLabel = requireText(input.senderLabel, "remote sender label");
|
|
629
|
+
const senderReference = requireText(input.senderReference, "remote sender reference");
|
|
630
|
+
const occurredAt = requireDate(input.occurredAt, "remote event occurredAt");
|
|
631
|
+
const expiresAt = requireDate(input.expiresAt, "remote task expiresAt");
|
|
632
|
+
const recordedAt = requireDate(input.recordedAt ?? new Date(), "remote event recordedAt");
|
|
633
|
+
if (expiresAt.getTime() <= occurredAt.getTime()) {
|
|
634
|
+
throw new Error("remote task expiry must be after the event timestamp");
|
|
635
|
+
}
|
|
636
|
+
validateRemoteKindState(input.kind, input.state);
|
|
637
|
+
const payloadJson = canonicalJson(input.payload);
|
|
638
|
+
const authenticated = input.authenticated ?? false;
|
|
639
|
+
if (this.getSession(input.localSessionId) === null) {
|
|
640
|
+
throw new Error(`unknown Portal session: ${input.localSessionId}`);
|
|
641
|
+
}
|
|
642
|
+
if (input.localMessageId !== undefined && input.localMessageId !== null) {
|
|
643
|
+
this.validateRemoteLocalMessage(input.localSessionId, input.localMessageId);
|
|
644
|
+
}
|
|
645
|
+
return this.database.transaction(() => {
|
|
646
|
+
const priorEvent = this.getRemoteTaskEvent(serverUrl, eventId);
|
|
647
|
+
if (priorEvent !== null) {
|
|
648
|
+
assertRemoteEventIdempotent(priorEvent, {
|
|
649
|
+
...input,
|
|
650
|
+
serverUrl,
|
|
651
|
+
taskId,
|
|
652
|
+
eventId,
|
|
653
|
+
envelopeId,
|
|
654
|
+
localRouteId,
|
|
655
|
+
remoteRouteId,
|
|
656
|
+
senderLabel,
|
|
657
|
+
senderReference,
|
|
658
|
+
authenticated,
|
|
659
|
+
payload: JSON.parse(payloadJson),
|
|
660
|
+
occurredAt,
|
|
661
|
+
expiresAt,
|
|
662
|
+
});
|
|
663
|
+
return { task: this.requireRemoteTask(serverUrl, taskId), event: priorEvent, inserted: false };
|
|
664
|
+
}
|
|
665
|
+
const priorEnvelope = this.getRemoteTaskEventByEnvelope(serverUrl, envelopeId);
|
|
666
|
+
if (priorEnvelope !== null) {
|
|
667
|
+
throw new Error(`remote envelope ID conflict: ${envelopeId}`);
|
|
668
|
+
}
|
|
669
|
+
let task = this.getRemoteTask(serverUrl, taskId);
|
|
670
|
+
if (task === null) {
|
|
671
|
+
if (input.kind !== "offer" || input.state !== "offered") {
|
|
672
|
+
throw new Error("a remote task must begin with an offered event");
|
|
673
|
+
}
|
|
674
|
+
this.database.connection.prepare(`
|
|
675
|
+
INSERT INTO remote_tasks (
|
|
676
|
+
server_url, task_id, local_session_id, local_route_id, remote_route_id,
|
|
677
|
+
direction, state, created_at, updated_at, expires_at
|
|
678
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
679
|
+
`).run(serverUrl, taskId, input.localSessionId, localRouteId, remoteRouteId, input.direction, input.state, occurredAt.getTime(), occurredAt.getTime(), expiresAt.getTime());
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
assertRemoteTaskIdentity(task, input.localSessionId, localRouteId, remoteRouteId, expiresAt);
|
|
683
|
+
validateRemoteTransition(task.state, input.state);
|
|
684
|
+
this.database.connection.prepare(`
|
|
685
|
+
UPDATE remote_tasks SET state = ?, updated_at = max(updated_at, ?)
|
|
686
|
+
WHERE server_url = ? AND task_id = ?
|
|
687
|
+
`).run(input.state, occurredAt.getTime(), serverUrl, taskId);
|
|
688
|
+
}
|
|
689
|
+
this.database.connection.prepare(`
|
|
690
|
+
INSERT INTO remote_task_events (
|
|
691
|
+
server_url, event_id, envelope_id, task_id, local_session_id,
|
|
692
|
+
local_route_id, remote_route_id, direction, kind, state, payload_json,
|
|
693
|
+
sender_label, sender_reference, authenticated, local_message_id,
|
|
694
|
+
occurred_at, expires_at, recorded_at, delivery_state
|
|
695
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
696
|
+
`).run(serverUrl, eventId, envelopeId, taskId, input.localSessionId, localRouteId, remoteRouteId, input.direction, input.kind, input.state, payloadJson, senderLabel, senderReference, Number(authenticated), input.localMessageId ?? null, occurredAt.getTime(), expiresAt.getTime(), recordedAt.getTime(), input.direction === "inbound" ? "queued" : "not_applicable");
|
|
697
|
+
task = this.requireRemoteTask(serverUrl, taskId);
|
|
698
|
+
return {
|
|
699
|
+
task,
|
|
700
|
+
event: this.requireRemoteTaskEvent(serverUrl, eventId),
|
|
701
|
+
inserted: true,
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
getRemoteTask(serverUrl, taskId) {
|
|
706
|
+
const row = this.database.connection.prepare(`
|
|
707
|
+
SELECT * FROM remote_tasks WHERE server_url = ? AND task_id = ?
|
|
708
|
+
`).get(serverUrl, taskId);
|
|
709
|
+
return row === undefined ? null : mapRemoteTask(row);
|
|
710
|
+
}
|
|
711
|
+
getRemoteTaskEvent(serverUrl, eventId) {
|
|
712
|
+
const row = this.database.connection.prepare(`
|
|
713
|
+
SELECT * FROM remote_task_events WHERE server_url = ? AND event_id = ?
|
|
714
|
+
`).get(serverUrl, eventId);
|
|
715
|
+
return row === undefined ? null : mapRemoteTaskEvent(row);
|
|
716
|
+
}
|
|
717
|
+
getRemoteTaskEventByEnvelope(serverUrl, envelopeId) {
|
|
718
|
+
const row = this.database.connection.prepare(`
|
|
719
|
+
SELECT * FROM remote_task_events WHERE server_url = ? AND envelope_id = ?
|
|
720
|
+
`).get(serverUrl, envelopeId);
|
|
721
|
+
return row === undefined ? null : mapRemoteTaskEvent(row);
|
|
722
|
+
}
|
|
723
|
+
getRemoteTaskEventByLocalMessage(localMessageId) {
|
|
724
|
+
const row = this.database.connection.prepare(`
|
|
725
|
+
SELECT * FROM remote_task_events WHERE local_message_id = ?
|
|
726
|
+
`).get(localMessageId);
|
|
727
|
+
return row === undefined ? null : mapRemoteTaskEvent(row);
|
|
728
|
+
}
|
|
729
|
+
listRemoteTaskEvents(serverUrl, taskId) {
|
|
730
|
+
const rows = this.database.connection.prepare(`
|
|
731
|
+
SELECT * FROM remote_task_events
|
|
732
|
+
WHERE server_url = ? AND task_id = ?
|
|
733
|
+
ORDER BY occurred_at, event_id
|
|
734
|
+
`).all(serverUrl, taskId);
|
|
735
|
+
return rows.map(mapRemoteTaskEvent);
|
|
736
|
+
}
|
|
737
|
+
linkRemoteTaskEventToLocalMessage(serverUrl, eventId, localMessageId) {
|
|
738
|
+
const event = this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
739
|
+
if (event.localMessageId !== null) {
|
|
740
|
+
if (event.localMessageId !== localMessageId) {
|
|
741
|
+
throw new Error(`remote event is already linked to another local message: ${eventId}`);
|
|
742
|
+
}
|
|
743
|
+
return event;
|
|
744
|
+
}
|
|
745
|
+
this.validateRemoteLocalMessage(event.localSessionId, localMessageId);
|
|
746
|
+
try {
|
|
747
|
+
const changed = this.database.connection.prepare(`
|
|
748
|
+
UPDATE remote_task_events SET local_message_id = ?
|
|
749
|
+
WHERE server_url = ? AND event_id = ? AND local_message_id IS NULL
|
|
750
|
+
`).run(localMessageId, serverUrl, eventId);
|
|
751
|
+
if (Number(changed.changes) !== 1)
|
|
752
|
+
throw new Error(`failed to link remote event: ${eventId}`);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (String(error).includes("UNIQUE constraint failed")) {
|
|
756
|
+
throw new Error(`local message is already linked to another remote event: ${localMessageId}`);
|
|
757
|
+
}
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
return this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
761
|
+
}
|
|
762
|
+
claimNextRemoteTaskEvent(localSessionId, claimTtlMs = 60_000) {
|
|
763
|
+
const now = Date.now();
|
|
764
|
+
const staleBefore = now - claimTtlMs;
|
|
765
|
+
return this.database.transaction(() => {
|
|
766
|
+
this.database.connection.prepare(`
|
|
767
|
+
UPDATE remote_task_events SET delivery_state = 'failed', delivery_claimed_at = NULL,
|
|
768
|
+
completed_at = ?, error = 'remote event expired before delivery'
|
|
769
|
+
WHERE local_session_id = ? AND direction = 'inbound'
|
|
770
|
+
AND delivery_state IN ('queued', 'delivering') AND runner_started_at IS NULL
|
|
771
|
+
AND expires_at <= ?
|
|
772
|
+
`).run(now, localSessionId, now);
|
|
773
|
+
const row = this.database.connection.prepare(`
|
|
774
|
+
SELECT * FROM remote_task_events
|
|
775
|
+
WHERE local_session_id = ? AND direction = 'inbound' AND (
|
|
776
|
+
delivery_state = 'queued' OR
|
|
777
|
+
(delivery_state = 'delivering' AND delivery_claimed_at <= ? AND runner_started_at IS NULL)
|
|
778
|
+
) AND expires_at > ?
|
|
779
|
+
ORDER BY occurred_at, event_id LIMIT 1
|
|
780
|
+
`).get(localSessionId, staleBefore, now);
|
|
781
|
+
if (row === undefined)
|
|
782
|
+
return null;
|
|
783
|
+
const changed = this.database.connection.prepare(`
|
|
784
|
+
UPDATE remote_task_events SET delivery_state = 'delivering',
|
|
785
|
+
delivery_claimed_at = ?, delivery_attempts = delivery_attempts + 1, error = NULL
|
|
786
|
+
WHERE server_url = ? AND event_id = ? AND (
|
|
787
|
+
delivery_state = 'queued' OR
|
|
788
|
+
(delivery_state = 'delivering' AND delivery_claimed_at <= ? AND runner_started_at IS NULL)
|
|
789
|
+
) AND expires_at > ?
|
|
790
|
+
`).run(now, row.server_url, row.event_id, staleBefore, now);
|
|
791
|
+
if (Number(changed.changes) !== 1)
|
|
792
|
+
return null;
|
|
793
|
+
return this.requireRemoteTaskEvent(row.server_url, row.event_id);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
markRemoteTaskEventStarting(serverUrl, eventId, localSessionId) {
|
|
797
|
+
const changed = this.database.connection.prepare(`
|
|
798
|
+
UPDATE remote_task_events SET runner_started_at = coalesce(runner_started_at, ?), error = NULL
|
|
799
|
+
WHERE server_url = ? AND event_id = ? AND local_session_id = ?
|
|
800
|
+
AND direction = 'inbound' AND delivery_state = 'delivering'
|
|
801
|
+
`).run(Date.now(), serverUrl, eventId, localSessionId);
|
|
802
|
+
if (Number(changed.changes) !== 1)
|
|
803
|
+
throw new Error(`remote event is not claimed by this runner: ${eventId}`);
|
|
804
|
+
return this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
805
|
+
}
|
|
806
|
+
markRemoteTaskEventReceived(serverUrl, eventId, localSessionId, turnId) {
|
|
807
|
+
const now = Date.now();
|
|
808
|
+
const changed = this.database.connection.prepare(`
|
|
809
|
+
UPDATE remote_task_events SET runner_started_at = coalesce(runner_started_at, ?),
|
|
810
|
+
target_turn_id = ?, received_at = ?, error = NULL
|
|
811
|
+
WHERE server_url = ? AND event_id = ? AND local_session_id = ?
|
|
812
|
+
AND direction = 'inbound' AND delivery_state = 'delivering'
|
|
813
|
+
`).run(now, requireText(turnId, "target turn ID"), now, serverUrl, eventId, localSessionId);
|
|
814
|
+
if (Number(changed.changes) !== 1)
|
|
815
|
+
throw new Error(`remote event is not claimed by this runner: ${eventId}`);
|
|
816
|
+
return this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
817
|
+
}
|
|
818
|
+
completeRemoteTaskEvent(serverUrl, eventId, localSessionId, input) {
|
|
819
|
+
const now = Date.now();
|
|
820
|
+
const toolResultsJson = canonicalJson(input.toolResults);
|
|
821
|
+
const changed = this.database.connection.prepare(`
|
|
822
|
+
UPDATE remote_task_events SET delivery_state = 'delivered', delivery_claimed_at = NULL,
|
|
823
|
+
target_turn_id = ?, completed_at = ?, answer = ?, tool_results_json = ?, error = NULL
|
|
824
|
+
WHERE server_url = ? AND event_id = ? AND local_session_id = ?
|
|
825
|
+
AND direction = 'inbound' AND delivery_state = 'delivering' AND runner_started_at IS NOT NULL
|
|
826
|
+
`).run(requireText(input.turnId, "target turn ID"), now, input.answer?.slice(0, 65_536) ?? null, toolResultsJson, serverUrl, eventId, localSessionId);
|
|
827
|
+
if (Number(changed.changes) !== 1)
|
|
828
|
+
throw new Error(`remote event is not running for this target: ${eventId}`);
|
|
829
|
+
return this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
830
|
+
}
|
|
831
|
+
failRemoteTaskEvent(serverUrl, eventId, localSessionId, error) {
|
|
832
|
+
const event = this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
833
|
+
if (event.localSessionId !== localSessionId || event.direction !== "inbound"
|
|
834
|
+
|| event.deliveryState !== "delivering") {
|
|
835
|
+
throw new Error(`remote event is not claimed by this runner: ${eventId}`);
|
|
836
|
+
}
|
|
837
|
+
const started = event.runnerStartedAt !== null;
|
|
838
|
+
const now = Date.now();
|
|
839
|
+
this.database.connection.prepare(`
|
|
840
|
+
UPDATE remote_task_events SET delivery_state = ?, delivery_claimed_at = NULL,
|
|
841
|
+
completed_at = ?, error = ?
|
|
842
|
+
WHERE server_url = ? AND event_id = ?
|
|
843
|
+
`).run(started ? "failed" : "queued", started ? now : null, error.slice(0, 4096), serverUrl, eventId);
|
|
844
|
+
return this.requireRemoteTaskEvent(serverUrl, eventId);
|
|
845
|
+
}
|
|
846
|
+
requireRemoteTask(serverUrl, taskId) {
|
|
847
|
+
const task = this.getRemoteTask(serverUrl, taskId);
|
|
848
|
+
if (task === null)
|
|
849
|
+
throw new Error(`unknown remote task: ${taskId}`);
|
|
850
|
+
return task;
|
|
851
|
+
}
|
|
852
|
+
requireRemoteTaskEvent(serverUrl, eventId) {
|
|
853
|
+
const event = this.getRemoteTaskEvent(serverUrl, eventId);
|
|
854
|
+
if (event === null)
|
|
855
|
+
throw new Error(`unknown remote task event: ${eventId}`);
|
|
856
|
+
return event;
|
|
857
|
+
}
|
|
858
|
+
validateRemoteLocalMessage(localSessionId, localMessageId) {
|
|
859
|
+
const message = this.getMessage(localMessageId);
|
|
860
|
+
if (message === null)
|
|
861
|
+
throw new Error(`unknown message: ${localMessageId}`);
|
|
862
|
+
if (message.senderSessionId !== localSessionId && message.targetSessionId !== localSessionId) {
|
|
863
|
+
throw new Error("local message does not belong to the remote task session");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
issueSessionCredential(sessionId) {
|
|
867
|
+
if (this.getSession(sessionId) === null)
|
|
868
|
+
throw new Error(`unknown Portal session: ${sessionId}`);
|
|
869
|
+
const credentialId = randomUUID();
|
|
870
|
+
const secret = randomBytes(32).toString("base64url");
|
|
871
|
+
this.database.connection
|
|
872
|
+
.prepare(`
|
|
873
|
+
INSERT INTO session_credentials (
|
|
874
|
+
credential_id, session_id, secret_hash, created_at, expires_at
|
|
875
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
876
|
+
`)
|
|
877
|
+
.run(credentialId, sessionId, digest(secret), Date.now(), Date.now() + 86_400_000);
|
|
878
|
+
return `pc_${credentialId}_${secret}`;
|
|
879
|
+
}
|
|
880
|
+
authenticateSession(sessionId, token) {
|
|
881
|
+
const parsed = parseCredential(token);
|
|
882
|
+
if (parsed === null)
|
|
883
|
+
throw new Error("invalid Portal session credential");
|
|
884
|
+
const row = this.database.connection
|
|
885
|
+
.prepare(`
|
|
886
|
+
SELECT session_id, secret_hash FROM session_credentials
|
|
887
|
+
WHERE credential_id = ? AND revoked_at IS NULL
|
|
888
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
889
|
+
`)
|
|
890
|
+
.get(parsed.credentialId, Date.now());
|
|
891
|
+
const candidate = digest(parsed.secret);
|
|
892
|
+
if (row === undefined ||
|
|
893
|
+
row.session_id !== sessionId ||
|
|
894
|
+
!safeEqual(candidate, row.secret_hash)) {
|
|
895
|
+
throw new Error("invalid Portal session credential");
|
|
896
|
+
}
|
|
897
|
+
const session = this.getSession(sessionId);
|
|
898
|
+
if (session === null)
|
|
899
|
+
throw new Error("credential references an unknown Portal session");
|
|
900
|
+
return session;
|
|
901
|
+
}
|
|
902
|
+
revokeSessionCredential(token) {
|
|
903
|
+
const parsed = parseCredential(token);
|
|
904
|
+
if (parsed === null)
|
|
905
|
+
return;
|
|
906
|
+
this.database.connection
|
|
907
|
+
.prepare("UPDATE session_credentials SET revoked_at = ? WHERE credential_id = ?")
|
|
908
|
+
.run(Date.now(), parsed.credentialId);
|
|
909
|
+
}
|
|
910
|
+
touchConnectionLease(sessionId, ttlMs = 120_000) {
|
|
911
|
+
if (!Number.isInteger(ttlMs) || ttlMs < 10_000)
|
|
912
|
+
throw new RangeError("connection lease TTL is too small");
|
|
913
|
+
if (this.getSession(sessionId) === null)
|
|
914
|
+
throw new Error(`unknown Portal session: ${sessionId}`);
|
|
915
|
+
const now = Date.now();
|
|
916
|
+
const expiresAt = new Date(now + ttlMs);
|
|
917
|
+
this.database.connection
|
|
918
|
+
.prepare(`
|
|
919
|
+
INSERT INTO connection_leases (session_id, last_seen, expires_at)
|
|
920
|
+
VALUES (?, ?, ?)
|
|
921
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
922
|
+
last_seen = excluded.last_seen,
|
|
923
|
+
expires_at = excluded.expires_at
|
|
924
|
+
`)
|
|
925
|
+
.run(sessionId, now, expiresAt.getTime());
|
|
926
|
+
return expiresAt;
|
|
927
|
+
}
|
|
928
|
+
hasActiveConnectionLease(sessionId, now = new Date()) {
|
|
929
|
+
const row = this.database.connection
|
|
930
|
+
.prepare("SELECT 1 AS active FROM connection_leases WHERE session_id = ? AND expires_at > ?")
|
|
931
|
+
.get(sessionId, now.getTime());
|
|
932
|
+
return row?.active === 1;
|
|
933
|
+
}
|
|
934
|
+
clearConnectionLease(sessionId) {
|
|
935
|
+
this.database.connection.prepare("DELETE FROM connection_leases WHERE session_id = ?").run(sessionId);
|
|
936
|
+
}
|
|
937
|
+
validateReply(messageId, workspace, senderSessionId, targetSessionId, maxDepth) {
|
|
938
|
+
let current = messageId;
|
|
939
|
+
const visited = new Set();
|
|
940
|
+
for (let depth = 0; current !== null; depth += 1) {
|
|
941
|
+
if (depth >= maxDepth)
|
|
942
|
+
throw new Error(`message reply depth exceeds ${maxDepth}`);
|
|
943
|
+
if (visited.has(current))
|
|
944
|
+
throw new Error("message reply chain contains a cycle");
|
|
945
|
+
visited.add(current);
|
|
946
|
+
const row = this.database.connection
|
|
947
|
+
.prepare(`
|
|
948
|
+
SELECT m.reply_to, m.sender_session_id, m.target_session_id,
|
|
949
|
+
sender.workspace AS sender_workspace, target.workspace AS target_workspace
|
|
950
|
+
FROM messages m
|
|
951
|
+
JOIN sessions sender ON sender.portal_session_id = m.sender_session_id
|
|
952
|
+
JOIN sessions target ON target.portal_session_id = m.target_session_id
|
|
953
|
+
WHERE m.message_id = ?
|
|
954
|
+
`)
|
|
955
|
+
.get(current);
|
|
956
|
+
if (row === undefined)
|
|
957
|
+
throw new Error(`unknown reply message: ${current}`);
|
|
958
|
+
if (row.sender_workspace !== workspace || row.target_workspace !== workspace) {
|
|
959
|
+
throw new Error("reply message is outside the sender workspace");
|
|
960
|
+
}
|
|
961
|
+
if (depth === 0 && (row.target_session_id !== senderSessionId || row.sender_session_id !== targetSessionId)) {
|
|
962
|
+
throw new Error("reply participants must reverse the parent message participants");
|
|
963
|
+
}
|
|
964
|
+
current = row.reply_to;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function mapSession(row) {
|
|
969
|
+
return {
|
|
970
|
+
portalSessionId: row.portal_session_id,
|
|
971
|
+
provider: row.provider,
|
|
972
|
+
installation: row.installation,
|
|
973
|
+
accountFingerprint: row.account_fingerprint,
|
|
974
|
+
vendorSessionId: row.vendor_session_id,
|
|
975
|
+
workspace: row.workspace,
|
|
976
|
+
title: row.title,
|
|
977
|
+
managedName: row.managed_name,
|
|
978
|
+
observedAt: new Date(row.observed_at),
|
|
979
|
+
updatedAt: fromEpoch(row.updated_at),
|
|
980
|
+
state: row.state,
|
|
981
|
+
stateConfidence: row.state_confidence,
|
|
982
|
+
indexed: row.indexed === 1,
|
|
983
|
+
connected: row.connected === 1,
|
|
984
|
+
messageable: row.messageable === 1,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function mapChunk(row) {
|
|
988
|
+
return {
|
|
989
|
+
chunkId: row.chunk_id,
|
|
990
|
+
sessionId: row.session_id,
|
|
991
|
+
text: row.text,
|
|
992
|
+
embeddingModel: row.embedding_model,
|
|
993
|
+
embedding: row.embedding_json === null ? null : JSON.parse(row.embedding_json),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function mapSessionProfile(row) {
|
|
997
|
+
return {
|
|
998
|
+
sessionId: row.session_id,
|
|
999
|
+
displayName: row.display_name,
|
|
1000
|
+
description: row.description,
|
|
1001
|
+
topics: JSON.parse(row.topics_json),
|
|
1002
|
+
source: row.source,
|
|
1003
|
+
transcriptWatermark: row.transcript_watermark,
|
|
1004
|
+
updatedAt: new Date(row.updated_at),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function mapCitation(row) {
|
|
1008
|
+
return {
|
|
1009
|
+
portalSessionId: row.portal_session_id,
|
|
1010
|
+
vendorSessionId: row.vendor_session_id,
|
|
1011
|
+
provider: row.provider,
|
|
1012
|
+
workspace: row.workspace,
|
|
1013
|
+
sourceId: row.source_id,
|
|
1014
|
+
entryId: row.entry_id,
|
|
1015
|
+
occurredAt: fromEpoch(row.occurred_at),
|
|
1016
|
+
uri: `portal://session/${encodeURIComponent(row.portal_session_id)}/entry/${encodeURIComponent(row.entry_id)}`,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function mapMessage(row) {
|
|
1020
|
+
return {
|
|
1021
|
+
messageId: row.message_id,
|
|
1022
|
+
senderSessionId: row.sender_session_id,
|
|
1023
|
+
targetSessionId: row.target_session_id,
|
|
1024
|
+
body: row.body,
|
|
1025
|
+
state: row.state,
|
|
1026
|
+
createdAt: new Date(row.created_at),
|
|
1027
|
+
deliveredAt: fromEpoch(row.delivered_at),
|
|
1028
|
+
replyTo: row.reply_to,
|
|
1029
|
+
idempotencyKey: row.idempotency_key,
|
|
1030
|
+
error: row.error,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function mapMessageReceipt(row) {
|
|
1034
|
+
return {
|
|
1035
|
+
messageId: row.message_id,
|
|
1036
|
+
targetTurnId: row.target_turn_id,
|
|
1037
|
+
receivedAt: fromEpoch(row.received_at),
|
|
1038
|
+
completedAt: fromEpoch(row.completed_at),
|
|
1039
|
+
status: row.status,
|
|
1040
|
+
answer: row.answer,
|
|
1041
|
+
toolResults: row.tool_results_json === null ? [] : JSON.parse(row.tool_results_json),
|
|
1042
|
+
error: row.error,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function mapRemoteTask(row) {
|
|
1046
|
+
return {
|
|
1047
|
+
serverUrl: row.server_url,
|
|
1048
|
+
taskId: row.task_id,
|
|
1049
|
+
localSessionId: row.local_session_id,
|
|
1050
|
+
localRouteId: row.local_route_id,
|
|
1051
|
+
remoteRouteId: row.remote_route_id,
|
|
1052
|
+
direction: row.direction,
|
|
1053
|
+
state: row.state,
|
|
1054
|
+
createdAt: new Date(row.created_at),
|
|
1055
|
+
updatedAt: new Date(row.updated_at),
|
|
1056
|
+
expiresAt: new Date(row.expires_at),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function mapRemoteTaskEvent(row) {
|
|
1060
|
+
return {
|
|
1061
|
+
serverUrl: row.server_url,
|
|
1062
|
+
taskId: row.task_id,
|
|
1063
|
+
eventId: row.event_id,
|
|
1064
|
+
envelopeId: row.envelope_id,
|
|
1065
|
+
localSessionId: row.local_session_id,
|
|
1066
|
+
localRouteId: row.local_route_id,
|
|
1067
|
+
remoteRouteId: row.remote_route_id,
|
|
1068
|
+
direction: row.direction,
|
|
1069
|
+
kind: row.kind,
|
|
1070
|
+
state: row.state,
|
|
1071
|
+
payload: JSON.parse(row.payload_json),
|
|
1072
|
+
senderLabel: row.sender_label,
|
|
1073
|
+
senderReference: row.sender_reference,
|
|
1074
|
+
authenticated: row.authenticated === 1,
|
|
1075
|
+
localMessageId: row.local_message_id,
|
|
1076
|
+
occurredAt: new Date(row.occurred_at),
|
|
1077
|
+
expiresAt: new Date(row.expires_at),
|
|
1078
|
+
recordedAt: new Date(row.recorded_at),
|
|
1079
|
+
deliveryState: row.delivery_state,
|
|
1080
|
+
deliveryClaimedAt: fromEpoch(row.delivery_claimed_at),
|
|
1081
|
+
deliveryAttempts: row.delivery_attempts,
|
|
1082
|
+
runnerStartedAt: fromEpoch(row.runner_started_at),
|
|
1083
|
+
targetTurnId: row.target_turn_id,
|
|
1084
|
+
receivedAt: fromEpoch(row.received_at),
|
|
1085
|
+
completedAt: fromEpoch(row.completed_at),
|
|
1086
|
+
answer: row.answer,
|
|
1087
|
+
toolResults: row.tool_results_json === null ? [] : JSON.parse(row.tool_results_json),
|
|
1088
|
+
error: row.error,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const REMOTE_EVENT_STATES = {
|
|
1092
|
+
offer: "offered",
|
|
1093
|
+
accepted: "accepted",
|
|
1094
|
+
declined: "declined",
|
|
1095
|
+
running: "running",
|
|
1096
|
+
progress: "running",
|
|
1097
|
+
question: "running",
|
|
1098
|
+
answer: "running",
|
|
1099
|
+
completed: "completed",
|
|
1100
|
+
failed: "failed",
|
|
1101
|
+
};
|
|
1102
|
+
const REMOTE_TRANSITIONS = {
|
|
1103
|
+
offered: new Set(["offered", "accepted", "declined", "failed"]),
|
|
1104
|
+
accepted: new Set(["accepted", "running", "completed", "failed"]),
|
|
1105
|
+
declined: new Set(),
|
|
1106
|
+
running: new Set(["running", "completed", "failed"]),
|
|
1107
|
+
completed: new Set(),
|
|
1108
|
+
failed: new Set(),
|
|
1109
|
+
};
|
|
1110
|
+
function validateRemoteKindState(kind, state) {
|
|
1111
|
+
if (REMOTE_EVENT_STATES[kind] !== state) {
|
|
1112
|
+
throw new Error(`remote event kind ${kind} cannot have task state ${state}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function validateRemoteTransition(from, to) {
|
|
1116
|
+
if (!REMOTE_TRANSITIONS[from].has(to)) {
|
|
1117
|
+
throw new Error(`invalid remote task state transition: ${from} -> ${to}`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function assertRemoteTaskIdentity(task, localSessionId, localRouteId, remoteRouteId, expiresAt) {
|
|
1121
|
+
if (task.localSessionId !== localSessionId
|
|
1122
|
+
|| task.localRouteId !== localRouteId
|
|
1123
|
+
|| task.remoteRouteId !== remoteRouteId
|
|
1124
|
+
|| task.expiresAt.getTime() !== expiresAt.getTime()) {
|
|
1125
|
+
throw new Error(`remote task identity conflict: ${task.taskId}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function assertRemoteEventIdempotent(existing, input) {
|
|
1129
|
+
const matches = existing.taskId === input.taskId
|
|
1130
|
+
&& existing.envelopeId === input.envelopeId
|
|
1131
|
+
&& existing.localSessionId === input.localSessionId
|
|
1132
|
+
&& existing.localRouteId === input.localRouteId
|
|
1133
|
+
&& existing.remoteRouteId === input.remoteRouteId
|
|
1134
|
+
&& existing.direction === input.direction
|
|
1135
|
+
&& existing.kind === input.kind
|
|
1136
|
+
&& existing.state === input.state
|
|
1137
|
+
&& canonicalJson(existing.payload) === canonicalJson(input.payload)
|
|
1138
|
+
&& existing.senderLabel === input.senderLabel
|
|
1139
|
+
&& existing.senderReference === input.senderReference
|
|
1140
|
+
&& existing.authenticated === input.authenticated
|
|
1141
|
+
&& existing.occurredAt.getTime() === input.occurredAt.getTime()
|
|
1142
|
+
&& existing.expiresAt.getTime() === input.expiresAt.getTime();
|
|
1143
|
+
if (!matches)
|
|
1144
|
+
throw new Error(`remote event ID conflict: ${existing.eventId}`);
|
|
1145
|
+
}
|
|
1146
|
+
function requireText(value, label) {
|
|
1147
|
+
const trimmed = value.trim();
|
|
1148
|
+
if (trimmed.length === 0)
|
|
1149
|
+
throw new TypeError(`${label} cannot be empty`);
|
|
1150
|
+
return trimmed;
|
|
1151
|
+
}
|
|
1152
|
+
function requireDate(value, label) {
|
|
1153
|
+
if (!Number.isFinite(value.getTime()))
|
|
1154
|
+
throw new TypeError(`${label} is invalid`);
|
|
1155
|
+
return value;
|
|
1156
|
+
}
|
|
1157
|
+
function canonicalJson(value) {
|
|
1158
|
+
const seen = new Set();
|
|
1159
|
+
const normalize = (candidate) => {
|
|
1160
|
+
if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean")
|
|
1161
|
+
return candidate;
|
|
1162
|
+
if (typeof candidate === "number") {
|
|
1163
|
+
if (!Number.isFinite(candidate))
|
|
1164
|
+
throw new TypeError("remote event payload contains a non-finite number");
|
|
1165
|
+
return candidate;
|
|
1166
|
+
}
|
|
1167
|
+
if (Array.isArray(candidate))
|
|
1168
|
+
return candidate.map(normalize);
|
|
1169
|
+
if (typeof candidate === "object") {
|
|
1170
|
+
if (seen.has(candidate))
|
|
1171
|
+
throw new TypeError("remote event payload contains a cycle");
|
|
1172
|
+
seen.add(candidate);
|
|
1173
|
+
const result = {};
|
|
1174
|
+
for (const key of Object.keys(candidate).sort()) {
|
|
1175
|
+
const normalized = normalize(candidate[key]);
|
|
1176
|
+
if (normalized !== undefined)
|
|
1177
|
+
result[key] = normalized;
|
|
1178
|
+
}
|
|
1179
|
+
seen.delete(candidate);
|
|
1180
|
+
return result;
|
|
1181
|
+
}
|
|
1182
|
+
if (candidate === undefined)
|
|
1183
|
+
throw new TypeError("remote event payload cannot be undefined");
|
|
1184
|
+
throw new TypeError(`remote event payload contains unsupported ${typeof candidate}`);
|
|
1185
|
+
};
|
|
1186
|
+
return JSON.stringify(normalize(value));
|
|
1187
|
+
}
|
|
1188
|
+
function stableId(prefix, ...parts) {
|
|
1189
|
+
return `${prefix}_${digest(...parts).slice(0, 32)}`;
|
|
1190
|
+
}
|
|
1191
|
+
function digest(...parts) {
|
|
1192
|
+
const hash = createHash("sha256");
|
|
1193
|
+
for (const part of parts) {
|
|
1194
|
+
hash.update(String(Buffer.byteLength(part)));
|
|
1195
|
+
hash.update(":");
|
|
1196
|
+
hash.update(part);
|
|
1197
|
+
hash.update(";");
|
|
1198
|
+
}
|
|
1199
|
+
return hash.digest("hex");
|
|
1200
|
+
}
|
|
1201
|
+
function parseCredential(token) {
|
|
1202
|
+
const match = /^pc_([0-9a-f-]{36})_([A-Za-z0-9_-]+)$/.exec(token);
|
|
1203
|
+
return match?.[1] && match[2] ? { credentialId: match[1], secret: match[2] } : null;
|
|
1204
|
+
}
|
|
1205
|
+
function safeEqual(left, right) {
|
|
1206
|
+
const leftBuffer = Buffer.from(left);
|
|
1207
|
+
const rightBuffer = Buffer.from(right);
|
|
1208
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
1209
|
+
}
|
|
1210
|
+
function toEpoch(date) {
|
|
1211
|
+
return date?.getTime() ?? null;
|
|
1212
|
+
}
|
|
1213
|
+
function fromEpoch(value) {
|
|
1214
|
+
return value === null ? null : new Date(value);
|
|
1215
|
+
}
|
|
1216
|
+
function splitIntoChunks(text, maxCharacters = 900) {
|
|
1217
|
+
const normalized = text.replace(/\r\n?/g, "\n").trim();
|
|
1218
|
+
if (normalized.length === 0)
|
|
1219
|
+
return [];
|
|
1220
|
+
const chunks = [];
|
|
1221
|
+
let remaining = normalized;
|
|
1222
|
+
while (remaining.length > maxCharacters) {
|
|
1223
|
+
const window = remaining.slice(0, maxCharacters + 1);
|
|
1224
|
+
const paragraphBoundary = window.lastIndexOf("\n\n");
|
|
1225
|
+
const sentenceBoundary = Math.max(window.lastIndexOf(". "), window.lastIndexOf("? "), window.lastIndexOf("! "));
|
|
1226
|
+
const wordBoundary = window.lastIndexOf(" ");
|
|
1227
|
+
const boundary = Math.max(paragraphBoundary >= Math.floor(maxCharacters * 0.45) ? paragraphBoundary + 2 : -1, sentenceBoundary >= Math.floor(maxCharacters * 0.55) ? sentenceBoundary + 1 : -1, wordBoundary >= Math.floor(maxCharacters * 0.7) ? wordBoundary : -1, maxCharacters);
|
|
1228
|
+
chunks.push(remaining.slice(0, boundary).trim());
|
|
1229
|
+
remaining = remaining.slice(boundary).trim();
|
|
1230
|
+
}
|
|
1231
|
+
if (remaining.length > 0)
|
|
1232
|
+
chunks.push(remaining);
|
|
1233
|
+
return chunks;
|
|
1234
|
+
}
|
|
1235
|
+
export function compileFtsQuery(query) {
|
|
1236
|
+
const tokens = query
|
|
1237
|
+
.normalize("NFKD")
|
|
1238
|
+
.replace(/\p{Mark}/gu, "")
|
|
1239
|
+
.toLocaleLowerCase("en-US")
|
|
1240
|
+
.match(/[\p{Letter}\p{Number}]+/gu);
|
|
1241
|
+
if (tokens === null || tokens.length === 0)
|
|
1242
|
+
return "";
|
|
1243
|
+
return [...new Set(tokens)].map((token) => `"${token.replaceAll('"', '""')}"`).join(" OR ");
|
|
1244
|
+
}
|
|
1245
|
+
function candidateSql(query, lexical) {
|
|
1246
|
+
const providerClause = providerFilter(query.providers);
|
|
1247
|
+
const timeClause = timeFilter(query);
|
|
1248
|
+
const source = lexical
|
|
1249
|
+
? "chunks_fts JOIN chunks c ON c.rowid = chunks_fts.rowid"
|
|
1250
|
+
: "chunks c";
|
|
1251
|
+
return {
|
|
1252
|
+
sql: `
|
|
1253
|
+
SELECT
|
|
1254
|
+
c.chunk_id, c.text,
|
|
1255
|
+
s.portal_session_id, s.vendor_session_id, s.provider, s.workspace,
|
|
1256
|
+
e.source_id, e.entry_id, c.occurred_at
|
|
1257
|
+
${lexical ? ", bm25(chunks_fts) AS lexical_rank" : ""}
|
|
1258
|
+
FROM ${source}
|
|
1259
|
+
JOIN sessions s ON s.portal_session_id = c.session_id
|
|
1260
|
+
JOIN transcript_entries e ON e.entry_id = c.first_entry_id
|
|
1261
|
+
WHERE ${lexical ? "chunks_fts MATCH ? AND" : ""} s.workspace = ?
|
|
1262
|
+
${providerClause.sql}
|
|
1263
|
+
${timeClause.sql}
|
|
1264
|
+
${lexical ? "ORDER BY lexical_rank, c.chunk_id" : "ORDER BY c.chunk_id"}
|
|
1265
|
+
LIMIT ?
|
|
1266
|
+
`,
|
|
1267
|
+
parameters: [...providerClause.parameters, ...timeClause.parameters],
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function providerFilter(providers) {
|
|
1271
|
+
if (providers === undefined || providers.length === 0)
|
|
1272
|
+
return { sql: "", parameters: [] };
|
|
1273
|
+
return {
|
|
1274
|
+
sql: `AND s.provider IN (${providers.map(() => "?").join(", ")})`,
|
|
1275
|
+
parameters: [...providers],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function timeFilter(query) {
|
|
1279
|
+
const clauses = [];
|
|
1280
|
+
const parameters = [];
|
|
1281
|
+
if (query.occurredAfter !== undefined) {
|
|
1282
|
+
clauses.push("AND c.occurred_at >= ?");
|
|
1283
|
+
parameters.push(query.occurredAfter.getTime());
|
|
1284
|
+
}
|
|
1285
|
+
if (query.occurredBefore !== undefined) {
|
|
1286
|
+
clauses.push("AND c.occurred_at <= ?");
|
|
1287
|
+
parameters.push(query.occurredBefore.getTime());
|
|
1288
|
+
}
|
|
1289
|
+
return { sql: clauses.join("\n"), parameters };
|
|
1290
|
+
}
|
|
1291
|
+
//# sourceMappingURL=repository.js.map
|