@junejs/server 1.0.0-dev.36 → 1.0.0-dev.45

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.
Files changed (45) hide show
  1. package/dist/agent-durable.d.ts +19 -2
  2. package/dist/agent-durable.js +118 -7
  3. package/dist/agent-native.d.ts +13 -2
  4. package/dist/agent-native.js +108 -6
  5. package/dist/app.d.ts +1 -0
  6. package/dist/app.js +10 -1
  7. package/dist/build.d.ts +3 -3
  8. package/dist/build.js +8 -2
  9. package/dist/{config-freeze-ZWC_Qd4h.d.ts → config-freeze-CyXvNqi2.d.ts} +1 -1
  10. package/dist/config-freeze.d.ts +1 -1
  11. package/dist/core-version.js +1 -1
  12. package/dist/dev.d.ts +6 -2
  13. package/dist/dev.js +40 -9
  14. package/dist/host.d.ts +1 -0
  15. package/dist/host.js +2 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{manifest-DSNX4I-G.d.ts → manifest-CwTT1zVs.d.ts} +1 -1
  19. package/dist/manifest.d.ts +1 -1
  20. package/dist/manifest.js +3 -1
  21. package/dist/pipeline.js +15 -6
  22. package/dist/route-rank.d.ts +10 -0
  23. package/dist/route-rank.js +38 -0
  24. package/dist/route-scan-BMbBzPIB.d.ts +20 -0
  25. package/dist/route-scan.d.ts +2 -2
  26. package/dist/route-scan.js +47 -2
  27. package/dist/router.js +58 -44
  28. package/dist/{worker-Dv4l6Ybs.d.ts → worker-2qbQjj2c.d.ts} +1 -0
  29. package/dist/worker.d.ts +1 -1
  30. package/dist/worker.js +52 -76
  31. package/package.json +3 -3
  32. package/src/agent-durable.ts +164 -11
  33. package/src/agent-native.ts +161 -7
  34. package/src/app.ts +22 -1
  35. package/src/build.ts +11 -1
  36. package/src/core-version.ts +1 -1
  37. package/src/dev.ts +41 -8
  38. package/src/host.ts +4 -3
  39. package/src/manifest.ts +2 -0
  40. package/src/pipeline.ts +20 -4
  41. package/src/route-rank.ts +60 -0
  42. package/src/route-scan.ts +78 -1
  43. package/src/router.ts +75 -58
  44. package/src/worker.ts +66 -82
  45. package/dist/route-scan-DMlwoESy.d.ts +0 -13
@@ -1,5 +1,5 @@
1
1
  import { Channel, ChannelFactory } from "@junejs/core/agent-config";
2
- import { AuthorizeAnswer, ChannelPolicy, InboundEvent, Model, Msg, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
2
+ import { AuthorizeAnswer, ChannelPolicy, InboundEvent, InputAnnouncement, Model, Msg, NoteKind, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
3
3
  import { Connection } from "@junejs/core/connections";
4
4
  import { Resources } from "@junejs/core/resources";
5
5
  //#region src/agent-durable.d.ts
@@ -14,6 +14,7 @@ export interface SqlStorage {
14
14
  export interface DurableStorage {
15
15
  sql: SqlStorage;
16
16
  transactionSync<T>(fn: () => T): T;
17
+ setAlarm?(scheduledTime: number): unknown;
17
18
  }
18
19
  export interface DurableObjectState {
19
20
  storage: DurableStorage;
@@ -40,7 +41,7 @@ export declare class DoSessionStore implements SessionStore {
40
41
  getSessionKey(): string | undefined;
41
42
  setSessionKey(key: string): void;
42
43
  tx<T>(fn: () => T): T;
43
- reset(): number;
44
+ reset(inTx?: () => void): number;
44
45
  unwrap<H = unknown>(): H;
45
46
  }
46
47
  export type DoAgentDef = {
@@ -61,6 +62,7 @@ export type DoAgentDef = {
61
62
  step?: string;
62
63
  }) => void | Promise<void>;
63
64
  authorizeAnswer?: AuthorizeAnswer;
65
+ onInputAnnouncement?: (a: InputAnnouncement) => void | Promise<void>;
64
66
  };
65
67
  export declare class AgentDurableObject {
66
68
  private session;
@@ -79,11 +81,18 @@ export declare class AgentDurableObject {
79
81
  private readonly doEnv;
80
82
  private readonly services;
81
83
  private readonly authorizeAnswer?;
84
+ private readonly onInputAnnouncement?;
85
+ private readonly storage;
86
+ private announceFailures;
82
87
  private resolveResources;
83
88
  private resolveConnections;
84
89
  private ready;
85
90
  constructor(state: DurableObjectState, def: DoAgentDef);
86
91
  private resolveSession;
92
+ alarm(): Promise<void>;
93
+ private armWatchdog;
94
+ private scheduleAlarm;
95
+ private deliverHeld;
87
96
  turn(input: {
88
97
  turnId?: string;
89
98
  userText: string;
@@ -100,6 +109,14 @@ export declare class AgentDurableObject {
100
109
  }): Promise<{
101
110
  turnId: string;
102
111
  }>;
112
+ note(input: {
113
+ by: string;
114
+ kind: NoteKind;
115
+ text: string;
116
+ session?: string;
117
+ }): Promise<{
118
+ noteId: string;
119
+ }>;
103
120
  reset(opts?: {
104
121
  session?: string;
105
122
  }): Promise<{
@@ -54,7 +54,7 @@ var DoSessionStore = class {
54
54
  tx(fn) {
55
55
  return this.storage.transactionSync(fn);
56
56
  }
57
- reset() {
57
+ reset(inTx) {
58
58
  return this.storage.transactionSync(() => {
59
59
  const sql = this.sql;
60
60
  sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
@@ -67,6 +67,7 @@ var DoSessionStore = class {
67
67
  sql.exec("DELETE FROM agent_steps");
68
68
  this.setStatus("new");
69
69
  sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
70
+ inTx?.();
70
71
  return generation;
71
72
  });
72
73
  }
@@ -108,6 +109,9 @@ var AgentDurableObject = class {
108
109
  doEnv;
109
110
  services;
110
111
  authorizeAnswer;
112
+ onInputAnnouncement;
113
+ storage;
114
+ announceFailures = 0;
111
115
  resolveResources() {
112
116
  return this.resourcesOpened ??= Promise.resolve(typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : this.resourcesInput ?? {});
113
117
  }
@@ -138,8 +142,13 @@ var AgentDurableObject = class {
138
142
  this.doEnv = def.env;
139
143
  this.services = def.services;
140
144
  this.authorizeAnswer = def.authorizeAnswer;
145
+ this.onInputAnnouncement = def.onInputAnnouncement;
146
+ this.storage = state.storage;
141
147
  const name = def.name ?? "agent";
142
148
  const sink = new InProcEventSink();
149
+ sink.subscribe((e) => {
150
+ if (e.type === "turn.started") this.armWatchdog();
151
+ });
143
152
  sink.subscribe((e) => {
144
153
  if (e.type !== "turn.failed") return;
145
154
  const defaultLog = () => console.error(`[june] agent "${name}" turn ${e.turnId} failed${e.step ? ` at ${e.step}` : ""}: ${e.error.stack ?? e.error.message}${e.error.causeChain?.length ? `\ncaused by: ${e.error.causeChain.join(" ← ")}` : ""}`);
@@ -193,8 +202,71 @@ var AgentDurableObject = class {
193
202
  if (key !== void 0 && key !== "self" && stored === void 0) this.store.setSessionKey(key);
194
203
  this.sessionKey = resolved;
195
204
  this.session = new AgentSession(this.name, resolved, this.store, this.sink, this.model, this.tools, crossDoUnsupported, this.channelInstructions);
205
+ this.session.onDequeue = (heldTurnId, held) => {
206
+ if (held.hostContext?.deliver && held.event) this.deliverHeld(this.session, heldTurnId, held.event);
207
+ };
208
+ const announce = this.onInputAnnouncement;
209
+ if (announce) {
210
+ this.session.onAnnounce = async (a) => {
211
+ await ensureScope();
212
+ const resources = await this.ready();
213
+ try {
214
+ await runInScope({
215
+ resources,
216
+ services: this.services
217
+ }, () => announce(a));
218
+ } catch (err) {
219
+ this.scheduleAlarm(Math.min(5e3 * 2 ** this.announceFailures, 3e5));
220
+ this.announceFailures++;
221
+ throw err;
222
+ }
223
+ this.announceFailures = 0;
224
+ };
225
+ this.session.flushAnnouncements();
226
+ }
196
227
  return this.session;
197
228
  }
229
+ async alarm() {
230
+ if (!this.onInputAnnouncement) return;
231
+ await this.resolveSession().flushAnnouncements();
232
+ }
233
+ armWatchdog() {
234
+ if (this.onInputAnnouncement) this.scheduleAlarm(6e4);
235
+ }
236
+ scheduleAlarm(inMs) {
237
+ const failed = (err) => console.error(`[june] agent "${this.name}": could not schedule the announcement alarm — delivery waits for the next announcement, turn or rebuild:`, err);
238
+ try {
239
+ Promise.resolve(this.storage.setAlarm?.(Date.now() + inMs)).catch(failed);
240
+ } catch (err) {
241
+ failed(err);
242
+ }
243
+ }
244
+ deliverHeld(session, turnId, event) {
245
+ const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for held turn ${turnId} failed:`, err);
246
+ try {
247
+ const channel = this.channels.find((c) => c.name === event.source);
248
+ if (!channel?.deliver) {
249
+ logRenderFailure(/* @__PURE__ */ new Error(`no deliver()-capable channel named "${event.source}" is wired into this DO — the held turn runs, but its reply is not rendered`));
250
+ return;
251
+ }
252
+ const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
253
+ const events = { [Symbol.asyncIterator]: () => it };
254
+ const target = {
255
+ channelId: event.channelId,
256
+ threadId: event.threadId,
257
+ recipientUserId: event.user?.id,
258
+ recipientTeamId: event.teamId
259
+ };
260
+ this.ready().then((resources) => runInScope({
261
+ resources,
262
+ services: this.services
263
+ }, () => channel.deliver(target, events, { session: this.sessionKey }))).catch(logRenderFailure).finally(() => {
264
+ it.return?.();
265
+ });
266
+ } catch (err) {
267
+ logRenderFailure(err);
268
+ }
269
+ }
198
270
  async turn(input) {
199
271
  await ensureScope();
200
272
  const resources = await this.ready();
@@ -213,8 +285,14 @@ var AgentDurableObject = class {
213
285
  services: this.services
214
286
  }, () => session.start(input));
215
287
  }
288
+ note(input) {
289
+ const { session, ...note } = input;
290
+ return this.resolveSession(session).note(note);
291
+ }
216
292
  reset(opts) {
217
- return this.resolveSession(opts?.session).reset();
293
+ const session = this.resolveSession(opts?.session);
294
+ this.armWatchdog();
295
+ return session.reset();
218
296
  }
219
297
  transcript() {
220
298
  if (this.session) return this.session.transcript();
@@ -225,10 +303,13 @@ var AgentDurableObject = class {
225
303
  const url = new URL(req.url);
226
304
  const key = req.headers.get("x-june-session") ?? void 0;
227
305
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
228
- const { userText, turnId, event, trigger } = await req.json();
306
+ const { userText, turnId, event, trigger, ifSuspended } = await req.json();
229
307
  await ensureScope();
230
308
  const replace = url.searchParams.get("replace") === "1";
231
309
  const wantsDeliver = url.searchParams.get("deliver") === "1";
310
+ const detached = url.searchParams.get("detach") === "1";
311
+ if (ifSuspended === "queue" && !wantsDeliver && !detached) return Response.json({ error: `ifSuspended "queue" needs deliver=1 or detach=1: a held turn runs later, when no one is streaming it` }, { status: 400 });
312
+ if (ifSuspended === "queue" && replace) return Response.json({ error: `ifSuspended "queue" and replace=1 cannot be combined — replacing would drop held turns that were accepted` }, { status: 400 });
232
313
  let deliverChannel;
233
314
  if (wantsDeliver) {
234
315
  if (!event?.channelId) return Response.json({ error: "deliver=1 needs an inbound event (event.channelId names the reply target)" }, { status: 400 });
@@ -240,6 +321,7 @@ var AgentDurableObject = class {
240
321
  let started;
241
322
  try {
242
323
  session = this.resolveSession(key);
324
+ this.armWatchdog();
243
325
  started = runInScope({
244
326
  resources,
245
327
  services: this.services
@@ -248,11 +330,17 @@ var AgentDurableObject = class {
248
330
  turnId,
249
331
  event,
250
332
  trigger,
251
- replace
333
+ replace,
334
+ ifSuspended,
335
+ hostContext: wantsDeliver ? { deliver: true } : void 0
252
336
  }));
253
337
  } catch (err) {
254
338
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
255
339
  }
340
+ if (started.queued) return Response.json({
341
+ turnId: started.turnId,
342
+ queued: true
343
+ }, { status: 202 });
256
344
  if (wantsDeliver) {
257
345
  const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
258
346
  try {
@@ -275,7 +363,7 @@ var AgentDurableObject = class {
275
363
  }
276
364
  return Response.json({ turnId: started.turnId }, { status: 202 });
277
365
  }
278
- if (url.searchParams.get("detach") === "1") return Response.json({ turnId: started.turnId }, { status: 202 });
366
+ if (detached) return Response.json({ turnId: started.turnId }, { status: 202 });
279
367
  return new Response(sseTurnStream(session, started.turnId), { headers: SSE_HEADERS });
280
368
  }
281
369
  if (req.method === "POST" && url.pathname.endsWith("/resume")) {
@@ -310,6 +398,7 @@ var AgentDurableObject = class {
310
398
  console.error(`[june] agent "${this.name}": authorizeAnswer failed for input "${inputId}" of turn ${turnId}:`, err);
311
399
  return Response.json({ error: `authorizeAnswer failed: ${err instanceof Error ? err.message : String(err)} — the answer was NOT applied` }, { status: 500 });
312
400
  }
401
+ this.armWatchdog();
313
402
  try {
314
403
  runInScope({
315
404
  resources,
@@ -347,12 +436,33 @@ var AgentDurableObject = class {
347
436
  } catch (err) {
348
437
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
349
438
  }
439
+ this.armWatchdog();
350
440
  try {
351
441
  return Response.json(await session.reset());
352
442
  } catch (err) {
353
443
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
354
444
  }
355
445
  }
446
+ if (req.method === "POST" && url.pathname.endsWith("/note")) {
447
+ let input;
448
+ try {
449
+ input = await req.json();
450
+ } catch {
451
+ return Response.json({ error: "note: the body must be JSON { by, kind, text }" }, { status: 400 });
452
+ }
453
+ let session;
454
+ try {
455
+ session = this.resolveSession(key);
456
+ } catch (err) {
457
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
458
+ }
459
+ try {
460
+ return Response.json(await session.note(input));
461
+ } catch (err) {
462
+ if (err instanceof TypeError) return Response.json({ error: err.message }, { status: 400 });
463
+ throw err;
464
+ }
465
+ }
356
466
  if (url.pathname.endsWith("/transcript")) {
357
467
  if (key !== void 0) try {
358
468
  return Response.json({ transcript: this.resolveSession(key).transcript() });
@@ -361,7 +471,7 @@ var AgentDurableObject = class {
361
471
  }
362
472
  return Response.json({ transcript: this.transcript() });
363
473
  }
364
- return new Response("agent DO — POST /turn, POST /resume, POST /reset, or GET /transcript", { status: 404 });
474
+ return new Response("agent DO — POST /turn, POST /resume, POST /reset, POST /note, or GET /transcript", { status: 404 });
365
475
  }
366
476
  };
367
477
  const SSE_HEADERS = {
@@ -608,7 +718,8 @@ function serializeTurn(userText, o) {
608
718
  userText,
609
719
  turnId: o?.turnId,
610
720
  event: o?.event,
611
- trigger: o?.trigger
721
+ trigger: o?.trigger,
722
+ ifSuspended: o?.ifSuspended
612
723
  };
613
724
  try {
614
725
  return JSON.stringify(payload);
@@ -1,12 +1,13 @@
1
1
  import { SyncSqlite } from "./sqlite-driver.js";
2
2
  import { AgentDefinition, ChannelContext } from "@junejs/core/agent-config";
3
- import { AgentSession, ChannelPolicy, Model, Runtime, Tool } from "@junejs/core/agent-runtime";
3
+ import { AgentSession, ChannelPolicy, InputAnnouncement, Model, Runtime, Tool } from "@junejs/core/agent-runtime";
4
4
  //#region src/agent-native.d.ts
5
5
  export type AgentDef = {
6
6
  model: Model;
7
7
  tools: Tool[];
8
8
  instructions?: string;
9
9
  channelInstructions?: Record<string, string | ChannelPolicy>;
10
+ onInputAnnouncement?: (a: InputAnnouncement) => void | Promise<void>;
10
11
  };
11
12
  export declare function toAgentDef(agent: AgentDefinition, model: Model): AgentDef;
12
13
  export type NativeRuntimeOptions = {
@@ -16,11 +17,15 @@ export declare class NativeRuntime implements Runtime {
16
17
  private actors;
17
18
  private readonly agents;
18
19
  private readonly db;
20
+ private readonly announceRetry;
19
21
  private readonly maxSessions;
22
+ private closing;
20
23
  constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts?: NativeRuntimeOptions);
21
24
  agentDef(name: string): AgentDef | undefined;
22
25
  session(agent: string, id: string): AgentSession;
23
26
  get sessionCount(): number;
27
+ recoverAnnouncements(): void;
28
+ close(): Promise<void>;
24
29
  private evictIdle;
25
30
  }
26
31
  export declare function createNativeRuntime(agents: Record<string, AgentDef>, path?: string, opts?: NativeRuntimeOptions): Promise<NativeRuntime>;
@@ -28,16 +33,22 @@ export declare class MemoryRuntime implements Runtime {
28
33
  private actors;
29
34
  private stores;
30
35
  private readonly agents;
36
+ private readonly announceRetry;
37
+ private closed;
31
38
  constructor(agents: Record<string, AgentDef>);
32
39
  agentDef(name: string): AgentDef | undefined;
33
40
  session(agent: string, id: string): AgentSession;
41
+ close(): Promise<void>;
34
42
  }
35
43
  export type AgentBackend = "native" | "memory" | "durable";
44
+ export type InProcessRuntime = Runtime & {
45
+ close(): Promise<void>;
46
+ };
36
47
  export declare function createAgentRuntime(agents: Record<string, AgentDef>, opts?: {
37
48
  backend?: AgentBackend;
38
49
  path?: string;
39
50
  maxSessions?: number;
40
- }): Promise<Runtime>;
51
+ }): Promise<InProcessRuntime>;
41
52
  export declare function mountAgent(agent: AgentDefinition, runtime: Runtime, opts?: {
42
53
  chatPath?: string;
43
54
  channels?: boolean;
@@ -53,7 +53,7 @@ var SqliteSessionStore = class {
53
53
  throw e;
54
54
  }
55
55
  }
56
- reset() {
56
+ reset(inTx) {
57
57
  return this.tx(() => {
58
58
  this.db.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (session_id TEXT, generation INTEGER, seq INTEGER, body TEXT)`);
59
59
  this.db.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (session_id TEXT, generation INTEGER, id TEXT, output TEXT)`);
@@ -65,6 +65,7 @@ var SqliteSessionStore = class {
65
65
  this.db.query("DELETE FROM agent_steps WHERE session_id = ?").run(this.sid);
66
66
  this.db.query("INSERT INTO agent_sessions (session_id, status) VALUES (?, 'new') ON CONFLICT(session_id) DO UPDATE SET status = 'new'").run(this.sid);
67
67
  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);
68
+ inTx?.();
68
69
  return generation;
69
70
  });
70
71
  }
@@ -89,19 +90,74 @@ var InProcEventSink = class {
89
90
  return this.subs.size;
90
91
  }
91
92
  };
93
+ var AnnouncementRetry = class {
94
+ flush;
95
+ state = /* @__PURE__ */ new Map();
96
+ stopped = false;
97
+ constructor(flush) {
98
+ this.flush = flush;
99
+ }
100
+ delivered(key) {
101
+ const s = this.state.get(key);
102
+ if (s?.timer) clearTimeout(s.timer);
103
+ this.state.delete(key);
104
+ }
105
+ failed(key, agent, id) {
106
+ if (this.stopped) return;
107
+ const s = this.state.get(key) ?? { failures: 0 };
108
+ this.state.set(key, s);
109
+ if (s.timer) return;
110
+ const delay = Math.min(5e3 * 2 ** s.failures, 3e5);
111
+ s.failures++;
112
+ s.timer = setTimeout(() => {
113
+ s.timer = void 0;
114
+ this.flush(agent, id);
115
+ }, delay);
116
+ s.timer.unref?.();
117
+ }
118
+ stop() {
119
+ this.stopped = true;
120
+ for (const s of this.state.values()) if (s.timer) clearTimeout(s.timer);
121
+ this.state.clear();
122
+ }
123
+ };
124
+ const ownedDbs = /* @__PURE__ */ new WeakSet();
125
+ function wireAnnouncements(session, def, retry) {
126
+ const hook = def.onInputAnnouncement;
127
+ if (hook) {
128
+ const key = `${session.agent}:${session.id}`;
129
+ session.onAnnounce = async (a) => {
130
+ try {
131
+ await hook(a);
132
+ } catch (err) {
133
+ retry.failed(key, session.agent, session.id);
134
+ throw err;
135
+ }
136
+ retry.delivered(key);
137
+ };
138
+ session.flushAnnouncements();
139
+ }
140
+ return session;
141
+ }
142
+ const ANNOUNCE_OUTBOX_STEP = "announce-outbox";
92
143
  function toAgentDef(agent, model) {
93
144
  return {
94
145
  model,
95
146
  tools: agent.tools,
96
147
  instructions: buildSystemPrompt(agent),
97
- ...agent.channelInstructions ? { channelInstructions: agent.channelInstructions } : {}
148
+ ...agent.channelInstructions ? { channelInstructions: agent.channelInstructions } : {},
149
+ ...agent.onInputAnnouncement ? { onInputAnnouncement: agent.onInputAnnouncement } : {}
98
150
  };
99
151
  }
100
152
  var NativeRuntime = class {
101
153
  actors = /* @__PURE__ */ new Map();
102
154
  agents;
103
155
  db;
156
+ announceRetry = new AnnouncementRetry((agent, id) => {
157
+ this.session(agent, id).flushAnnouncements();
158
+ });
104
159
  maxSessions;
160
+ closing;
105
161
  constructor(agents, db, opts = {}) {
106
162
  assertCoreRuntimeVersion("NativeRuntime");
107
163
  this.agents = agents;
@@ -117,6 +173,7 @@ var NativeRuntime = class {
117
173
  session(agent, id) {
118
174
  const key = `${agent}:${id}`;
119
175
  const hit = this.actors.get(key);
176
+ if (!hit && this.closing) throw new Error(`NativeRuntime is closed: cannot open session "${key}"`);
120
177
  if (hit) {
121
178
  this.actors.delete(key);
122
179
  this.actors.set(key, hit);
@@ -126,7 +183,7 @@ var NativeRuntime = class {
126
183
  if (!def) throw new Error(`unknown agent: ${agent}`);
127
184
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
128
185
  const sink = new InProcEventSink();
129
- const session = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions);
186
+ const session = wireAnnouncements(new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions), def, this.announceRetry);
130
187
  this.evictIdle();
131
188
  this.actors.set(key, {
132
189
  session,
@@ -137,6 +194,25 @@ var NativeRuntime = class {
137
194
  get sessionCount() {
138
195
  return this.actors.size;
139
196
  }
197
+ recoverAnnouncements() {
198
+ const rows = this.db.query("SELECT output FROM agent_steps WHERE id = ?").all(ANNOUNCE_OUTBOX_STEP);
199
+ for (const { output } of rows) {
200
+ const [first] = JSON.parse(output);
201
+ if (first && this.agents[first.agent]?.onInputAnnouncement) this.session(first.agent, first.session);
202
+ }
203
+ }
204
+ close() {
205
+ this.closing ??= (async () => {
206
+ this.announceRetry.stop();
207
+ while ([...this.actors.values()].some((a) => !a.session.idle())) await new Promise((r) => setTimeout(r, 10));
208
+ this.actors.clear();
209
+ if (ownedDbs.has(this.db)) {
210
+ ownedDbs.delete(this.db);
211
+ this.db.close();
212
+ }
213
+ })();
214
+ return this.closing;
215
+ }
140
216
  evictIdle() {
141
217
  if (this.actors.size < this.maxSessions) return;
142
218
  for (const [key, a] of this.actors) {
@@ -146,7 +222,21 @@ var NativeRuntime = class {
146
222
  }
147
223
  };
148
224
  async function createNativeRuntime(agents, path = ":memory:", opts = {}) {
149
- return new NativeRuntime(agents, await openLocalSqliteSync(path), opts);
225
+ const db = await openLocalSqliteSync(path);
226
+ ownedDbs.add(db);
227
+ let runtime;
228
+ try {
229
+ runtime = new NativeRuntime(agents, db, opts);
230
+ runtime.recoverAnnouncements();
231
+ return runtime;
232
+ } catch (err) {
233
+ if (runtime) await runtime.close();
234
+ else {
235
+ ownedDbs.delete(db);
236
+ db.close();
237
+ }
238
+ throw err;
239
+ }
150
240
  }
151
241
  var MemorySessionStore = class {
152
242
  msgs = [];
@@ -181,7 +271,7 @@ var MemorySessionStore = class {
181
271
  tx(fn) {
182
272
  return fn();
183
273
  }
184
- reset() {
274
+ reset(inTx) {
185
275
  const generation = this.generation++;
186
276
  this.archives.push({
187
277
  generation,
@@ -191,6 +281,7 @@ var MemorySessionStore = class {
191
281
  this.msgs = [];
192
282
  this.steps = /* @__PURE__ */ new Map();
193
283
  this.status = "new";
284
+ inTx?.();
194
285
  return generation;
195
286
  }
196
287
  unwrap() {}
@@ -199,6 +290,10 @@ var MemoryRuntime = class {
199
290
  actors = /* @__PURE__ */ new Map();
200
291
  stores = /* @__PURE__ */ new Map();
201
292
  agents;
293
+ announceRetry = new AnnouncementRetry((agent, id) => {
294
+ this.session(agent, id).flushAnnouncements();
295
+ });
296
+ closed = false;
202
297
  constructor(agents) {
203
298
  assertCoreRuntimeVersion("MemoryRuntime");
204
299
  this.agents = agents;
@@ -210,16 +305,23 @@ var MemoryRuntime = class {
210
305
  const key = `${agent}:${id}`;
211
306
  let a = this.actors.get(key);
212
307
  if (!a) {
308
+ if (this.closed) throw new Error(`MemoryRuntime is closed: cannot open session "${key}"`);
213
309
  const def = this.agents[agent];
214
310
  if (!def) throw new Error(`unknown agent: ${agent}`);
215
311
  const store = new MemorySessionStore();
216
312
  this.stores.set(key, store);
217
313
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
218
- a = new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions);
314
+ a = wireAnnouncements(new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions), def, this.announceRetry);
219
315
  this.actors.set(key, a);
220
316
  }
221
317
  return a;
222
318
  }
319
+ async close() {
320
+ this.closed = true;
321
+ this.announceRetry.stop();
322
+ this.actors.clear();
323
+ this.stores.clear();
324
+ }
223
325
  };
224
326
  async function createAgentRuntime(agents, opts = {}) {
225
327
  const backend = opts.backend ?? "native";
package/dist/app.d.ts CHANGED
@@ -9,6 +9,7 @@ export type JuneApp = {
9
9
  warmup(): Promise<void>;
10
10
  routePaths(): Promise<string[]>;
11
11
  earlyHints(): string[];
12
+ close(): Promise<void>;
12
13
  };
13
14
  export declare function createApp({ appDir: appDirInput, config }: CreateAppOptions): JuneApp;
14
15
  //#endregion
package/dist/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { discoverAgent } from "./agent-discover.js";
2
2
  import { createAgentRuntime, mountAgent, toAgentDef } from "./agent-native.js";
3
3
  import { findMiddlewareFile, isResourceFile, listRoutes, matchRouteTree, resolveNotFound, routeFiles } from "./router.js";
4
+ import { routeProblems, scanRoutes } from "./route-scan.js";
4
5
  import { createPipeline } from "./pipeline.js";
5
6
  import { resolveBoundary } from "./segment.js";
6
7
  import { memoizeResources } from "./resources.js";
@@ -115,6 +116,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
115
116
  config.dataLayer?.install();
116
117
  let pipelinePromise;
117
118
  const getPipeline = () => pipelinePromise ??= buildPipeline();
119
+ let agentRuntime;
118
120
  async function buildPipeline() {
119
121
  const { maps, css } = await getModuleCss();
120
122
  await registerCssModules(maps);
@@ -138,6 +140,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
138
140
  const backend = agent.runtime.backend === "durable" ? "native" : agent.runtime.backend;
139
141
  const model = anthropic({ model: def.model });
140
142
  const rt = await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, { backend });
143
+ agentRuntime = rt;
141
144
  const mounted = mountAgent(def, rt, {
142
145
  chatPath: agent.runtime.chat.path,
143
146
  channels: agent.runtime.channels
@@ -257,6 +260,8 @@ function createApp({ appDir: appDirInput, config = {} }) {
257
260
  return toPipeline();
258
261
  },
259
262
  async warmup() {
263
+ const problems = routeProblems([await scanRoutes(appDir), hasJuneRoutes ? await scanRoutes(juneRoutesDir) : []], dirname(appDir));
264
+ if (problems) console.error(problems);
260
265
  const files = await routeFiles(appDir, { pageConvention: true });
261
266
  if (hasJuneRoutes) files.push(...await routeFiles(juneRoutesDir, { pageConvention: true }));
262
267
  for (const file of files) await import(pathToFileURL(file).href).catch((err) => {
@@ -264,7 +269,11 @@ function createApp({ appDir: appDirInput, config = {} }) {
264
269
  });
265
270
  },
266
271
  routePaths,
267
- earlyHints: () => config.earlyHints ?? []
272
+ earlyHints: () => config.earlyHints ?? [],
273
+ async close() {
274
+ await pipelinePromise?.catch(() => {});
275
+ await agentRuntime?.close();
276
+ }
268
277
  };
269
278
  }
270
279
  //#endregion
package/dist/build.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { n as scanAppRoutes, r as scanRoutes, t as RouteEntry } from "./route-scan-DMlwoESy.js";
1
+ import { a as scanAppRoutes, o as scanRoutes, t as RouteEntry } from "./route-scan-BMbBzPIB.js";
2
2
  import { t as generateContent } from "./content-freeze-CyiW2fRK.js";
3
- import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-ZWC_Qd4h.js";
4
- import { n as buildManifest } from "./manifest-DSNX4I-G.js";
3
+ import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-CyXvNqi2.js";
4
+ import { n as buildManifest } from "./manifest-CwTT1zVs.js";
5
5
  //#region src/build.d.ts
6
6
  export type BuildResult = {
7
7
  outFile: string;
package/dist/build.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsxTransform } from "./tsconfig-jsx.js";
2
2
  import { generateAgentModule } from "./agent-compile.js";
3
3
  import { findMiddlewareFile } from "./router.js";
4
+ import { scanAppRoutes, scanRoutes } from "./route-scan.js";
4
5
  import { resolveBoundary } from "./segment.js";
5
6
  import { buildModuleCss, registerCssModules, rolldownCssModulesPlugin } from "./css-modules.js";
6
7
  import { bundleClientToFile, findClientEntry } from "./client-bundle.js";
@@ -12,7 +13,6 @@ import { loadJuneConfig } from "./config-loader.js";
12
13
  import { freezeConfig, normalizeBase, resolveDeployAdapter, workerName } from "./config-freeze.js";
13
14
  import { generateContent } from "./content-freeze.js";
14
15
  import { createWorker } from "./worker.js";
15
- import { scanAppRoutes, scanRoutes } from "./route-scan.js";
16
16
  import { buildManifest, importLayout } from "./manifest.js";
17
17
  import { buildRsc, findRscRoutes } from "./rsc-build.js";
18
18
  import { existsSync, lstatSync } from "node:fs";
@@ -154,6 +154,8 @@ async function juneBuild(appRoot, options = {}) {
154
154
  if (r.loading) loadings.push(` ${JSON.stringify(r.path)}: ${loadingId(r.loading)},`);
155
155
  });
156
156
  const resourceRoutesField = resources.length ? `\n resourceRoutes: [\n${resources.join("\n")}\n ],` : "";
157
+ const generatedPaths = routes.filter((r) => r.generated).map((r) => r.path);
158
+ const generatedRoutesField = generatedPaths.length ? `\n generatedRoutes: ${JSON.stringify(generatedPaths)},` : "";
157
159
  const layoutBoundariesField = boundaries.length ? `\n layoutBoundaries: {\n${boundaries.join("\n")}\n },` : "";
158
160
  const builtExtraFile = findMiddlewareFile(appDir);
159
161
  if (builtExtraFile) imports.push(`import extra from ${JSON.stringify(importPath(genDir, builtExtraFile))};`);
@@ -208,6 +210,10 @@ export class JuneAgentDO extends DurableObject {
208
210
  fetch(req: Request): Promise<Response> {
209
211
  return this.#agent.fetch(req);
210
212
  }
213
+ // Delivers input announcements left undelivered — a failed hook's retry, or the watchdog (#260).
214
+ alarm(): Promise<void> {
215
+ return this.#agent.alarm();
216
+ }
211
217
  }
212
218
  ` : "";
213
219
  const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
@@ -220,7 +226,7 @@ ${statics.join("\n")}
220
226
  },
221
227
  dynamicRoutes: [
222
228
  ${dynamics.join("\n")}
223
- ],${resourceRoutesField}
229
+ ],${resourceRoutesField}${generatedRoutesField}
224
230
  layoutChains: {
225
231
  ${chains.join("\n")}
226
232
  },${layoutBoundariesField}
@@ -1,5 +1,5 @@
1
1
  import { i as JuneAdapter } from "./adapter-DcZ-W1-7.js";
2
- import { n as WorkerManifest } from "./worker-Dv4l6Ybs.js";
2
+ import { n as WorkerManifest } from "./worker-2qbQjj2c.js";
3
3
  import { JuneConfig } from "@junejs/core/config";
4
4
  import { DocumentConfig } from "@junejs/core/document";
5
5
  //#region src/config-freeze.d.ts
@@ -1,2 +1,2 @@
1
- import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-ZWC_Qd4h.js";
1
+ import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-CyXvNqi2.js";
2
2
  export { freezeConfig, normalizeBase, resolveDeployAdapter, workerName };