@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,637 @@
|
|
|
1
|
+
import { ClaudeProvider } from "./provider/claude.js";
|
|
2
|
+
import { CodexProvider } from "./provider/codex/provider.js";
|
|
3
|
+
import { deriveSessionProfile, normalizeSessionReference, rankSessionCandidates, } from "./session-profile.js";
|
|
4
|
+
import { PortalIndex, redactSecrets } from "./portal-index.js";
|
|
5
|
+
/** Coordinates adapters, the durable index, recall, and the message queue. */
|
|
6
|
+
export class PortalService {
|
|
7
|
+
index;
|
|
8
|
+
providers;
|
|
9
|
+
#closed = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.index = new PortalIndex({ databasePath: options.databasePath });
|
|
12
|
+
this.providers = options.providers ?? [new CodexProvider(), new ClaudeProvider()];
|
|
13
|
+
}
|
|
14
|
+
async close() {
|
|
15
|
+
this.#closed = true;
|
|
16
|
+
for (const provider of this.providers) {
|
|
17
|
+
const close = provider.close;
|
|
18
|
+
if (close !== undefined)
|
|
19
|
+
await close.call(provider);
|
|
20
|
+
}
|
|
21
|
+
this.index.close();
|
|
22
|
+
}
|
|
23
|
+
async refresh(options = {}) {
|
|
24
|
+
const selected = new Set(options.providers ?? this.providers.map(({ name }) => name));
|
|
25
|
+
const result = { observed: 0, indexed: 0, unchanged: 0, errors: [], sessions: [] };
|
|
26
|
+
await Promise.all(this.providers.filter(({ name }) => selected.has(name)).map(async (provider) => {
|
|
27
|
+
let sessions;
|
|
28
|
+
try {
|
|
29
|
+
sessions = await provider.listSessions();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
result.errors.push({ provider: provider.name, vendorSessionId: null, message: errorMessage(error) });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const observedPortalIds = new Set();
|
|
36
|
+
for (const observed of sessions) {
|
|
37
|
+
if (options.workspace !== undefined && observed.workspace !== options.workspace)
|
|
38
|
+
continue;
|
|
39
|
+
result.observed += 1;
|
|
40
|
+
const existing = this.index.repository.findSession({
|
|
41
|
+
provider: observed.provider,
|
|
42
|
+
vendorSessionId: observed.vendorSessionId,
|
|
43
|
+
});
|
|
44
|
+
const stored = this.index.upsertSession({
|
|
45
|
+
provider: observed.provider,
|
|
46
|
+
vendorSessionId: observed.vendorSessionId,
|
|
47
|
+
workspace: observed.workspace,
|
|
48
|
+
title: observed.title,
|
|
49
|
+
updatedAt: observed.updatedAt,
|
|
50
|
+
state: observed.state,
|
|
51
|
+
stateConfidence: observed.stateConfidence,
|
|
52
|
+
indexed: existing?.indexed ?? false,
|
|
53
|
+
connected: observed.connected,
|
|
54
|
+
messageable: observed.messageable,
|
|
55
|
+
});
|
|
56
|
+
observedPortalIds.add(stored.portalSessionId);
|
|
57
|
+
const sourceScope = `session:${stored.portalSessionId}`;
|
|
58
|
+
const cursor = this.index.repository.getCursor(provider.name, sourceScope);
|
|
59
|
+
if (isUnchanged(existing, observed, cursor?.watermark ?? null) &&
|
|
60
|
+
this.index.repository.getSessionProfile(stored.portalSessionId) !== null) {
|
|
61
|
+
result.unchanged += 1;
|
|
62
|
+
returnSession(result, stored);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const transcript = await provider.readTranscript(observed.vendorSessionId);
|
|
67
|
+
if (transcript.length === 0 &&
|
|
68
|
+
existing?.indexed === true &&
|
|
69
|
+
this.index.repository.countTranscriptEntries(stored.portalSessionId) > 0) {
|
|
70
|
+
throw new Error("provider returned an empty transcript for a previously non-empty session");
|
|
71
|
+
}
|
|
72
|
+
await this.index.ingestTranscript(stored.portalSessionId, transcript);
|
|
73
|
+
this.saveDerivedProfile(stored, transcript, observed.updatedAt?.toISOString() ?? null);
|
|
74
|
+
this.index.repository.upsertCursor({
|
|
75
|
+
provider: provider.name,
|
|
76
|
+
sourceScope,
|
|
77
|
+
cursor: null,
|
|
78
|
+
watermark: observed.updatedAt?.toISOString() ?? null,
|
|
79
|
+
lastSuccess: new Date(),
|
|
80
|
+
lastError: null,
|
|
81
|
+
});
|
|
82
|
+
result.indexed += 1;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.index.repository.upsertCursor({
|
|
86
|
+
provider: provider.name,
|
|
87
|
+
sourceScope,
|
|
88
|
+
cursor: cursor?.cursor ?? null,
|
|
89
|
+
watermark: cursor?.watermark ?? null,
|
|
90
|
+
lastSuccess: cursor?.lastSuccess ?? null,
|
|
91
|
+
lastError: errorMessage(error),
|
|
92
|
+
});
|
|
93
|
+
result.errors.push({
|
|
94
|
+
provider: provider.name,
|
|
95
|
+
vendorSessionId: observed.vendorSessionId,
|
|
96
|
+
message: errorMessage(error),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
returnSession(result, this.index.repository.getSession(stored.portalSessionId) ?? stored);
|
|
100
|
+
}
|
|
101
|
+
const priorSessions = this.index.repository
|
|
102
|
+
.listSessions(options.workspace)
|
|
103
|
+
.filter(({ provider: name }) => name === provider.name);
|
|
104
|
+
for (const prior of priorSessions) {
|
|
105
|
+
if (observedPortalIds.has(prior.portalSessionId))
|
|
106
|
+
continue;
|
|
107
|
+
this.index.upsertSession({
|
|
108
|
+
provider: prior.provider,
|
|
109
|
+
vendorSessionId: prior.vendorSessionId,
|
|
110
|
+
workspace: prior.workspace,
|
|
111
|
+
title: prior.title,
|
|
112
|
+
updatedAt: prior.updatedAt,
|
|
113
|
+
state: "unknown",
|
|
114
|
+
stateConfidence: "unavailable",
|
|
115
|
+
indexed: prior.indexed,
|
|
116
|
+
connected: false,
|
|
117
|
+
messageable: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
result.sessions.sort(compareSessions);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
listSessions(workspace) {
|
|
125
|
+
return this.index.repository.listSessions(workspace).map((session) => {
|
|
126
|
+
const portalConnected = this.index.repository.hasActiveConnectionLease(session.portalSessionId);
|
|
127
|
+
const resolved = portalConnected
|
|
128
|
+
? { ...session, connected: true, messageable: true }
|
|
129
|
+
: session;
|
|
130
|
+
return {
|
|
131
|
+
...resolved,
|
|
132
|
+
profile: this.index.repository.getSessionProfile(session.portalSessionId),
|
|
133
|
+
reachability: this.sessionReachability(session),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
resolveSession(reference, workspace) {
|
|
138
|
+
const sessions = this.listSessions(workspace);
|
|
139
|
+
const portalIdMatches = sessions.filter((session) => session.portalSessionId === reference);
|
|
140
|
+
if (portalIdMatches.length > 0)
|
|
141
|
+
return requireUniqueSession(reference, portalIdMatches);
|
|
142
|
+
const vendorIdMatches = sessions.filter((session) => session.vendorSessionId === reference);
|
|
143
|
+
if (vendorIdMatches.length > 0)
|
|
144
|
+
return requireUniqueSession(reference, vendorIdMatches);
|
|
145
|
+
const exactManaged = sessions.filter((session) => session.managedName === reference);
|
|
146
|
+
if (exactManaged.length > 0)
|
|
147
|
+
return requirePreferredSession(reference, exactManaged);
|
|
148
|
+
const exactTitles = sessions.filter((session) => session.title === reference);
|
|
149
|
+
if (exactTitles.length > 0)
|
|
150
|
+
return requirePreferredSession(reference, exactTitles);
|
|
151
|
+
const normalized = normalizeSessionReference(reference);
|
|
152
|
+
const normalizedManaged = sessions.filter((session) => session.managedName !== null && normalizeSessionReference(session.managedName) === normalized);
|
|
153
|
+
if (normalizedManaged.length > 0)
|
|
154
|
+
return requirePreferredSession(reference, normalizedManaged);
|
|
155
|
+
const profileMatches = sessions.filter((session) => session.profile !== null && normalizeSessionReference(session.profile.displayName) === normalized);
|
|
156
|
+
if (profileMatches.length > 0)
|
|
157
|
+
return requirePreferredSession(reference, profileMatches);
|
|
158
|
+
const normalizedTitles = sessions.filter((session) => session.title !== null && normalizeSessionReference(session.title) === normalized);
|
|
159
|
+
if (normalizedTitles.length > 0)
|
|
160
|
+
return requirePreferredSession(reference, normalizedTitles);
|
|
161
|
+
throw new Error(`no Portal session matches: ${reference}`);
|
|
162
|
+
}
|
|
163
|
+
issueSessionCredential(reference, workspace) {
|
|
164
|
+
const session = this.resolveSession(reference, workspace);
|
|
165
|
+
return {
|
|
166
|
+
sessionId: session.portalSessionId,
|
|
167
|
+
token: this.index.repository.issueSessionCredential(session.portalSessionId),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
prepareManagedSession(input) {
|
|
171
|
+
if (input.workspace.trim().length === 0)
|
|
172
|
+
throw new TypeError("managed session requires a workspace");
|
|
173
|
+
const managedName = input.managedName ?? input.title ?? null;
|
|
174
|
+
if (managedName !== null && managedName.trim().length === 0) {
|
|
175
|
+
throw new TypeError("managed session name cannot be empty");
|
|
176
|
+
}
|
|
177
|
+
const session = this.index.upsertSession({
|
|
178
|
+
provider: input.provider,
|
|
179
|
+
vendorSessionId: input.vendorSessionId,
|
|
180
|
+
workspace: input.workspace,
|
|
181
|
+
title: null,
|
|
182
|
+
managedName,
|
|
183
|
+
updatedAt: new Date(),
|
|
184
|
+
state: "idle",
|
|
185
|
+
stateConfidence: "inferred",
|
|
186
|
+
indexed: false,
|
|
187
|
+
connected: false,
|
|
188
|
+
messageable: false,
|
|
189
|
+
});
|
|
190
|
+
const profile = deriveSessionProfile({
|
|
191
|
+
managedName,
|
|
192
|
+
initialPrompt: input.initialPrompt === undefined || input.initialPrompt === null
|
|
193
|
+
? null
|
|
194
|
+
: redactSecrets(input.initialPrompt),
|
|
195
|
+
});
|
|
196
|
+
this.index.repository.upsertSessionProfile({
|
|
197
|
+
sessionId: session.portalSessionId,
|
|
198
|
+
...profile,
|
|
199
|
+
source: managedName === null ? "derived" : "explicit",
|
|
200
|
+
transcriptWatermark: null,
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
session,
|
|
204
|
+
token: this.index.repository.issueSessionCredential(session.portalSessionId),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
authenticateSession(sessionId, token) {
|
|
208
|
+
const session = this.index.repository.authenticateSession(sessionId, token);
|
|
209
|
+
this.index.repository.touchConnectionLease(sessionId);
|
|
210
|
+
return session;
|
|
211
|
+
}
|
|
212
|
+
disconnectSession(sessionId, token) {
|
|
213
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
214
|
+
this.index.repository.clearConnectionLease(sessionId);
|
|
215
|
+
}
|
|
216
|
+
finishManagedSession(sessionId, token) {
|
|
217
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
218
|
+
this.index.repository.clearConnectionLease(sessionId);
|
|
219
|
+
this.index.repository.clearRunnerLease(sessionId);
|
|
220
|
+
this.index.repository.revokeSessionCredential(token);
|
|
221
|
+
}
|
|
222
|
+
bindManagedSession(sessionId, token, vendorSessionId) {
|
|
223
|
+
const session = this.index.repository.authenticateSession(sessionId, token);
|
|
224
|
+
if (session.provider !== "codex" || !session.vendorSessionId.startsWith("pending:")) {
|
|
225
|
+
throw new Error("managed session is not awaiting a Codex vendor binding");
|
|
226
|
+
}
|
|
227
|
+
if (vendorSessionId.trim().length === 0 || vendorSessionId.startsWith("pending:")) {
|
|
228
|
+
throw new Error("Codex vendor session ID is invalid");
|
|
229
|
+
}
|
|
230
|
+
return this.index.repository.bindSessionVendorId(sessionId, vendorSessionId);
|
|
231
|
+
}
|
|
232
|
+
async recall(query, options) {
|
|
233
|
+
if (options.refresh !== false) {
|
|
234
|
+
await this.refresh({
|
|
235
|
+
workspace: options.workspace,
|
|
236
|
+
...(options.providers === undefined ? {} : { providers: options.providers }),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const { refresh: _refresh, ...recallOptions } = options;
|
|
240
|
+
return this.index.recall(query, recallOptions);
|
|
241
|
+
}
|
|
242
|
+
async send(input) {
|
|
243
|
+
if (input.body.trim().length === 0)
|
|
244
|
+
throw new TypeError("message body cannot be empty");
|
|
245
|
+
if (Buffer.byteLength(input.body, "utf8") > 32 * 1024) {
|
|
246
|
+
throw new RangeError("message body exceeds 32 KiB");
|
|
247
|
+
}
|
|
248
|
+
const sender = this.index.repository.getSession(input.senderSessionId);
|
|
249
|
+
if (sender === null)
|
|
250
|
+
throw new Error(`unknown Portal session: ${input.senderSessionId}`);
|
|
251
|
+
if (sender.workspace === null)
|
|
252
|
+
throw new Error("message sender has no workspace");
|
|
253
|
+
const existing = input.idempotencyKey === undefined
|
|
254
|
+
? null
|
|
255
|
+
: this.index.repository.getMessageByIdempotency(input.senderSessionId, input.idempotencyKey);
|
|
256
|
+
if (existing === null) {
|
|
257
|
+
const recent = this.index.repository.countMessagesSince(input.senderSessionId, new Date(Date.now() - 60_000));
|
|
258
|
+
if (recent >= 20)
|
|
259
|
+
throw new Error("message rate limit exceeded: 20 messages per minute");
|
|
260
|
+
}
|
|
261
|
+
const resolved = existing === null
|
|
262
|
+
? this.resolveMessageTarget(sender, input.targetSessionId, input.targetReference, input.body)
|
|
263
|
+
: {
|
|
264
|
+
targetSessionId: existing.targetSessionId,
|
|
265
|
+
resolution: {
|
|
266
|
+
status: "resolved",
|
|
267
|
+
query: input.targetReference ?? input.body,
|
|
268
|
+
strategy: "idempotent",
|
|
269
|
+
targetSessionId: existing.targetSessionId,
|
|
270
|
+
candidates: [],
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
if (resolved.targetSessionId === null) {
|
|
274
|
+
return { message: null, delivery: "not_sent", resolution: resolved.resolution };
|
|
275
|
+
}
|
|
276
|
+
if (resolved.targetSessionId === input.senderSessionId) {
|
|
277
|
+
throw new Error("a session cannot send a blocking Portal message to itself");
|
|
278
|
+
}
|
|
279
|
+
if (this.index.repository.hasRunningDelivery(resolved.targetSessionId, input.senderSessionId)) {
|
|
280
|
+
throw new Error("Portal rejected a reciprocal delivery cycle while the reverse message is still running");
|
|
281
|
+
}
|
|
282
|
+
const message = this.index.repository.enqueueMessage({
|
|
283
|
+
senderSessionId: input.senderSessionId,
|
|
284
|
+
targetSessionId: resolved.targetSessionId,
|
|
285
|
+
body: input.body,
|
|
286
|
+
idempotencyKey: input.idempotencyKey ?? this.index.repository.createMessageIdempotencyKey(),
|
|
287
|
+
...(input.replyTo === undefined ? {} : { replyTo: input.replyTo }),
|
|
288
|
+
});
|
|
289
|
+
if (message.state === "delivered") {
|
|
290
|
+
const receipt = this.index.repository.getMessageReceipt(message.messageId);
|
|
291
|
+
return receipt === null
|
|
292
|
+
? { message, delivery: "delivered", resolution: resolved.resolution }
|
|
293
|
+
: { message, delivery: "completed", receipt, resolution: resolved.resolution };
|
|
294
|
+
}
|
|
295
|
+
if (this.index.repository.hasActiveRunnerLease(resolved.targetSessionId)) {
|
|
296
|
+
const receipt = await this.waitForRunnerCompletion(message.messageId, resolved.targetSessionId);
|
|
297
|
+
const updated = this.index.repository.getMessage(message.messageId) ?? message;
|
|
298
|
+
if (receipt?.status === "completed") {
|
|
299
|
+
return { message: updated, delivery: "completed", receipt, resolution: resolved.resolution };
|
|
300
|
+
}
|
|
301
|
+
if (receipt?.status === "failed") {
|
|
302
|
+
return { message: updated, delivery: "failed", receipt, resolution: resolved.resolution };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const inboxConnected = this.index.repository.hasActiveConnectionLease(resolved.targetSessionId);
|
|
306
|
+
return {
|
|
307
|
+
message,
|
|
308
|
+
delivery: inboxConnected ? "queued_inbox" : "queued_unavailable",
|
|
309
|
+
resolution: resolved.resolution,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
resolveMessageTarget(sender, targetSessionId, targetReference, body) {
|
|
313
|
+
const query = targetReference ?? body;
|
|
314
|
+
if (targetSessionId !== undefined) {
|
|
315
|
+
const target = this.index.repository.getSession(targetSessionId);
|
|
316
|
+
if (target === null)
|
|
317
|
+
throw new Error(`unknown Portal session: ${targetSessionId}`);
|
|
318
|
+
return {
|
|
319
|
+
targetSessionId,
|
|
320
|
+
resolution: { status: "resolved", query, strategy: "id", targetSessionId, candidates: [] },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (targetReference !== undefined) {
|
|
324
|
+
try {
|
|
325
|
+
const target = this.resolveSession(targetReference, sender.workspace ?? undefined);
|
|
326
|
+
const normalized = normalizeSessionReference(targetReference);
|
|
327
|
+
const exact = target.portalSessionId === targetReference || target.vendorSessionId === targetReference;
|
|
328
|
+
const strategy = exact ? "id" :
|
|
329
|
+
target.managedName === targetReference || target.title === targetReference ? "exact" :
|
|
330
|
+
target.managedName !== null && normalizeSessionReference(target.managedName) === normalized
|
|
331
|
+
? "normalized"
|
|
332
|
+
: "profile";
|
|
333
|
+
return {
|
|
334
|
+
targetSessionId: target.portalSessionId,
|
|
335
|
+
resolution: {
|
|
336
|
+
status: "resolved", query, strategy, targetSessionId: target.portalSessionId, candidates: [],
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (!errorMessage(error).startsWith("no Portal session matches:"))
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const ranked = this.rankedRoutingCandidates(query, sender);
|
|
346
|
+
const active = ranked.filter(({ reachability }) => reachability === "active_runner");
|
|
347
|
+
const top = active[0];
|
|
348
|
+
const second = active[1];
|
|
349
|
+
const sufficientlyRelevant = top !== undefined && top.score >= 0.3;
|
|
350
|
+
const clearlySeparated = top !== undefined && (second === undefined || top.score - second.score >= 0.15);
|
|
351
|
+
const candidates = ranked.slice(0, 5).map(toResolutionCandidate);
|
|
352
|
+
if (sufficientlyRelevant && clearlySeparated) {
|
|
353
|
+
return {
|
|
354
|
+
targetSessionId: top.portalSessionId,
|
|
355
|
+
resolution: {
|
|
356
|
+
status: "resolved",
|
|
357
|
+
query,
|
|
358
|
+
strategy: "intent",
|
|
359
|
+
targetSessionId: top.portalSessionId,
|
|
360
|
+
candidates: [toResolutionCandidate(top)],
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
targetSessionId: null,
|
|
366
|
+
resolution: {
|
|
367
|
+
status: sufficientlyRelevant ? "ambiguous" : "unresolved",
|
|
368
|
+
query,
|
|
369
|
+
candidates,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
rankedRoutingCandidates(query, sender) {
|
|
374
|
+
const sessions = this.listSessions(sender.workspace ?? undefined);
|
|
375
|
+
return rankSessionCandidates(query, sessions.map((session) => ({
|
|
376
|
+
portalSessionId: session.portalSessionId,
|
|
377
|
+
workspace: session.workspace,
|
|
378
|
+
displayName: session.profile?.displayName ?? session.managedName ?? session.title ?? "unnamed-session",
|
|
379
|
+
description: session.profile?.description ?? session.title ?? "Unidentified session",
|
|
380
|
+
topics: session.profile?.topics ?? [],
|
|
381
|
+
reachability: session.reachability,
|
|
382
|
+
})), { workspace: sender.workspace, excludeSessionId: sender.portalSessionId });
|
|
383
|
+
}
|
|
384
|
+
saveDerivedProfile(session, entries, transcriptWatermark) {
|
|
385
|
+
const profile = deriveSessionProfile({
|
|
386
|
+
managedName: session.managedName,
|
|
387
|
+
title: session.title === null ? null : redactSecrets(session.title),
|
|
388
|
+
entries: entries.map((entry) => ({ ...entry, text: redactSecrets(entry.text) })),
|
|
389
|
+
});
|
|
390
|
+
this.index.repository.upsertSessionProfile({
|
|
391
|
+
sessionId: session.portalSessionId,
|
|
392
|
+
...profile,
|
|
393
|
+
source: session.managedName === null ? "derived" : "explicit",
|
|
394
|
+
transcriptWatermark,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
sessionReachability(session) {
|
|
398
|
+
if (this.index.repository.hasActiveRunnerLease(session.portalSessionId))
|
|
399
|
+
return "active_runner";
|
|
400
|
+
if (this.index.repository.hasActiveConnectionLease(session.portalSessionId))
|
|
401
|
+
return "inbox_only";
|
|
402
|
+
return session.state === "historical" || session.state === "unknown" ? "historical" : "unavailable";
|
|
403
|
+
}
|
|
404
|
+
async nextRunnerMessage(sessionId, token, waitMs = 25_000) {
|
|
405
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
406
|
+
const deadline = Date.now() + Math.min(Math.max(waitMs, 0), 25_000);
|
|
407
|
+
do {
|
|
408
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
409
|
+
const message = this.index.repository.claimNextRunnerMessage(sessionId);
|
|
410
|
+
if (message !== null) {
|
|
411
|
+
const sender = this.index.repository.getSession(message.senderSessionId);
|
|
412
|
+
return {
|
|
413
|
+
source: "local",
|
|
414
|
+
message,
|
|
415
|
+
senderLabel: sender?.managedName ?? sender?.title ?? sender?.provider ?? "unknown",
|
|
416
|
+
senderReference: message.senderSessionId,
|
|
417
|
+
authenticated: true,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const remoteEvent = this.index.repository.claimNextRemoteTaskEvent(sessionId);
|
|
421
|
+
if (remoteEvent !== null) {
|
|
422
|
+
return {
|
|
423
|
+
source: "remote",
|
|
424
|
+
message: remoteEventMessage(remoteEvent),
|
|
425
|
+
senderLabel: remoteEvent.senderLabel,
|
|
426
|
+
senderReference: remoteEvent.senderReference,
|
|
427
|
+
authenticated: remoteEvent.authenticated,
|
|
428
|
+
remoteEvent,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (this.#closed || Date.now() >= deadline)
|
|
432
|
+
return null;
|
|
433
|
+
await delay(Math.min(100, deadline - Date.now()));
|
|
434
|
+
} while (!this.#closed);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
runnerMessageReceived(sessionId, token, messageId, turnId) {
|
|
438
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
439
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
440
|
+
const remote = decodeRemoteDeliveryId(messageId);
|
|
441
|
+
return remote === null
|
|
442
|
+
? this.index.repository.markRunnerMessageReceived(messageId, sessionId, turnId)
|
|
443
|
+
: this.index.repository.markRemoteTaskEventReceived(remote.serverUrl, remote.eventId, sessionId, turnId);
|
|
444
|
+
}
|
|
445
|
+
runnerMessageStarting(sessionId, token, messageId) {
|
|
446
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
447
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
448
|
+
const remote = decodeRemoteDeliveryId(messageId);
|
|
449
|
+
return remote === null
|
|
450
|
+
? this.index.repository.markRunnerMessageStarting(messageId, sessionId)
|
|
451
|
+
: this.index.repository.markRemoteTaskEventStarting(remote.serverUrl, remote.eventId, sessionId);
|
|
452
|
+
}
|
|
453
|
+
heartbeatRunner(sessionId, token) {
|
|
454
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
455
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
456
|
+
return { active: true };
|
|
457
|
+
}
|
|
458
|
+
runnerMessageCompleted(sessionId, token, messageId, input) {
|
|
459
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
460
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
461
|
+
const remote = decodeRemoteDeliveryId(messageId);
|
|
462
|
+
return remote === null
|
|
463
|
+
? this.index.repository.completeRunnerMessage(messageId, sessionId, input)
|
|
464
|
+
: this.index.repository.completeRemoteTaskEvent(remote.serverUrl, remote.eventId, sessionId, input);
|
|
465
|
+
}
|
|
466
|
+
runnerMessageFailed(sessionId, token, messageId, error) {
|
|
467
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
468
|
+
this.index.repository.touchRunnerLease(sessionId);
|
|
469
|
+
const remote = decodeRemoteDeliveryId(messageId);
|
|
470
|
+
return remote === null
|
|
471
|
+
? this.index.repository.failRunnerMessage(messageId, sessionId, error)
|
|
472
|
+
: this.index.repository.failRemoteTaskEvent(remote.serverUrl, remote.eventId, sessionId, error);
|
|
473
|
+
}
|
|
474
|
+
disconnectRunner(sessionId, token) {
|
|
475
|
+
this.index.repository.authenticateSession(sessionId, token);
|
|
476
|
+
this.index.repository.clearRunnerLease(sessionId);
|
|
477
|
+
}
|
|
478
|
+
async waitForRunnerCompletion(messageId, targetSessionId) {
|
|
479
|
+
const deadline = Date.now() + 15 * 60_000;
|
|
480
|
+
while (!this.#closed && Date.now() < deadline) {
|
|
481
|
+
const receipt = this.index.repository.getMessageReceipt(messageId);
|
|
482
|
+
if (receipt?.status === "completed" || receipt?.status === "failed")
|
|
483
|
+
return receipt;
|
|
484
|
+
if (!this.index.repository.hasActiveRunnerLease(targetSessionId)) {
|
|
485
|
+
if (receipt?.status === "running") {
|
|
486
|
+
return this.index.repository.failRunnerMessage(messageId, targetSessionId, "managed runner disconnected after accepting the message; delivery outcome is unknown");
|
|
487
|
+
}
|
|
488
|
+
return receipt;
|
|
489
|
+
}
|
|
490
|
+
await delay(100);
|
|
491
|
+
}
|
|
492
|
+
return this.index.repository.getMessageReceipt(messageId);
|
|
493
|
+
}
|
|
494
|
+
inbox(targetSessionId) {
|
|
495
|
+
if (this.index.repository.getSession(targetSessionId) === null) {
|
|
496
|
+
throw new Error(`unknown Portal session: ${targetSessionId}`);
|
|
497
|
+
}
|
|
498
|
+
return this.index.repository.listInbox(targetSessionId);
|
|
499
|
+
}
|
|
500
|
+
receiveInbox(targetSessionId, acknowledgements = []) {
|
|
501
|
+
if (this.index.repository.getSession(targetSessionId) === null) {
|
|
502
|
+
throw new Error(`unknown Portal session: ${targetSessionId}`);
|
|
503
|
+
}
|
|
504
|
+
return this.index.repository.receiveQueuedMessages(targetSessionId, acknowledgements);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function requireUniqueSession(reference, matches) {
|
|
508
|
+
if (matches.length > 1) {
|
|
509
|
+
throw new Error(`session reference is ambiguous: ${reference}; use a Portal session ID`);
|
|
510
|
+
}
|
|
511
|
+
return matches[0];
|
|
512
|
+
}
|
|
513
|
+
function requirePreferredSession(reference, matches) {
|
|
514
|
+
const active = matches.filter(({ reachability }) => reachability === "active_runner");
|
|
515
|
+
if (active.length > 0)
|
|
516
|
+
return requireUniqueSession(reference, active);
|
|
517
|
+
const connected = matches.filter(({ connected }) => connected);
|
|
518
|
+
return requireUniqueSession(reference, connected.length > 0 ? connected : matches);
|
|
519
|
+
}
|
|
520
|
+
function toResolutionCandidate(candidate) {
|
|
521
|
+
return {
|
|
522
|
+
portalSessionId: candidate.portalSessionId,
|
|
523
|
+
displayName: candidate.displayName,
|
|
524
|
+
description: candidate.description,
|
|
525
|
+
reachability: candidate.reachability,
|
|
526
|
+
score: candidate.score,
|
|
527
|
+
reasons: candidate.reasons,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function isUnchanged(existing, observed, indexedWatermark) {
|
|
531
|
+
return (existing?.indexed === true &&
|
|
532
|
+
observed.updatedAt !== null &&
|
|
533
|
+
indexedWatermark === observed.updatedAt.toISOString());
|
|
534
|
+
}
|
|
535
|
+
function returnSession(result, session) {
|
|
536
|
+
const index = result.sessions.findIndex(({ portalSessionId }) => portalSessionId === session.portalSessionId);
|
|
537
|
+
if (index < 0)
|
|
538
|
+
result.sessions.push(session);
|
|
539
|
+
else
|
|
540
|
+
result.sessions[index] = session;
|
|
541
|
+
}
|
|
542
|
+
function compareSessions(left, right) {
|
|
543
|
+
return (right.updatedAt?.getTime() ?? 0) - (left.updatedAt?.getTime() ?? 0) ||
|
|
544
|
+
left.portalSessionId.localeCompare(right.portalSessionId);
|
|
545
|
+
}
|
|
546
|
+
function remoteEventMessage(event) {
|
|
547
|
+
return {
|
|
548
|
+
messageId: encodeRemoteDeliveryId(event.serverUrl, event.eventId),
|
|
549
|
+
senderSessionId: `remote:${event.senderReference}`,
|
|
550
|
+
targetSessionId: event.localSessionId,
|
|
551
|
+
body: formatRemoteEvent(event),
|
|
552
|
+
state: "delivering",
|
|
553
|
+
createdAt: event.occurredAt,
|
|
554
|
+
deliveredAt: null,
|
|
555
|
+
replyTo: null,
|
|
556
|
+
idempotencyKey: event.eventId,
|
|
557
|
+
error: event.error,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function formatRemoteEvent(event) {
|
|
561
|
+
const payload = event.payload;
|
|
562
|
+
const heading = `Portal · ${event.senderLabel} (unverified; sandbox and approval policy still apply) · ${event.kind} · ${event.taskId}`;
|
|
563
|
+
switch (event.kind) {
|
|
564
|
+
case "offer":
|
|
565
|
+
return [
|
|
566
|
+
heading,
|
|
567
|
+
field(payload, "objective"),
|
|
568
|
+
optionalField(payload, "Context", "context"),
|
|
569
|
+
optionalField(payload, "Expected", "expectedResult"),
|
|
570
|
+
"Use portal.task.update to accept or decline before starting material work.",
|
|
571
|
+
].filter(Boolean).join("\n");
|
|
572
|
+
case "accepted":
|
|
573
|
+
return [heading, optionalField(payload, "", "message")].filter(Boolean).join("\n");
|
|
574
|
+
case "declined":
|
|
575
|
+
return [heading, optionalField(payload, "Reason", "reason")].filter(Boolean).join("\n");
|
|
576
|
+
case "running":
|
|
577
|
+
case "progress":
|
|
578
|
+
return [heading, optionalField(payload, "", "message")].filter(Boolean).join("\n");
|
|
579
|
+
case "question":
|
|
580
|
+
return [heading, optionalField(payload, "Question", "question")].filter(Boolean).join("\n");
|
|
581
|
+
case "answer":
|
|
582
|
+
return [heading, optionalField(payload, "Answer", "answer")].filter(Boolean).join("\n");
|
|
583
|
+
case "completed":
|
|
584
|
+
return [
|
|
585
|
+
heading,
|
|
586
|
+
field(payload, "result"),
|
|
587
|
+
listField(payload, "Artifacts", "artifactReferences"),
|
|
588
|
+
].filter(Boolean).join("\n");
|
|
589
|
+
case "failed":
|
|
590
|
+
return [heading, optionalField(payload, "Error", "error")].filter(Boolean).join("\n");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function field(payload, key) {
|
|
594
|
+
return typeof payload[key] === "string" ? payload[key] : "";
|
|
595
|
+
}
|
|
596
|
+
function optionalField(payload, label, key) {
|
|
597
|
+
const value = field(payload, key);
|
|
598
|
+
if (!value)
|
|
599
|
+
return "";
|
|
600
|
+
return label ? `${label}: ${value}` : value;
|
|
601
|
+
}
|
|
602
|
+
function listField(payload, label, key) {
|
|
603
|
+
const values = payload[key];
|
|
604
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
605
|
+
return "";
|
|
606
|
+
return `${label}: ${values.filter((value) => typeof value === "string").join(", ")}`;
|
|
607
|
+
}
|
|
608
|
+
function encodeRemoteDeliveryId(serverUrl, eventId) {
|
|
609
|
+
return `remote_${Buffer.from(JSON.stringify({ serverUrl, eventId }), "utf8").toString("base64url")}`;
|
|
610
|
+
}
|
|
611
|
+
function decodeRemoteDeliveryId(value) {
|
|
612
|
+
if (!value.startsWith("remote_"))
|
|
613
|
+
return null;
|
|
614
|
+
try {
|
|
615
|
+
const decoded = JSON.parse(Buffer.from(value.slice("remote_".length), "base64url").toString("utf8"));
|
|
616
|
+
if (typeof decoded !== "object" || decoded === null)
|
|
617
|
+
throw new Error("not an object");
|
|
618
|
+
const { serverUrl, eventId } = decoded;
|
|
619
|
+
if (typeof serverUrl !== "string" || typeof eventId !== "string" || !serverUrl || !eventId) {
|
|
620
|
+
throw new Error("missing fields");
|
|
621
|
+
}
|
|
622
|
+
return { serverUrl, eventId };
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
throw new Error("invalid remote runner delivery ID");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function errorMessage(error) {
|
|
629
|
+
return error instanceof Error ? error.message : String(error);
|
|
630
|
+
}
|
|
631
|
+
function delay(milliseconds) {
|
|
632
|
+
return new Promise((resolve) => {
|
|
633
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
634
|
+
timer.unref();
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
//# sourceMappingURL=service.js.map
|