@mercury-fw/channel-http 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @mercury-fw/channel-http
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 812c9c5: - A conversation reopened on the HTTP surface after Mercury freed its history comes back to the model: the summaries of its older part plus its newest messages from the archive, within a budget.
8
+ - What a resumed conversation brings back isn't captured to long-term memory again, only the new messages are.
9
+ - `SESSION_RESUME_MAX_CHARS` sets how much comes back (default 30000, `0` turns it off); `HTTP_SURFACE_RESUME_MAX_CHARS` sets it for the HTTP surface only.
10
+ - `InboundTurn.resume` lets a channel mark a conversation its client reopens.
11
+ - The episodic collection gets a `sessionKey` index.
12
+ - `mfw create` lists `HTTP_SURFACE_RESUME_MAX_CHARS` for the HTTP channel.
13
+ - Core's README explains how Mercury remembers a conversation.
14
+
3
15
  ## 0.7.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -21,6 +21,7 @@ It needs an auth provider next to it: [`@mercury-fw/auth-oidc`](../../auth/auth-
21
21
  | `HTTP_SURFACE_PORT` | The port (default `4100`). |
22
22
  | `HTTP_SURFACE_CORS_ORIGIN` | The origin a browser UI calls from (default `*`). |
23
23
  | `HTTP_SURFACE_PUBLIC_URL` | The address people's browsers reach the surface at (e.g. `https://mercury.example.com`). With it, people log in here to the services plugins act on as them (see [`GET /login/callback`](#get-logincallback)); without it, nobody can. |
24
+ | `HTTP_SURFACE_RESUME_MAX_CHARS` | How many characters of a reopened conversation come back to the model once Mercury has freed its history (default: the core's `SESSION_RESUME_MAX_CHARS`; `0` turns resuming off on this surface). See [how Mercury remembers a conversation](../../libs/core/README.md#how-mercury-remembers-a-conversation). |
24
25
 
25
26
  ## API
26
27
 
@@ -45,7 +46,7 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
45
46
  | field | type | required | description |
46
47
  |---|---|---|---|
47
48
  | `text` | string | yes | The user message. A bare confirmation token here confirms a staged action (see the `pending` event) without invoking the model. |
48
- | `conversationId` | string | no | Opaque, client-owned id that continues one of your conversations: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Omitted ⇒ a fresh one-off session. |
49
+ | `conversationId` | string | no | Opaque, client-owned id that continues one of your conversations: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Sent again, however long after, it continues that conversation, rebuilt from the archive if Mercury had freed it (see `HTTP_SURFACE_RESUME_MAX_CHARS`). Omitted ⇒ a fresh one-off session that can't be continued. |
49
50
 
50
51
  **Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing, the body isn't JSON, or `conversationId` has other characters than the ones above.
51
52
 
@@ -34,6 +34,8 @@ export type HttpProviderDeps = {
34
34
  };
35
35
  /** Linking another account to the caller (`POST /link`). */
36
36
  linking?: ChannelLinking;
37
+ /** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
38
+ resumeMaxChars?: number;
37
39
  };
38
40
  export declare function createHttpProvider(deps: HttpProviderDeps): Provider & {
39
41
  stop(): Promise<void>;
@@ -40,6 +40,8 @@ export type TurnRequestDeps = {
40
40
  corsOrigin?: string;
41
41
  /** Test seam for the ephemeral session key when the client sends no conversationId. */
42
42
  newSessionKey?: () => string;
43
+ /** How much of a reopened conversation comes back once the core let its history go (`HTTP_SURFACE_RESUME_MAX_CHARS`); absent, the core's own. */
44
+ resumeMaxChars?: number;
43
45
  };
44
46
  /**
45
47
  * Serves the OpenAPI document that describes this surface — the single source
package/http-provider.ts CHANGED
@@ -31,6 +31,8 @@ export type HttpProviderDeps = {
31
31
  logins?: { callbackUrl: string; accept: (callbackUrl: string) => void; complete: CompleteLoginFn };
32
32
  /** Linking another account to the caller (`POST /link`). */
33
33
  linking?: ChannelLinking;
34
+ /** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
35
+ resumeMaxChars?: number;
34
36
  };
35
37
 
36
38
  export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop(): Promise<void> } {
@@ -48,6 +50,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
48
50
  corsOrigin: deps.corsOrigin,
49
51
  completeLogin: deps.logins?.complete,
50
52
  ...(deps.linking === undefined ? {} : { linking: deps.linking }),
53
+ ...(deps.resumeMaxChars === undefined ? {} : { resumeMaxChars: deps.resumeMaxChars }),
51
54
  });
52
55
  deps.logins?.accept(deps.logins.callbackUrl);
53
56
  },
package/http-server.ts CHANGED
@@ -69,6 +69,8 @@ export type TurnRequestDeps = {
69
69
  corsOrigin?: string;
70
70
  /** Test seam for the ephemeral session key when the client sends no conversationId. */
71
71
  newSessionKey?: () => string;
72
+ /** How much of a reopened conversation comes back once the core let its history go (`HTTP_SURFACE_RESUME_MAX_CHARS`); absent, the core's own. */
73
+ resumeMaxChars?: number;
72
74
  };
73
75
 
74
76
  const SSE_HEADERS = {
@@ -161,10 +163,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
161
163
  return Response.json({ ok: false, error: "missing text" }, { status: 400, headers: cors });
162
164
  }
163
165
  const text = body.text;
164
- const conversationId =
165
- typeof body.conversationId === "string" && body.conversationId.trim().length > 0
166
- ? body.conversationId.trim()
167
- : (deps.newSessionKey ?? (() => crypto.randomUUID()))();
166
+ // A conversation the client names is one it reopens; an ephemeral one never comes back.
167
+ const named = typeof body.conversationId === "string" && body.conversationId.trim().length > 0;
168
+ const conversationId = named ? (body.conversationId as string).trim() : (deps.newSessionKey ?? (() => crypto.randomUUID()))();
168
169
  if (!CONVERSATION_ID.test(conversationId)) {
169
170
  return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
170
171
  }
@@ -234,6 +235,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
234
235
  // The caller's id comes from the auth provider, unchecked here.
235
236
  logPrefix: `[http:${forLog(sessionKey)}] `,
236
237
  abortSignal: abort.signal,
238
+ ...(named ? { resume: deps.resumeMaxChars === undefined ? {} : { maxChars: deps.resumeMaxChars } } : {}),
237
239
  },
238
240
  sink,
239
241
  );
package/index.ts CHANGED
@@ -35,6 +35,7 @@ export const httpChannel: ChannelPlugin = {
35
35
  // the address their browser reaches the surface at.
36
36
  const publicUrl = ctx.env.HTTP_SURFACE_PUBLIC_URL?.trim().replace(/\/+$/, "");
37
37
  const logins = publicUrl ? ctx.logins : undefined;
38
+ const resumeMaxChars = parseResumeMaxChars(ctx.env.HTTP_SURFACE_RESUME_MAX_CHARS);
38
39
  return createHttpProvider({
39
40
  port: Number(ctx.env.HTTP_SURFACE_PORT ?? "4100"),
40
41
  corsOrigin: ctx.env.HTTP_SURFACE_CORS_ORIGIN ?? "*",
@@ -44,7 +45,16 @@ export const httpChannel: ChannelPlugin = {
44
45
  authenticate: ctx.authenticate,
45
46
  admit: ctx.admit,
46
47
  ...(ctx.linking === undefined ? {} : { linking: ctx.linking }),
48
+ ...(resumeMaxChars === undefined ? {} : { resumeMaxChars }),
47
49
  ...(logins ? { logins: { callbackUrl: `${publicUrl}/login/callback`, accept: logins.accept, complete: logins.complete } } : {}),
48
50
  });
49
51
  },
50
52
  };
53
+
54
+ /** `HTTP_SURFACE_RESUME_MAX_CHARS`: unset or empty leaves the core's budget, otherwise a whole number from 0 up (0 turns resuming off on this surface); throws on anything else. */
55
+ function parseResumeMaxChars(text: string | undefined): number | undefined {
56
+ const trimmed = text?.trim() ?? "";
57
+ if (trimmed === "") return undefined;
58
+ if (!/^\d+$/.test(trimmed)) throw new Error(`HTTP_SURFACE_RESUME_MAX_CHARS must be a whole number from 0 up, got "${trimmed}"`);
59
+ return Number(trimmed);
60
+ }
package/openapi.yaml CHANGED
@@ -49,6 +49,13 @@ info:
49
49
  by the verbatim archive; they degrade to empty when the vector store is
50
50
  unreachable).
51
51
 
52
+ A conversation never ends: sent again with the same `conversationId`, a
53
+ turn continues it however long after. Mercury frees a conversation's
54
+ in-memory history once it has been idle a while; the next turn rebuilds
55
+ it from the archive (a summary of its older part and its newest messages,
56
+ within `HTTP_SURFACE_RESUME_MAX_CHARS`), so the model still knows what the
57
+ person sees on screen.
58
+
52
59
 
53
60
  ## Streaming and cancellation
54
61
 
@@ -520,7 +527,7 @@ components:
520
527
  conversationId:
521
528
  type: string
522
529
  pattern: "^[A-Za-z0-9_-]{1,128}$"
523
- description: Opaque, client-owned; letters, digits, `-` and `_` (a UUID fits). Omit to start a fresh ephemeral conversation.
530
+ description: Opaque, client-owned; letters, digits, `-` and `_` (a UUID fits). Omit to start a fresh ephemeral conversation, which can't be continued.
524
531
  TurnEvent:
525
532
  type: object
526
533
  description: The shape carried by each SSE `data:` line, per event type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-http",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "@mercury-fw/channel-types": ">=0.36.0 <1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@mercury-fw/channel-types": "0.44.0",
25
- "@mercury-fw/plugin-types": "0.44.0",
24
+ "@mercury-fw/channel-types": "0.45.0",
25
+ "@mercury-fw/plugin-types": "0.45.0",
26
26
  "@mercury-fw/typescript-config": "*",
27
27
  "@types/bun": "^1.4.2",
28
28
  "typescript": "^6.0.3"