@haikit/server 0.17.0 → 0.18.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,YAAY,CA0L1E"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,YAAY,CA2L1E"}
package/dist/store.js CHANGED
@@ -87,6 +87,7 @@ export function memoryStore(options = {}) {
87
87
  wokeThrough: 0,
88
88
  wakeTurns: 0,
89
89
  superseded: {},
90
+ announcing: null,
90
91
  };
91
92
  conversations.set(conversation.id, conversation);
92
93
  return copy(conversation);
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,UAAU,GAKX,MAAM,cAAc,CAAC;AAEtB,MAAM,QAAQ,GAAG,OAAO,CAAC;AAczB;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CAAC,UAA8B,EAAE;IAC1D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC5C,6EAA6E;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,iEAAiE;IACjE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,UAAU,CAAC,kEAAkE,OAAO,GAAG,CAAC,CAAC;IACrG,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,4EAA4E;IAC5E,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IACnD,mEAAmE;IACnE,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0C,CAAC;IACtE,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,MAAM,IAAI,GAAG,CAAI,KAAQ,EAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAM,CAAC;IACxE,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAEtD;;;;;;;8CAO0C;IAC1C,MAAM,KAAK,GAAG,CAAC,cAAsB,EAAE,UAAyB,EAAE,EAAE;QAClE,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACjD,mEAAmE;QACnE,2EAA2E;QAC3E,yEAAyE;QACzE,uEAAuE;QACvE,4DAA4D;QAC5D,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,kDAAkD,CAAC,CAAC;QACtG,IAAI,MAAM,CAAC,UAAU,KAAK,UAAU;YAAE,MAAM,IAAI,UAAU,CAAC,cAAc,CAAC,CAAC;IAC7E,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,gBAAgB,CAAC,EAAE;YACvB,IAAI,EAAE,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC;gBACtC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,IAAI,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;oBACjE,MAAM,IAAI,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACxC,CAAC;gBACD,iEAAiE;gBACjE,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;gBACzC,MAAM,CAAC,UAAU,GAAG,QAAQ,EAAE,CAAC;gBAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;YACtB,CAAC;YACD,MAAM,YAAY,GAAiB;gBACjC,2EAA2E;gBAC3E,EAAE,EAAE,QAAQ,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE;gBACtE,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,EAAE;gBACZ,OAAO,EAAE,EAAE;gBACX,MAAM,EAAE,EAAE;gBACV,OAAO,EAAE,IAAI;gBACb,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO;gBAChC,UAAU,EAAE,QAAQ,EAAE;gBACtB,cAAc,EAAE,CAAC;gBACjB,KAAK,EAAE,EAAE;gBACT,WAAW,EAAE,CAAC;gBACd,SAAS,EAAE,CAAC;gBACZ,UAAU,EAAE,EAAE;aACf,CAAC;YACF,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;YACjD,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC;QAC5B,CAAC;QAED,KAAK,CAAC,gBAAgB,CAAC,YAAY;YACjC,uEAAuE;YACvE,sEAAsE;YACtE,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;YAChD,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QACzD,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU;YACjC,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;YAC5D,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YACpH,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,mEAAmE;QACnE,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,cAAc;YACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,cAAc,KAAK,cAAc;gBAAE,OAAO,IAAI,CAAC;YACrE,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,cAAc;YACvC,MAAM,GAAG,GAAoB,EAAE,CAAC;YAChC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACpC,IAAI,MAAM,IAAI,MAAM,CAAC,cAAc,KAAK,cAAc;oBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YACjF,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QAED,6EAA6E;QAC7E,KAAK,CAAC,SAAS,CAAC,MAAM;YACpB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;gBAC9C,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,cAAc,iBAAiB,CAAC,CAAC;YAC1E,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YACtD,MAAM,MAAM,GAAiB,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC1G,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAClB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;YACzC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE;gBAAE,IAAI,EAAE,CAAC;YACpE,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,GAAG,QAAQ;YACtD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAC/C,yEAAyE;YACzE,wEAAwE;YACxE,wCAAwC;YACxC,IAAI,EAAE,GAAG,CAAC,CAAC;YACX,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;YACrB,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;gBACf,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,KAAK;oBAAE,EAAE,GAAG,GAAG,CAAC;;oBAC/B,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;YACpB,CAAC;YACD,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QAED;;;WAGG;QACH,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO;YACnC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QACzF,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,cAAc,EAAE,SAAS,EAAE,MAAM;YAC/C,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO;YAC3B,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;YACzD,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACnB,WAAW,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;YACrC,MAAM,CAAC,gBAAgB,CACrB,OAAO,EACP,GAAG,EAAE;gBACH,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACtB,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,GAAG;oBAAE,WAAW,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YAC/F,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,MAAM;YACjC,qEAAqE;YACrE,sEAAsE;YACtE,IAAI,MAAM,GAAG,KAAK,CAAC;YACnB,IAAI,IAAI,GAAwB,IAAI,CAAC;YACrC,MAAM,QAAQ,GAAG,GAAG,EAAE;gBACpB,MAAM,GAAG,IAAI,CAAC;gBACd,IAAI,EAAE,EAAE,CAAC;YACX,CAAC,CAAC;YACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAClB,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;YACjC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC1C,IAAI,CAAC;gBACH,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACvB,IAAI,CAAC,MAAM;wBAAE,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;oBACxD,IAAI,GAAG,IAAI,CAAC;oBACZ,IAAI,MAAM,CAAC,OAAO;wBAAE,OAAO;oBAC3B,MAAM,GAAG,KAAK,CAAC;oBACf,KAAK,CAAC;gBACR,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC7C,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACrB,IAAI,CAAC,GAAG,CAAC,IAAI;oBAAE,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haikit/server",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "The haikit server runtime: agent loop, elicit state machine, derived query_ui, and the interaction security boundary.",
5
5
  "keywords": [
6
6
  "haikit",
@@ -36,7 +36,7 @@
36
36
  "prepack": "npm --prefix ../.. run build"
37
37
  },
38
38
  "dependencies": {
39
- "@haikit/core": "0.17.0",
39
+ "@haikit/core": "0.18.0",
40
40
  "@types/node": ">=22"
41
41
  },
42
42
  "publishConfig": {
package/src/routes.ts CHANGED
@@ -232,6 +232,33 @@ async function streamNotices(hai: Hai, req: IncomingMessage, res: ServerResponse
232
232
  });
233
233
  };
234
234
 
235
+ // An update notice goes out as the revision it announces: the surface, with
236
+ // how long ago it was stored, and its props. Only the last frame carries the
237
+ // notice's id, so a stream cut off between the two resumes with both. A
238
+ // revision whose payload is gone, swept with an old conversation's, is
239
+ // passed over with its id alone, so a browser resumes after it rather than
240
+ // being sent it again on every reconnect.
241
+ const sendRevision = async (handle: string, replaces: string, seq: number) => {
242
+ const record = await store.getPayload(handle, conversationId);
243
+ if (signal.aborted) return;
244
+ if (!record) return write(`id: ${seq}\n\n`);
245
+ const window = typeof record.staleAfterMs === "number" ? record.staleAfterMs : undefined;
246
+ const open: WireEvent = {
247
+ type: "ui_open",
248
+ handle,
249
+ // no tool call made it: the browser keeps the row of the one it replaces
250
+ toolId: "",
251
+ component: record.component,
252
+ version: record.version,
253
+ mode: record.mode,
254
+ ...(window === undefined ? {} : { staleAfterMs: window }),
255
+ ageMs: Math.max(0, Date.now() - record.createdAt),
256
+ replaces,
257
+ };
258
+ await write(`data: ${JSON.stringify(open)}\n\n`);
259
+ await write(`id: ${seq}\ndata: ${JSON.stringify({ type: "ui_props", handle, props: record.props } satisfies WireEvent)}\n\n`);
260
+ };
261
+
235
262
  // Everything after `after`, a page at a time, until a page comes back short.
236
263
  // A conversation deleted since the stream opened ends it: there is nothing
237
264
  // more to wait for, and the browser's reconnect is then refused.
@@ -243,6 +270,12 @@ async function streamNotices(hai: Hai, req: IncomingMessage, res: ServerResponse
243
270
  for (const n of records) {
244
271
  if (signal.aborted) return;
245
272
  if (n.seq <= after) continue;
273
+ if (n.replaces !== undefined && n.handle !== undefined) {
274
+ await sendRevision(n.handle, n.replaces, n.seq);
275
+ after = n.seq;
276
+ woken ||= n.kind === "wake";
277
+ continue;
278
+ }
246
279
  const event: WireEvent = {
247
280
  type: "notice",
248
281
  seq: n.seq,
@@ -338,7 +371,15 @@ function remember(shown: WireEvent[], event: WireEvent) {
338
371
  else {
339
372
  // A revision of a surface this turn showed takes its place: a stream
340
373
  // joining later sees the surface once, as it is now. Its old props go.
341
- delete copy.replaces;
374
+ // It keeps `replaces`, so a browser that did see the old one swaps it in
375
+ // place rather than adding a second, and the old one's tool row, which
376
+ // the browser keeps through a revision too.
377
+ const before = shown[replaced] as Extract<WireEvent, { type: "ui_open" }>;
378
+ copy.toolId = before.toolId;
379
+ // and every handle the surface had before, for a browser that last saw
380
+ // one of those and missed the revisions between
381
+ const earlier = [...(before.alsoReplaces ?? []), ...(before.replaces === undefined ? [] : [before.replaces])];
382
+ if (earlier.length) copy.alsoReplaces = earlier;
342
383
  shown[replaced] = copy;
343
384
  for (let i = shown.length - 1; i >= 0; i--) {
344
385
  const e = shown[i];
package/src/runtime.ts CHANGED
@@ -14,6 +14,8 @@ import {
14
14
  type Tool,
15
15
  type ToolCtx,
16
16
  type ToolReturn,
17
+ type Update,
18
+ type UpdateOptions,
17
19
  type WireEvent,
18
20
  } from "@haikit/core";
19
21
 
@@ -65,6 +67,13 @@ export interface HaiConfig {
65
67
  * browser still shows it at once.
66
68
  */
67
69
  maxWakes?: { count: number; perMs: number };
70
+ /**
71
+ * Set when app code revises surfaces with `hai.update`, which refuses
72
+ * without it. The browser then opens the events stream, which is how a
73
+ * revision reaches it, and the events route starts the turns an update sent
74
+ * with `wake` asks for.
75
+ */
76
+ updates?: boolean;
68
77
  }
69
78
 
70
79
  /**
@@ -111,9 +120,37 @@ interface Hop {
111
120
  * so two renders at once cannot both ask; the handle is known once it shows,
112
121
  * and `settled` resolves when its render has shown it or failed.
113
122
  */
114
- asking: { handle: string | null; settled: Promise<void> } | null;
123
+ asking: {
124
+ handle: string | null;
125
+ settled: Promise<void>;
126
+ /** The digest it was asked under, and its latest, once a later call in the reply has revised it. */
127
+ question?: string;
128
+ digest?: string;
129
+ } | null;
130
+ }
131
+
132
+ /** A `hai.update` write, and every call it answers. */
133
+ interface UpdateJob {
134
+ props: unknown;
135
+ model: string | undefined;
136
+ wake: boolean;
137
+ timeoutMs: number;
138
+ waiters: { resolve: (written: { handle: string }) => void; reject: (err: unknown) => void }[];
139
+ }
140
+
141
+ /** The `hai.update` calls for one surface: the next to write, once the one writing is done. */
142
+ interface UpdateQueue {
143
+ next: UpdateJob | null;
115
144
  }
116
145
 
146
+ /** How long `hai.update` waits for a turn to let the conversation go, by default. */
147
+ const UPDATE_TIMEOUT_MS = 30_000;
148
+ /** How many conversations `hai.update` remembers its surfaces' handles for. */
149
+ const UPDATE_NAMES_KEPT = 10_000;
150
+ /** The first wait before `hai.update` tries a busy conversation again, and the longest. */
151
+ const UPDATE_RETRY_MS = 100;
152
+ const UPDATE_RETRY_MAX_MS = 2_000;
153
+
117
154
  /** An elicit surface a tool call showed, and the digest its answer goes out under. */
118
155
  interface Question {
119
156
  handle: string;
@@ -130,6 +167,15 @@ export class Hai {
130
167
  private readonly queryUi: Tool;
131
168
  /** Handles each conversation has a revision of in flight, so two calls can't revise one at once. */
132
169
  private readonly revising = new WeakMap<Conversation, Set<string>>();
170
+ /** `hai.update` calls by conversation and surface: one writing, and the newest waiting behind it. */
171
+ private readonly updating = new Map<string, UpdateQueue>();
172
+ /**
173
+ * The surfaces `hai.update` has revised, by conversation: each one's first
174
+ * handle and its latest, both mapped to the first, so calls naming either
175
+ * share a queue. Kept for the most recent conversations only; one let go of
176
+ * costs coalescing, never correctness, since every write revises the latest.
177
+ */
178
+ private readonly surfaceNames = new Map<string, Map<string, string>>();
133
179
  /** What the model gets as its system prompt: the app's, then the scope. */
134
180
  readonly system: string;
135
181
  private seq = 0;
@@ -171,14 +217,20 @@ export class Hai {
171
217
  }
172
218
  }
173
219
 
174
- /** Whether this app sends notices, so the browser should open the events stream. */
220
+ /**
221
+ * Whether this app sends notices, `hai.update`'s included, so the browser
222
+ * should open the events stream.
223
+ */
175
224
  get sendsNotices(): boolean {
176
- return this.notices.size > 0;
225
+ return this.notices.size > 0 || this.config.updates === true;
177
226
  }
178
227
 
179
- /** Whether any of its notices are `wake` notices, which the events route starts turns for. */
228
+ /**
229
+ * Whether any of its notices may be `wake` notices, which the events route
230
+ * starts turns for: a registered one, or an update sent with `wake`.
231
+ */
180
232
  get wakes(): boolean {
181
- return [...this.notices.values()].some((n) => n.notice.kind === "wake");
233
+ return this.config.updates === true || [...this.notices.values()].some((n) => n.notice.kind === "wake");
182
234
  }
183
235
 
184
236
  /**
@@ -225,6 +277,233 @@ export class Hai {
225
277
  return { seq: record.seq };
226
278
  };
227
279
 
280
+ /**
281
+ * Revise a surface from outside any turn: a webhook, a job, a price feed.
282
+ * The revision is stored and attached as `ctx.update` does, under the
283
+ * conversation's lease, so a click can never act on props the server doesn't
284
+ * hold. Then an *update notice* tells both sides: the events stream sends
285
+ * the browser the revision, which it swaps into the component on screen,
286
+ * and the model hears which handle replaced which, with the new digest, in
287
+ * the next user message (or at once, with `wake`).
288
+ *
289
+ * `handle` may be any handle the surface has had: the revision replaces the
290
+ * latest. It throws if that one was answered, if the conversation is out of
291
+ * date, and, after waiting `timeoutMs` for a turn to let the conversation
292
+ * go, with `ConversationBusy`. A question waiting on the surface waits on
293
+ * the revision.
294
+ *
295
+ * One write per surface is in flight at a time. A call made meanwhile
296
+ * replaces any still waiting behind it, and every call it replaced resolves
297
+ * to the handle it writes; a wake any of them asked for still wakes.
298
+ */
299
+ readonly update: Update = (conversationId, surface, handle, props, options = {}) => {
300
+ const impl = surface as AnySurfaceImpl;
301
+ const name = impl?.surface?.name;
302
+ if (this.surfaces.get(name) !== impl) {
303
+ return Promise.reject(new Error(`surface "${name}" is not registered: list it in createHai({ surfaces })`));
304
+ }
305
+ // Required even when notices open the events stream: it is also what has
306
+ // the events route start the turn an update sent with `wake` asks for.
307
+ if (this.config.updates !== true) {
308
+ return Promise.reject(new Error("hai.update is off: set createHai({ updates: true })"));
309
+ }
310
+ const { timeoutMs = UPDATE_TIMEOUT_MS, model, wake } = options as UpdateOptions;
311
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0) {
312
+ return Promise.reject(new RangeError(`timeoutMs must be a finite number of milliseconds, zero or more (got ${String(timeoutMs)})`));
313
+ }
314
+ if (model !== undefined && typeof model !== "string") return Promise.reject(new TypeError("model must be a string"));
315
+ let parsed: unknown;
316
+ try {
317
+ // Validated before anything waits: props may come from outside this process.
318
+ parsed = impl.surface.props.parse(props);
319
+ } catch (err) {
320
+ return Promise.reject(err);
321
+ }
322
+
323
+ return new Promise((resolve, reject) => {
324
+ // the surface's first handle, when it is one `hai.update` has revised here
325
+ const first = this.surfaceNames.get(conversationId)?.get(handle) ?? handle;
326
+ const key = `${conversationId}\u0000${first}`;
327
+ const waiter = { resolve, reject };
328
+ const queue = this.updating.get(key);
329
+ if (queue) {
330
+ const behind = queue.next;
331
+ queue.next = {
332
+ props: parsed,
333
+ model,
334
+ wake: wake === true || behind?.wake === true,
335
+ timeoutMs,
336
+ waiters: [...(behind?.waiters ?? []), waiter],
337
+ };
338
+ return;
339
+ }
340
+ const fresh: UpdateQueue = { next: { props: parsed, model, wake: wake === true, timeoutMs, waiters: [waiter] } };
341
+ this.updating.set(key, fresh);
342
+ void (async () => {
343
+ for (let job = fresh.next; job; job = fresh.next) {
344
+ fresh.next = null;
345
+ try {
346
+ const { replaced, ...written } = await this.revise(conversationId, impl, handle, job);
347
+ this.name(conversationId, first, replaced, written.handle);
348
+ for (const w of job.waiters) w.resolve(written);
349
+ } catch (err) {
350
+ for (const w of job.waiters) w.reject(err);
351
+ }
352
+ }
353
+ this.updating.delete(key);
354
+ })();
355
+ });
356
+ };
357
+
358
+ /** Record that `first`'s surface went from `replaced` to `latest`, for the next call's queue. */
359
+ private name(conversationId: string, first: string, replaced: string, latest: string) {
360
+ const names = this.surfaceNames.get(conversationId) ?? new Map<string, string>();
361
+ // only the first and the latest are kept: what a caller is likely to hold
362
+ if (replaced !== first) names.delete(replaced);
363
+ names.set(first, first).set(latest, first);
364
+ // most recently revised last, so the oldest conversation goes first
365
+ this.surfaceNames.delete(conversationId);
366
+ this.surfaceNames.set(conversationId, names);
367
+ if (this.surfaceNames.size > UPDATE_NAMES_KEPT) this.surfaceNames.delete(this.surfaceNames.keys().next().value!);
368
+ }
369
+
370
+ /**
371
+ * Append the update notice this conversation owes, if any, before anything
372
+ * else is done with it: a revision whose own attempt to append it failed.
373
+ * One a holder appended before its save failed is not appended again.
374
+ */
375
+ private async announce(conversation: Conversation): Promise<void> {
376
+ const owed = conversation.announcing;
377
+ if (!owed) return;
378
+ // The model hasn't taken it in either way: that commits with clearing this.
379
+ const unread = (await this.config.store.getNotices(conversation.id, conversation.noticedThrough ?? 0)) ?? [];
380
+ if (!unread.some((n) => n.handle === owed.handle && n.replaces === owed.replaces)) await this.config.store.putNotice(owed);
381
+ conversation.announcing = null;
382
+ }
383
+
384
+ /** One `hai.update` write: take the lease, revise, let the conversation go. */
385
+ private async revise(
386
+ conversationId: string,
387
+ impl: AnySurfaceImpl,
388
+ handle: string,
389
+ job: UpdateJob,
390
+ ): Promise<{ handle: string; replaced: string }> {
391
+ const { store } = this.config;
392
+ // Loading an unknown id would start a new conversation, so make sure it
393
+ // exists first, with the cheapest read there is.
394
+ if ((await store.getNotices(conversationId, Number.MAX_SAFE_INTEGER, 1)) === null) {
395
+ throw new Error(`no conversation ${conversationId}`);
396
+ }
397
+ // A turn in progress lands first: wait for it, backing off, up to the timeout.
398
+ const deadline = Date.now() + job.timeoutMs;
399
+ let conversation: Conversation | null = null;
400
+ for (let wait = UPDATE_RETRY_MS; !conversation; wait = Math.min(wait * 2, UPDATE_RETRY_MAX_MS)) {
401
+ try {
402
+ conversation = await store.loadConversation(conversationId);
403
+ } catch (err) {
404
+ const left = deadline - Date.now();
405
+ if (!isConversationBusy(err) || left <= 0) throw err;
406
+ await new Promise((r) => setTimeout(r, Math.min(wait, left)));
407
+ // not another try once the timeout has passed, even if the turn has gone
408
+ if (Date.now() >= deadline) throw err;
409
+ }
410
+ }
411
+
412
+ let failure: unknown = null;
413
+ let written: { handle: string; replaced: string } | null = null;
414
+ try {
415
+ // one an earlier update still owes goes first, in order
416
+ await this.announce(conversation);
417
+ written = await this.reviseHeld(conversation, impl, handle, job);
418
+ } catch (err) {
419
+ failure = err;
420
+ }
421
+ // As a route does at the end of a request: let the conversation go, with
422
+ // the token kept so the save can prove this is still the rightful holder.
423
+ conversation.leaseUntil = null;
424
+ if (!isStaleLease(failure)) {
425
+ try {
426
+ await store.saveConversation(conversation);
427
+ } catch (err) {
428
+ failure ??= err;
429
+ }
430
+ }
431
+ if (failure) throw failure;
432
+ return written!;
433
+ }
434
+
435
+ private async reviseHeld(
436
+ conversation: Conversation,
437
+ impl: AnySurfaceImpl,
438
+ handle: string,
439
+ job: UpdateJob,
440
+ ): Promise<{ handle: string; replaced: string }> {
441
+ const { store } = this.config;
442
+ if (!conversation.handles.includes(handle)) throw new Error(`no surface ${handle} in conversation ${conversation.id}`);
443
+ // the surface as it is now: updates queued one behind another each revise the last
444
+ const current = currentHandle(conversation, handle);
445
+ if (conversation.frozen.includes(current)) throw new Error(`${current} was answered, so it can't be revised`);
446
+ const record = await store.getPayload(current, conversation.id);
447
+ if (!record) throw new Error(`no surface ${current} in conversation ${conversation.id}`);
448
+ if (record.component !== impl.surface.name) throw new Error(`${current} is a ${record.component}, not a ${impl.surface.name}`);
449
+ // After the reads, right before the write, as for `ctx.update`: a revision
450
+ // must not bring an out-of-date conversation back by superseding the
451
+ // surface that closed it.
452
+ if (await this.refuseIfExpired(conversation, () => {})) {
453
+ throw new Error("this conversation is out of date; nothing more can be revised in it");
454
+ }
455
+
456
+ const next = await store.putPayload(
457
+ {
458
+ conversationId: conversation.id,
459
+ component: impl.surface.name,
460
+ version: impl.surface.version,
461
+ props: job.props,
462
+ mode: record.mode,
463
+ staleAfterMs: windowOf(impl),
464
+ },
465
+ conversation.leaseToken,
466
+ );
467
+ const digest = impl.impl.digest(job.props, { handle: next });
468
+ // …and once more as it takes effect: the write and the digest take a while too
469
+ const window = strictest(record.staleAfterMs, windowOf(impl));
470
+ if (window !== "never" && !(Number.isFinite(record.createdAt) && window !== undefined && record.createdAt + window > Date.now())) {
471
+ throw new Error("this conversation went out of date while the revision was made; it was not applied");
472
+ }
473
+
474
+ conversation.handles.push(next);
475
+ (conversation.superseded ??= {})[current] = next;
476
+ // A question waiting on the surface waits on its revision. Its digest stays
477
+ // as the model was shown it: the update's text rides in the same message as
478
+ // the answer, after it, saying what changed.
479
+ if (conversation.pending?.handle === current) conversation.pending = { ...conversation.pending, handle: next };
480
+ // The notice it owes commits with it, in one fenced save, so the revision
481
+ // and its notice can't come apart. Saved before the notice is appended,
482
+ // which proves the lease is still held: a notice for a revision the history
483
+ // lost would show the browser a surface every click on is refused.
484
+ const notice = {
485
+ conversationId: conversation.id,
486
+ name: impl.surface.name,
487
+ version: impl.surface.version,
488
+ payload: null,
489
+ model: `${job.model ? `${job.model} ` : ""}${current} was replaced by ${next}; its earlier digest is out of date. Now: ${digest}`,
490
+ handle: next,
491
+ replaces: current,
492
+ ...(job.wake ? { kind: "wake" as const } : {}),
493
+ };
494
+ conversation.announcing = notice;
495
+ await store.saveConversation(conversation);
496
+ try {
497
+ await store.putNotice(notice);
498
+ } catch (err) {
499
+ // Still owed, and appended by whoever holds the conversation next, before
500
+ // anything else: the model hears of the revision before it runs again.
501
+ throw new Error(`${next} is saved, but its update notice could not be stored yet: it goes out with the conversation's next request (${(err as Error).message})`);
502
+ }
503
+ conversation.announcing = null;
504
+ return { handle: next, replaced: current };
505
+ }
506
+
228
507
  /**
229
508
  * Start a turn for the `wake` notices this conversation hasn't taken in, if
230
509
  * it can take one now. The events route calls this, and streams the turn
@@ -279,6 +558,7 @@ export class Hai {
279
558
  }
280
559
 
281
560
  private async wakeTurn(conversation: Conversation, emit: Emit): Promise<WakeOutcome> {
561
+ await this.announce(conversation);
282
562
  // a load of an unknown id starts a new conversation, which has not begun either
283
563
  if (!conversation.messages.length || conversation.status === "awaiting") return "idle";
284
564
  if (await this.refuseIfExpired(conversation, () => {})) return "idle";
@@ -373,11 +653,13 @@ export class Hai {
373
653
  conversation: Conversation,
374
654
  ): Promise<{ blocks: TextBlock[]; through: number; wake: number }> {
375
655
  const through = conversation.noticedThrough ?? 0;
376
- if (!this.notices.size) return { blocks: [], through, wake: 0 };
656
+ if (!this.sendsNotices) return { blocks: [], through, wake: 0 };
377
657
  const fresh = (await this.config.store.getNotices(conversation.id, through)) ?? [];
378
658
  return {
379
659
  blocks: fresh.flatMap((n) =>
380
- n.model === null ? [] : [{ type: "text" as const, text: `[App notification: ${n.name}] ${n.model}` }],
660
+ n.model === null
661
+ ? []
662
+ : [{ type: "text" as const, text: n.replaces === undefined ? `[App notification: ${n.name}] ${n.model}` : `[UI update] ${n.model}` }],
381
663
  ),
382
664
  through: fresh.at(-1)?.seq ?? through,
383
665
  // the newest unread wake notice, or 0
@@ -474,6 +756,7 @@ export class Hai {
474
756
  // ─────────────────────────────────────────────────── public API
475
757
 
476
758
  async send(conversation: Conversation, text: string, emit: Emit): Promise<void> {
759
+ await this.announce(conversation);
477
760
  if (await this.refuseIfExpired(conversation, emit)) return;
478
761
 
479
762
  // The first message is the one that starts the conversation.
@@ -523,6 +806,7 @@ export class Hai {
523
806
  */
524
807
  async start(conversation: Conversation, emit: Emit): Promise<void> {
525
808
  if (!this.config.init || conversation.messages.length > 0) return;
809
+ await this.announce(conversation);
526
810
  const unread = await this.unreadNotices(conversation);
527
811
  if (await this.runInit(conversation, withText(unread.blocks, STARTED), emit, unread.through)) return; // parked on its surface
528
812
  conversation.status = "idle";
@@ -619,6 +903,7 @@ export class Hai {
619
903
  input: { handle: string; action: string; value: unknown },
620
904
  emit: Emit,
621
905
  ): Promise<void> {
906
+ await this.announce(conversation);
622
907
  if (await this.refuseIfExpired(conversation, emit)) return;
623
908
 
624
909
  // The payload row carries a conversation id, but that alone is not enough.
@@ -757,8 +1042,9 @@ export class Hai {
757
1042
  */
758
1043
  private async refuseIfExpired(conversation: Conversation, emit: Emit): Promise<boolean> {
759
1044
  if (!conversation.handles.length) return false;
760
- const now = Date.now();
761
1045
  const records = await this.config.store.getPayloads(conversation.handles, conversation.id);
1046
+ // after the read, so a window that ran out while it was in flight counts
1047
+ const now = Date.now();
762
1048
 
763
1049
  // the surface that went out of date first is the one that closed it
764
1050
  let first: { expiredAt: number; age: number; window: number | undefined } | null = null;
@@ -866,8 +1152,18 @@ export class Hai {
866
1152
  // Hold the resolved siblings too — the API is all-or-nothing per batch.
867
1153
  conversation.status = "awaiting";
868
1154
  // At the question's latest revision: a later call in this reply may
869
- // have revised it.
870
- conversation.pending = { ...parked, handle: currentHandle(conversation, parked.handle), results };
1155
+ // have revised it, and then its answer goes out under the revision's
1156
+ // digest, not the original's.
1157
+ // Only the question's part is replaced: anything the asking call said
1158
+ // after it rides along as before.
1159
+ const handle = currentHandle(conversation, parked.handle);
1160
+ const asking = hop.asking;
1161
+ let digest = parked.digest;
1162
+ if (handle !== parked.handle && asking?.handle === handle && asking.digest !== undefined) {
1163
+ const asked = asking.question;
1164
+ digest = asked !== undefined && parked.digest.startsWith(asked) ? asking.digest + parked.digest.slice(asked.length) : asking.digest;
1165
+ }
1166
+ conversation.pending = { ...parked, handle, digest, results };
871
1167
  emit({ type: "status", status: "awaiting" });
872
1168
  await this.emitContext(conversation, emit);
873
1169
  return;
@@ -916,6 +1212,9 @@ export class Hai {
916
1212
  surfaceMode: "display" | "elicit",
917
1213
  // the handle this revises in place, for `update`
918
1214
  replaces?: string,
1215
+ // last word before the surface is attached and sent: throwing leaves the
1216
+ // stored payload unreferenced, inert as an overtaken turn's
1217
+ beforeAttach?: () => void,
919
1218
  ) => {
920
1219
  // Validate before storing: props may originate outside this process.
921
1220
  const parsed = impl.surface.props.parse(props);
@@ -941,6 +1240,7 @@ export class Hai {
941
1240
 
942
1241
  // The split, in two lines. Digest -> model. Payload -> browser.
943
1242
  const digest = impl.impl.digest(parsed, { handle });
1243
+ beforeAttach?.();
944
1244
  // Recorded only once the digest is in hand. A surface that fails here is
945
1245
  // never shown, and a handle in the history counts toward its freshness,
946
1246
  // so it could otherwise close the conversation over something unseen.
@@ -1000,6 +1300,7 @@ export class Hai {
1000
1300
  try {
1001
1301
  const ret = await show(impl, props, "elicit");
1002
1302
  claim.handle = ret.handle;
1303
+ claim.question = ret.model;
1003
1304
  asked = { handle: ret.handle, digest: ret.model };
1004
1305
  return ret;
1005
1306
  } catch (err) {
@@ -1028,11 +1329,33 @@ export class Hai {
1028
1329
  if (record.component !== impl.surface.name) {
1029
1330
  throw new Error(`${handle} is a ${record.component}, not a ${impl.surface.name}`);
1030
1331
  }
1031
- const ret = await show(impl, props, record.mode, handle);
1332
+ // Freshness again, last, right before the write: a long turn can
1333
+ // outlast a window that was open when it began, and so can the reads
1334
+ // above. A revision must not bring an out-of-date conversation back
1335
+ // by superseding the surface that closed it.
1336
+ if (await this.refuseIfExpired(conversation, () => {})) {
1337
+ throw new Error("this conversation is out of date; nothing more can be revised in it");
1338
+ }
1339
+ // …and once more as it takes effect: the write and the digest above can
1340
+ // take a while too, and the browser closes the conversation at the old
1341
+ // surface's deadline whether or not a revision is on its way.
1342
+ const window = strictest(record.staleAfterMs, windowOf(impl));
1343
+ const stillFresh = () => {
1344
+ if (window === "never") return;
1345
+ if (!(Number.isFinite(record.createdAt) && window !== undefined && record.createdAt + window > Date.now())) {
1346
+ throw new Error("this conversation went out of date while the revision was made; it was not applied");
1347
+ }
1348
+ };
1349
+ const ret = await show(impl, props, record.mode, handle, stillFresh);
1032
1350
  (conversation.superseded ??= {})[handle] = ret.handle!;
1033
- // a question waiting on the old surface waits on the new one
1034
- if (asked?.handle === handle) asked = { handle: ret.handle!, digest: ret.model };
1035
- if (hop.asking?.handle === handle) hop.asking.handle = ret.handle!;
1351
+ // a question waiting on the old surface waits on the new one; when this
1352
+ // call asked it, its result starts with the revision's digest now, so
1353
+ // that is the part a later call's revision replaces
1354
+ const mine = asked?.handle === handle;
1355
+ if (mine) asked = { handle: ret.handle!, digest: ret.model };
1356
+ if (hop.asking?.handle === handle) {
1357
+ Object.assign(hop.asking, { handle: ret.handle!, digest: ret.model }, mine ? { question: ret.model } : {});
1358
+ }
1036
1359
  return ret;
1037
1360
  } finally {
1038
1361
  revising.delete(handle);
package/src/store.ts CHANGED
@@ -109,6 +109,7 @@ export function memoryStore(options: MemoryStoreOptions = {}): StoreAdapter {
109
109
  wokeThrough: 0,
110
110
  wakeTurns: 0,
111
111
  superseded: {},
112
+ announcing: null,
112
113
  };
113
114
  conversations.set(conversation.id, conversation);
114
115
  return copy(conversation);