@junejs/server 1.0.0-dev.12 → 1.0.0-dev.14
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/agent-durable.d.ts +7 -0
- package/dist/agent-durable.js +100 -13
- package/dist/agent-native.js +35 -3
- package/package.json +2 -2
- package/src/agent-durable.ts +164 -16
- package/src/agent-native.ts +38 -2
package/dist/agent-durable.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ declare class DoSessionStore implements SessionStore {
|
|
|
39
39
|
getSessionKey(): string | undefined;
|
|
40
40
|
setSessionKey(key: string): void;
|
|
41
41
|
tx<T>(fn: () => T): T;
|
|
42
|
+
reset(): number;
|
|
42
43
|
unwrap<H = unknown>(): H;
|
|
43
44
|
}
|
|
44
45
|
type DoAgentDef = {
|
|
@@ -88,6 +89,12 @@ declare class AgentDurableObject {
|
|
|
88
89
|
}): Promise<{
|
|
89
90
|
turnId: string;
|
|
90
91
|
}>;
|
|
92
|
+
reset(opts?: {
|
|
93
|
+
session?: string;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
previousSession: string;
|
|
96
|
+
generation: number;
|
|
97
|
+
}>;
|
|
91
98
|
transcript(): import("@junejs/core/agent-runtime").Turn[];
|
|
92
99
|
fetch(req: Request): Promise<Response>;
|
|
93
100
|
}
|
package/dist/agent-durable.js
CHANGED
|
@@ -52,6 +52,22 @@ var DoSessionStore = class {
|
|
|
52
52
|
tx(fn) {
|
|
53
53
|
return this.storage.transactionSync(fn);
|
|
54
54
|
}
|
|
55
|
+
reset() {
|
|
56
|
+
return this.storage.transactionSync(() => {
|
|
57
|
+
const sql = this.sql;
|
|
58
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
|
|
59
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (generation INTEGER, id TEXT, output TEXT)`);
|
|
60
|
+
const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'generation'").toArray();
|
|
61
|
+
const generation = rows.length ? Number(rows[0].v) : 0;
|
|
62
|
+
sql.exec("INSERT INTO agent_messages_archive (generation, seq, body) SELECT ?, seq, body FROM agent_messages", generation);
|
|
63
|
+
sql.exec("DELETE FROM agent_messages");
|
|
64
|
+
sql.exec("INSERT INTO agent_steps_archive (generation, id, output) SELECT ?, id, output FROM agent_steps", generation);
|
|
65
|
+
sql.exec("DELETE FROM agent_steps");
|
|
66
|
+
this.setStatus("new");
|
|
67
|
+
sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
|
|
68
|
+
return generation;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
55
71
|
unwrap() {
|
|
56
72
|
return this.sql;
|
|
57
73
|
}
|
|
@@ -160,6 +176,9 @@ var AgentDurableObject = class {
|
|
|
160
176
|
services: this.services
|
|
161
177
|
}, () => session.start(input));
|
|
162
178
|
}
|
|
179
|
+
reset(opts) {
|
|
180
|
+
return this.resolveSession(opts?.session).reset();
|
|
181
|
+
}
|
|
163
182
|
transcript() {
|
|
164
183
|
if (this.session) return this.session.transcript();
|
|
165
184
|
if (this.store.getSessionKey() !== void 0) return this.resolveSession().transcript();
|
|
@@ -171,6 +190,7 @@ var AgentDurableObject = class {
|
|
|
171
190
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
172
191
|
const { userText, turnId, event, trigger } = await req.json();
|
|
173
192
|
await ensureScope();
|
|
193
|
+
const replace = url.searchParams.get("replace") === "1";
|
|
174
194
|
const wantsDeliver = url.searchParams.get("deliver") === "1";
|
|
175
195
|
let deliverChannel;
|
|
176
196
|
if (wantsDeliver) {
|
|
@@ -189,7 +209,8 @@ var AgentDurableObject = class {
|
|
|
189
209
|
userText,
|
|
190
210
|
turnId,
|
|
191
211
|
event,
|
|
192
|
-
trigger
|
|
212
|
+
trigger,
|
|
213
|
+
replace
|
|
193
214
|
}));
|
|
194
215
|
} catch (err) {
|
|
195
216
|
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
@@ -197,7 +218,8 @@ var AgentDurableObject = class {
|
|
|
197
218
|
if (wantsDeliver) {
|
|
198
219
|
const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
|
|
199
220
|
try {
|
|
200
|
-
const
|
|
221
|
+
const it = observeTurnEvents(session, started.turnId)[Symbol.asyncIterator]();
|
|
222
|
+
const events = { [Symbol.asyncIterator]: () => it };
|
|
201
223
|
const target = {
|
|
202
224
|
channelId: event.channelId,
|
|
203
225
|
threadId: event.threadId,
|
|
@@ -207,7 +229,9 @@ var AgentDurableObject = class {
|
|
|
207
229
|
Promise.resolve().then(() => runInScope({
|
|
208
230
|
resources: this.resources,
|
|
209
231
|
services: this.services
|
|
210
|
-
}, () => deliverChannel.deliver(target, events, { session: this.sessionKey }))).catch(logRenderFailure)
|
|
232
|
+
}, () => deliverChannel.deliver(target, events, { session: this.sessionKey }))).catch(logRenderFailure).finally(() => {
|
|
233
|
+
it.return?.();
|
|
234
|
+
});
|
|
211
235
|
} catch (err) {
|
|
212
236
|
logRenderFailure(err);
|
|
213
237
|
}
|
|
@@ -217,8 +241,15 @@ var AgentDurableObject = class {
|
|
|
217
241
|
return new Response(sseTurnStream(session, started.turnId), { headers: SSE_HEADERS });
|
|
218
242
|
}
|
|
219
243
|
if (req.method === "POST" && url.pathname.endsWith("/resume")) {
|
|
220
|
-
const { turnId, inputId, input, by } = await req.json();
|
|
244
|
+
const { turnId, inputId, input, by, source, target } = await req.json();
|
|
221
245
|
await ensureScope();
|
|
246
|
+
const wantsDeliver = url.searchParams.get("deliver") === "1";
|
|
247
|
+
let resumeChannel;
|
|
248
|
+
if (wantsDeliver) {
|
|
249
|
+
if (!source || !target?.channelId || !target.messageTs) return Response.json({ error: "resume deliver=1 needs `source` (the channel name) and `target` (channelId + messageTs of the prompt message)" }, { status: 400 });
|
|
250
|
+
resumeChannel = this.channels.find((c) => c.name === source);
|
|
251
|
+
if (!resumeChannel?.deliverResume) return Response.json({ error: `agent "${this.name}": no deliverResume()-capable channel named "${source}" is wired into this DO (DoAgentDef.channels) — the answer was NOT applied` }, { status: 501 });
|
|
252
|
+
}
|
|
222
253
|
let session;
|
|
223
254
|
try {
|
|
224
255
|
session = this.resolveSession(key);
|
|
@@ -230,8 +261,37 @@ var AgentDurableObject = class {
|
|
|
230
261
|
const message = err instanceof Error ? err.message : String(err);
|
|
231
262
|
return Response.json({ error: message }, { status: err instanceof ResumeAuthorizationError ? 403 : 409 });
|
|
232
263
|
}
|
|
264
|
+
if (wantsDeliver) {
|
|
265
|
+
const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered resume render for turn ${turnId} failed:`, err);
|
|
266
|
+
try {
|
|
267
|
+
const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
|
|
268
|
+
const events = { [Symbol.asyncIterator]: () => it };
|
|
269
|
+
Promise.resolve().then(() => runInScope({
|
|
270
|
+
resources: this.resources,
|
|
271
|
+
services: this.services
|
|
272
|
+
}, () => resumeChannel.deliverResume(target, events, { session: this.sessionKey }))).catch(logRenderFailure).finally(() => {
|
|
273
|
+
it.return?.();
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
276
|
+
logRenderFailure(err);
|
|
277
|
+
}
|
|
278
|
+
return Response.json({ turnId }, { status: 202 });
|
|
279
|
+
}
|
|
233
280
|
return new Response(sseTurnStream(session, turnId), { headers: SSE_HEADERS });
|
|
234
281
|
}
|
|
282
|
+
if (req.method === "POST" && url.pathname.endsWith("/reset")) {
|
|
283
|
+
let session;
|
|
284
|
+
try {
|
|
285
|
+
session = this.resolveSession(key);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
return Response.json(await session.reset());
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
235
295
|
if (url.pathname.endsWith("/transcript")) {
|
|
236
296
|
if (key !== void 0) try {
|
|
237
297
|
return Response.json({ transcript: this.resolveSession(key).transcript() });
|
|
@@ -240,7 +300,7 @@ var AgentDurableObject = class {
|
|
|
240
300
|
}
|
|
241
301
|
return Response.json({ transcript: this.transcript() });
|
|
242
302
|
}
|
|
243
|
-
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
303
|
+
return new Response("agent DO — POST /turn, POST /resume, POST /reset, or GET /transcript", { status: 404 });
|
|
244
304
|
}
|
|
245
305
|
};
|
|
246
306
|
const SSE_HEADERS = {
|
|
@@ -266,7 +326,7 @@ function sseTurnStream(session, turnId) {
|
|
|
266
326
|
}, 2e4);
|
|
267
327
|
unsub = session.observe((e) => {
|
|
268
328
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
269
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") {
|
|
329
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") {
|
|
270
330
|
stop();
|
|
271
331
|
controller.close();
|
|
272
332
|
}
|
|
@@ -283,7 +343,7 @@ function observeTurnEvents(session, turnId) {
|
|
|
283
343
|
let notify;
|
|
284
344
|
const unsub = session.observe((e) => {
|
|
285
345
|
queue.push(e);
|
|
286
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
|
|
346
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
287
347
|
notify?.();
|
|
288
348
|
}, { turnId });
|
|
289
349
|
return { [Symbol.asyncIterator]() {
|
|
@@ -340,6 +400,7 @@ async function sseTurnFinalText(res) {
|
|
|
340
400
|
const e = JSON.parse(line.slice(5).trim());
|
|
341
401
|
if (e.type === "turn.completed") return e.text;
|
|
342
402
|
if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error });
|
|
403
|
+
if (e.type === "turn.cancelled") throw new Error(`turn ${e.turnId} was cancelled (${e.reason})`);
|
|
343
404
|
}
|
|
344
405
|
if (done) break;
|
|
345
406
|
}
|
|
@@ -425,7 +486,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
425
486
|
run: async (message, o) => {
|
|
426
487
|
const namespace = getNamespace();
|
|
427
488
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
428
|
-
return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
489
|
+
return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
|
|
429
490
|
method: "POST",
|
|
430
491
|
body: serializeTurn(message, o)
|
|
431
492
|
})));
|
|
@@ -433,7 +494,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
433
494
|
runDetached: async (message, o) => {
|
|
434
495
|
const namespace = getNamespace();
|
|
435
496
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
436
|
-
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
497
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?detach=1${o?.replace ? "&replace=1" : ""}`, {
|
|
437
498
|
method: "POST",
|
|
438
499
|
body: serializeTurn(message, o)
|
|
439
500
|
}));
|
|
@@ -446,7 +507,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
446
507
|
runDelivered: async (message, o) => {
|
|
447
508
|
const namespace = getNamespace();
|
|
448
509
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
449
|
-
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
510
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?deliver=1${o?.replace ? "&replace=1" : ""}`, {
|
|
450
511
|
method: "POST",
|
|
451
512
|
body: serializeTurn(message, o)
|
|
452
513
|
}));
|
|
@@ -460,7 +521,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
460
521
|
runStream: async function* (message, o) {
|
|
461
522
|
const namespace = getNamespace();
|
|
462
523
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
463
|
-
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
524
|
+
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
|
|
464
525
|
method: "POST",
|
|
465
526
|
body: serializeTurn(message, o)
|
|
466
527
|
})));
|
|
@@ -472,6 +533,30 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
472
533
|
method: "POST",
|
|
473
534
|
body: serializeResume(o)
|
|
474
535
|
})));
|
|
536
|
+
},
|
|
537
|
+
resumeDelivered: async (o) => {
|
|
538
|
+
const namespace = getNamespace();
|
|
539
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
540
|
+
const res = await durableFetch(namespace, opts.agentName, o.session ?? "default", new Request("https://do/resume?deliver=1", {
|
|
541
|
+
method: "POST",
|
|
542
|
+
body: serializeResume(o)
|
|
543
|
+
}));
|
|
544
|
+
if (res.status === 501) throw new DeliverUnsupportedError(`resumeDelivered: ${(await res.json().catch(() => void 0))?.error ?? "the turn host cannot deliver this continuation"}`);
|
|
545
|
+
if (res.status !== 202) {
|
|
546
|
+
const detail = (await res.text()).slice(0, 200);
|
|
547
|
+
throw new Error(`resumeDelivered: resume was not accepted (status ${res.status}): ${detail}`);
|
|
548
|
+
}
|
|
549
|
+
return await res.json();
|
|
550
|
+
},
|
|
551
|
+
resetSession: async (o) => {
|
|
552
|
+
const namespace = getNamespace();
|
|
553
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
554
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/reset", { method: "POST" }));
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
const detail = (await res.text()).slice(0, 200);
|
|
557
|
+
throw new Error(`resetSession: reset was not accepted (status ${res.status}): ${detail}`);
|
|
558
|
+
}
|
|
559
|
+
return await res.json();
|
|
475
560
|
}
|
|
476
561
|
});
|
|
477
562
|
}
|
|
@@ -481,10 +566,12 @@ function serializeResume(o) {
|
|
|
481
566
|
turnId: o.turnId,
|
|
482
567
|
inputId: o.inputId,
|
|
483
568
|
input: o.input,
|
|
484
|
-
by: o.by
|
|
569
|
+
by: o.by,
|
|
570
|
+
source: o.source,
|
|
571
|
+
target: o.target
|
|
485
572
|
});
|
|
486
573
|
} catch (err) {
|
|
487
|
-
throw new Error(`
|
|
574
|
+
throw new Error(`resume: input is not JSON-serializable (${err.message}) — a resume answer must round-trip to the DO`);
|
|
488
575
|
}
|
|
489
576
|
}
|
|
490
577
|
function serializeTurn(userText, o) {
|
package/dist/agent-native.js
CHANGED
|
@@ -51,6 +51,21 @@ var SqliteSessionStore = class {
|
|
|
51
51
|
throw e;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
reset() {
|
|
55
|
+
return this.tx(() => {
|
|
56
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (session_id TEXT, generation INTEGER, seq INTEGER, body TEXT)`);
|
|
57
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (session_id TEXT, generation INTEGER, id TEXT, output TEXT)`);
|
|
58
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_session_generations (session_id TEXT PRIMARY KEY, generation INTEGER)`);
|
|
59
|
+
const generation = this.db.query("SELECT generation FROM agent_session_generations WHERE session_id = ?").get(this.sid)?.generation ?? 0;
|
|
60
|
+
this.db.query("INSERT INTO agent_messages_archive (session_id, generation, seq, body) SELECT session_id, ?, seq, body FROM agent_messages WHERE session_id = ?").run(generation, this.sid);
|
|
61
|
+
this.db.query("DELETE FROM agent_messages WHERE session_id = ?").run(this.sid);
|
|
62
|
+
this.db.query("INSERT INTO agent_steps_archive (session_id, generation, id, output) SELECT session_id, ?, id, output FROM agent_steps WHERE session_id = ?").run(generation, this.sid);
|
|
63
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ?").run(this.sid);
|
|
64
|
+
this.db.query("INSERT INTO agent_sessions (session_id, status) VALUES (?, 'new') ON CONFLICT(session_id) DO UPDATE SET status = 'new'").run(this.sid);
|
|
65
|
+
this.db.query("INSERT INTO agent_session_generations (session_id, generation) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET generation = ?").run(this.sid, generation + 1, generation + 1);
|
|
66
|
+
return generation;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
54
69
|
unwrap() {
|
|
55
70
|
return this.db;
|
|
56
71
|
}
|
|
@@ -99,6 +114,8 @@ var MemorySessionStore = class {
|
|
|
99
114
|
msgs = [];
|
|
100
115
|
steps = /* @__PURE__ */ new Map();
|
|
101
116
|
status = "new";
|
|
117
|
+
generation = 0;
|
|
118
|
+
archives = [];
|
|
102
119
|
appendMessage(m) {
|
|
103
120
|
this.msgs.push(m);
|
|
104
121
|
}
|
|
@@ -126,6 +143,18 @@ var MemorySessionStore = class {
|
|
|
126
143
|
tx(fn) {
|
|
127
144
|
return fn();
|
|
128
145
|
}
|
|
146
|
+
reset() {
|
|
147
|
+
const generation = this.generation++;
|
|
148
|
+
this.archives.push({
|
|
149
|
+
generation,
|
|
150
|
+
msgs: this.msgs,
|
|
151
|
+
steps: this.steps
|
|
152
|
+
});
|
|
153
|
+
this.msgs = [];
|
|
154
|
+
this.steps = /* @__PURE__ */ new Map();
|
|
155
|
+
this.status = "new";
|
|
156
|
+
return generation;
|
|
157
|
+
}
|
|
129
158
|
unwrap() {}
|
|
130
159
|
};
|
|
131
160
|
var MemoryRuntime = class {
|
|
@@ -166,14 +195,17 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
166
195
|
turnId: o?.turnId,
|
|
167
196
|
userText: message,
|
|
168
197
|
event: o?.event,
|
|
169
|
-
trigger: o?.trigger
|
|
198
|
+
trigger: o?.trigger,
|
|
199
|
+
replace: o?.replace
|
|
170
200
|
}),
|
|
171
201
|
runDetached: async (message, o) => runtime.session(agent.name, o?.session ?? "default").start({
|
|
172
202
|
turnId: o?.turnId,
|
|
173
203
|
userText: message,
|
|
174
204
|
event: o?.event,
|
|
175
|
-
trigger: o?.trigger
|
|
176
|
-
|
|
205
|
+
trigger: o?.trigger,
|
|
206
|
+
replace: o?.replace
|
|
207
|
+
}),
|
|
208
|
+
resetSession: (o) => runtime.session(agent.name, o?.session ?? "default").reset()
|
|
177
209
|
};
|
|
178
210
|
const channels = channelFetch(agent, ctx);
|
|
179
211
|
const surface = async (req) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.14",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"rolldown": "^1.1.0"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
135
|
+
"@junejs/core": ">=0.2.0-dev.32",
|
|
136
136
|
"react": "^19.2.0",
|
|
137
137
|
"react-dom": "^19.2.0",
|
|
138
138
|
"react-server-dom-webpack": "19.2.7",
|
package/src/agent-durable.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type Channel,
|
|
41
41
|
type ChannelContext,
|
|
42
42
|
type ChannelFactory,
|
|
43
|
+
type ResumeDeliveryTarget,
|
|
43
44
|
} from "@junejs/core/agent-config";
|
|
44
45
|
import { ensureScope, runInScope } from "@junejs/db";
|
|
45
46
|
import { isolateLocal } from "./isolate-local";
|
|
@@ -136,6 +137,28 @@ export class DoSessionStore implements SessionStore {
|
|
|
136
137
|
tx<T>(fn: () => T): T {
|
|
137
138
|
return this.storage.transactionSync(fn);
|
|
138
139
|
}
|
|
140
|
+
// Session reset (#129): ARCHIVE messages/steps under the current generation (the audit
|
|
141
|
+
// trail — never deleted), clear the live tables, status → "new". The session key in
|
|
142
|
+
// agent_meta is deliberately untouched: the DO's identity survives its history. Archive
|
|
143
|
+
// tables + the generation counter are created lazily, so existing objects stay
|
|
144
|
+
// untouched until their first reset. seq keeps counting across generations (DELETE
|
|
145
|
+
// doesn't reset AUTOINCREMENT), so an archived (generation, seq) stays unique.
|
|
146
|
+
reset(): number {
|
|
147
|
+
return this.storage.transactionSync(() => {
|
|
148
|
+
const sql = this.sql;
|
|
149
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
|
|
150
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (generation INTEGER, id TEXT, output TEXT)`);
|
|
151
|
+
const rows = this.sql.exec<{ v: string }>("SELECT v FROM agent_meta WHERE k = 'generation'").toArray();
|
|
152
|
+
const generation = rows.length ? Number(rows[0]!.v) : 0;
|
|
153
|
+
sql.exec("INSERT INTO agent_messages_archive (generation, seq, body) SELECT ?, seq, body FROM agent_messages", generation);
|
|
154
|
+
sql.exec("DELETE FROM agent_messages");
|
|
155
|
+
sql.exec("INSERT INTO agent_steps_archive (generation, id, output) SELECT ?, id, output FROM agent_steps", generation);
|
|
156
|
+
sql.exec("DELETE FROM agent_steps");
|
|
157
|
+
this.setStatus("new");
|
|
158
|
+
sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
|
|
159
|
+
return generation;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
139
162
|
unwrap<H = unknown>(): H {
|
|
140
163
|
return this.sql as unknown as H;
|
|
141
164
|
}
|
|
@@ -357,6 +380,11 @@ export class AgentDurableObject {
|
|
|
357
380
|
const session = this.resolveSession(input.session);
|
|
358
381
|
return runInScope({ resources: this.resources, services: this.services }, () => session.start(input));
|
|
359
382
|
}
|
|
383
|
+
// Session reset (#129) for custom shells — the direct sibling of the /reset route. No
|
|
384
|
+
// scope needed: reset touches only the store (no tools run).
|
|
385
|
+
reset(opts?: { session?: string }): Promise<{ previousSession: string; generation: number }> {
|
|
386
|
+
return this.resolveSession(opts?.session).reset();
|
|
387
|
+
}
|
|
360
388
|
// Read-only: folds the durable log. When an identity exists (live, or persisted from a
|
|
361
389
|
// prior life) the session resolves and caches like any other path. Only a read on a
|
|
362
390
|
// NEVER-keyed object stays non-committal — caching there would bake "self" in as the
|
|
@@ -377,6 +405,11 @@ export class AgentDurableObject {
|
|
|
377
405
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
378
406
|
const { userText, turnId, event, trigger } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger };
|
|
379
407
|
await ensureScope();
|
|
408
|
+
// CANCEL-AND-REPLACE (#129): supersede every unfinished turn on this session before
|
|
409
|
+
// queuing this one (debounce). A mode flag like deliver/detach, so it rides the query
|
|
410
|
+
// string and composes with both. A suspended session still 409s below — replace
|
|
411
|
+
// never cancels a parked approval.
|
|
412
|
+
const replace = url.searchParams.get("replace") === "1";
|
|
380
413
|
// DELIVERED: like detach=1, the caller gets a 202 and goes away — but the reply is NOT
|
|
381
414
|
// dropped: this DO renders the turn's event stream through the source channel's own
|
|
382
415
|
// deliver() (the inbound renderer), under the DO's lifetime. Capability is checked
|
|
@@ -404,7 +437,7 @@ export class AgentDurableObject {
|
|
|
404
437
|
let started: { turnId: string };
|
|
405
438
|
try {
|
|
406
439
|
session = this.resolveSession(key);
|
|
407
|
-
started = runInScope({ resources: this.resources, services: this.services }, () => session.start({ userText, turnId, event, trigger }));
|
|
440
|
+
started = runInScope({ resources: this.resources, services: this.services }, () => session.start({ userText, turnId, event, trigger, replace }));
|
|
408
441
|
} catch (err) {
|
|
409
442
|
// e.g. the session is suspended awaiting input, or the key mis-matches this
|
|
410
443
|
// object's identity — a client-resolvable conflict, not a crash
|
|
@@ -429,11 +462,17 @@ export class AgentDurableObject {
|
|
|
429
462
|
const logRenderFailure = (err: unknown) =>
|
|
430
463
|
console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
|
|
431
464
|
try {
|
|
432
|
-
|
|
465
|
+
// Own the iterator here: observeTurnEvents subscribes EAGERLY, and a deliver()
|
|
466
|
+
// that throws before ever iterating would otherwise leave that subscription
|
|
467
|
+
// attached, buffering this turn's events forever. return() is idempotent, so
|
|
468
|
+
// closing in finally is safe whether deliver consumed the stream or died early.
|
|
469
|
+
const it = observeTurnEvents(session, started.turnId)[Symbol.asyncIterator]();
|
|
470
|
+
const events: AsyncIterable<TurnEvent> = { [Symbol.asyncIterator]: () => it };
|
|
433
471
|
const target = { channelId: event!.channelId, threadId: event!.threadId, recipientUserId: event!.user?.id, recipientTeamId: event!.teamId };
|
|
434
472
|
Promise.resolve()
|
|
435
473
|
.then(() => runInScope({ resources: this.resources, services: this.services }, () => deliverChannel!.deliver!(target, events, { session: this.sessionKey })))
|
|
436
|
-
.catch(logRenderFailure)
|
|
474
|
+
.catch(logRenderFailure)
|
|
475
|
+
.finally(() => { void it.return?.(); });
|
|
437
476
|
} catch (err) {
|
|
438
477
|
logRenderFailure(err);
|
|
439
478
|
}
|
|
@@ -451,8 +490,31 @@ export class AgentDurableObject {
|
|
|
451
490
|
// VERIFIED identity, so the app must authenticate it upstream (e.g. take the user id from a
|
|
452
491
|
// signature-checked Slack interaction payload), never expose this endpoint raw to clients.
|
|
453
492
|
if (req.method === "POST" && url.pathname.endsWith("/resume")) {
|
|
454
|
-
const { turnId, inputId, input, by } = (await req.json()) as {
|
|
493
|
+
const { turnId, inputId, input, by, source, target } = (await req.json()) as {
|
|
494
|
+
turnId: string; inputId: string; input: unknown; by?: string;
|
|
495
|
+
source?: string; target?: ResumeDeliveryTarget;
|
|
496
|
+
};
|
|
455
497
|
await ensureScope();
|
|
498
|
+
// DELIVERED resume: like /turn?deliver=1, the caller 202s away and THIS DO renders the
|
|
499
|
+
// continuation through the source channel's deliverResume() — a long continuation no
|
|
500
|
+
// longer dies with the webhook isolate. Capability is checked BEFORE resume() applies
|
|
501
|
+
// the answer — the DeliverUnsupportedError contract at the surface: on a pre-effect
|
|
502
|
+
// refusal the caller may fall back to consumer-side rendering without double-answering
|
|
503
|
+
// (a second resume of an already-applied input would 409).
|
|
504
|
+
const wantsDeliver = url.searchParams.get("deliver") === "1";
|
|
505
|
+
let resumeChannel: Channel | undefined;
|
|
506
|
+
if (wantsDeliver) {
|
|
507
|
+
if (!source || !target?.channelId || !target.messageTs) {
|
|
508
|
+
return Response.json({ error: "resume deliver=1 needs `source` (the channel name) and `target` (channelId + messageTs of the prompt message)" }, { status: 400 });
|
|
509
|
+
}
|
|
510
|
+
resumeChannel = this.channels.find((c) => c.name === source);
|
|
511
|
+
if (!resumeChannel?.deliverResume) {
|
|
512
|
+
return Response.json(
|
|
513
|
+
{ error: `agent "${this.name}": no deliverResume()-capable channel named "${source}" is wired into this DO (DoAgentDef.channels) — the answer was NOT applied` },
|
|
514
|
+
{ status: 501 },
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
456
518
|
let session: AgentSession;
|
|
457
519
|
try {
|
|
458
520
|
session = this.resolveSession(key);
|
|
@@ -462,8 +524,49 @@ export class AgentDurableObject {
|
|
|
462
524
|
// 403: the resumer may not answer; 409: not suspended / wrong turn / wrong input id / key mismatch
|
|
463
525
|
return Response.json({ error: message }, { status: err instanceof ResumeAuthorizationError ? 403 : 409 });
|
|
464
526
|
}
|
|
527
|
+
if (wantsDeliver) {
|
|
528
|
+
// Same post-acceptance discipline as delivered turns: subscribe synchronously, render
|
|
529
|
+
// under this DO's lifetime, and let NOTHING escape past the accepted resume — the log
|
|
530
|
+
// is the render failure's only surfacing path (the TURN's own failures surface via
|
|
531
|
+
// the #76 sink subscription).
|
|
532
|
+
const logRenderFailure = (err: unknown) =>
|
|
533
|
+
console.error(`[june] agent "${this.name}": delivered resume render for turn ${turnId} failed:`, err);
|
|
534
|
+
try {
|
|
535
|
+
// Own the iterator (same rationale as the delivered-turn path): the eager
|
|
536
|
+
// subscription must be released even when deliverResume throws before
|
|
537
|
+
// iterating — e.g. its very first message update fails.
|
|
538
|
+
const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
|
|
539
|
+
const events: AsyncIterable<TurnEvent> = { [Symbol.asyncIterator]: () => it };
|
|
540
|
+
Promise.resolve()
|
|
541
|
+
.then(() => runInScope({ resources: this.resources, services: this.services }, () => resumeChannel!.deliverResume!(target!, events, { session: this.sessionKey })))
|
|
542
|
+
.catch(logRenderFailure)
|
|
543
|
+
.finally(() => { void it.return?.(); });
|
|
544
|
+
} catch (err) {
|
|
545
|
+
logRenderFailure(err);
|
|
546
|
+
}
|
|
547
|
+
return Response.json({ turnId }, { status: 202 });
|
|
548
|
+
}
|
|
465
549
|
return new Response(sseTurnStream(session, turnId), { headers: SSE_HEADERS });
|
|
466
550
|
}
|
|
551
|
+
// SESSION RESET (#129): terminally retire this session's history — supersede unfinished
|
|
552
|
+
// turns, archive messages/steps under the current generation (the audit trail), and
|
|
553
|
+
// answer with the audit handle. The response waits for in-flight turns to unwind at
|
|
554
|
+
// their next checkpoint boundary (the reset op runs on the turn chain), which is fine:
|
|
555
|
+
// a DO stays alive while it has pending work.
|
|
556
|
+
if (req.method === "POST" && url.pathname.endsWith("/reset")) {
|
|
557
|
+
let session: AgentSession;
|
|
558
|
+
try {
|
|
559
|
+
session = this.resolveSession(key);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
return Response.json(await session.reset());
|
|
565
|
+
} catch (err) {
|
|
566
|
+
// e.g. a store without reset() — a caller-resolvable refusal, not a crash
|
|
567
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
467
570
|
if (url.pathname.endsWith("/transcript")) {
|
|
468
571
|
// A keyed read still conflict-checks (and may commit the key — it is authentic,
|
|
469
572
|
// idFromName routed it here); a key-less read stays non-committal via transcript().
|
|
@@ -476,7 +579,7 @@ export class AgentDurableObject {
|
|
|
476
579
|
}
|
|
477
580
|
return Response.json({ transcript: this.transcript() });
|
|
478
581
|
}
|
|
479
|
-
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
582
|
+
return new Response("agent DO — POST /turn, POST /resume, POST /reset, or GET /transcript", { status: 404 });
|
|
480
583
|
}
|
|
481
584
|
}
|
|
482
585
|
|
|
@@ -504,9 +607,9 @@ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Ui
|
|
|
504
607
|
}, 20_000);
|
|
505
608
|
unsub = session.observe((e) => {
|
|
506
609
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
507
|
-
// terminal for the STREAM: completed, failed, OR suspended (input.requested
|
|
508
|
-
// parked; the stream ends and a later /resume opens a fresh continuation stream).
|
|
509
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") { stop(); controller.close(); }
|
|
610
|
+
// terminal for the STREAM: completed, failed, cancelled, OR suspended (input.requested
|
|
611
|
+
// → the turn parked; the stream ends and a later /resume opens a fresh continuation stream).
|
|
612
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") { stop(); controller.close(); }
|
|
510
613
|
}, { turnId });
|
|
511
614
|
},
|
|
512
615
|
cancel() { stop(); },
|
|
@@ -526,7 +629,7 @@ function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable
|
|
|
526
629
|
let notify: (() => void) | undefined;
|
|
527
630
|
const unsub = session.observe((e) => {
|
|
528
631
|
queue.push(e);
|
|
529
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
|
|
632
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
530
633
|
notify?.();
|
|
531
634
|
}, { turnId });
|
|
532
635
|
return {
|
|
@@ -575,6 +678,7 @@ export async function sseTurnFinalText(res: Response): Promise<string> {
|
|
|
575
678
|
const e = JSON.parse(line.slice(5).trim()) as TurnEvent;
|
|
576
679
|
if (e.type === "turn.completed") return e.text;
|
|
577
680
|
if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error }); // full TurnError rides along
|
|
681
|
+
if (e.type === "turn.cancelled") throw new Error(`turn ${e.turnId} was cancelled (${e.reason})`);
|
|
578
682
|
}
|
|
579
683
|
if (done) break;
|
|
580
684
|
}
|
|
@@ -775,7 +879,7 @@ export function durableChannelSurface(
|
|
|
775
879
|
namespace,
|
|
776
880
|
opts.agentName,
|
|
777
881
|
o?.session ?? "default",
|
|
778
|
-
new Request(
|
|
882
|
+
new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
779
883
|
);
|
|
780
884
|
// /turn streams SSE; the simple path collapses it to the final text.
|
|
781
885
|
return sseTurnFinalText(res);
|
|
@@ -791,7 +895,7 @@ export function durableChannelSurface(
|
|
|
791
895
|
namespace,
|
|
792
896
|
opts.agentName,
|
|
793
897
|
o?.session ?? "default",
|
|
794
|
-
new Request(
|
|
898
|
+
new Request(`https://do/turn?detach=1${o?.replace ? "&replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
795
899
|
);
|
|
796
900
|
if (res.status !== 202) {
|
|
797
901
|
const detail = (await res.text()).slice(0, 200);
|
|
@@ -812,7 +916,7 @@ export function durableChannelSurface(
|
|
|
812
916
|
namespace,
|
|
813
917
|
opts.agentName,
|
|
814
918
|
o?.session ?? "default",
|
|
815
|
-
new Request(
|
|
919
|
+
new Request(`https://do/turn?deliver=1${o?.replace ? "&replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
816
920
|
);
|
|
817
921
|
if (res.status === 501) {
|
|
818
922
|
throw new DeliverUnsupportedError(`runDelivered: ${((await res.json().catch(() => undefined)) as { error?: string } | undefined)?.error ?? "the turn host cannot deliver this turn"}`);
|
|
@@ -831,7 +935,7 @@ export function durableChannelSurface(
|
|
|
831
935
|
namespace,
|
|
832
936
|
opts.agentName,
|
|
833
937
|
o?.session ?? "default",
|
|
834
|
-
new Request(
|
|
938
|
+
new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
835
939
|
);
|
|
836
940
|
yield* sseTurnEvents(res);
|
|
837
941
|
},
|
|
@@ -847,6 +951,49 @@ export function durableChannelSurface(
|
|
|
847
951
|
);
|
|
848
952
|
yield* sseTurnEvents(res);
|
|
849
953
|
},
|
|
954
|
+
// DELIVERED resume (runDelivered's sibling for the HITL leg): the DO applies the answer
|
|
955
|
+
// AND renders the continuation through the source channel's deliverResume() under its
|
|
956
|
+
// OWN lifetime. A 501 means the DO refused BEFORE applying the answer (channel not
|
|
957
|
+
// wired there / no deliverResume) — surfaced as DeliverUnsupportedError, the one
|
|
958
|
+
// rejection a channel may answer with a consumer-side fallback without double-answering.
|
|
959
|
+
// Engine rejections (403 unauthorized clicker / 409 stale-or-double click) throw as
|
|
960
|
+
// ordinary errors, mirroring what resumeStream's first pull would do.
|
|
961
|
+
resumeDelivered: async (o) => {
|
|
962
|
+
const namespace = getNamespace();
|
|
963
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
964
|
+
const res = await durableFetch(
|
|
965
|
+
namespace,
|
|
966
|
+
opts.agentName,
|
|
967
|
+
o.session ?? "default",
|
|
968
|
+
new Request("https://do/resume?deliver=1", { method: "POST", body: serializeResume(o) }),
|
|
969
|
+
);
|
|
970
|
+
if (res.status === 501) {
|
|
971
|
+
throw new DeliverUnsupportedError(`resumeDelivered: ${((await res.json().catch(() => undefined)) as { error?: string } | undefined)?.error ?? "the turn host cannot deliver this continuation"}`);
|
|
972
|
+
}
|
|
973
|
+
if (res.status !== 202) {
|
|
974
|
+
const detail = (await res.text()).slice(0, 200);
|
|
975
|
+
throw new Error(`resumeDelivered: resume was not accepted (status ${res.status}): ${detail}`);
|
|
976
|
+
}
|
|
977
|
+
return (await res.json()) as { turnId: string };
|
|
978
|
+
},
|
|
979
|
+
// SESSION RESET (#129): retire the session's history on its DO. The response carries
|
|
980
|
+
// the audit handle ({ previousSession, generation }); a 409 (store without reset(),
|
|
981
|
+
// key mismatch) surfaces as an ordinary error — there is no fallback to invent.
|
|
982
|
+
resetSession: async (o) => {
|
|
983
|
+
const namespace = getNamespace();
|
|
984
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
985
|
+
const res = await durableFetch(
|
|
986
|
+
namespace,
|
|
987
|
+
opts.agentName,
|
|
988
|
+
o?.session ?? "default",
|
|
989
|
+
new Request("https://do/reset", { method: "POST" }),
|
|
990
|
+
);
|
|
991
|
+
if (!res.ok) {
|
|
992
|
+
const detail = (await res.text()).slice(0, 200);
|
|
993
|
+
throw new Error(`resetSession: reset was not accepted (status ${res.status}): ${detail}`);
|
|
994
|
+
}
|
|
995
|
+
return (await res.json()) as { previousSession: string; generation: number };
|
|
996
|
+
},
|
|
850
997
|
};
|
|
851
998
|
return channelDispatch(resolved, ctx);
|
|
852
999
|
}
|
|
@@ -861,11 +1008,12 @@ export function durableChannelSurface(
|
|
|
861
1008
|
// (third-party) host could hand us something JSON.stringify chokes on (BigInt, circular).
|
|
862
1009
|
// Unlike serializeTurn's `raw`, `input` is essential: silently dropping it would resume the
|
|
863
1010
|
// turn with the wrong answer. So fail loudly with a clear message rather than corrupt the resume.
|
|
864
|
-
function serializeResume(o: { turnId: string; inputId: string; input: unknown; by?: string }): string {
|
|
1011
|
+
function serializeResume(o: { turnId: string; inputId: string; input: unknown; by?: string; source?: string; target?: ResumeDeliveryTarget }): string {
|
|
865
1012
|
try {
|
|
866
|
-
return JSON.stringify({ turnId: o.turnId, inputId: o.inputId, input: o.input, by: o.by });
|
|
1013
|
+
return JSON.stringify({ turnId: o.turnId, inputId: o.inputId, input: o.input, by: o.by, source: o.source, target: o.target });
|
|
867
1014
|
} catch (err) {
|
|
868
|
-
|
|
1015
|
+
// API-neutral prefix: this serializer backs both resumeStream and resumeDelivered.
|
|
1016
|
+
throw new Error(`resume: input is not JSON-serializable (${(err as Error).message}) — a resume answer must round-trip to the DO`);
|
|
869
1017
|
}
|
|
870
1018
|
}
|
|
871
1019
|
|
package/src/agent-native.ts
CHANGED
|
@@ -79,6 +79,26 @@ class SqliteSessionStore implements SessionStore {
|
|
|
79
79
|
throw e;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
// Session reset (#129): ARCHIVE this session's messages/steps under the current
|
|
83
|
+
// generation (audit trail — never deleted), clear the live rows, status → "new".
|
|
84
|
+
// Archive tables + the generation counter are created lazily here, so existing
|
|
85
|
+
// databases stay untouched until the first reset.
|
|
86
|
+
reset(): number {
|
|
87
|
+
return this.tx(() => {
|
|
88
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (session_id TEXT, generation INTEGER, seq INTEGER, body TEXT)`);
|
|
89
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (session_id TEXT, generation INTEGER, id TEXT, output TEXT)`);
|
|
90
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_session_generations (session_id TEXT PRIMARY KEY, generation INTEGER)`);
|
|
91
|
+
const r = this.db.query("SELECT generation FROM agent_session_generations WHERE session_id = ?").get(this.sid) as { generation: number } | undefined;
|
|
92
|
+
const generation = r?.generation ?? 0;
|
|
93
|
+
this.db.query("INSERT INTO agent_messages_archive (session_id, generation, seq, body) SELECT session_id, ?, seq, body FROM agent_messages WHERE session_id = ?").run(generation, this.sid);
|
|
94
|
+
this.db.query("DELETE FROM agent_messages WHERE session_id = ?").run(this.sid);
|
|
95
|
+
this.db.query("INSERT INTO agent_steps_archive (session_id, generation, id, output) SELECT session_id, ?, id, output FROM agent_steps WHERE session_id = ?").run(generation, this.sid);
|
|
96
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ?").run(this.sid);
|
|
97
|
+
this.db.query("INSERT INTO agent_sessions (session_id, status) VALUES (?, 'new') ON CONFLICT(session_id) DO UPDATE SET status = 'new'").run(this.sid);
|
|
98
|
+
this.db.query("INSERT INTO agent_session_generations (session_id, generation) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET generation = ?").run(this.sid, generation + 1, generation + 1);
|
|
99
|
+
return generation;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
82
102
|
unwrap<H = unknown>(): H { return this.db as unknown as H; }
|
|
83
103
|
}
|
|
84
104
|
|
|
@@ -139,6 +159,10 @@ class MemorySessionStore implements SessionStore {
|
|
|
139
159
|
private msgs: Msg[] = [];
|
|
140
160
|
private steps = new Map<string, unknown>();
|
|
141
161
|
private status = "new";
|
|
162
|
+
private generation = 0;
|
|
163
|
+
// Retired generations (#129) — kept for the audit contract even in memory, so the
|
|
164
|
+
// backend behaves like the durable tiers within one process lifetime.
|
|
165
|
+
private readonly archives: { generation: number; msgs: Msg[]; steps: Map<string, unknown> }[] = [];
|
|
142
166
|
appendMessage(m: Msg) { this.msgs.push(m); }
|
|
143
167
|
messages(): Msg[] { return this.msgs.slice(); }
|
|
144
168
|
hasOpeningMessage(turnId: string): boolean { return this.msgs.some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId); }
|
|
@@ -148,6 +172,14 @@ class MemorySessionStore implements SessionStore {
|
|
|
148
172
|
getStatus(): string { return this.status; }
|
|
149
173
|
setStatus(s: string) { this.status = s; }
|
|
150
174
|
tx<T>(fn: () => T): T { return fn(); } // no rollback: an in-memory store is not a durability tier
|
|
175
|
+
reset(): number {
|
|
176
|
+
const generation = this.generation++;
|
|
177
|
+
this.archives.push({ generation, msgs: this.msgs, steps: this.steps });
|
|
178
|
+
this.msgs = [];
|
|
179
|
+
this.steps = new Map();
|
|
180
|
+
this.status = "new";
|
|
181
|
+
return generation;
|
|
182
|
+
}
|
|
151
183
|
unwrap<H = unknown>(): H { return undefined as unknown as H; }
|
|
152
184
|
}
|
|
153
185
|
|
|
@@ -216,12 +248,16 @@ export function mountAgent(
|
|
|
216
248
|
agent,
|
|
217
249
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
218
250
|
run: (message, o) =>
|
|
219
|
-
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger }),
|
|
251
|
+
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
220
252
|
// FIRE-AND-FORGET (#77): start() without awaiting the result. Native has no waitUntil
|
|
221
253
|
// ceiling (Node keeps floating promises alive), but the seam is the same so a channel
|
|
222
254
|
// written against ctx.runDetached behaves identically on both targets.
|
|
223
255
|
runDetached: async (message, o) =>
|
|
224
|
-
runtime.session(agent.name, o?.session ?? "default").start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger }),
|
|
256
|
+
runtime.session(agent.name, o?.session ?? "default").start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
257
|
+
// SESSION RESET (#129): same seam as durableChannelSurface — the in-process runtimes'
|
|
258
|
+
// stores implement archival, so a channel written against ctx.resetSession behaves
|
|
259
|
+
// identically on both targets.
|
|
260
|
+
resetSession: (o) => runtime.session(agent.name, o?.session ?? "default").reset(),
|
|
225
261
|
};
|
|
226
262
|
const channels = channelFetch(agent, ctx);
|
|
227
263
|
const surface = async (req: Request): Promise<Response | null> => {
|