@rubytech/create-maxy-code 0.1.350 → 0.1.351
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +14 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/skills/agent-builder/SKILL.md +107 -0
- package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +103 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +165 -88
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +8 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +21 -14
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +12 -37
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts +35 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js +52 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-mcp.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.d.ts +29 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.js +121 -0
- package/payload/platform/services/claude-session-manager/dist/telegram-channel-store.js.map +1 -0
- package/payload/platform/services/telegram-channel/package.json +20 -0
- package/payload/server/public/assets/{AdminLoginScreens-BEzoTk56.js → AdminLoginScreens-B1cvICYI.js} +1 -1
- package/payload/server/public/assets/AdminShell-B-GeoG3j.js +1 -0
- package/payload/server/public/assets/{Checkbox-CvTFOczj.js → Checkbox-DoGMXVpF.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-CnP9Y6g2.css → OperatorConversations-C8G6Gfgu.css} +1 -1
- package/payload/server/public/assets/{admin-Bhxa1HOy.js → admin-DF9JDP6a.js} +1 -1
- package/payload/server/public/assets/{browser-BAQFuyma.js → browser-Cyi6v8BS.js} +1 -1
- package/payload/server/public/assets/{calendar-sSr6zUjW.js → calendar-CA3_Dm8j.js} +1 -1
- package/payload/server/public/assets/chat-C4hHkcTu.js +1 -0
- package/payload/server/public/assets/{data-CBrgiyPM.js → data-CrJZjlQ1.js} +1 -1
- package/payload/server/public/assets/{graph-DbGjFYPS.js → graph-CutaO0w1.js} +2 -2
- package/payload/server/public/assets/{graph-labels-IGIEr-uc.js → graph-labels-CH0sWvcA.js} +1 -1
- package/payload/server/public/assets/{operator-Ba5afDyM.js → operator-CqKk6Twn.js} +1 -1
- package/payload/server/public/assets/page-Bfm1FN4n.js +30 -0
- package/payload/server/public/assets/{public-DikYWzOd.js → public-D5lkOp3H.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +4609 -4478
- package/payload/server/public/assets/AdminShell-BG7YD4UO.js +0 -1
- package/payload/server/public/assets/chat-ZH4Fsyu0.js +0 -1
- package/payload/server/public/assets/page-D7uwMUOy.js +0 -30
- /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,
|
|
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
|
|
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
|
|
2017
|
-
// in-memory
|
|
2018
|
-
//
|
|
2019
|
-
// process this manager cannot own after its own restart and cannot
|
|
2020
|
-
//
|
|
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
|
|
2598
|
-
// fired at most once
|
|
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);
|