opencode-gateway 0.2.3 → 0.2.5
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/dist/cli.js +0 -0
- package/dist/index.js +36910 -56
- package/dist/runtime/delay.d.ts +1 -0
- package/dist/store/database.d.ts +22 -0
- package/dist/store/migrations.d.ts +2 -2
- package/dist/store/sqlite.d.ts +2 -2
- package/package.json +9 -3
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -180
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -166
- package/dist/telegram/runtime.js +0 -157
- package/dist/telegram/state.js +0 -149
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/runtime/executor.js
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import { ConversationCoordinator } from "./conversation-coordinator";
|
|
2
|
-
import { runOpencodeDriver } from "./opencode-runner";
|
|
3
|
-
const SESSION_ABORT_SETTLE_TIMEOUT_MS = 5_000;
|
|
4
|
-
const SESSION_ABORT_POLL_MS = 250;
|
|
5
|
-
const SESSION_RESIDUAL_BUSY_GRACE_POLLS = 3;
|
|
6
|
-
export class GatewayExecutor {
|
|
7
|
-
module;
|
|
8
|
-
store;
|
|
9
|
-
opencode;
|
|
10
|
-
events;
|
|
11
|
-
delivery;
|
|
12
|
-
logger;
|
|
13
|
-
coordinator;
|
|
14
|
-
internalPromptSequence = 0;
|
|
15
|
-
constructor(module, store, opencode, events, delivery, logger, coordinator = new ConversationCoordinator()) {
|
|
16
|
-
this.module = module;
|
|
17
|
-
this.store = store;
|
|
18
|
-
this.opencode = opencode;
|
|
19
|
-
this.events = events;
|
|
20
|
-
this.delivery = delivery;
|
|
21
|
-
this.logger = logger;
|
|
22
|
-
this.coordinator = coordinator;
|
|
23
|
-
}
|
|
24
|
-
prepareInboundMessage(message) {
|
|
25
|
-
return this.module.prepareInboundExecution(message);
|
|
26
|
-
}
|
|
27
|
-
async handleInboundMessage(message) {
|
|
28
|
-
const prepared = this.prepareInboundMessage(message);
|
|
29
|
-
const syntheticEntry = {
|
|
30
|
-
id: Date.now(),
|
|
31
|
-
mailboxKey: prepared.conversationKey,
|
|
32
|
-
sourceKind: "direct_runtime",
|
|
33
|
-
externalId: `direct:${Date.now()}`,
|
|
34
|
-
sender: normalizeRequiredField(message.sender, "message sender"),
|
|
35
|
-
text: message.text,
|
|
36
|
-
attachments: withAttachmentOrdinals(message.attachments),
|
|
37
|
-
replyChannel: message.deliveryTarget.channel,
|
|
38
|
-
replyTarget: message.deliveryTarget.target,
|
|
39
|
-
replyTopic: message.deliveryTarget.topic,
|
|
40
|
-
createdAtMs: Date.now(),
|
|
41
|
-
};
|
|
42
|
-
return await this.executeMailboxEntries([syntheticEntry]);
|
|
43
|
-
}
|
|
44
|
-
async executeMailboxEntries(entries) {
|
|
45
|
-
if (entries.length === 0) {
|
|
46
|
-
throw new Error("mailbox execution requires at least one entry");
|
|
47
|
-
}
|
|
48
|
-
const preparedEntries = entries.map((entry) => {
|
|
49
|
-
const message = mailboxEntryToInboundMessage(entry);
|
|
50
|
-
return {
|
|
51
|
-
entry,
|
|
52
|
-
message,
|
|
53
|
-
prepared: this.prepareInboundMessage(message),
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
const conversationKey = preparedEntries[0].prepared.conversationKey;
|
|
57
|
-
if (preparedEntries.some((entry) => entry.prepared.conversationKey !== conversationKey)) {
|
|
58
|
-
throw new Error("mailbox batch contains mixed conversation keys");
|
|
59
|
-
}
|
|
60
|
-
const recordedAtMs = Date.now();
|
|
61
|
-
this.logger.log("info", "handling inbound gateway message");
|
|
62
|
-
return await this.coordinator.runExclusive(conversationKey, async () => {
|
|
63
|
-
this.store.appendJournal(createJournalEntry("mailbox_flush", recordedAtMs, conversationKey, {
|
|
64
|
-
entryIds: preparedEntries.map((entry) => entry.entry.id),
|
|
65
|
-
count: preparedEntries.length,
|
|
66
|
-
}));
|
|
67
|
-
for (const entry of preparedEntries) {
|
|
68
|
-
this.store.appendJournal(createJournalEntry("inbound_message", entry.entry.createdAtMs, conversationKey, {
|
|
69
|
-
deliveryTarget: entry.prepared.replyTarget,
|
|
70
|
-
sender: entry.message.sender,
|
|
71
|
-
text: entry.message.text,
|
|
72
|
-
attachments: entry.message.attachments,
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
return await this.executePreparedBatch(preparedEntries, recordedAtMs);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
async dispatchCronJob(job) {
|
|
79
|
-
const prepared = this.module.prepareCronExecution(job);
|
|
80
|
-
const id = normalizeRequiredField(job.id, "cron job id");
|
|
81
|
-
const schedule = normalizeRequiredField(job.schedule, "cron schedule");
|
|
82
|
-
const recordedAtMs = Date.now();
|
|
83
|
-
this.logger.log("info", "dispatching cron gateway job");
|
|
84
|
-
this.store.appendJournal(createJournalEntry("cron_dispatch", recordedAtMs, prepared.conversationKey, {
|
|
85
|
-
id,
|
|
86
|
-
schedule,
|
|
87
|
-
promptParts: prepared.promptParts,
|
|
88
|
-
deliveryChannel: prepared.replyTarget?.channel ?? null,
|
|
89
|
-
deliveryTarget: prepared.replyTarget?.target ?? null,
|
|
90
|
-
deliveryTopic: prepared.replyTarget?.topic ?? null,
|
|
91
|
-
}));
|
|
92
|
-
return await this.coordinator.runExclusive(prepared.conversationKey, async () => {
|
|
93
|
-
return await this.executePreparedBatch([
|
|
94
|
-
{
|
|
95
|
-
entry: null,
|
|
96
|
-
message: null,
|
|
97
|
-
prepared,
|
|
98
|
-
},
|
|
99
|
-
], recordedAtMs);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async dispatchScheduledJob(input) {
|
|
103
|
-
const prepared = prepareTextExecution(input.conversationKey, input.prompt, input.replyTarget);
|
|
104
|
-
const recordedAtMs = Date.now();
|
|
105
|
-
this.logger.log("info", "dispatching scheduled gateway job");
|
|
106
|
-
return await this.coordinator.runExclusive(prepared.conversationKey, async () => {
|
|
107
|
-
this.store.appendJournal(createJournalEntry("cron_dispatch", recordedAtMs, prepared.conversationKey, {
|
|
108
|
-
id: input.jobId,
|
|
109
|
-
kind: input.jobKind,
|
|
110
|
-
promptParts: prepared.promptParts,
|
|
111
|
-
deliveryChannel: prepared.replyTarget?.channel ?? null,
|
|
112
|
-
deliveryTarget: prepared.replyTarget?.target ?? null,
|
|
113
|
-
deliveryTopic: prepared.replyTarget?.topic ?? null,
|
|
114
|
-
}));
|
|
115
|
-
return await this.executePreparedBatch([
|
|
116
|
-
{
|
|
117
|
-
entry: null,
|
|
118
|
-
message: null,
|
|
119
|
-
prepared,
|
|
120
|
-
},
|
|
121
|
-
], recordedAtMs);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
async appendContextToConversation(input) {
|
|
125
|
-
const conversationKey = normalizeRequiredField(input.conversationKey, "conversation key");
|
|
126
|
-
const body = normalizeRequiredField(input.body, "context body");
|
|
127
|
-
await this.coordinator.runExclusive(conversationKey, async () => {
|
|
128
|
-
const sessionId = await this.ensureConversationSession(conversationKey, input.recordedAtMs, input.replyTarget === null ? [] : [input.replyTarget]);
|
|
129
|
-
const promptIdentity = this.createInternalPromptIdentity("context", input.recordedAtMs);
|
|
130
|
-
await this.appendPrompt(sessionId, promptIdentity.messageId, [
|
|
131
|
-
{
|
|
132
|
-
kind: "text",
|
|
133
|
-
partId: promptIdentity.partId,
|
|
134
|
-
text: body,
|
|
135
|
-
},
|
|
136
|
-
]);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
async executePreparedBatch(entries, recordedAtMs) {
|
|
140
|
-
const conversationKey = entries[0].prepared.conversationKey;
|
|
141
|
-
const persistedSessionId = this.store.getSessionBinding(conversationKey);
|
|
142
|
-
const preparedSessionId = await this.preparePersistedSessionForPrompt(persistedSessionId);
|
|
143
|
-
const replyTargets = dedupeReplyTargets(entries.flatMap((entry) => (entry.prepared.replyTarget === null ? [] : [entry.prepared.replyTarget])));
|
|
144
|
-
const [deliverySession] = replyTargets.length === 0 ? [null] : await this.delivery.openMany(replyTargets, "auto");
|
|
145
|
-
const promptResult = await this.executeDriver(entries, recordedAtMs, preparedSessionId, deliverySession, replyTargets);
|
|
146
|
-
await this.cleanupResidualBusySession(promptResult.sessionId);
|
|
147
|
-
this.store.putSessionBindingIfUnchanged(conversationKey, persistedSessionId, promptResult.sessionId, recordedAtMs);
|
|
148
|
-
let delivered = false;
|
|
149
|
-
if (deliverySession !== null) {
|
|
150
|
-
delivered = await deliverySession.finish(promptResult.finalText);
|
|
151
|
-
if (promptResult.finalText !== null) {
|
|
152
|
-
this.store.appendJournal(createJournalEntry("delivery", recordedAtMs, conversationKey, {
|
|
153
|
-
deliveryTargets: replyTargets,
|
|
154
|
-
body: promptResult.finalText,
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
conversationKey,
|
|
160
|
-
responseText: promptResult.responseText,
|
|
161
|
-
delivered,
|
|
162
|
-
recordedAtMs: BigInt(recordedAtMs),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async ensureConversationSession(conversationKey, recordedAtMs, replyTargets) {
|
|
166
|
-
const persistedSessionId = this.store.getSessionBinding(conversationKey);
|
|
167
|
-
let sessionId = await this.preparePersistedSessionForPrompt(persistedSessionId);
|
|
168
|
-
let retryMissingSession = true;
|
|
169
|
-
for (;;) {
|
|
170
|
-
if (sessionId === null) {
|
|
171
|
-
sessionId = await this.createSession(conversationKey);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
await this.waitUntilIdle(sessionId);
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
if (retryMissingSession && error instanceof MissingSessionCommandError) {
|
|
179
|
-
retryMissingSession = false;
|
|
180
|
-
sessionId = null;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
this.store.putSessionBindingIfUnchanged(conversationKey, persistedSessionId, sessionId, recordedAtMs);
|
|
187
|
-
this.initializeReplyTargetsIfMissing(sessionId, conversationKey, replyTargets, recordedAtMs);
|
|
188
|
-
return sessionId;
|
|
189
|
-
}
|
|
190
|
-
initializeReplyTargetsIfMissing(sessionId, conversationKey, replyTargets, recordedAtMs) {
|
|
191
|
-
if (replyTargets.length === 0 || this.store.listSessionReplyTargets(sessionId).length > 0) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
this.store.replaceSessionReplyTargets({
|
|
195
|
-
sessionId,
|
|
196
|
-
conversationKey,
|
|
197
|
-
targets: replyTargets,
|
|
198
|
-
recordedAtMs,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
async preparePersistedSessionForPrompt(sessionId) {
|
|
202
|
-
if (sessionId === null) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
const found = await this.lookupSession(sessionId);
|
|
206
|
-
if (!found) {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
if (!(await this.opencode.isSessionBusy(sessionId))) {
|
|
210
|
-
return sessionId;
|
|
211
|
-
}
|
|
212
|
-
this.logger.log("warn", `aborting busy gateway session before prompt dispatch: ${sessionId}`);
|
|
213
|
-
try {
|
|
214
|
-
await this.abortSessionAndWaitForSettle(sessionId);
|
|
215
|
-
return sessionId;
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
this.logger.log("warn", `busy gateway session did not settle and will be replaced: ${sessionId}: ${extractErrorMessage(error)}`);
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async lookupSession(sessionId) {
|
|
223
|
-
const result = await this.opencode.execute({
|
|
224
|
-
kind: "lookupSession",
|
|
225
|
-
sessionId,
|
|
226
|
-
});
|
|
227
|
-
const lookup = expectCommandResult(result, "lookupSession");
|
|
228
|
-
return lookup.found;
|
|
229
|
-
}
|
|
230
|
-
async createSession(conversationKey) {
|
|
231
|
-
const result = await this.opencode.execute({
|
|
232
|
-
kind: "createSession",
|
|
233
|
-
title: `Gateway ${conversationKey}`,
|
|
234
|
-
});
|
|
235
|
-
return expectCommandResult(result, "createSession").sessionId;
|
|
236
|
-
}
|
|
237
|
-
async waitUntilIdle(sessionId) {
|
|
238
|
-
const result = await this.opencode.execute({
|
|
239
|
-
kind: "waitUntilIdle",
|
|
240
|
-
sessionId,
|
|
241
|
-
});
|
|
242
|
-
expectCommandResult(result, "waitUntilIdle");
|
|
243
|
-
}
|
|
244
|
-
async appendPrompt(sessionId, messageId, parts) {
|
|
245
|
-
const result = await this.opencode.execute({
|
|
246
|
-
kind: "appendPrompt",
|
|
247
|
-
sessionId,
|
|
248
|
-
messageId,
|
|
249
|
-
parts,
|
|
250
|
-
});
|
|
251
|
-
expectCommandResult(result, "appendPrompt");
|
|
252
|
-
}
|
|
253
|
-
async cleanupResidualBusySession(sessionId) {
|
|
254
|
-
if (await this.waitForSessionToSettle(sessionId, SESSION_RESIDUAL_BUSY_GRACE_POLLS)) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
this.logger.log("debug", `aborting residual busy gateway session after prompt completion: ${sessionId}`);
|
|
258
|
-
try {
|
|
259
|
-
await this.abortSessionAndWaitForSettle(sessionId);
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
this.logger.log("warn", `residual busy gateway session did not settle after abort: ${sessionId}: ${extractErrorMessage(error)}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
async waitForSessionToSettle(sessionId, extraPolls) {
|
|
266
|
-
for (let attempt = 0; attempt <= extraPolls; attempt += 1) {
|
|
267
|
-
if (!(await this.opencode.isSessionBusy(sessionId))) {
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
if (attempt < extraPolls) {
|
|
271
|
-
await Bun.sleep(SESSION_ABORT_POLL_MS);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
async abortSessionAndWaitForSettle(sessionId) {
|
|
277
|
-
await this.opencode.abortSession(sessionId);
|
|
278
|
-
const deadline = Date.now() + SESSION_ABORT_SETTLE_TIMEOUT_MS;
|
|
279
|
-
for (;;) {
|
|
280
|
-
if (!(await this.opencode.isSessionBusy(sessionId))) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (Date.now() >= deadline) {
|
|
284
|
-
throw new Error(`session remained busy after abort for ${SESSION_ABORT_SETTLE_TIMEOUT_MS}ms`);
|
|
285
|
-
}
|
|
286
|
-
await Bun.sleep(SESSION_ABORT_POLL_MS);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
createInternalPromptIdentity(prefix, recordedAtMs) {
|
|
290
|
-
const suffix = `${recordedAtMs}_${this.internalPromptSequence}`;
|
|
291
|
-
this.internalPromptSequence += 1;
|
|
292
|
-
const normalizedPrefix = prefix.replaceAll(":", "_");
|
|
293
|
-
return {
|
|
294
|
-
messageId: `msg_gateway_${normalizedPrefix}_${suffix}`,
|
|
295
|
-
partId: `prt_gateway_${normalizedPrefix}_${suffix}_0`,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
async executeDriver(entries, recordedAtMs, persistedSessionId, deliverySession, replyTargets) {
|
|
299
|
-
return await runOpencodeDriver({
|
|
300
|
-
module: this.module,
|
|
301
|
-
opencode: this.opencode,
|
|
302
|
-
events: this.events,
|
|
303
|
-
conversationKey: entries[0].prepared.conversationKey,
|
|
304
|
-
persistedSessionId,
|
|
305
|
-
deliverySession,
|
|
306
|
-
prompts: entries.map((entry, index) => ({
|
|
307
|
-
promptKey: createPromptKey(entry, recordedAtMs, index),
|
|
308
|
-
parts: entry.prepared.promptParts,
|
|
309
|
-
})),
|
|
310
|
-
onSessionAvailable: async (sessionId) => {
|
|
311
|
-
this.store.replaceSessionReplyTargets({
|
|
312
|
-
sessionId,
|
|
313
|
-
conversationKey: entries[0].prepared.conversationKey,
|
|
314
|
-
targets: replyTargets,
|
|
315
|
-
recordedAtMs,
|
|
316
|
-
});
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function createJournalEntry(kind, recordedAtMs, conversationKey, payload) {
|
|
322
|
-
return {
|
|
323
|
-
kind,
|
|
324
|
-
recordedAtMs,
|
|
325
|
-
conversationKey,
|
|
326
|
-
payload,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
function prepareTextExecution(conversationKey, prompt, replyTarget) {
|
|
330
|
-
return {
|
|
331
|
-
conversationKey: normalizeRequiredField(conversationKey, "conversation key"),
|
|
332
|
-
promptParts: [
|
|
333
|
-
{
|
|
334
|
-
kind: "text",
|
|
335
|
-
text: normalizeRequiredField(prompt, "schedule prompt"),
|
|
336
|
-
},
|
|
337
|
-
],
|
|
338
|
-
replyTarget,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function mailboxEntryToInboundMessage(entry) {
|
|
342
|
-
return {
|
|
343
|
-
deliveryTarget: {
|
|
344
|
-
channel: normalizeRequiredField(entry.replyChannel ?? "", "mailbox reply channel"),
|
|
345
|
-
target: normalizeRequiredField(entry.replyTarget ?? "", "mailbox reply target"),
|
|
346
|
-
topic: entry.replyTopic,
|
|
347
|
-
},
|
|
348
|
-
sender: entry.sender,
|
|
349
|
-
text: entry.text,
|
|
350
|
-
attachments: entry.attachments,
|
|
351
|
-
mailboxKey: entry.mailboxKey,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
function dedupeReplyTargets(targets) {
|
|
355
|
-
const seen = new Set();
|
|
356
|
-
return targets.filter((target) => {
|
|
357
|
-
const key = `${target.channel}:${target.target}:${target.topic ?? ""}`;
|
|
358
|
-
if (seen.has(key)) {
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
seen.add(key);
|
|
362
|
-
return true;
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
function normalizeRequiredField(value, field) {
|
|
366
|
-
const trimmed = value.trim();
|
|
367
|
-
if (trimmed.length === 0) {
|
|
368
|
-
throw new Error(`${field} must not be empty`);
|
|
369
|
-
}
|
|
370
|
-
return trimmed;
|
|
371
|
-
}
|
|
372
|
-
function extractErrorMessage(error) {
|
|
373
|
-
if (error instanceof Error && error.message.trim().length > 0) {
|
|
374
|
-
return error.message;
|
|
375
|
-
}
|
|
376
|
-
return String(error);
|
|
377
|
-
}
|
|
378
|
-
function expectCommandResult(result, expectedKind) {
|
|
379
|
-
if (result.kind === "error") {
|
|
380
|
-
if (result.code === "missingSession") {
|
|
381
|
-
throw new MissingSessionCommandError(result.message);
|
|
382
|
-
}
|
|
383
|
-
throw new Error(result.message);
|
|
384
|
-
}
|
|
385
|
-
if (result.kind !== expectedKind) {
|
|
386
|
-
throw new Error(`unexpected OpenCode result kind: expected ${expectedKind}, received ${result.kind}`);
|
|
387
|
-
}
|
|
388
|
-
return result;
|
|
389
|
-
}
|
|
390
|
-
class MissingSessionCommandError extends Error {
|
|
391
|
-
constructor(message) {
|
|
392
|
-
super(message);
|
|
393
|
-
this.name = "MissingSessionCommandError";
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
function createPromptKey(entry, recordedAtMs, index) {
|
|
397
|
-
if (entry.entry !== null) {
|
|
398
|
-
return `mailbox:${entry.entry.id}:${recordedAtMs}`;
|
|
399
|
-
}
|
|
400
|
-
return `synthetic:${recordedAtMs}:${index}`;
|
|
401
|
-
}
|
|
402
|
-
function withAttachmentOrdinals(messageAttachments) {
|
|
403
|
-
return messageAttachments.map((attachment, ordinal) => ({
|
|
404
|
-
...attachment,
|
|
405
|
-
ordinal,
|
|
406
|
-
}));
|
|
407
|
-
}
|
package/dist/runtime/mailbox.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { formatError } from "../utils/error";
|
|
2
|
-
import { deleteInboundAttachmentFiles } from "./attachments";
|
|
3
|
-
const RETRY_DELAY_MS = 1_000;
|
|
4
|
-
export class GatewayMailboxRuntime {
|
|
5
|
-
executor;
|
|
6
|
-
store;
|
|
7
|
-
logger;
|
|
8
|
-
config;
|
|
9
|
-
questions;
|
|
10
|
-
activeMailboxes = new Set();
|
|
11
|
-
scheduledMailboxes = new Map();
|
|
12
|
-
constructor(executor, store, logger, config, questions) {
|
|
13
|
-
this.executor = executor;
|
|
14
|
-
this.store = store;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
this.config = config;
|
|
17
|
-
this.questions = questions;
|
|
18
|
-
}
|
|
19
|
-
start() {
|
|
20
|
-
for (const mailboxKey of this.store.listPendingMailboxKeys()) {
|
|
21
|
-
this.scheduleImmediate(mailboxKey);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
async enqueueInboundMessage(message, sourceKind, externalId) {
|
|
25
|
-
if (await this.questions.tryHandleInboundMessage(message)) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const prepared = this.executor.prepareInboundMessage(message);
|
|
29
|
-
const recordedAtMs = Date.now();
|
|
30
|
-
this.store.enqueueMailboxEntry({
|
|
31
|
-
mailboxKey: prepared.conversationKey,
|
|
32
|
-
sourceKind,
|
|
33
|
-
externalId,
|
|
34
|
-
sender: message.sender,
|
|
35
|
-
text: message.text,
|
|
36
|
-
attachments: message.attachments,
|
|
37
|
-
replyChannel: message.deliveryTarget.channel,
|
|
38
|
-
replyTarget: message.deliveryTarget.target,
|
|
39
|
-
replyTopic: message.deliveryTarget.topic,
|
|
40
|
-
recordedAtMs,
|
|
41
|
-
});
|
|
42
|
-
this.store.appendJournal(createJournalEntry("mailbox_enqueue", recordedAtMs, prepared.conversationKey, {
|
|
43
|
-
sourceKind,
|
|
44
|
-
externalId,
|
|
45
|
-
sender: message.sender,
|
|
46
|
-
text: message.text,
|
|
47
|
-
attachments: message.attachments,
|
|
48
|
-
deliveryTarget: message.deliveryTarget,
|
|
49
|
-
}));
|
|
50
|
-
this.scheduleAfterEnqueue(prepared.conversationKey);
|
|
51
|
-
}
|
|
52
|
-
scheduleAfterEnqueue(mailboxKey) {
|
|
53
|
-
if (this.activeMailboxes.has(mailboxKey) || this.scheduledMailboxes.has(mailboxKey)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
this.schedule(mailboxKey, this.config.batchReplies ? this.config.batchWindowMs : 0);
|
|
57
|
-
}
|
|
58
|
-
scheduleImmediate(mailboxKey) {
|
|
59
|
-
if (this.activeMailboxes.has(mailboxKey) || this.scheduledMailboxes.has(mailboxKey)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
this.schedule(mailboxKey, 0);
|
|
63
|
-
}
|
|
64
|
-
scheduleRetry(mailboxKey) {
|
|
65
|
-
if (this.activeMailboxes.has(mailboxKey) || this.scheduledMailboxes.has(mailboxKey)) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
this.schedule(mailboxKey, RETRY_DELAY_MS);
|
|
69
|
-
}
|
|
70
|
-
schedule(mailboxKey, delayMs) {
|
|
71
|
-
const handle = setTimeout(() => {
|
|
72
|
-
this.scheduledMailboxes.delete(mailboxKey);
|
|
73
|
-
void this.processMailbox(mailboxKey);
|
|
74
|
-
}, delayMs);
|
|
75
|
-
this.scheduledMailboxes.set(mailboxKey, handle);
|
|
76
|
-
}
|
|
77
|
-
async processMailbox(mailboxKey) {
|
|
78
|
-
if (this.activeMailboxes.has(mailboxKey)) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
this.activeMailboxes.add(mailboxKey);
|
|
82
|
-
try {
|
|
83
|
-
const entries = this.store.listMailboxEntries(mailboxKey);
|
|
84
|
-
if (entries.length === 0) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const batch = this.config.batchReplies ? entries : [entries[0]];
|
|
88
|
-
await this.executor.executeMailboxEntries(batch);
|
|
89
|
-
this.store.deleteMailboxEntries(batch.map((entry) => entry.id));
|
|
90
|
-
await deleteInboundAttachmentFiles(batch, this.logger);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.logger.log("warn", `mailbox flush failed for ${mailboxKey}: ${formatError(error)}`);
|
|
94
|
-
this.scheduleRetry(mailboxKey);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
this.activeMailboxes.delete(mailboxKey);
|
|
99
|
-
}
|
|
100
|
-
if (this.store.listMailboxEntries(mailboxKey).length > 0) {
|
|
101
|
-
this.scheduleImmediate(mailboxKey);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function createJournalEntry(kind, recordedAtMs, conversationKey, payload) {
|
|
106
|
-
return {
|
|
107
|
-
kind,
|
|
108
|
-
recordedAtMs,
|
|
109
|
-
conversationKey,
|
|
110
|
-
payload,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
const DEFAULT_FLUSH_INTERVAL_MS = 400;
|
|
2
|
-
export async function runOpencodeDriver(options) {
|
|
3
|
-
const driver = new options.module.OpencodeExecutionDriver({
|
|
4
|
-
conversationKey: options.conversationKey,
|
|
5
|
-
persistedSessionId: options.persistedSessionId,
|
|
6
|
-
mode: options.deliverySession?.mode === "progressive" ? "progressive" : "oneshot",
|
|
7
|
-
flushIntervalMs: DEFAULT_FLUSH_INTERVAL_MS,
|
|
8
|
-
prompts: options.prompts,
|
|
9
|
-
});
|
|
10
|
-
let registration = null;
|
|
11
|
-
let activeSessionId = null;
|
|
12
|
-
try {
|
|
13
|
-
let step = driver.start();
|
|
14
|
-
for (;;) {
|
|
15
|
-
if (step.kind === "command") {
|
|
16
|
-
activeSessionId = await syncSessionContext(activeSessionId, step.command, options.onSessionAvailable);
|
|
17
|
-
registration = syncDriverRegistration(registration, step.command, driver, options);
|
|
18
|
-
const result = await options.opencode.execute(step.command);
|
|
19
|
-
activeSessionId = await syncSessionContext(activeSessionId, result, options.onSessionAvailable);
|
|
20
|
-
registration = syncDriverRegistration(registration, result, driver, options);
|
|
21
|
-
step = driver.resume(result);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (step.kind === "complete") {
|
|
25
|
-
return {
|
|
26
|
-
sessionId: step.sessionId,
|
|
27
|
-
responseText: step.responseText,
|
|
28
|
-
finalText: step.finalText,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
throw new Error(step.message);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
registration?.dispose();
|
|
36
|
-
driver.free?.();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function syncSessionContext(currentSessionId, value, onSessionAvailable) {
|
|
40
|
-
const sessionId = sessionIdFromCommandOrResult(value);
|
|
41
|
-
if (sessionId === null || sessionId === currentSessionId) {
|
|
42
|
-
return currentSessionId;
|
|
43
|
-
}
|
|
44
|
-
await onSessionAvailable?.(sessionId);
|
|
45
|
-
return sessionId;
|
|
46
|
-
}
|
|
47
|
-
function syncDriverRegistration(registration, value, driver, options) {
|
|
48
|
-
const sessionId = sessionIdFromCommandOrResult(value);
|
|
49
|
-
if (sessionId === null) {
|
|
50
|
-
return registration;
|
|
51
|
-
}
|
|
52
|
-
if (registration !== null) {
|
|
53
|
-
registration.updateSession(sessionId);
|
|
54
|
-
return registration;
|
|
55
|
-
}
|
|
56
|
-
const deliverySession = options.deliverySession;
|
|
57
|
-
return options.events.registerDriver(sessionId, driver, async (snapshot) => {
|
|
58
|
-
if (deliverySession?.mode !== "progressive") {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
await deliverySession.preview(snapshot);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
function sessionIdFromCommandOrResult(value) {
|
|
65
|
-
switch (value.kind) {
|
|
66
|
-
case "lookupSession":
|
|
67
|
-
case "waitUntilIdle":
|
|
68
|
-
case "appendPrompt":
|
|
69
|
-
case "sendPromptAsync":
|
|
70
|
-
case "awaitPromptResponse":
|
|
71
|
-
case "readMessage":
|
|
72
|
-
case "listMessages":
|
|
73
|
-
return value.sessionId;
|
|
74
|
-
case "createSession":
|
|
75
|
-
return "sessionId" in value ? value.sessionId : null;
|
|
76
|
-
case "error":
|
|
77
|
-
return value.sessionId;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const RUNTIME_CACHE_KEY = Symbol.for("opencode-gateway.runtime-cache");
|
|
2
|
-
export function getOrCreateRuntimeSingleton(key, factory) {
|
|
3
|
-
const cache = getRuntimeCache();
|
|
4
|
-
const existing = cache.get(key);
|
|
5
|
-
if (existing !== undefined) {
|
|
6
|
-
return existing;
|
|
7
|
-
}
|
|
8
|
-
const promise = factory().catch((error) => {
|
|
9
|
-
if (cache.get(key) === promise) {
|
|
10
|
-
cache.delete(key);
|
|
11
|
-
}
|
|
12
|
-
throw error;
|
|
13
|
-
});
|
|
14
|
-
cache.set(key, promise);
|
|
15
|
-
return promise;
|
|
16
|
-
}
|
|
17
|
-
export function clearRuntimeSingletonForTests(key) {
|
|
18
|
-
getRuntimeCache().delete(key);
|
|
19
|
-
}
|
|
20
|
-
function getRuntimeCache() {
|
|
21
|
-
const globalScope = globalThis;
|
|
22
|
-
let cache = globalScope[RUNTIME_CACHE_KEY];
|
|
23
|
-
if (cache === undefined) {
|
|
24
|
-
cache = new Map();
|
|
25
|
-
globalScope[RUNTIME_CACHE_KEY] = cache;
|
|
26
|
-
}
|
|
27
|
-
return cache;
|
|
28
|
-
}
|
package/dist/session/context.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export class GatewaySessionContext {
|
|
2
|
-
store;
|
|
3
|
-
constructor(store) {
|
|
4
|
-
this.store = store;
|
|
5
|
-
}
|
|
6
|
-
replaceReplyTargets(sessionId, conversationKey, targets, recordedAtMs) {
|
|
7
|
-
this.store.replaceSessionReplyTargets({
|
|
8
|
-
sessionId,
|
|
9
|
-
conversationKey,
|
|
10
|
-
targets,
|
|
11
|
-
recordedAtMs,
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
listReplyTargets(sessionId) {
|
|
15
|
-
return this.store.listSessionReplyTargets(sessionId);
|
|
16
|
-
}
|
|
17
|
-
getDefaultReplyTarget(sessionId) {
|
|
18
|
-
return this.store.getDefaultSessionReplyTarget(sessionId);
|
|
19
|
-
}
|
|
20
|
-
isGatewaySession(sessionId) {
|
|
21
|
-
return this.store.hasGatewaySession(sessionId);
|
|
22
|
-
}
|
|
23
|
-
}
|