@rubytech/create-maxy-code 0.1.321 → 0.1.322

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 (46) hide show
  1. package/dist/index.js +82 -1
  2. package/package.json +1 -1
  3. package/payload/platform/lib/embed-client/dist/index.d.ts +2 -0
  4. package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/embed-client/dist/index.js +50 -10
  6. package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
  7. package/payload/platform/lib/embed-client/src/index.ts +57 -9
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -1
  9. package/payload/platform/plugins/docs/references/deployment.md +1 -0
  10. package/payload/platform/plugins/docs/references/internals.md +2 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +41 -2
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  14. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +34 -4
  15. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js +75 -2
  18. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +13 -0
  20. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +16 -0
  22. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  23. package/payload/server/public/assets/{AdminLoginScreens-DKwWudsP.js → AdminLoginScreens-D1X4M9wX.js} +1 -1
  24. package/payload/server/public/assets/{AdminShell-BsPsXvJv.js → AdminShell-BGZ3d3gY.js} +1 -1
  25. package/payload/server/public/assets/{Checkbox-V5ubVZG1.js → Checkbox-DjANdKoK.js} +1 -1
  26. package/payload/server/public/assets/{OperatorConversations-DOGCHZhv.css → OperatorConversations-C-eFXT7w.css} +1 -1
  27. package/payload/server/public/assets/{admin-Cfqj5N4P.js → admin--FlkQOMu.js} +1 -1
  28. package/payload/server/public/assets/{browser-CtMwlpyW.js → browser-vowQc0TJ.js} +1 -1
  29. package/payload/server/public/assets/chat-Cw6rUWiC.js +1 -0
  30. package/payload/server/public/assets/{data-7_dN5vN5.js → data-9ArhwX1q.js} +1 -1
  31. package/payload/server/public/assets/{graph-Bez5QF37.js → graph-B-64Cq-y.js} +1 -1
  32. package/payload/server/public/assets/{graph-labels-BkbaBPIP.js → graph-labels-Dtd2ckXH.js} +1 -1
  33. package/payload/server/public/assets/{operator-F5KHg5WG.js → operator-Cqn1Ne60.js} +1 -1
  34. package/payload/server/public/assets/page-DQ5D6Dg9.js +30 -0
  35. package/payload/server/public/assets/{public-wy93nfmL.js → public-CrFepd06.js} +1 -1
  36. package/payload/server/public/browser.html +4 -4
  37. package/payload/server/public/chat.html +5 -5
  38. package/payload/server/public/data.html +4 -4
  39. package/payload/server/public/graph.html +6 -6
  40. package/payload/server/public/index.html +6 -6
  41. package/payload/server/public/operator.html +7 -7
  42. package/payload/server/public/public.html +5 -5
  43. package/payload/server/server.js +185 -12
  44. package/payload/server/public/assets/chat-Bm11OGsU.js +0 -1
  45. package/payload/server/public/assets/page-CExyEcvh.js +0 -30
  46. /package/payload/server/public/assets/{OperatorConversations-DPRRlS_z.js → OperatorConversations-BkitSIiT.js} +0 -0
@@ -5,13 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Graph — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/graph-Bez5QF37.js"></script>
8
+ <script type="module" crossorigin src="/assets/graph-B-64Cq-y.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
10
- <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BkbaBPIP.js">
12
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
13
- <link rel="modulepreload" crossorigin href="/assets/Checkbox-V5ubVZG1.js">
14
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
10
+ <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-BkitSIiT.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/graph-labels-Dtd2ckXH.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-BGZ3d3gY.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/Checkbox-DjANdKoK.js">
14
+ <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C-eFXT7w.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -5,14 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/admin-Cfqj5N4P.js"></script>
8
+ <script type="module" crossorigin src="/assets/admin--FlkQOMu.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
10
- <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-BkitSIiT.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-BGZ3d3gY.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
13
- <link rel="modulepreload" crossorigin href="/assets/Checkbox-V5ubVZG1.js">
14
- <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-DKwWudsP.js">
15
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
13
+ <link rel="modulepreload" crossorigin href="/assets/Checkbox-DjANdKoK.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-D1X4M9wX.js">
15
+ <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C-eFXT7w.css">
16
16
  <link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
17
17
  <link rel="stylesheet" href="/brand-defaults.css">
18
18
  </head>
@@ -5,16 +5,16 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Operator — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/operator-F5KHg5WG.js"></script>
8
+ <script type="module" crossorigin src="/assets/operator-Cqn1Ne60.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
10
- <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-BkitSIiT.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-BGZ3d3gY.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
13
- <link rel="modulepreload" crossorigin href="/assets/Checkbox-V5ubVZG1.js">
14
- <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-DKwWudsP.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/Checkbox-DjANdKoK.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-D1X4M9wX.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-Bf_JiD2A.js">
16
- <link rel="modulepreload" crossorigin href="/assets/page-CExyEcvh.js">
17
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
16
+ <link rel="modulepreload" crossorigin href="/assets/page-DQ5D6Dg9.js">
17
+ <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C-eFXT7w.css">
18
18
  <link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
19
19
  </head>
20
20
  <body>
@@ -5,14 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/public-wy93nfmL.js"></script>
8
+ <script type="module" crossorigin src="/assets/public-CrFepd06.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
10
- <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/OperatorConversations-BkitSIiT.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-BGZ3d3gY.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-Bf_JiD2A.js">
14
- <link rel="modulepreload" crossorigin href="/assets/page-CExyEcvh.js">
15
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
14
+ <link rel="modulepreload" crossorigin href="/assets/page-DQ5D6Dg9.js">
15
+ <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-C-eFXT7w.css">
16
16
  <link rel="stylesheet" href="/brand-defaults.css">
17
17
  </head>
18
18
  <body class="public-surface">
@@ -8637,7 +8637,7 @@ ${note}` : note;
8637
8637
  }
8638
8638
  const known = resolvedProjectDir !== null || sessionSidecarExists(target);
8639
8639
  const indicators2 = await fetchComposerIndicators(target);
8640
- return c.json({ sessionId: target, projectDir: resolvedProjectDir, transcriptSessionId, channelBinding: conflictingBinding(channel), known, sizeBytes: jsonlSizeBytes(projectDir2, target), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${target}`) ?? null, ...indicators2, ...readLevers() });
8640
+ return c.json({ sessionId: target, projectDir: resolvedProjectDir, transcriptSessionId, channelBinding: conflictingBinding(channel), known, sizeBytes: jsonlSizeBytes(projectDir2, target), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${target}`) ?? null, deliveryFailure: deps.deliveryFailureFor?.(`session:${target}`) ?? null, ...indicators2, ...readLevers() });
8641
8641
  }
8642
8642
  let account;
8643
8643
  try {
@@ -8670,7 +8670,7 @@ ${note}` : note;
8670
8670
  }
8671
8671
  }
8672
8672
  const indicators = await fetchComposerIndicators(sessionId);
8673
- return c.json({ sessionId, projectDir, sizeBytes: jsonlSizeBytes(projectDir, sessionId), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${sessionId}`) ?? null, ...indicators, ...readLevers(account) });
8673
+ return c.json({ sessionId, projectDir, sizeBytes: jsonlSizeBytes(projectDir, sessionId), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${sessionId}`) ?? null, deliveryFailure: deps.deliveryFailureFor?.(`session:${sessionId}`) ?? null, ...indicators, ...readLevers(account) });
8674
8674
  });
8675
8675
  app53.post("/permission-verdict", requireAdminSession, async (c) => {
8676
8676
  const body = await c.req.json().catch(() => null);
@@ -8914,6 +8914,33 @@ async function managerRcSpawn(opts) {
8914
8914
  const out = await r.json();
8915
8915
  return { sessionId: out.sessionId ?? opts.sessionId, pid: out.spawnedPid ?? null };
8916
8916
  }
8917
+ async function managerRcRecover(opts) {
8918
+ const auth = await ensureAuth();
8919
+ if (auth.status === "dead" || auth.status === "missing") {
8920
+ console.log(`[channel-pty-bridge] rc-recover-refused reason=auth-${auth.status} ${opts.logContext}`);
8921
+ return { error: "claude-auth-dead", status: 503 };
8922
+ }
8923
+ const res = await fetch(`${managerBase2()}/rc-spawn`, {
8924
+ method: "POST",
8925
+ headers: { "content-type": "application/json" },
8926
+ body: JSON.stringify({
8927
+ sessionId: opts.sessionId,
8928
+ recover: true,
8929
+ role: "admin",
8930
+ channel: "webchat",
8931
+ senderId: opts.webchatChannel.key,
8932
+ adminUserId: opts.adminUserId,
8933
+ webchatChannel: opts.webchatChannel
8934
+ })
8935
+ }).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
8936
+ if ("__throw" in res) {
8937
+ return { error: res.__throw, status: 0 };
8938
+ }
8939
+ const r = res;
8940
+ if (!r.ok) return { error: `manager-status-${r.status}`, status: r.status };
8941
+ const out = await r.json();
8942
+ return { live: out.live === true, classification: out.classification, recovered: out.recovered === true };
8943
+ }
8917
8944
  async function managerWriteInput(sessionId, text) {
8918
8945
  const res = await fetch(`${managerBase2()}/${sessionId}/input`, {
8919
8946
  method: "POST",
@@ -19439,12 +19466,12 @@ var InboundHub2 = class {
19439
19466
  * that never reached the agent:
19440
19467
  * - a still-queued message older than queuedThresholdMs WHILE a subscriber
19441
19468
  * is attached -> inbound-undelivered (the readiness handoff broke);
19442
- * - a still-queued message older than queuedThresholdMs with NO subscriber
19443
- * attached -> spawn-undelivered (the channel server never attached: spawn
19444
- * failed, or the session ended and never resumed);
19445
19469
  * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
19446
19470
  * channel server never confirmed receipt — the silent cold-start drop).
19447
- * Each message is emitted at most once (removed when emitted). */
19471
+ * A still-queued message with NO subscriber is NOT handled here: the gateway
19472
+ * owns that policy via staleQueued/dropQueued (Task-962) — a live session is
19473
+ * given a recovery window + respawn before any drop; a not-live one keeps the
19474
+ * cold-start drop. Each message is emitted at most once (removed when emitted). */
19448
19475
  sweep(now, t) {
19449
19476
  const events = [];
19450
19477
  for (const [key, s] of this.keys) {
@@ -19453,10 +19480,6 @@ var InboundHub2 = class {
19453
19480
  events.push({ kind: "inbound-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
19454
19481
  return false;
19455
19482
  }
19456
- if (e.state === "queued" && !s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
19457
- events.push({ kind: "spawn-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
19458
- return false;
19459
- }
19460
19483
  if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
19461
19484
  events.push({ kind: "drain-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.drainedAt });
19462
19485
  return false;
@@ -19466,6 +19489,32 @@ var InboundHub2 = class {
19466
19489
  }
19467
19490
  return events;
19468
19491
  }
19492
+ /** Task-962 — queued messages with NO subscriber attached, older than
19493
+ * thresholdMs. Read-only: it does NOT remove anything. The gateway drives
19494
+ * recovery/drop for these (a live session gets a redelivery window + a wedged
19495
+ * respawn; a not-live one keeps the immediate cold-start drop). */
19496
+ staleQueued(now, thresholdMs) {
19497
+ const out = [];
19498
+ for (const [key, s] of this.keys) {
19499
+ if (s.subscriber) continue;
19500
+ for (const e of s.inFlight) {
19501
+ if (e.state === "queued" && now - e.enqueuedAt > thresholdMs) {
19502
+ out.push({ key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
19503
+ }
19504
+ }
19505
+ }
19506
+ return out;
19507
+ }
19508
+ /** Task-962 — terminal-drop one still-queued in-flight message by id. Returns
19509
+ * whether a matching queued entry was found and removed. */
19510
+ dropQueued(key, messageId) {
19511
+ const s = this.keys.get(key);
19512
+ if (!s) return false;
19513
+ const i = s.inFlight.findIndex((e) => e.payload.messageId === messageId && e.state === "queued");
19514
+ if (i < 0) return false;
19515
+ s.inFlight.splice(i, 1);
19516
+ return true;
19517
+ }
19469
19518
  /** Whether a key currently has a live channel server attached. The gateway
19470
19519
  * uses this to decide whether an inbound needs a cold-start spawn/resume. */
19471
19520
  hasSubscriber(key) {
@@ -19550,6 +19599,8 @@ function createWebchatChannelRoutes(deps) {
19550
19599
  var QUEUED_THRESHOLD_MS2 = 3e4;
19551
19600
  var ACK_THRESHOLD_MS2 = 3e4;
19552
19601
  var SWEEP_INTERVAL_MS2 = 15e3;
19602
+ var RECOVER_TRIGGER_MS = Number(process.env.WEBCHAT_RECOVER_TRIGGER_MS ?? String(3e4));
19603
+ var DELIVER_FINAL_MS = Number(process.env.WEBCHAT_DELIVER_FINAL_MS ?? String(9e4));
19553
19604
  function channelIdleMs() {
19554
19605
  return Number(process.env.CHANNEL_PTY_IDLE_MS ?? String(5 * 6e4));
19555
19606
  }
@@ -19581,6 +19632,18 @@ var WebchatGateway = class _WebchatGateway {
19581
19632
  /** Task 756 — live public sessions, for the idle reaper + session-end review.
19582
19633
  * Admin sessions are never recorded here (they never reap). */
19583
19634
  publicSessions = /* @__PURE__ */ new Map();
19635
+ /** Task-962 — per-key recover context, set on each admin inbound so the
19636
+ * recovery pass can ask the manager to respawn a wedged live child. */
19637
+ recoverable = /* @__PURE__ */ new Map();
19638
+ /** Task-962 — per-session delivery-failure markers (set when redelivery is
19639
+ * exhausted; cleared on the next successful drain/ack). Surfaced on the
19640
+ * /session pointer the operator /chat polls. */
19641
+ deliveryFailures = /* @__PURE__ */ new Map();
19642
+ /** Task-962 — `${key}::${messageId}` for messages a recovery call has already
19643
+ * been issued for, so recovery is attempted at most once per message. */
19644
+ recoveryAttempted = /* @__PURE__ */ new Set();
19645
+ /** Task-962 — guards against overlapping async recovery passes. */
19646
+ recovering = false;
19584
19647
  seq = 0;
19585
19648
  /** Register a one-shot awaiter for the next reply on `key`. Resolves with the
19586
19649
  * reply text, or `{ timeout: true }` if no reply arrives within `timeoutMs`.
@@ -19702,6 +19765,7 @@ var WebchatGateway = class _WebchatGateway {
19702
19765
  if (count > 0) {
19703
19766
  console.error(`[webchat:inbound] op=drained key=${key} count=${count}`);
19704
19767
  this.lastDeliveredAt.set(key, now);
19768
+ this.deliveryFailures.delete(key);
19705
19769
  }
19706
19770
  },
19707
19771
  onReceived: (key, messageId) => {
@@ -19709,6 +19773,10 @@ var WebchatGateway = class _WebchatGateway {
19709
19773
  console.error(
19710
19774
  `[webchat:inbound] op=channel-received key=${key} messageId=${messageId}${found ? "" : " note=unknown"}`
19711
19775
  );
19776
+ if (found) {
19777
+ this.recoveryAttempted.delete(`${key}::${messageId}`);
19778
+ this.deliveryFailures.delete(key);
19779
+ }
19712
19780
  },
19713
19781
  onReply: (key, text) => this.handleReply(key, text),
19714
19782
  awaitPermissionVerdict: (p) => this.awaitPermissionVerdict(p, permissionVerdictTtlMs())
@@ -19727,9 +19795,85 @@ var WebchatGateway = class _WebchatGateway {
19727
19795
  }
19728
19796
  return events;
19729
19797
  }
19730
- /** Start the periodic reconciliation sweep in the long-lived UI process. */
19798
+ /** Start the periodic reconciliation sweep in the long-lived UI process. Each
19799
+ * tick runs the sync sweep (drain-/inbound-undelivered) AND the async
19800
+ * recovery pass for queued-no-subscriber messages (Task-962). */
19731
19801
  startSweeper(intervalMs = SWEEP_INTERVAL_MS2) {
19732
- return setInterval(() => this.reconcile(Date.now()), intervalMs);
19802
+ return setInterval(() => {
19803
+ const now = Date.now();
19804
+ this.reconcile(now);
19805
+ void this.recoverStalled(now);
19806
+ }, intervalMs);
19807
+ }
19808
+ /** Task-962 — the per-session delivery-failure marker for the /session pointer.
19809
+ * Non-null only when redelivery was exhausted for `key`; cleared on the next
19810
+ * successful drain/ack. */
19811
+ deliveryFailureFor(key) {
19812
+ return this.deliveryFailures.get(key) ?? null;
19813
+ }
19814
+ /** Task-962 — clear a key's delivery-failure marker (a later message drained,
19815
+ * or the operator dismissed/retried). */
19816
+ clearDeliveryFailure(key) {
19817
+ this.deliveryFailures.delete(key);
19818
+ }
19819
+ /** Task-962 — recovery-aware pass over queued-no-subscriber messages.
19820
+ * - age in [RECOVER_TRIGGER_MS, DELIVER_FINAL_MS): ask the manager to recover
19821
+ * the live session ONCE. busy → leave queued for the channel server's
19822
+ * auto-reconnect to drain; wedged → the manager respawns and the resumed
19823
+ * child re-binds the channel + drains. A not-live target (or no recover
19824
+ * dep/descriptor) is dropped now — the cold-start class, out of scope.
19825
+ * - age ≥ DELIVER_FINAL_MS: terminal-drop and record a delivery-failure
19826
+ * marker (redelivery exhausted).
19827
+ * Single-flight; safe to call every sweep tick. */
19828
+ async recoverStalled(now) {
19829
+ if (this.recovering) return;
19830
+ this.recovering = true;
19831
+ try {
19832
+ for (const c of this.hub.staleQueued(now, RECOVER_TRIGGER_MS)) {
19833
+ const attemptKey = `${c.key}::${c.messageId}`;
19834
+ if (c.ageMs >= DELIVER_FINAL_MS) {
19835
+ this.hub.dropQueued(c.key, c.messageId);
19836
+ this.recoveryAttempted.delete(attemptKey);
19837
+ this.deliveryFailures.set(c.key, { messageId: c.messageId, at: now });
19838
+ console.error(
19839
+ `[webchat:inbound] op=delivery-failed key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs}`
19840
+ );
19841
+ continue;
19842
+ }
19843
+ if (this.recoveryAttempted.has(attemptKey)) continue;
19844
+ const d = this.recoverable.get(c.key);
19845
+ if (!d || !this.deps.recoverLiveSession) {
19846
+ this.hub.dropQueued(c.key, c.messageId);
19847
+ console.error(
19848
+ `[webchat:inbound] op=spawn-undelivered key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs} reason=no-descriptor`
19849
+ );
19850
+ continue;
19851
+ }
19852
+ this.recoveryAttempted.add(attemptKey);
19853
+ let r;
19854
+ try {
19855
+ r = await this.deps.recoverLiveSession(d);
19856
+ } catch (err) {
19857
+ console.error(
19858
+ `[webchat:inbound] op=recover-error key=${keyDisplay(c.key)} messageId=${c.messageId} err=${err instanceof Error ? err.message : String(err)}`
19859
+ );
19860
+ continue;
19861
+ }
19862
+ if (!r.live) {
19863
+ this.hub.dropQueued(c.key, c.messageId);
19864
+ this.recoveryAttempted.delete(attemptKey);
19865
+ console.error(
19866
+ `[webchat:inbound] op=spawn-undelivered key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs} reason=not-live`
19867
+ );
19868
+ } else {
19869
+ console.error(
19870
+ `[webchat:inbound] op=recover key=${keyDisplay(c.key)} messageId=${c.messageId} classification=${r.classification ?? "unknown"} respawned=${r.recovered === true}`
19871
+ );
19872
+ }
19873
+ }
19874
+ } finally {
19875
+ this.recovering = false;
19876
+ }
19733
19877
  }
19734
19878
  /** Handle one inbound webchat message (browser POST → here). */
19735
19879
  async handleInbound(input) {
@@ -19744,6 +19888,15 @@ var WebchatGateway = class _WebchatGateway {
19744
19888
  console.error(
19745
19889
  `[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
19746
19890
  );
19891
+ if (input.role === "admin") {
19892
+ this.recoverable.set(input.key, {
19893
+ accountId: input.accountId,
19894
+ key: input.key,
19895
+ role: input.role,
19896
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
19897
+ ...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
19898
+ });
19899
+ }
19747
19900
  const existing = this.publicSessions.get(input.key);
19748
19901
  if (existing) existing.lastActivityAt = Date.now();
19749
19902
  if (!hadSubscriber && !this.spawning.has(input.key)) {
@@ -20313,6 +20466,25 @@ var webchatGateway = new WebchatGateway({
20313
20466
  }
20314
20467
  return { sessionId: result.sessionId };
20315
20468
  },
20469
+ // Task-962 — the recovery pass asks the manager to respawn a wedged already-
20470
+ // live admin session (rc-spawn recover:true). Resolve the session id the same
20471
+ // way ensureChannelSession does. Public sessions recover via the idle reaper,
20472
+ // so a non-admin descriptor is reported not-live (no respawn).
20473
+ recoverLiveSession: async ({ accountId, key, role, sessionId, adminUserId }) => {
20474
+ if (role !== "admin") return { live: false };
20475
+ const id = sessionId ?? adminSessionIdFor(accountId, key, adminUserId);
20476
+ const r = await managerRcRecover({
20477
+ sessionId: id,
20478
+ webchatChannel: { key, gatewayUrl: webchatGatewayUrl(), serverPath: webchatServerPath() },
20479
+ ...adminUserId !== void 0 ? { adminUserId } : {},
20480
+ logContext: `channel=webchat key=${key} recover`
20481
+ });
20482
+ if ("error" in r) {
20483
+ console.error(`[webchat:inbound] op=recover-failed key=${key} error=${r.error} status=${r.status}`);
20484
+ return { live: false };
20485
+ }
20486
+ return r;
20487
+ },
20316
20488
  // Task 756 — the public idle reaper deletes the manager PTY and dispatches the
20317
20489
  // session-end review (gated visitors), since the native path has no bridge
20318
20490
  // entry to drive them. Admin sessions are never recorded, so never reaped.
@@ -20641,6 +20813,7 @@ app52.route("/api/webchat", createWebchatRoutes({
20641
20813
  handleInbound: (input) => webchatGateway.handleInbound(input),
20642
20814
  // Task 940 — the permission relay's pointer read + verdict write.
20643
20815
  pendingPromptFor: (key) => webchatGateway.pendingPromptFor(key),
20816
+ deliveryFailureFor: (key) => webchatGateway.deliveryFailureFor(key),
20644
20817
  resolvePermissionVerdict: (key, requestId, behavior) => webchatGateway.resolvePermissionVerdict(key, requestId, behavior)
20645
20818
  }));
20646
20819
  app52.route("/api/webchat/greeting", webchat_greeting_default);
@@ -1 +0,0 @@
1
- import{L as e,x as t}from"./OperatorConversations-DPRRlS_z.js";import"./admin-types-CJrGd46U.js";import"./AdminShell-BsPsXvJv.js";import{n}from"./page-CExyEcvh.js";var r=e(),i=t();(0,r.createRoot)(document.getElementById(`root`)).render((0,i.jsx)(n,{}));