@rubytech/create-maxy-code 0.1.350 → 0.1.352

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 (108) hide show
  1. package/dist/index.js +14 -0
  2. package/package.json +2 -2
  3. package/payload/platform/config/brand.json +1 -1
  4. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  5. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  6. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  7. package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js +27 -10
  8. package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js.map +1 -1
  9. package/payload/platform/plugins/admin/skills/agent-builder/SKILL.md +107 -0
  10. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +103 -0
  11. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +40 -2
  12. package/payload/platform/plugins/connector/.claude-plugin/plugin.json +21 -0
  13. package/payload/platform/plugins/connector/PLUGIN.md +55 -0
  14. package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +193 -0
  15. package/payload/platform/plugins/connector/lib/mcp-spawn-tee/package.json +3 -0
  16. package/payload/platform/plugins/connector/mcp/dist/index.d.ts +2 -0
  17. package/payload/platform/plugins/connector/mcp/dist/index.d.ts.map +1 -0
  18. package/payload/platform/plugins/connector/mcp/dist/index.js +119 -0
  19. package/payload/platform/plugins/connector/mcp/dist/index.js.map +1 -0
  20. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +36 -0
  21. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -0
  22. package/payload/platform/plugins/connector/mcp/dist/lib/call.js +84 -0
  23. package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -0
  24. package/payload/platform/plugins/connector/mcp/dist/lib/redact.d.ts +10 -0
  25. package/payload/platform/plugins/connector/mcp/dist/lib/redact.d.ts.map +1 -0
  26. package/payload/platform/plugins/connector/mcp/dist/lib/redact.js +29 -0
  27. package/payload/platform/plugins/connector/mcp/dist/lib/redact.js.map +1 -0
  28. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +18 -0
  29. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -0
  30. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +94 -0
  31. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -0
  32. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts +25 -0
  33. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts.map +1 -0
  34. package/payload/platform/plugins/connector/mcp/dist/lib/store.js +63 -0
  35. package/payload/platform/plugins/connector/mcp/dist/lib/store.js.map +1 -0
  36. package/payload/platform/plugins/connector/mcp/dist/scripts/audit-connectors.d.ts +9 -0
  37. package/payload/platform/plugins/connector/mcp/dist/scripts/audit-connectors.d.ts.map +1 -0
  38. package/payload/platform/plugins/connector/mcp/dist/scripts/audit-connectors.js +63 -0
  39. package/payload/platform/plugins/connector/mcp/dist/scripts/audit-connectors.js.map +1 -0
  40. package/payload/platform/plugins/connector/mcp/package.json +10 -0
  41. package/payload/platform/plugins/docs/references/connector.md +33 -0
  42. package/payload/platform/plugins/docs/references/getting-started.md +1 -1
  43. package/payload/platform/scripts/check-agent-contract.mjs +97 -0
  44. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +5 -0
  46. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  47. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +6 -0
  48. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  49. package/payload/platform/services/claude-session-manager/dist/http-server.js +165 -88
  50. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  51. package/payload/platform/services/claude-session-manager/dist/index.js +8 -0
  52. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +21 -14
  54. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  55. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +12 -37
  56. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  57. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts +35 -0
  58. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts.map +1 -0
  59. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js +52 -0
  60. package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js.map +1 -0
  61. package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.d.ts +29 -0
  62. package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.d.ts.map +1 -0
  63. package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.js +121 -0
  64. package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.js.map +1 -0
  65. package/payload/platform/services/telegram-channel/package.json +20 -0
  66. package/payload/platform/templates/agents/admin/IDENTITY.md +4 -0
  67. package/payload/platform/templates/specialists/agents/citation-auditor.md +5 -1
  68. package/payload/platform/templates/specialists/agents/coding-assistant.md +8 -0
  69. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +4 -0
  70. package/payload/platform/templates/specialists/agents/content-producer.md +8 -0
  71. package/payload/platform/templates/specialists/agents/data-manager.md +9 -1
  72. package/payload/platform/templates/specialists/agents/database-operator.md +12 -0
  73. package/payload/platform/templates/specialists/agents/librarian.md +9 -1
  74. package/payload/platform/templates/specialists/agents/personal-assistant.md +8 -0
  75. package/payload/platform/templates/specialists/agents/project-manager.md +8 -0
  76. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +9 -1
  77. package/payload/platform/templates/specialists/agents/research-assistant.md +8 -0
  78. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +4 -2
  79. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +12 -0
  80. package/payload/server/public/assets/{AdminLoginScreens-BEzoTk56.js → AdminLoginScreens-lt72mYrz.js} +1 -1
  81. package/payload/server/public/assets/AdminShell-B-GeoG3j.js +1 -0
  82. package/payload/server/public/assets/{Checkbox-CvTFOczj.js → Checkbox-DoGMXVpF.js} +1 -1
  83. package/payload/server/public/assets/{OperatorConversations-CnP9Y6g2.css → OperatorConversations-C8G6Gfgu.css} +1 -1
  84. package/payload/server/public/assets/{admin-Bhxa1HOy.js → admin-DzLxv-nz.js} +1 -1
  85. package/payload/server/public/assets/admin-types-DJoj6VJv.js +1 -0
  86. package/payload/server/public/assets/{browser-BAQFuyma.js → browser-Cyi6v8BS.js} +1 -1
  87. package/payload/server/public/assets/{calendar-sSr6zUjW.js → calendar-CA3_Dm8j.js} +1 -1
  88. package/payload/server/public/assets/chat-B9aNK1FG.js +1 -0
  89. package/payload/server/public/assets/{data-CBrgiyPM.js → data-CrJZjlQ1.js} +1 -1
  90. package/payload/server/public/assets/{graph-DbGjFYPS.js → graph-CutaO0w1.js} +2 -2
  91. package/payload/server/public/assets/{graph-labels-IGIEr-uc.js → graph-labels-CH0sWvcA.js} +1 -1
  92. package/payload/server/public/assets/{operator-Ba5afDyM.js → operator-MASl7IqX.js} +1 -1
  93. package/payload/server/public/assets/page-CZy33qUH.js +30 -0
  94. package/payload/server/public/assets/{public-DikYWzOd.js → public-77Ubicd7.js} +1 -1
  95. package/payload/server/public/browser.html +4 -4
  96. package/payload/server/public/calendar.html +4 -4
  97. package/payload/server/public/chat.html +6 -6
  98. package/payload/server/public/data.html +4 -4
  99. package/payload/server/public/graph.html +6 -6
  100. package/payload/server/public/index.html +7 -7
  101. package/payload/server/public/operator.html +8 -8
  102. package/payload/server/public/public.html +6 -6
  103. package/payload/server/server.js +4755 -4595
  104. package/payload/server/public/assets/AdminShell-BG7YD4UO.js +0 -1
  105. package/payload/server/public/assets/admin-types-Dg11L4MQ.js +0 -1
  106. package/payload/server/public/assets/chat-ZH4Fsyu0.js +0 -1
  107. package/payload/server/public/assets/page-D7uwMUOy.js +0 -30
  108. /package/payload/server/public/assets/{OperatorConversations-DU8CqO-z.js → OperatorConversations-CfG1EYyP.js} +0 -0
@@ -9,7 +9,6 @@
9
9
  // DELETE /:id -> 204 (rmSync JSONL + subdir)
10
10
  // 409 if PTY still alive
11
11
  // 404 if no transcript on disk
12
- // POST /:id/input { text }
13
12
  // GET /:id/log -> { jsonlPath, sizeBytes, exists }
14
13
  // GET /:id/log?follow=1 -> streamed file content (current + new)
15
14
  // GET /:id/meta -> SessionMeta payload
@@ -21,17 +20,18 @@
21
20
  // manager does not know.
22
21
  import { Hono } from 'hono';
23
22
  import { stream } from 'hono/streaming';
24
- import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, mkdirSync, renameSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, mkdirSync, renameSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from 'node:fs';
25
24
  import { tmpdir } from 'node:os';
26
25
  import { buildWaChannelMcpServers, buildWaChannelArgv, waChannelMcpConfigPath, isDevChannelPrompt } from './wa-channel-mcp.js';
27
26
  import { buildWebchatChannelMcpServers, buildWebchatChannelArgv, webchatChannelMcpConfigPath } from './webchat-channel-mcp.js';
27
+ import { buildTelegramChannelMcpServers, buildTelegramChannelArgv, telegramChannelMcpConfigPath } from './telegram-channel-mcp.js';
28
28
  import { buildChannelMcpServers, buildChannelArgv, channelMcpConfigPath } from './channel-mcp.js';
29
29
  import { mergeTarget, isMultiTarget, targetKinds } from './channel-targets.js';
30
30
  import { randomUUID } from 'node:crypto';
31
31
  import { execFileSync } from 'node:child_process';
32
32
  import { buildRcChildEnv, reapplyAdminLevers } from './rc-daemon.js';
33
33
  import { isEffortLevel, isNewSessionEffortLevel, isPermissionMode, SONNET_MODEL } from '../../../lib/models/dist/index.js';
34
- import { spawnClaudeSession, stopSession, writeInputToPty, writeInterruptToPty, isLive, onPtyExit, livePtyCount, liveSessionIds, getPtyTrackerForTests, livePidForSession, classifyLiveness, priorExitedCountForSession, PERMISSION_MODES, openFdCount, registerRcSpawnPty, } from './pty-spawner.js';
34
+ import { spawnClaudeSession, stopSession, writeInterruptToPty, isLive, onPtyExit, livePtyCount, liveSessionIds, getPtyTrackerForTests, livePidForSession, classifyLiveness, priorExitedCountForSession, PERMISSION_MODES, openFdCount, registerRcSpawnPty, } from './pty-spawner.js';
35
35
  import { writeScopeRecord, removeScopeRecord } from './scope-record.js';
36
36
  import { writeReseatMarker, resolveReseatChain, clearReseatMarker } from './reseat-ledger.js';
37
37
  import { getScopeMainPid, defaultProcessSignals, defaultSystemctlRunner } from './systemd-scope.js';
@@ -40,7 +40,6 @@ import { resolveSettingsPermissionMode, resolveSettingsModel, resolveSettingsEff
40
40
  import { emitRcLife } from './rc-life.js';
41
41
  import { createRcChildLog } from './rc-child-log.js';
42
42
  import { readJsonlSession } from './jsonl-enumerator.js';
43
- import { watchInputSubmission, countUserEntries } from './input-postcondition.js';
44
43
  import { claudeStateRoot, projectSlugForCwd, jsonlPathForSessionId, sidecarPathForSessionId, permissionModeLogPathForSessionId, findExistingJsonlForSessionId, detectRemoteControlSlug, classifyBindBlocker, } from './jsonl-path.js';
45
44
  import { basename, dirname, join } from 'node:path';
46
45
  import { validateUserTitle } from './user-title-store.js';
@@ -156,6 +155,62 @@ function finitePositive(raw, fallback) {
156
155
  const n = Number(raw);
157
156
  return Number.isFinite(n) && n > 0 ? n : fallback;
158
157
  }
158
+ /** Task 1090 Phase 2 — the post-interrupt transcript-integrity check. Reads a
159
+ * bounded tail of the session JSONL and reports whether a `tool_use` block in
160
+ * that window has no matching `tool_result` after it — the signature of an
161
+ * interrupt that landed mid-tool-call, which would corrupt a later `--resume`.
162
+ * Bounded (last 64KB) so a large transcript never blocks the post-check; the
163
+ * window only needs the last turn's tool pairing. Returns false on any read
164
+ * error (no file yet, gone) — absence of evidence is not a dangling use. */
165
+ function tailHasDanglingToolUse(jsonlPath) {
166
+ try {
167
+ if (!existsSync(jsonlPath))
168
+ return false;
169
+ const size = statSync(jsonlPath).size;
170
+ const window = 64 * 1024;
171
+ const fd = openSync(jsonlPath, 'r');
172
+ try {
173
+ const start = size > window ? size - window : 0;
174
+ const len = size - start;
175
+ const buf = Buffer.alloc(len);
176
+ readSync(fd, buf, 0, len, start);
177
+ const lines = buf.toString('utf8').split('\n');
178
+ const used = new Set();
179
+ const resulted = new Set();
180
+ for (const line of lines) {
181
+ if (!line)
182
+ continue;
183
+ let o;
184
+ try {
185
+ o = JSON.parse(line);
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ const rec = o;
191
+ const content = rec.message?.content;
192
+ if (!Array.isArray(content))
193
+ continue;
194
+ for (const c of content) {
195
+ if (c.type === 'tool_use' && typeof c.id === 'string')
196
+ used.add(c.id);
197
+ if (c.type === 'tool_result' && typeof c.tool_use_id === 'string')
198
+ resulted.add(c.tool_use_id);
199
+ }
200
+ }
201
+ for (const id of used)
202
+ if (!resulted.has(id))
203
+ return true;
204
+ return false;
205
+ }
206
+ finally {
207
+ closeSync(fd);
208
+ }
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
159
214
  /** Resolve any of the 4 keys to the canonical (sessionId, row) pair.
160
215
  * Tries: intrinsic sessionId, bridgeSessionId, bridgeSuffix, numeric pid.
161
216
  * Returns undefined when nothing matches. */
@@ -1489,84 +1544,13 @@ export function buildHttpApp(deps) {
1489
1544
  // binding. Best-effort; the store logs its own failure.
1490
1545
  deps.waChannelStore.remove(sessionId);
1491
1546
  deps.webchatChannelStore?.remove(sessionId);
1547
+ deps.telegramChannelStore?.remove(sessionId);
1492
1548
  // Task 702 — the transcript is gone; its reattach hint must go too.
1493
1549
  removeScopeRecord(sessionsDir, sessionId, deps.logger);
1494
1550
  deps.logger(`purge sessionId=${sessionId.slice(0, 8)} bytes=${bytes}`);
1495
1551
  timed(deps.logger, 'DELETE', `/${id}`, 204, Date.now() - start);
1496
1552
  return c.body(null, 204);
1497
1553
  });
1498
- app.post('/:sessionId/input', async (c) => {
1499
- const start = Date.now();
1500
- const id = c.req.param('sessionId');
1501
- const row = resolveRow(deps.watcher, id);
1502
- const sessionId = row?.sessionId ?? id;
1503
- let body;
1504
- try {
1505
- body = await c.req.json();
1506
- }
1507
- catch {
1508
- timed(deps.logger, 'POST', `/${id}/input`, 400, Date.now() - start);
1509
- return c.json({ error: 'body-not-json' }, 400);
1510
- }
1511
- if (typeof body.text !== 'string') {
1512
- timed(deps.logger, 'POST', `/${id}/input`, 400, Date.now() - start);
1513
- return c.json({ error: 'text-required' }, 400);
1514
- }
1515
- const channel = row?.channel ?? null;
1516
- const result = writeInputToPty(sessionId, body.text, deps.logger, channel);
1517
- if (!result.ok) {
1518
- if (result.error === 'not-found') {
1519
- timed(deps.logger, 'POST', `/${id}/input`, 404, Date.now() - start);
1520
- return c.json({ error: 'session-not-found' }, 404);
1521
- }
1522
- if (result.error === 'exited') {
1523
- timed(deps.logger, 'POST', `/${id}/input`, 410, Date.now() - start);
1524
- return c.json({ error: 'session-exited' }, 410);
1525
- }
1526
- timed(deps.logger, 'POST', `/${id}/input`, 500, Date.now() - start);
1527
- return c.json({ error: 'pty-write-failed', detail: result.detail }, 500);
1528
- }
1529
- // Task 666 — the 200 means the write was accepted, not that a turn
1530
- // submitted. Fire a bounded post-condition poll (non-awaited) that confirms
1531
- // the PTY went `busy` or a new user JSONL entry landed, or emits
1532
- // `op=input-no-turn` so an accepted-but-unsubmitted input is visible without
1533
- // waiting for the 300s turn-timeout. The 200 is no longer the success signal.
1534
- // Finite-guard the tunables: a non-numeric env override would otherwise make
1535
- // windowMs/intervalMs NaN, so the elapse check never fires and the poll spins
1536
- // forever (a timer leak per /input). Fall back to the defaults instead.
1537
- const windowMs = finitePositive(process.env.INPUT_SUBMIT_POLL_MS, 8000);
1538
- const intervalMs = finitePositive(process.env.INPUT_SUBMIT_POLL_INTERVAL_MS, 250);
1539
- const postcondJsonlPath = jsonlPathForSessionId(deps.spawnCwd, sessionId);
1540
- const baselineUserCount = countUserEntries(postcondJsonlPath);
1541
- const pid = resolveRow(deps.watcher, id)?.pid ?? null;
1542
- void watchInputSubmission({
1543
- pid,
1544
- sessionId,
1545
- bytes: result.bytes ?? 0,
1546
- baselineUserCount,
1547
- deps: {
1548
- getPtyStatus: () => deps.watcher.getBySessionId(sessionId)?.status ?? null,
1549
- getUserEntryCount: () => countUserEntries(postcondJsonlPath),
1550
- getLastEntryAgeMs: () => {
1551
- try {
1552
- return Date.now() - statSync(postcondJsonlPath).mtimeMs;
1553
- }
1554
- catch {
1555
- return null;
1556
- }
1557
- },
1558
- now: () => Date.now(),
1559
- sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
1560
- log: deps.logger,
1561
- windowMs,
1562
- intervalMs,
1563
- },
1564
- }).catch((err) => {
1565
- deps.logger(`op=input-postcondition-error sessionId=${sessionId.slice(0, 8)} message=${err instanceof Error ? err.message : String(err)}`);
1566
- });
1567
- timed(deps.logger, 'POST', `/${id}/input`, 200, Date.now() - start);
1568
- return c.json({ ok: true, bytes: result.bytes }, 200);
1569
- });
1570
1554
  // Task 1090 Phase 1 — POST /:sessionId/interrupt writes one raw control byte
1571
1555
  // (ESC or Ctrl-C) to the PTY master via writeInterruptToPty: the soft
1572
1556
  // interrupt, session-preserving, distinct from /stop's SIGTERM recycle. The
@@ -1592,8 +1576,9 @@ export function buildHttpApp(deps) {
1592
1576
  return c.json({ error: 'byte-invalid' }, 400);
1593
1577
  }
1594
1578
  const byte = body.byte;
1579
+ const trigger = typeof body.trigger === 'string' ? body.trigger : 'manual';
1595
1580
  const intId = `${sessionId.slice(0, 8)}-${start}`;
1596
- deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=manual-spike`);
1581
+ deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=${trigger}`);
1597
1582
  const result = writeInterruptToPty(sessionId, byte, deps.logger);
1598
1583
  if (!result.ok) {
1599
1584
  if (result.error === 'not-found') {
@@ -1608,8 +1593,49 @@ export function buildHttpApp(deps) {
1608
1593
  return c.json({ error: 'pty-write-failed', detail: result.detail }, 500);
1609
1594
  }
1610
1595
  deps.logger(`[webchat-interrupt] op=interrupt-sent intId=${intId} byte=${byte} bytes=${result.bytes}`);
1596
+ // Task 1090 Phase 2 — confirm generation actually stopped, rather than
1597
+ // assuming the byte worked. The fs-watcher row status is `busy` while a turn
1598
+ // streams; poll it until it leaves `busy` (idle/gone) within the bounded
1599
+ // window. This is the gate the webchat send path waits on before delivering
1600
+ // a mid-turn message, so it is awaited (not fire-and-forget).
1601
+ const stopMs = finitePositive(process.env.MAXY_INTERRUPT_STOP_MS, 2500);
1602
+ const sentAt = Date.now();
1603
+ let stopped = false;
1604
+ while (Date.now() - sentAt < stopMs) {
1605
+ const st = deps.watcher.getBySessionId(sessionId)?.status;
1606
+ if (st !== 'busy') {
1607
+ stopped = true;
1608
+ break;
1609
+ }
1610
+ await new Promise((r) => setTimeout(r, 120));
1611
+ }
1612
+ const stoppedMs = stopped ? Date.now() - sentAt : null;
1613
+ if (stopped)
1614
+ deps.logger(`[webchat-interrupt] op=generation-stopped intId=${intId} stoppedMs=${stoppedMs}`);
1615
+ else
1616
+ deps.logger(`[webchat-interrupt] op=interrupt-ineffective intId=${intId} ranMs=${Date.now() - sentAt}`);
1617
+ // Non-awaited post-check (mirrors the /input postcondition watcher): a
1618
+ // delayed liveness probe catches a deferred death the synchronous return
1619
+ // cannot, and the tail reconcile surfaces a mid-tool-call interrupt that
1620
+ // would corrupt a later --resume. Both are no-event standing signals.
1621
+ const surviveMs = finitePositive(process.env.MAXY_INTERRUPT_SURVIVE_MS, 1500);
1622
+ const postPath = jsonlPathForSessionId(deps.spawnCwd, sessionId);
1623
+ void (async () => {
1624
+ await new Promise((r) => setTimeout(r, surviveMs));
1625
+ const liveRow = deps.watcher.getBySessionId(sessionId);
1626
+ const alive = !!liveRow && liveRow.status !== 'gone';
1627
+ deps.logger(`[webchat-interrupt] op=survives-interrupt intId=${intId} alive=${alive}`);
1628
+ if (!alive)
1629
+ deps.logger(`[webchat-interrupt] op=interrupt-killed-session intId=${intId} alive=false`);
1630
+ const dangling = tailHasDanglingToolUse(postPath);
1631
+ deps.logger(`[webchat-interrupt] op=tail-reconcile intId=${intId} danglingToolUse=${dangling}`);
1632
+ if (dangling)
1633
+ deps.logger(`[webchat-interrupt] op=interrupt-dangling-tooluse intId=${intId}`);
1634
+ })().catch((err) => {
1635
+ deps.logger(`[webchat-interrupt] op=postcheck-error intId=${intId} message=${err instanceof Error ? err.message : String(err)}`);
1636
+ });
1611
1637
  timed(deps.logger, 'POST', `/${id}/interrupt`, 200, Date.now() - start);
1612
- return c.json({ ok: true, intId, byte, bytes: result.bytes }, 200);
1638
+ return c.json({ ok: true, intId, byte, bytes: result.bytes, stopped, stoppedMs }, 200);
1613
1639
  });
1614
1640
  app.get('/:sessionId/log', (c) => {
1615
1641
  const start = Date.now();
@@ -1915,6 +1941,18 @@ export function buildHttpApp(deps) {
1915
1941
  typeof webchatChannelRaw.serverPath === 'string'
1916
1942
  ? webchatChannelRaw
1917
1943
  : undefined;
1944
+ // Optional telegram native-channel attach, the telegram twin of
1945
+ // webchatChannel. The binding field is senderId (not key). All three fields
1946
+ // must be present strings or the attach is skipped. A session is whatsapp
1947
+ // XOR webchat XOR telegram, so at most one channel block below fires.
1948
+ const telegramChannelRaw = body.telegramChannel;
1949
+ const telegramChannel = telegramChannelRaw !== null &&
1950
+ typeof telegramChannelRaw === 'object' &&
1951
+ typeof telegramChannelRaw.senderId === 'string' &&
1952
+ typeof telegramChannelRaw.gatewayUrl === 'string' &&
1953
+ typeof telegramChannelRaw.serverPath === 'string'
1954
+ ? telegramChannelRaw
1955
+ : undefined;
1918
1956
  // Task-962 — recover an already-live but unresponsive webchat session. When
1919
1957
  // set, the already-live branch classifies the child busy vs wedged and
1920
1958
  // respawns ONLY a wedged one (busy → idempotent-busy reply; not-live → a
@@ -2013,12 +2051,11 @@ export function buildHttpApp(deps) {
2013
2051
  }
2014
2052
  }
2015
2053
  // Task 702 — never run two processes on one sessionId. If we already own a
2016
- // live PTY for this id, the spawn is a no-op (a bridge that lost its
2017
- // in-memory index after a restart re-asks; the live PTY takes the next
2018
- // /input). If a reattached survivor scope holds the id (status=detached, a
2019
- // process this manager cannot own after its own restart and cannot feed
2020
- // via /input), stop that scope first so the fresh --resume below is the
2021
- // only process.
2054
+ // live PTY for this id, the spawn is a no-op (a caller that lost its
2055
+ // in-memory state after a restart re-asks; the live PTY takes the next
2056
+ // turn). If a reattached survivor scope holds the id (status=detached, a
2057
+ // process this manager cannot own after its own restart and cannot drive),
2058
+ // stop that scope first so the fresh --resume below is the only process.
2022
2059
  // Task-962 — set on a wedged-child respawn so the spawn-success response
2023
2060
  // below reports the recovery verdict to the gateway.
2024
2061
  let recoverOutcome = null;
@@ -2188,6 +2225,7 @@ export function buildHttpApp(deps) {
2188
2225
  // web), and the two are fanned by one unified channel server below.
2189
2226
  let effectiveWaChannel;
2190
2227
  let effectiveWebchatChannel;
2228
+ let effectiveTelegramChannel;
2191
2229
  if (sessionId) {
2192
2230
  if (waChannel) {
2193
2231
  effectiveWaChannel = waChannel;
@@ -2211,6 +2249,17 @@ export function buildHttpApp(deps) {
2211
2249
  deps.logger(`[rc-spawn] op=webchat-channel-rebind sessionId=${sessionId.slice(0, 8)} key=${stored.key} source=store`);
2212
2250
  }
2213
2251
  }
2252
+ if (telegramChannel) {
2253
+ effectiveTelegramChannel = telegramChannel;
2254
+ deps.telegramChannelStore?.set(sessionId, telegramChannel);
2255
+ }
2256
+ else {
2257
+ const stored = deps.telegramChannelStore?.get(sessionId);
2258
+ if (stored) {
2259
+ effectiveTelegramChannel = { senderId: stored.senderId, gatewayUrl: stored.gatewayUrl, serverPath: stored.serverPath };
2260
+ deps.logger(`[rc-spawn] op=telegram-channel-rebind sessionId=${sessionId.slice(0, 8)} senderId=${stored.senderId} source=store`);
2261
+ }
2262
+ }
2214
2263
  }
2215
2264
  // Task 933 — read the persisted sidecar once so the identity gate below can
2216
2265
  // resolve role + name + adminUserId from disk when the caller omitted them
@@ -2237,6 +2286,7 @@ export function buildHttpApp(deps) {
2237
2286
  targetSet = mergeTarget(targetSet, { kind: 'webchat', ...effectiveWebchatChannel });
2238
2287
  let waPath = null;
2239
2288
  let webchatPath = null;
2289
+ let telegramPath = null;
2240
2290
  if (sessionId && isMultiTarget(targetSet)) {
2241
2291
  const unifiedServers = buildChannelMcpServers({ sessionId, targets: targetSet });
2242
2292
  const unifiedPath = channelMcpConfigPath(sessionId, tmpdir());
@@ -2288,6 +2338,29 @@ export function buildHttpApp(deps) {
2288
2338
  deps.logger(`[rc-spawn] op=webchat-channel-write-failed err=${JSON.stringify(err instanceof Error ? err.message : String(err))}`);
2289
2339
  }
2290
2340
  }
2341
+ // Telegram is a standalone per-kind channel (not in the unified target
2342
+ // set). A session is whatsapp XOR webchat XOR telegram, so the telegram
2343
+ // block fires only when neither wa nor webchat is bound, so exactly one
2344
+ // channel argv block ever runs.
2345
+ if (effectiveTelegramChannel && sessionId && !effectiveWaChannel && !effectiveWebchatChannel) {
2346
+ const telegramServers = buildTelegramChannelMcpServers({
2347
+ sessionId,
2348
+ senderId: effectiveTelegramChannel.senderId,
2349
+ gatewayUrl: effectiveTelegramChannel.gatewayUrl,
2350
+ serverPath: effectiveTelegramChannel.serverPath,
2351
+ role: rcRole,
2352
+ });
2353
+ telegramPath = telegramChannelMcpConfigPath(sessionId, tmpdir());
2354
+ try {
2355
+ writeFileSync(telegramPath, JSON.stringify({ mcpServers: telegramServers }, null, 2), 'utf8');
2356
+ argv.push(...buildTelegramChannelArgv(telegramPath));
2357
+ deps.logger(`[rc-spawn] op=telegram-channel senderId=${effectiveTelegramChannel.senderId} gateway=${effectiveTelegramChannel.gatewayUrl} path=${telegramPath}`);
2358
+ }
2359
+ catch (err) {
2360
+ telegramPath = null;
2361
+ deps.logger(`[rc-spawn] op=telegram-channel-write-failed err=${JSON.stringify(err instanceof Error ? err.message : String(err))}`);
2362
+ }
2363
+ }
2291
2364
  }
2292
2365
  // Task 889 — a webchat-bound admin rc session is the /chat admin surface.
2293
2366
  // Inject the authenticated-identity line (resolved name, or the explicit
@@ -2444,6 +2517,10 @@ export function buildHttpApp(deps) {
2444
2517
  if (sessionId && deps.webchatChannelStore?.get(sessionId) && webchatPath === null) {
2445
2518
  deps.logger(`[rc-spawn] op=webchat-channel-binding-lost sessionId=${sessionId} pid=${pty.pid} reason=resume-without-mcp-config`);
2446
2519
  }
2520
+ // The same post-condition for the telegram binding.
2521
+ if (sessionId && deps.telegramChannelStore?.get(sessionId) && telegramPath === null) {
2522
+ deps.logger(`[rc-spawn] op=telegram-channel-binding-lost sessionId=${sessionId} pid=${pty.pid} reason=resume-without-mcp-config`);
2523
+ }
2447
2524
  // Tracker registration — closes the leak gap the Task 552 path opened.
2448
2525
  // For fresh spawns without a caller-supplied sessionId we register the
2449
2526
  // tracker under the unitToken so the entry has a stable key; the
@@ -2479,7 +2556,7 @@ export function buildHttpApp(deps) {
2479
2556
  // a failed classification sidecar must not tear it down, so it logs and the
2480
2557
  // session keeps running (the pre-fix reader-blind behaviour, no worse).
2481
2558
  const classifyChannel = rcRole !== undefined ? adminChannel : undefined;
2482
- const classifySenderId = adminSenderId ?? effectiveWaChannel?.senderId ?? effectiveWebchatChannel?.key;
2559
+ const classifySenderId = adminSenderId ?? effectiveWaChannel?.senderId ?? effectiveWebchatChannel?.key ?? effectiveTelegramChannel?.senderId;
2483
2560
  if (sessionId && classifyChannel && classifySenderId) {
2484
2561
  const classifySidecarPath = sidecarPathForSessionId(deps.spawnCwd, sessionId);
2485
2562
  if (!existsSync(classifySidecarPath)) {
@@ -2594,8 +2671,8 @@ export function buildHttpApp(deps) {
2594
2671
  // no literal spaces, so match on the ANSI/space-stripped letter stream.
2595
2672
  // Bounded startup keystroke only — inbound message submission stays a
2596
2673
  // channel event (the whole point of the migration). Armed only when a
2597
- // native channel (waChannel or webchatChannel, Task 772) is attached,
2598
- // fired at most once the prompt text is channel-agnostic.
2674
+ // native channel (waChannel, webchatChannel, or telegramChannel) is
2675
+ // attached, fired at most once. The prompt text is channel-agnostic.
2599
2676
  let devChannelConfirmed = false;
2600
2677
  let devChannelScan = '';
2601
2678
  let resolveStdoutSlug = () => { };
@@ -2604,7 +2681,7 @@ export function buildHttpApp(deps) {
2604
2681
  pty.onData((data) => {
2605
2682
  stdoutBytes += data.length;
2606
2683
  childLog.write(data);
2607
- if ((effectiveWaChannel || effectiveWebchatChannel) && !devChannelConfirmed) {
2684
+ if ((effectiveWaChannel || effectiveWebchatChannel || effectiveTelegramChannel) && !devChannelConfirmed) {
2608
2685
  devChannelScan += data;
2609
2686
  if (devChannelScan.length > 4096)
2610
2687
  devChannelScan = devChannelScan.slice(devChannelScan.length - 4096);