@rubytech/create-maxy-code 0.1.441 → 0.1.442

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 (63) hide show
  1. package/dist/index.js +99 -1
  2. package/package.json +1 -1
  3. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -2
  4. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  5. package/payload/platform/plugins/docs/references/troubleshooting.md +35 -0
  6. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +37 -4
  7. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
  8. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +27 -3
  9. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
  10. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +52 -11
  11. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
  12. package/payload/platform/plugins/outlook/mcp/dist/index.js +1 -1
  13. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +5 -0
  15. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
  16. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +32 -0
  17. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
  18. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +20 -22
  19. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
  21. package/payload/platform/scripts/logs-read.sh +24 -6
  22. package/payload/platform/scripts/logs-read.test.sh +42 -0
  23. package/payload/platform/scripts/rss-sampler.sh +107 -0
  24. package/payload/server/public/assets/{AdminLoginScreens-CukwQufW.js → AdminLoginScreens-BJVwtpLH.js} +1 -1
  25. package/payload/server/public/assets/{AdminShell-BwEBH6tT.js → AdminShell-CMu8_Xwn.js} +1 -1
  26. package/payload/server/public/assets/{Checkbox-B629mOPp.js → Checkbox-CzD0ORqA.js} +1 -1
  27. package/payload/server/public/assets/{admin-CdFRCUUZ.js → admin-D0yk2LMq.js} +1 -1
  28. package/payload/server/public/assets/{browser-BWR59IkB.js → browser-CvfbgznO.js} +1 -1
  29. package/payload/server/public/assets/{calendar-kuTcxX1t.js → calendar-fw4WethJ.js} +1 -1
  30. package/payload/server/public/assets/chat-DhD-NES_.js +1 -0
  31. package/payload/server/public/assets/chevron-left-B3xSeEDR.js +1 -0
  32. package/payload/server/public/assets/data-CUnDzDox.js +1 -0
  33. package/payload/server/public/assets/{file-download-4mKJxUi-.js → file-download-Px4s0Bhp.js} +1 -1
  34. package/payload/server/public/assets/{graph-CboWM2_-.js → graph-DbT8nA6R.js} +1 -1
  35. package/payload/server/public/assets/{graph-labels-B8YAdo3Z.js → graph-labels-C1YIyLiA.js} +1 -1
  36. package/payload/server/public/assets/{maximize-2-CxstTgoK.js → maximize-2-BwphzI9S.js} +1 -1
  37. package/payload/server/public/assets/{operator-oSaB1mya.js → operator-zcIfeB14.js} +1 -1
  38. package/payload/server/public/assets/{page-DMeJHVfA.js → page-6fCnIb45.js} +2 -2
  39. package/payload/server/public/assets/page-CfQFAJia.js +1 -0
  40. package/payload/server/public/assets/{public-C54tl6V4.js → public-C0ehZCdE.js} +1 -1
  41. package/payload/server/public/assets/{rotate-ccw-COGQTi7n.js → rotate-ccw-C7BqkMec.js} +1 -1
  42. package/payload/server/public/assets/{tasks-B97VpwEJ.js → tasks-DlgiCafL.js} +1 -1
  43. package/payload/server/public/assets/{time-entry-format-C09rDHo9.js → time-entry-format-CyNYVDkq.js} +1 -1
  44. package/payload/server/public/assets/{useCopyFeedback-DHoYHiK9.js → useCopyFeedback-ByGUMN94.js} +1 -1
  45. package/payload/server/public/assets/useSubAccountSwitcher-1izQvPiD.css +1 -0
  46. package/payload/server/public/assets/useVoiceRecorder-XgpWvXpk.js +2 -0
  47. package/payload/server/public/browser.html +5 -5
  48. package/payload/server/public/calendar.html +6 -6
  49. package/payload/server/public/chat.html +12 -12
  50. package/payload/server/public/data.html +10 -10
  51. package/payload/server/public/graph.html +11 -11
  52. package/payload/server/public/index.html +13 -13
  53. package/payload/server/public/operator.html +14 -14
  54. package/payload/server/public/public.html +12 -12
  55. package/payload/server/public/tasks.html +5 -5
  56. package/payload/server/server.js +431 -182
  57. package/payload/server/public/assets/chat-ejnq2rck.js +0 -1
  58. package/payload/server/public/assets/chevron-left-u4tjq7fI.js +0 -1
  59. package/payload/server/public/assets/data-dZbOyLdT.js +0 -1
  60. package/payload/server/public/assets/page-DEkI-Gl1.js +0 -1
  61. package/payload/server/public/assets/useSubAccountSwitcher-Dzmd7Aip.css +0 -1
  62. package/payload/server/public/assets/useVoiceRecorder-b7HzTFs0.js +0 -2
  63. /package/payload/server/public/assets/{useSubAccountSwitcher-DLJFISqf.js → useSubAccountSwitcher-s6WlRn52.js} +0 -0
package/dist/index.js CHANGED
@@ -856,6 +856,89 @@ function installSystemDeps() {
856
856
  catch (err) {
857
857
  console.error(` WARNING: failed to provision swap: ${err instanceof Error ? err.message : String(err)}`);
858
858
  }
859
+ // Task 1593 — zram compressed-RAM swap ahead of the SD dphys swapfile. On a
860
+ // Pi, swapping to the SD card thrashes I/O and turns an approach-to-OOM into
861
+ // an apparent hard-freeze (the muvin 2026-07-13 incident). zram swaps to
862
+ // compressed RAM first (default priority 100 vs dphys -2), removing the
863
+ // thrash and adding effective headroom; dphys stays as the low-priority
864
+ // last-resort overflow tier. Best-effort: a failure warns but never aborts.
865
+ try {
866
+ console.log(" [privileged] apt-get install zram-tools");
867
+ shell("apt-get", ["install", "-y", "zram-tools"], { sudo: true, bestEffort: true });
868
+ const desiredZram = "ALGO=zstd\nPERCENT=50\n";
869
+ const zramConfPath = "/etc/default/zramswap";
870
+ let zramCurrent = "";
871
+ if (existsSync(zramConfPath)) {
872
+ try {
873
+ zramCurrent = readFileSync(zramConfPath, "utf-8");
874
+ }
875
+ catch {
876
+ zramCurrent = "";
877
+ }
878
+ }
879
+ if (zramCurrent.includes("ALGO=zstd") && zramCurrent.includes("PERCENT=50")) {
880
+ logFile(" zram: already configured (ALGO=zstd PERCENT=50)");
881
+ }
882
+ else {
883
+ const zramTmp = `/tmp/zramswap.${process.pid}`;
884
+ writeFileSync(zramTmp, desiredZram);
885
+ console.log(" [privileged] install /etc/default/zramswap (ALGO=zstd PERCENT=50)");
886
+ shell("cp", [zramTmp, zramConfPath], { sudo: true });
887
+ spawnSync("rm", ["-f", zramTmp]);
888
+ console.log(" [privileged] systemctl restart zramswap");
889
+ shell("systemctl", ["restart", "zramswap"], { sudo: true, bestEffort: true });
890
+ logFile(" zram: configured ALGO=zstd PERCENT=50 and restarted zramswap");
891
+ }
892
+ }
893
+ catch (err) {
894
+ console.error(` WARNING: failed to provision zram: ${err instanceof Error ? err.message : String(err)}`);
895
+ }
896
+ // Task 1593 — earlyoom userspace killer. Under sustained pressure it SIGTERMs
897
+ // the largest process at 10% MemAvailable (SIGKILL at 5%) BEFORE the kernel
898
+ // oom-killer wedges the box; systemd's Restart=on-failure then restarts the
899
+ // service, so a leak becomes "one process dies and comes back" instead of a
900
+ // hard freeze. --avoid protects sshd (keeps the box reachable, so SSH never
901
+ // drops during a kill) and systemd (init). Neo4j runs as comm=java and is
902
+ // crash-safe (recovers from its transaction log on restart), so it is
903
+ // intentionally not protected. Best-effort. The 10%/5% thresholds are the
904
+ // calibration target of the on-device induced-pressure test.
905
+ try {
906
+ console.log(" [privileged] apt-get install earlyoom");
907
+ shell("apt-get", ["install", "-y", "earlyoom"], { sudo: true, bestEffort: true });
908
+ // No inner quotes around the --avoid regex: the Debian earlyoom.service
909
+ // runs `ExecStart=/usr/bin/earlyoom $EARLYOOM_ARGS`, and systemd expands the
910
+ // variable by whitespace-splitting WITHOUT shell quote removal, so inner
911
+ // single-quotes would survive into the regex and never match sshd/systemd.
912
+ // The regex `^(sshd|systemd)$` contains no whitespace, so it stays one token.
913
+ const desiredEarlyoom = "EARLYOOM_ARGS=\"-m 10,5 -r 3600 --avoid ^(sshd|systemd)$\"\n";
914
+ const earlyoomConfPath = "/etc/default/earlyoom";
915
+ let earlyoomCurrent = "";
916
+ if (existsSync(earlyoomConfPath)) {
917
+ try {
918
+ earlyoomCurrent = readFileSync(earlyoomConfPath, "utf-8");
919
+ }
920
+ catch {
921
+ earlyoomCurrent = "";
922
+ }
923
+ }
924
+ if (earlyoomCurrent.includes("-m 10,5") && earlyoomCurrent.includes("--avoid ^(sshd|systemd)$")) {
925
+ logFile(" earlyoom: already configured (-m 10,5 --avoid sshd|systemd)");
926
+ }
927
+ else {
928
+ const eoTmp = `/tmp/earlyoom.${process.pid}`;
929
+ writeFileSync(eoTmp, desiredEarlyoom);
930
+ console.log(" [privileged] install /etc/default/earlyoom (-m 10,5 -r 3600)");
931
+ shell("cp", [eoTmp, earlyoomConfPath], { sudo: true });
932
+ spawnSync("rm", ["-f", eoTmp]);
933
+ console.log(" [privileged] systemctl enable --now earlyoom");
934
+ shell("systemctl", ["enable", "--now", "earlyoom"], { sudo: true, bestEffort: true });
935
+ shell("systemctl", ["restart", "earlyoom"], { sudo: true, bestEffort: true });
936
+ logFile(" earlyoom: configured -m 10,5 -r 3600 --avoid sshd|systemd and enabled");
937
+ }
938
+ }
939
+ catch (err) {
940
+ console.error(` WARNING: failed to provision earlyoom: ${err instanceof Error ? err.message : String(err)}`);
941
+ }
859
942
  // Hostname resolution — four sources, in priority order:
860
943
  // 1. --hostname flag (unconditional — the caller is the authority)
861
944
  // 2. OS detection on same-brand upgrade (service exists → keep whatever is currently set)
@@ -4218,6 +4301,17 @@ function installService() {
4218
4301
  const rssSamplerUnitName = `${BRAND.hostname}-rss-sampler.service`;
4219
4302
  const rssSamplerScriptPath = resolve(INSTALL_DIR, "platform/scripts/rss-sampler.sh");
4220
4303
  const rssSamplerLogPath = join(persistDir, "rss-sampler.log");
4304
+ // Task 1593 — the per-service unit names the sampler reads for its
4305
+ // op=mem-sample line, kept brand-agnostic by passing them as env. Neo4j runs
4306
+ // as a dedicated per-brand unit when it is off the default port, else the
4307
+ // shared system unit. MEM_PRESSURE_PCT sets the approach-to-OOM warning line
4308
+ // as a percentage of MemTotal; 20% sits above earlyoom's 10% SIGTERM line on
4309
+ // every fleet RAM size (8-16 GB), so the warning is logged before the kill
4310
+ // (a fixed MB value cannot stay above a percentage kill line across both).
4311
+ const memBrandService = `${BRAND.hostname}.service`;
4312
+ const memEdgeService = edgeUnitName;
4313
+ const memNeo4jUnit = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
4314
+ const memPressurePct = "20";
4221
4315
  const rssSamplerUnit = `[Unit]
4222
4316
  Description=${BRAND.productName} RSS Sampler
4223
4317
 
@@ -4225,6 +4319,10 @@ Description=${BRAND.productName} RSS Sampler
4225
4319
  Type=simple
4226
4320
  ExecStart=/bin/bash ${rssSamplerScriptPath}
4227
4321
  Environment=LOG_PATH=${rssSamplerLogPath}
4322
+ Environment=BRAND_SERVICE=${memBrandService}
4323
+ Environment=EDGE_SERVICE=${memEdgeService}
4324
+ Environment=NEO4J_UNIT=${memNeo4jUnit}
4325
+ Environment=MEM_PRESSURE_PCT=${memPressurePct}
4228
4326
  Restart=on-failure
4229
4327
  RestartSec=10
4230
4328
 
@@ -4232,7 +4330,7 @@ RestartSec=10
4232
4330
  WantedBy=default.target
4233
4331
  `;
4234
4332
  writeFileSync(join(serviceDir, rssSamplerUnitName), rssSamplerUnit);
4235
- logFile(` rss-sampler installed: ${rssSamplerUnitName} LOG_PATH=${rssSamplerLogPath}`);
4333
+ logFile(` rss-sampler installed: ${rssSamplerUnitName} LOG_PATH=${rssSamplerLogPath} BRAND_SERVICE=${memBrandService} EDGE_SERVICE=${memEdgeService} NEO4J_UNIT=${memNeo4jUnit} MEM_PRESSURE_PCT=${memPressurePct}`);
4236
4334
  // the edge service: always-on front door that owns the public
4237
4335
  // port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
4238
4336
  // independent of the main brand service, so an in-place upgrade triggered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.441",
3
+ "version": "0.1.442",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:9f7fc73400d2f9cf942e2df6f544bc69b95abe820ac1ff9b529807c3c7fd6407
4
+ content-hash: sha256:93408025e99adfc1e44893f43734ca04b95c7991c596a4af5c7f471e00e884d8
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2763,7 +2763,7 @@ either is a regression.
2763
2763
 
2764
2764
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
2765
2765
 
2766
- **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
2766
+ **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
2767
2767
 
2768
2768
  **`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
2769
2769
 
@@ -4720,6 +4720,41 @@ Maxy has access to all platform logs and can filter them for you.
4720
4720
 
4721
4721
  ---
4722
4722
 
4723
+ ## Box went unreachable, suspected memory exhaustion
4724
+
4725
+ **Symptom:** The Pi stopped responding to everything (no web, no SSH, no ping) and needed a power-cycle. This is the signature of memory exhaustion: the box fills RAM and swap, then wedges while the kernel tries to free memory.
4726
+
4727
+ **What protects against it:** Every install runs a standing memory audit every 30 seconds, zram compressed swap so pressure hits RAM instead of the SD card, and earlyoom, which kills the single largest process before the kernel freezes the box. A kill shows up as one service restarting, not a whole-box outage, and SSH is protected so the box stays reachable.
4728
+
4729
+ **Check the approach to OOM after a reboot.** The audit log survives reboot. Read it:
4730
+
4731
+ ```
4732
+ ~/<install>/platform/scripts/logs-read.sh --tail mem 100
4733
+ ```
4734
+
4735
+ Replace `<install>` with the install directory (for example `realagent-code`). Then:
4736
+
4737
+ - Look for `op=mem-pressure` lines. They fire when available memory crosses the low-water mark, before any kill, and name the heaviest service (`topSlice`).
4738
+ - A single service's reading (`ptys`, `neo4j`, `brand`, `edge`, `vnc`) rising over time while the others stay flat is a memory leak in that service.
4739
+ - `op=mem-sample` lines that read `na` for the per-service fields are normal until the kernel memory controller is enabled on that box; available memory, swap, and the pressure warning still work.
4740
+
4741
+ **Confirm a kill happened and the service came back:**
4742
+
4743
+ ```
4744
+ journalctl -t earlyoom --since -1h
4745
+ journalctl --user -u <hostname>.service --since -1h | grep -iE "Started|Scheduled restart"
4746
+ ```
4747
+
4748
+ **Check whether the kernel OOM-killer fired anyway** (it should not, if earlyoom pre-empted it):
4749
+
4750
+ ```
4751
+ journalctl -b -1 -k | grep -i oom
4752
+ ```
4753
+
4754
+ A kernel `invoked oom-killer` line means the mitigation did not pre-empt the kernel on that occasion. A gap in the `op=mem-sample` timestamps means the sampler itself stopped. Both are worth escalating.
4755
+
4756
+ ---
4757
+
4723
4758
  ## Cloudflare Tunnel Down (Remote Access Broken)
4724
4759
 
4725
4760
  **Symptom:** You can reach Maxy on your local network but not via your public domain.
@@ -75,7 +75,7 @@ either is a regression.
75
75
 
76
76
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
77
77
 
78
- **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
78
+ **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
79
79
 
80
80
  **`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
81
81
 
@@ -224,6 +224,41 @@ For specific plugin logs: "Show Telegram logs" or "Show contacts plugin logs."
224
224
 
225
225
  ---
226
226
 
227
+ ## Box went unreachable, suspected memory exhaustion
228
+
229
+ **Symptom:** The Pi stopped responding to everything (no web, no SSH, no ping) and needed a power-cycle. This is the signature of memory exhaustion: the box fills RAM and swap, then wedges while the kernel tries to free memory.
230
+
231
+ **What protects against it:** Every install runs a standing memory audit every 30 seconds, zram compressed swap so pressure hits RAM instead of the SD card, and earlyoom, which kills the single largest process before the kernel freezes the box. A kill shows up as one service restarting, not a whole-box outage, and SSH is protected so the box stays reachable.
232
+
233
+ **Check the approach to OOM after a reboot.** The audit log survives reboot. Read it:
234
+
235
+ ```
236
+ ~/<install>/platform/scripts/logs-read.sh --tail mem 100
237
+ ```
238
+
239
+ Replace `<install>` with the install directory (for example `realagent-code`). Then:
240
+
241
+ - Look for `op=mem-pressure` lines. They fire when available memory crosses the low-water mark, before any kill, and name the heaviest service (`topSlice`).
242
+ - A single service's reading (`ptys`, `neo4j`, `brand`, `edge`, `vnc`) rising over time while the others stay flat is a memory leak in that service.
243
+ - `op=mem-sample` lines that read `na` for the per-service fields are normal until the kernel memory controller is enabled on that box; available memory, swap, and the pressure warning still work.
244
+
245
+ **Confirm a kill happened and the service came back:**
246
+
247
+ ```
248
+ journalctl -t earlyoom --since -1h
249
+ journalctl --user -u <hostname>.service --since -1h | grep -iE "Started|Scheduled restart"
250
+ ```
251
+
252
+ **Check whether the kernel OOM-killer fired anyway** (it should not, if earlyoom pre-empted it):
253
+
254
+ ```
255
+ journalctl -b -1 -k | grep -i oom
256
+ ```
257
+
258
+ A kernel `invoked oom-killer` line means the mitigation did not pre-empt the kernel on that occasion. A gap in the `op=mem-sample` timestamps means the sampler itself stopped. Both are worth escalating.
259
+
260
+ ---
261
+
227
262
  ## Cloudflare Tunnel Down (Remote Access Broken)
228
263
 
229
264
  **Symptom:** You can reach {{productName}} on your local network but not via your public domain.
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, resolve } from "node:path";
6
- import { _internals, startDeviceCode, pollDeviceToken } from "../auth/device-flow.js";
6
+ import { _internals, startDeviceCode, pollDeviceToken, parseAzureTokenError } from "../auth/device-flow.js";
7
7
  function mockFetch(handler) {
8
8
  const calls = [];
9
9
  const original = globalThis.fetch;
@@ -75,15 +75,48 @@ test("pollDeviceToken maps slow_down / expired_token / access_denied", async ()
75
75
  }
76
76
  }
77
77
  });
78
- test("pollDeviceToken throws on an unrecognised error", async () => {
79
- const m = mockFetch(() => json({ error: "invalid_client" }, 400));
78
+ test("pollDeviceToken returns a structured error (not a throw) on an unrecognised error", async () => {
79
+ const m = mockFetch(() => json({
80
+ error: "invalid_grant",
81
+ error_description: "AADSTS70008: The provided authorization code or refresh token has expired due to inactivity.",
82
+ error_codes: [70008],
83
+ suberror: "token_expired",
84
+ correlation_id: "corr-1",
85
+ trace_id: "trace-1",
86
+ timestamp: "2026-07-13 16:10:17Z",
87
+ }, 400));
80
88
  try {
81
- await assert.rejects(pollDeviceToken({ clientId: "c", tenantId: "common" }, "D"), /invalid_client/);
89
+ const out = await pollDeviceToken({ clientId: "c", tenantId: "common" }, "D");
90
+ assert.equal(out.kind, "error");
91
+ if (out.kind === "error") {
92
+ assert.equal(out.httpStatus, 400);
93
+ assert.equal(out.error, "invalid_grant");
94
+ assert.equal(out.aadsts, "AADSTS70008");
95
+ assert.equal(out.suberror, "token_expired");
96
+ assert.equal(out.correlationId, "corr-1");
97
+ assert.equal(out.traceId, "trace-1");
98
+ assert.match(out.errorDescription, /expired due to inactivity/);
99
+ }
82
100
  }
83
101
  finally {
84
102
  m.restore();
85
103
  }
86
104
  });
105
+ test("parseAzureTokenError recovers the AADSTS code from the description when error_codes is absent", () => {
106
+ const e = parseAzureTokenError(400, JSON.stringify({
107
+ error: "invalid_grant",
108
+ error_description: "AADSTS50011: The redirect URI does not match.",
109
+ }));
110
+ assert.equal(e.aadsts, "AADSTS50011");
111
+ assert.equal(e.error, "invalid_grant");
112
+ });
113
+ test("parseAzureTokenError keeps a non-JSON body in raw without throwing", () => {
114
+ const e = parseAzureTokenError(502, "<html>Bad Gateway</html>");
115
+ assert.equal(e.kind, "error");
116
+ assert.equal(e.httpStatus, 502);
117
+ assert.equal(e.aadsts, null);
118
+ assert.equal(e.raw, "<html>Bad Gateway</html>");
119
+ });
87
120
  test("pollDeviceToken 200 -> success with tokenResponse + scopes", async () => {
88
121
  const m = mockFetch(() => json({
89
122
  access_token: "AT", refresh_token: "RT", expires_in: 3600, token_type: "Bearer",
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/device-flow.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"device-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/device-flow.test.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,22 @@ export declare function startDeviceCode(config: {
36
36
  clientId: string;
37
37
  tenantId: string;
38
38
  }): Promise<DeviceCodeStart>;
39
+ export interface AzureTokenError {
40
+ kind: "error";
41
+ httpStatus: number;
42
+ error: string;
43
+ errorDescription: string;
44
+ /** The AADSTSnnnnn code, from `error_codes[0]` or parsed out of the description. */
45
+ aadsts: string | null;
46
+ /** OAuth sub-error (e.g. `token_expired`, `bad_token`, `consent_required`) — the
47
+ * field that distinguishes causes sharing one top-level `invalid_grant`. */
48
+ suberror: string | null;
49
+ correlationId: string | null;
50
+ traceId: string | null;
51
+ timestamp: string | null;
52
+ /** The verbatim response body — never discarded, so no diagnostic detail is lost. */
53
+ raw: string;
54
+ }
39
55
  export type PollOutcome = {
40
56
  kind: "pending";
41
57
  } | {
@@ -48,11 +64,19 @@ export type PollOutcome = {
48
64
  kind: "success";
49
65
  tokenResponse: TokenResponse;
50
66
  scopes: string[];
51
- };
67
+ } | AzureTokenError;
68
+ /**
69
+ * Parse a Microsoft token-endpoint error body into structured fields. Every part
70
+ * Azure returns — AADSTS code, sub-error, description, correlation/trace IDs — is
71
+ * captured so a poll failure is diagnosable from the log without re-deriving it
72
+ * from an opaque string. A non-JSON body still yields a value: `raw` carries it.
73
+ */
74
+ export declare function parseAzureTokenError(httpStatus: number, text: string): AzureTokenError;
52
75
  /**
53
76
  * One device-code token poll against `/oauth2/v2.0/token`. 200 yields tokens;
54
- * the Microsoft error codes map to the polling states an RFC 8628 client must
55
- * handle. Any unrecognised error throws.
77
+ * the four RFC 8628 polling states map to their kinds. Any other error is
78
+ * returned as a structured `AzureTokenError` (never thrown, never discarded) so
79
+ * the caller can log the full Microsoft reason instead of an opaque string.
56
80
  */
57
81
  export declare function pollDeviceToken(config: {
58
82
  clientId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6B3B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAExE;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC9C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAoCtB;AAMD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBzB;AAED,eAAO,MAAM,UAAU;;CAAa,CAAC"}
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6B3B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;iFAC6E;IAC7E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GACnE,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA6CtF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC9C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAMD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBzB;AAED,eAAO,MAAM,UAAU;;CAAa,CAAC"}
@@ -60,10 +60,57 @@ export async function startDeviceCode(config) {
60
60
  scopes: [...SCOPES],
61
61
  };
62
62
  }
63
+ /**
64
+ * Parse a Microsoft token-endpoint error body into structured fields. Every part
65
+ * Azure returns — AADSTS code, sub-error, description, correlation/trace IDs — is
66
+ * captured so a poll failure is diagnosable from the log without re-deriving it
67
+ * from an opaque string. A non-JSON body still yields a value: `raw` carries it.
68
+ */
69
+ export function parseAzureTokenError(httpStatus, text) {
70
+ let error = "";
71
+ let errorDescription = "";
72
+ let suberror = null;
73
+ let correlationId = null;
74
+ let traceId = null;
75
+ let timestamp = null;
76
+ let aadsts = null;
77
+ try {
78
+ const b = JSON.parse(text);
79
+ error = b.error ?? "";
80
+ errorDescription = b.error_description ?? "";
81
+ suberror = b.suberror ?? null;
82
+ correlationId = b.correlation_id ?? null;
83
+ traceId = b.trace_id ?? null;
84
+ timestamp = b.timestamp ?? null;
85
+ if (b.error_codes && b.error_codes.length > 0)
86
+ aadsts = `AADSTS${b.error_codes[0]}`;
87
+ }
88
+ catch {
89
+ // Non-JSON body (gateway HTML, empty, etc.): keep the structured shape and let
90
+ // `raw` carry the body so the failure is still fully recorded.
91
+ }
92
+ if (!aadsts) {
93
+ const m = /AADSTS\d+/.exec(errorDescription || text);
94
+ aadsts = m ? m[0] : null;
95
+ }
96
+ return {
97
+ kind: "error",
98
+ httpStatus,
99
+ error,
100
+ errorDescription,
101
+ aadsts,
102
+ suberror,
103
+ correlationId,
104
+ traceId,
105
+ timestamp,
106
+ raw: text,
107
+ };
108
+ }
63
109
  /**
64
110
  * One device-code token poll against `/oauth2/v2.0/token`. 200 yields tokens;
65
- * the Microsoft error codes map to the polling states an RFC 8628 client must
66
- * handle. Any unrecognised error throws.
111
+ * the four RFC 8628 polling states map to their kinds. Any other error is
112
+ * returned as a structured `AzureTokenError` (never thrown, never discarded) so
113
+ * the caller can log the full Microsoft reason instead of an opaque string.
67
114
  */
68
115
  export async function pollDeviceToken(config, deviceCode) {
69
116
  const { tokenUrl } = buildUrls(config.tenantId);
@@ -83,14 +130,8 @@ export async function pollDeviceToken(config, deviceCode) {
83
130
  return { kind: "success", tokenResponse, scopes };
84
131
  }
85
132
  const text = await response.text();
86
- let errorCode = "";
87
- try {
88
- errorCode = JSON.parse(text).error ?? "";
89
- }
90
- catch {
91
- errorCode = "";
92
- }
93
- switch (errorCode) {
133
+ const parsed = parseAzureTokenError(response.status, text);
134
+ switch (parsed.error) {
94
135
  case "authorization_pending":
95
136
  return { kind: "pending" };
96
137
  case "slow_down":
@@ -100,7 +141,7 @@ export async function pollDeviceToken(config, deviceCode) {
100
141
  case "access_denied":
101
142
  return { kind: "denied" };
102
143
  default:
103
- throw new Error(`Device-token poll failed (${response.status}): ${text}`);
144
+ return parsed;
104
145
  }
105
146
  }
106
147
  // ---------------------------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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"}
@@ -161,7 +161,7 @@ lifelineTool(server, "outlook-account-register", "Start the OAuth device-code fl
161
161
  return fail(`outlook-account-register failed: ${err.message}`);
162
162
  }
163
163
  });
164
- lifelineTool(server, "outlook-account-register-poll", "Poll for completion of the device-code flow started by outlook-account-register. One check per call. Returns status: pending (wait intervalSec, then call again), registered (done — tokens persisted), expired (code timed out — restart with outlook-account-register), or denied (operator declined).", {}, async () => {
164
+ lifelineTool(server, "outlook-account-register-poll", "Poll for completion of the device-code flow started by outlook-account-register. One check per call. Returns status: pending (wait intervalSec, then call again), registered (done — tokens persisted), expired (code timed out — restart with outlook-account-register), denied (operator declined), or error (Microsoft rejected the sign-in — message carries the AADSTS code + reason; a 4xx clears the pending code so you can restart, a 5xx keeps it for re-poll).", {}, async () => {
165
165
  if (!ACCOUNT_ID)
166
166
  return refuseNoAccount("outlook-account-register-poll");
167
167
  try {