@mercury-fw/channel-http 0.11.0 → 0.12.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,22 @@
1
1
  # @mercury-fw/channel-http
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1126106: Every turn tells the model when and where the person wrote it.
8
+
9
+ - Each message reaches the model after a line with the weekday, date, time and time zone it was written in (`[Wednesday 2026-10-21 14:32 Europe/Rome]`), and the system prompt says what that line is. The conversation's archive and what a UI shows of it keep the message as written.
10
+ - A resumed conversation's messages come back stamped from the archive's timestamps.
11
+ - The Layer 1 summary keeps when things were said; the episodic summary quotes a date only from those lines, never guessing one. The semantic-fact extractor and the capture card's preview leave the line out.
12
+ - The time zone, always an IANA name (`Europe/Rome`), is the one the person saved, else the one their client is in when the channel passes it on, else the new `i18n.timeZone` in `mercury.config.ts`, else the machine's. A `timeZone` that isn't a zone's name stops the start.
13
+ - The person saves theirs by telling the assistant (the new `set_time_zone` tool), or from a channel's settings.
14
+ - Channel contract: `InboundTurn.timeZoneHint`, and `ctx.settings.setTimeZone` for a channel with a settings area; `reads.me` also returns the saved `timeZone`. Both are optional additions: the contract version stays 6.
15
+ - HTTP channel: the `X-Time-Zone` header on `POST /turn` (allowed by CORS), `timeZone` in `GET /me`, and `PATCH /me` with `{ timeZone }` to save it. Needs the framework at 0.56 or later; an older core ignores the header and offers no `PATCH /me`.
16
+ - Google Chat channel: the client's time zone (`common.timeZone`), when the event carries it, on messages and on the turn a click resumes.
17
+
18
+ To set a default for an app's people, add `timeZone: "Europe/Rome"` (their zone) to `i18n` in `mercury.config.ts`; without it the machine's zone is used, often UTC inside a container.
19
+
3
20
  ## 0.11.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gives a [Mercury](https://github.com/lucabro81/mercury-fw) agent an HTTP surface to build a custom UI on: one conversational turn per request, streamed back as Server-Sent Events, plus confirmation and read-only routes over what the agent holds (conversations, the wiki, memory, its plugins).
4
4
 
5
- This version is written for version 6 of the channel contract, the one that brings the channels' texts in the person's language, and needs the framework at 0.54 or later: a core with an older contract skips it, and the log says why.
5
+ This version is written for version 6 of the channel contract, the one that brings the channels' texts in the person's language, and needs the framework at 0.54 or later: a core with an older contract skips it, and the log says why. `X-Time-Zone` and `PATCH /me` need the framework at 0.56 or later: an older core ignores the header and offers no `PATCH /me`.
6
6
 
7
7
  ```bash
8
8
  bun add @mercury-fw/channel-http
@@ -56,6 +56,8 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
56
56
 
57
57
  **Languages.** Every text a person reads that isn't the model's (a refusal, a tool's status line, a choice's label, a confirmation's reply, the login page) is in their language: the one they asked the assistant for, else the one the request's `Accept-Language` header asks for (what browsers send on their own), else the app's default. The responses of `POST /turn` and `POST /confirm`, a refusal and the login page say which with `Content-Language` (as it stood when the response started: a language picked during a turn counts from the next one).
58
58
 
59
+ **Time zone.** The model reads each message with the time the person wrote it, in their time zone: the one they saved, else the one the request's `X-Time-Zone` header names (an IANA name such as `Europe/Rome`, what `Intl.DateTimeFormat().resolvedOptions().timeZone` gives in a browser), else the app's. A UI saves the person's with `PATCH /me`.
60
+
59
61
  Reasoning and answer text arrive as **incremental deltas** — never one finished block — so a UI renders them live; treat the `final` event as the authoritative text. **To cancel** a turn (e.g. a generation that's diverging), close the connection: Mercury aborts the in-flight generation.
60
62
 
61
63
  **SSE events** — each is `event: <name>` followed by `data: <json>`:
@@ -89,6 +91,10 @@ Whoever opens the link and accepts decides which account gets linked: a link bel
89
91
 
90
92
  A code for linking another of the caller's accounts to them, for the UI to show: `{ ok: true, code, expiresAt }`, or `400` with the reason when there's none. The person sends the code from the other account, on any channel, twice: the first time Mercury says whom the account would be linked to, the second time it links it, and from then on both accounts are one person (one private area, the same roles). The code lasts ten minutes, works once, and only the caller sees it; the model never does. It belongs to the first account that sends it, so it should be sent in a one-to-one conversation, never in a shared space where someone else could send it first. Over HTTP only an account the instance already admits can send it (anyone else is refused before the turn), so an account a closed instance doesn't know links from a chat.
91
93
 
94
+ ## `PATCH /me`
95
+
96
+ Saves the caller's time zone, for a UI's settings: body `{ timeZone }`, an IANA name such as `Europe/Rome`; returns `{ ok: true, timeZone }` as saved (`europe/rome` is saved as `Europe/Rome`), or `400` with the reason for what isn't a zone's name (a fixed offset such as `+02:00` isn't one) and for a body without it. From then on the caller's turns are stamped with the time there, whatever `X-Time-Zone` says. Not there on a core without settings.
97
+
92
98
  ## `POST /confirm`
93
99
 
94
100
  Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation (`text`: the core's reply in the caller's language, "Confirmed and done." or the failure, never the action's raw result), `{ ok: true, resolved: false }` otherwise. When the confirmed action hands its result back to the model, it returns `{ ok: true, resolved: true, resume: true }` instead: open `POST /turn { resume: true, conversationId }` to hear the model's answer. `400` if `token` or `conversationId` is missing, or the id has other characters than `/turn` accepts. Never invokes the model.
@@ -103,7 +109,7 @@ All `GET`, all JSON, all reporting state already held in-process. They need an a
103
109
  | `GET /conversations?limit=<n>` | `{ conversations: [{ conversationId, lastTimestamp, preview }] }` — your conversations, most-recently-active first |
104
110
  | `GET /manifest` | `{ manifest: { coreApiVersion, plugins: [{ name, apiVersion, active, skills, hasBuild, customStatus }], activeClis, skills } }` |
105
111
  | `GET /confirmations` | `{ pending: [{ sessionKey, summary, expiresAt }] }` — your pending confirmations; tokens are deliberately never included |
106
- | `GET /me` | `{ name, language }` — how you asked the assistant to call you and the language you asked it to use, `null` when you never said; `language` is missing on a core that doesn't know languages, and the route isn't there on a core older than this channel |
112
+ | `GET /me` | `{ name, language, timeZone }` — how you asked the assistant to call you, the language you asked it to use and the time zone you saved, `null` when you never said; `language` and `timeZone` are missing on a core that doesn't know them, and the route isn't there on a core older than this channel |
107
113
  | `GET /tool-log` | `{ entries: [...] }` — the tool calls of your recent turns |
108
114
  | `GET /health` | `{ uptimeSeconds, memory, qdrantReachable, ollamaReachable }` |
109
115
  | `GET /wiki/list` | `{ files: [...] }` — `curated/...` and `personal/...` paths |
@@ -11,7 +11,7 @@
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
13
  import { type AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
- import type { Authenticate, Provider, ChannelHostReads, ChannelI18n, ChannelLinking } from "@mercury-fw/channel-types";
14
+ import type { Authenticate, Provider, ChannelHostReads, ChannelI18n, ChannelLinking, ChannelSettings } from "@mercury-fw/channel-types";
15
15
  export type HttpProviderDeps = {
16
16
  port: number;
17
17
  /** Bare-token interception before the model. */
@@ -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 person's own settings (`PATCH /me`). */
38
+ settings?: ChannelSettings;
37
39
  /** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
38
40
  resumeMaxChars?: number;
39
41
  /** What the pages people read call the assistant. */
@@ -22,7 +22,7 @@
22
22
  * Confirm resolution is injected too (`confirm`/`resolveConfirmation`), so this
23
23
  * package never imports the app.
24
24
  */
25
- import { type HandleTurn, type Authenticate, type ChannelHostReads, type ChannelI18n, type ChannelRuntimeContext, type LoginOutcome, type Principal, type Admission, type ChannelLinking } from "@mercury-fw/channel-types";
25
+ import { type HandleTurn, type Authenticate, type ChannelHostReads, type ChannelI18n, type ChannelRuntimeContext, type LoginOutcome, type Principal, type Admission, type ChannelLinking, type ChannelSettings } from "@mercury-fw/channel-types";
26
26
  /** Resolves a bare confirmation token to what to tell the person, or `null` if the input isn't a token. Injected by the core (`ctx.confirm`). */
27
27
  export type ConfirmFn = ChannelRuntimeContext["confirm"];
28
28
  /** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
@@ -124,9 +124,11 @@ export declare function handleLoginCallback(req: Request, deps: {
124
124
  assistantName?: string;
125
125
  i18n?: ChannelI18n;
126
126
  }): Promise<Response>;
127
- /** A single read route: its `GET` handler plus the shared `OPTIONS` preflight. */
127
+ /** A single read route: its `GET` handler plus the shared `OPTIONS` preflight, and on `/me` the `PATCH` saving a setting. */
128
128
  type ReadRoute = {
129
129
  GET: (req: Request) => Response | Promise<Response>;
130
+ /** Only on `/me`, when the core offers the person's settings (`PATCH /me`). */
131
+ PATCH?: (req: Request) => Promise<Response>;
130
132
  OPTIONS: () => Response;
131
133
  };
132
134
  /**
@@ -138,9 +140,10 @@ type ReadRoute = {
138
140
  * their data only; a conversation is named by its conversationId, as on
139
141
  * `/turn`, and its session key is built from the caller.
140
142
  * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
141
- * same for a 400.
143
+ * same for a 400. With `settings`, `/me` also takes a `PATCH` saving the
144
+ * caller's time zone (a UI's settings area), asking `authenticate` first too.
142
145
  */
143
- export declare function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin?: string, admit?: AdmitFn): Record<string, ReadRoute>;
146
+ export declare function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin?: string, admit?: AdmitFn, settings?: ChannelSettings): Record<string, ReadRoute>;
144
147
  export type HttpServerDeps = TurnRequestDeps & {
145
148
  port: number;
146
149
  reads?: ChannelHostReads;
@@ -150,6 +153,8 @@ export type HttpServerDeps = TurnRequestDeps & {
150
153
  completeLogin?: CompleteLoginFn;
151
154
  /** Linking another account to the caller; `/link` is mounted only with it. */
152
155
  linking?: ChannelLinking;
156
+ /** The person's own settings; `PATCH /me` is mounted only with it. */
157
+ settings?: ChannelSettings;
153
158
  /** What the pages people read call the assistant. */
154
159
  assistantName?: string;
155
160
  };
package/http-provider.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
13
  import { startHttpServer, type AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
- import type { Authenticate, Provider, HandleTurn, ChannelHostReads, ChannelI18n, ChannelLinking } from "@mercury-fw/channel-types";
14
+ import type { Authenticate, Provider, HandleTurn, ChannelHostReads, ChannelI18n, ChannelLinking, ChannelSettings } from "@mercury-fw/channel-types";
15
15
 
16
16
  export type HttpProviderDeps = {
17
17
  port: number;
@@ -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 person's own settings (`PATCH /me`). */
35
+ settings?: ChannelSettings;
34
36
  /** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
35
37
  resumeMaxChars?: number;
36
38
  /** What the pages people read call the assistant. */
@@ -54,6 +56,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
54
56
  corsOrigin: deps.corsOrigin,
55
57
  completeLogin: deps.logins?.complete,
56
58
  ...(deps.linking === undefined ? {} : { linking: deps.linking }),
59
+ ...(deps.settings === undefined ? {} : { settings: deps.settings }),
57
60
  ...(deps.resumeMaxChars === undefined ? {} : { resumeMaxChars: deps.resumeMaxChars }),
58
61
  ...(deps.assistantName === undefined ? {} : { assistantName: deps.assistantName }),
59
62
  ...(deps.i18n === undefined ? {} : { i18n: deps.i18n }),
package/http-server.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  type Principal,
36
36
  type Admission,
37
37
  type ChannelLinking,
38
+ type ChannelSettings,
38
39
  } from "@mercury-fw/channel-types";
39
40
  import { englishHttp } from "./messages.ts";
40
41
 
@@ -65,6 +66,12 @@ function languageHint(req: Request): string | undefined {
65
66
  return value ? value : undefined;
66
67
  }
67
68
 
69
+ /** The time zone the caller's client says it's in (`X-Time-Zone`, an IANA name), when it says; the core ignores a name that isn't a zone. */
70
+ function timeZoneHint(req: Request): string | undefined {
71
+ const value = req.headers.get("x-time-zone")?.trim();
72
+ return value ? value : undefined;
73
+ }
74
+
68
75
  /** `Content-Language` for a response to `principal` asking for `hint`, when the core says which language its texts are in. */
69
76
  async function contentLanguage(i18n: ChannelI18n | undefined, principal: Principal | null, hint: string | undefined): Promise<Record<string, string>> {
70
77
  return i18n ? { "content-language": (await i18n.forRequest(principal, hint)).locale } : {};
@@ -104,8 +111,8 @@ const SSE_HEADERS = {
104
111
  function corsHeaders(origin: string): Record<string, string> {
105
112
  return {
106
113
  "access-control-allow-origin": origin,
107
- "access-control-allow-methods": "GET, POST, OPTIONS",
108
- "access-control-allow-headers": "content-type, authorization, accept-language",
114
+ "access-control-allow-methods": "GET, POST, PATCH, OPTIONS",
115
+ "access-control-allow-headers": "content-type, authorization, accept-language, x-time-zone",
109
116
  };
110
117
  }
111
118
 
@@ -207,6 +214,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
207
214
  }
208
215
  const sessionKey = `${principal.id}:${conversationId}`;
209
216
  const hint = languageHint(req);
217
+ const zone = timeZoneHint(req);
210
218
  const language = await contentLanguage(deps.i18n, principal, hint);
211
219
 
212
220
  // Aborted when the client disconnects (see the stream's `cancel` below) so
@@ -280,6 +288,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
280
288
  ...(opening ? { opening: true as const } : {}),
281
289
  ...(resuming ? { resumeAfterConfirm: true as const } : {}),
282
290
  ...(hint === undefined ? {} : { languageHint: hint }),
291
+ ...(zone === undefined ? {} : { timeZoneHint: zone }),
283
292
  },
284
293
  sink,
285
294
  );
@@ -449,9 +458,11 @@ function loginRoutes(
449
458
  };
450
459
  }
451
460
 
452
- /** A single read route: its `GET` handler plus the shared `OPTIONS` preflight. */
461
+ /** A single read route: its `GET` handler plus the shared `OPTIONS` preflight, and on `/me` the `PATCH` saving a setting. */
453
462
  type ReadRoute = {
454
463
  GET: (req: Request) => Response | Promise<Response>;
464
+ /** Only on `/me`, when the core offers the person's settings (`PATCH /me`). */
465
+ PATCH?: (req: Request) => Promise<Response>;
455
466
  OPTIONS: () => Response;
456
467
  };
457
468
 
@@ -464,13 +475,15 @@ type ReadRoute = {
464
475
  * their data only; a conversation is named by its conversationId, as on
465
476
  * `/turn`, and its session key is built from the caller.
466
477
  * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
467
- * same for a 400.
478
+ * same for a 400. With `settings`, `/me` also takes a `PATCH` saving the
479
+ * caller's time zone (a UI's settings area), asking `authenticate` first too.
468
480
  */
469
481
  export function readRoutes(
470
482
  reads: ChannelHostReads,
471
483
  authenticate: Authenticate,
472
484
  corsOrigin = "*",
473
485
  admit?: AdmitFn,
486
+ settings?: ChannelSettings,
474
487
  ): Record<string, ReadRoute> {
475
488
  const cors = corsHeaders(corsOrigin);
476
489
  const json = (payload: object, status = 200): Response =>
@@ -515,12 +528,31 @@ export function readRoutes(
515
528
  ...(reads.me === undefined
516
529
  ? {}
517
530
  : {
518
- "/me": route(async (_req, principal) => {
519
- // Null when the core no longer admits the caller: still a name, null.
520
- const me = (await reads.me!(principal)) as { name: string | null; language?: string | null } | null;
521
- // An older core says no language: the reply then has none either.
522
- return json({ ok: true, name: me?.name ?? null, ...(me?.language === undefined ? {} : { language: me.language }) });
523
- }),
531
+ "/me": {
532
+ ...route(async (_req, principal) => {
533
+ // Null when the core no longer admits the caller: still a name, null.
534
+ const me = (await reads.me!(principal)) as { name: string | null; language?: string | null; timeZone?: string | null } | null;
535
+ // An older core says no language or time zone: the reply then has none either.
536
+ return json({
537
+ ok: true,
538
+ name: me?.name ?? null,
539
+ ...(me?.language === undefined ? {} : { language: me.language }),
540
+ ...(me?.timeZone === undefined ? {} : { timeZone: me.timeZone }),
541
+ });
542
+ }),
543
+ ...(settings === undefined
544
+ ? {}
545
+ : {
546
+ PATCH: async (req: Request) => {
547
+ const principal = await caller(req, { authenticate, ...(admit === undefined ? {} : { admit }) }, corsOrigin);
548
+ if (principal instanceof Response) return principal;
549
+ const body = (await req.json().catch(() => null)) as { timeZone?: unknown } | null;
550
+ if (typeof body?.timeZone !== "string") return badRequest('the body must be {"timeZone": "<IANA name>"}');
551
+ const saved = await settings.setTimeZone(principal, body.timeZone);
552
+ return saved.ok ? json({ ok: true, timeZone: saved.value }) : badRequest(saved.error);
553
+ },
554
+ }),
555
+ },
524
556
  }),
525
557
  "/health": route(async () => json({ ok: true, ...(await reads.health() as object) })),
526
558
  "/wiki/list": route(async (_req, principal) => json({ ok: true, files: await reads.wikiList(principal) })),
@@ -556,6 +588,8 @@ export type HttpServerDeps = TurnRequestDeps & {
556
588
  completeLogin?: CompleteLoginFn;
557
589
  /** Linking another account to the caller; `/link` is mounted only with it. */
558
590
  linking?: ChannelLinking;
591
+ /** The person's own settings; `PATCH /me` is mounted only with it. */
592
+ settings?: ChannelSettings;
559
593
  /** What the pages people read call the assistant. */
560
594
  assistantName?: string;
561
595
  };
@@ -589,7 +623,7 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
589
623
  OPTIONS: () => preflight(origin),
590
624
  },
591
625
  "/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
592
- ...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin, deps.admit) : {}),
626
+ ...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin, deps.admit, deps.settings) : {}),
593
627
  ...(deps.completeLogin ? loginRoutes(deps.completeLogin, deps.assistantName, deps.i18n) : {}),
594
628
  ...(deps.linking ? linkRoutes({ ...deps, start: deps.linking.start, corsOrigin: origin }) : {}),
595
629
  },
package/index.ts CHANGED
@@ -48,6 +48,7 @@ export const httpChannel: ChannelPlugin = {
48
48
  admit: ctx.admit,
49
49
  i18n: ctx.i18n,
50
50
  ...(ctx.linking === undefined ? {} : { linking: ctx.linking }),
51
+ ...(ctx.settings === undefined ? {} : { settings: ctx.settings }),
51
52
  ...(resumeMaxChars === undefined ? {} : { resumeMaxChars }),
52
53
  ...(ctx.assistantName === undefined ? {} : { assistantName: ctx.assistantName }),
53
54
  ...(logins ? { logins: { callbackUrl: `${publicUrl}/login/callback`, accept: logins.accept, complete: logins.complete } } : {}),
package/openapi.yaml CHANGED
@@ -104,9 +104,12 @@ paths:
104
104
  an empty text. Every text the person reads that isn't the model's (a
105
105
  refusal, a status line, a confirmation's reply) is in their language:
106
106
  the one they saved, else the one `Accept-Language` asks for, else the
107
- instance's default.
107
+ instance's default. The model reads the person's message after when
108
+ and where they wrote it: in the time zone they saved, else the one
109
+ `X-Time-Zone` says, else the instance's.
108
110
  parameters:
109
111
  - $ref: "#/components/parameters/AcceptLanguage"
112
+ - $ref: "#/components/parameters/TimeZone"
110
113
  requestBody:
111
114
  required: true
112
115
  content:
@@ -344,9 +347,9 @@ paths:
344
347
  description: >
345
348
  How the caller asked to be called, `null` when they never said: a UI
346
349
  asks for an opening turn after a sign-in only then. Also the language
347
- they asked the assistant to use, `null` when they never said (missing
348
- when the core doesn't know languages). Not there on an older core,
349
- which can't say.
350
+ they asked the assistant to use and the time zone they saved, `null`
351
+ when they never said (missing on a core that doesn't know them). Not
352
+ there on an older core, which can't say.
350
353
  responses:
351
354
  "401":
352
355
  $ref: "#/components/responses/Unauthorized"
@@ -355,7 +358,7 @@ paths:
355
358
  "503":
356
359
  $ref: "#/components/responses/NotAdmitted"
357
360
  "200":
358
- description: The caller's name and language, or null.
361
+ description: The caller's name, language and time zone, or null.
359
362
  content:
360
363
  application/json:
361
364
  schema:
@@ -369,6 +372,52 @@ paths:
369
372
  language:
370
373
  type: [string, "null"]
371
374
  description: A language tag such as `it` or `pt-br`, one the instance ships.
375
+ timeZone:
376
+ type: [string, "null"]
377
+ description: The time zone's IANA name, such as `Europe/Rome`.
378
+ patch:
379
+ summary: Saves the caller's time zone
380
+ description: >
381
+ The time zone the caller is in, by its IANA name, for a UI's settings:
382
+ from then on their turns are stamped with the time there, whatever
383
+ `X-Time-Zone` says. Saved as the IANA list writes it (`europe/rome` is
384
+ `Europe/Rome`). Not there on a core without settings.
385
+ requestBody:
386
+ required: true
387
+ content:
388
+ application/json:
389
+ schema:
390
+ type: object
391
+ required: [timeZone]
392
+ properties:
393
+ timeZone:
394
+ type: string
395
+ description: An IANA name such as `Europe/Rome`; a fixed offset (`+02:00`) isn't one.
396
+ responses:
397
+ "400":
398
+ description: The body has no `timeZone`, or it isn't a zone's name.
399
+ content:
400
+ application/json:
401
+ schema:
402
+ $ref: "#/components/schemas/Error"
403
+ "401":
404
+ $ref: "#/components/responses/Unauthorized"
405
+ "403":
406
+ $ref: "#/components/responses/NotAdmitted"
407
+ "503":
408
+ $ref: "#/components/responses/NotAdmitted"
409
+ "200":
410
+ description: The time zone as saved.
411
+ content:
412
+ application/json:
413
+ schema:
414
+ type: object
415
+ required: [ok, timeZone]
416
+ properties:
417
+ ok:
418
+ type: boolean
419
+ timeZone:
420
+ type: string
372
421
  /tool-log:
373
422
  get:
374
423
  summary: The tool calls made in the caller's recent turns
@@ -564,6 +613,17 @@ components:
564
613
  ship is passed over.
565
614
  schema:
566
615
  type: string
616
+ TimeZone:
617
+ name: X-Time-Zone
618
+ in: header
619
+ required: false
620
+ description: >
621
+ The time zone the client is in, by its IANA name, as a browser gives
622
+ it (`Intl.DateTimeFormat().resolvedOptions().timeZone`). Used for the
623
+ turn's time when the person saved no time zone of their own; a name
624
+ that isn't a zone is passed over.
625
+ schema:
626
+ type: string
567
627
  headers:
568
628
  ContentLanguage:
569
629
  description: >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-http",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "@mercury-fw/channel-types": ">=0.54.0 <1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@mercury-fw/channel-types": "0.54.0",
25
- "@mercury-fw/plugin-types": "0.54.0",
24
+ "@mercury-fw/channel-types": "0.56.0",
25
+ "@mercury-fw/plugin-types": "0.56.0",
26
26
  "@mercury-fw/typescript-config": "*",
27
27
  "@types/bun": "^1.4.2",
28
28
  "typescript": "^6.0.3"