@rubytech/create-maxy-code 0.1.349 → 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/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +544 -0
- package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +170 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +68 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +108 -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/scripts/lib/provision-account-dir.sh +22 -3
- 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 +184 -63
- 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 +27 -9
- 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 +18 -25
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +16 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.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/platform/templates/account-schema/SCHEMA.md +54 -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-CrJZjlQ1.js +1 -0
- 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 +4657 -4482
- 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/data-CH6GNBO3.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,13 +1544,21 @@ 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
|
-
|
|
1554
|
+
// Task 1090 Phase 1 — POST /:sessionId/interrupt writes one raw control byte
|
|
1555
|
+
// (ESC or Ctrl-C) to the PTY master via writeInterruptToPty: the soft
|
|
1556
|
+
// interrupt, session-preserving, distinct from /stop's SIGTERM recycle. The
|
|
1557
|
+
// byte is a body parameter so the device spike can test both candidates
|
|
1558
|
+
// against one live session. This logs the requested/sent lifecycle; the
|
|
1559
|
+
// deeper post-conditions (generation-stopped, survival, tail-reconcile) are
|
|
1560
|
+
// observed manually during the spike and engineered in Phase 2 on a GO.
|
|
1561
|
+
app.post('/:sessionId/interrupt', async (c) => {
|
|
1499
1562
|
const start = Date.now();
|
|
1500
1563
|
const id = c.req.param('sessionId');
|
|
1501
1564
|
const row = resolveRow(deps.watcher, id);
|
|
@@ -1505,67 +1568,74 @@ export function buildHttpApp(deps) {
|
|
|
1505
1568
|
body = await c.req.json();
|
|
1506
1569
|
}
|
|
1507
1570
|
catch {
|
|
1508
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1571
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 400, Date.now() - start);
|
|
1509
1572
|
return c.json({ error: 'body-not-json' }, 400);
|
|
1510
1573
|
}
|
|
1511
|
-
if (
|
|
1512
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1513
|
-
return c.json({ error: '
|
|
1574
|
+
if (body.byte !== 'esc' && body.byte !== 'ctrl-c') {
|
|
1575
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 400, Date.now() - start);
|
|
1576
|
+
return c.json({ error: 'byte-invalid' }, 400);
|
|
1514
1577
|
}
|
|
1515
|
-
const
|
|
1516
|
-
const
|
|
1578
|
+
const byte = body.byte;
|
|
1579
|
+
const trigger = typeof body.trigger === 'string' ? body.trigger : 'manual';
|
|
1580
|
+
const intId = `${sessionId.slice(0, 8)}-${start}`;
|
|
1581
|
+
deps.logger(`[webchat-interrupt] op=interrupt-requested intId=${intId} sessionId=${sessionId.slice(0, 8)} trigger=${trigger}`);
|
|
1582
|
+
const result = writeInterruptToPty(sessionId, byte, deps.logger);
|
|
1517
1583
|
if (!result.ok) {
|
|
1518
1584
|
if (result.error === 'not-found') {
|
|
1519
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1585
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 404, Date.now() - start);
|
|
1520
1586
|
return c.json({ error: 'session-not-found' }, 404);
|
|
1521
1587
|
}
|
|
1522
1588
|
if (result.error === 'exited') {
|
|
1523
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1589
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 410, Date.now() - start);
|
|
1524
1590
|
return c.json({ error: 'session-exited' }, 410);
|
|
1525
1591
|
}
|
|
1526
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1592
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 500, Date.now() - start);
|
|
1527
1593
|
return c.json({ error: 'pty-write-failed', detail: result.detail }, 500);
|
|
1528
1594
|
}
|
|
1529
|
-
|
|
1530
|
-
//
|
|
1531
|
-
// the
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
deps.logger(`op=
|
|
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)}`);
|
|
1566
1636
|
});
|
|
1567
|
-
timed(deps.logger, 'POST', `/${id}/
|
|
1568
|
-
return c.json({ ok: true, bytes: result.bytes }, 200);
|
|
1637
|
+
timed(deps.logger, 'POST', `/${id}/interrupt`, 200, Date.now() - start);
|
|
1638
|
+
return c.json({ ok: true, intId, byte, bytes: result.bytes, stopped, stoppedMs }, 200);
|
|
1569
1639
|
});
|
|
1570
1640
|
app.get('/:sessionId/log', (c) => {
|
|
1571
1641
|
const start = Date.now();
|
|
@@ -1871,6 +1941,18 @@ export function buildHttpApp(deps) {
|
|
|
1871
1941
|
typeof webchatChannelRaw.serverPath === 'string'
|
|
1872
1942
|
? webchatChannelRaw
|
|
1873
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;
|
|
1874
1956
|
// Task-962 — recover an already-live but unresponsive webchat session. When
|
|
1875
1957
|
// set, the already-live branch classifies the child busy vs wedged and
|
|
1876
1958
|
// respawns ONLY a wedged one (busy → idempotent-busy reply; not-live → a
|
|
@@ -1969,12 +2051,11 @@ export function buildHttpApp(deps) {
|
|
|
1969
2051
|
}
|
|
1970
2052
|
}
|
|
1971
2053
|
// Task 702 — never run two processes on one sessionId. If we already own a
|
|
1972
|
-
// live PTY for this id, the spawn is a no-op (a
|
|
1973
|
-
// in-memory
|
|
1974
|
-
//
|
|
1975
|
-
// process this manager cannot own after its own restart and cannot
|
|
1976
|
-
//
|
|
1977
|
-
// 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.
|
|
1978
2059
|
// Task-962 — set on a wedged-child respawn so the spawn-success response
|
|
1979
2060
|
// below reports the recovery verdict to the gateway.
|
|
1980
2061
|
let recoverOutcome = null;
|
|
@@ -2144,6 +2225,7 @@ export function buildHttpApp(deps) {
|
|
|
2144
2225
|
// web), and the two are fanned by one unified channel server below.
|
|
2145
2226
|
let effectiveWaChannel;
|
|
2146
2227
|
let effectiveWebchatChannel;
|
|
2228
|
+
let effectiveTelegramChannel;
|
|
2147
2229
|
if (sessionId) {
|
|
2148
2230
|
if (waChannel) {
|
|
2149
2231
|
effectiveWaChannel = waChannel;
|
|
@@ -2167,6 +2249,17 @@ export function buildHttpApp(deps) {
|
|
|
2167
2249
|
deps.logger(`[rc-spawn] op=webchat-channel-rebind sessionId=${sessionId.slice(0, 8)} key=${stored.key} source=store`);
|
|
2168
2250
|
}
|
|
2169
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
|
+
}
|
|
2170
2263
|
}
|
|
2171
2264
|
// Task 933 — read the persisted sidecar once so the identity gate below can
|
|
2172
2265
|
// resolve role + name + adminUserId from disk when the caller omitted them
|
|
@@ -2193,6 +2286,7 @@ export function buildHttpApp(deps) {
|
|
|
2193
2286
|
targetSet = mergeTarget(targetSet, { kind: 'webchat', ...effectiveWebchatChannel });
|
|
2194
2287
|
let waPath = null;
|
|
2195
2288
|
let webchatPath = null;
|
|
2289
|
+
let telegramPath = null;
|
|
2196
2290
|
if (sessionId && isMultiTarget(targetSet)) {
|
|
2197
2291
|
const unifiedServers = buildChannelMcpServers({ sessionId, targets: targetSet });
|
|
2198
2292
|
const unifiedPath = channelMcpConfigPath(sessionId, tmpdir());
|
|
@@ -2244,6 +2338,29 @@ export function buildHttpApp(deps) {
|
|
|
2244
2338
|
deps.logger(`[rc-spawn] op=webchat-channel-write-failed err=${JSON.stringify(err instanceof Error ? err.message : String(err))}`);
|
|
2245
2339
|
}
|
|
2246
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
|
+
}
|
|
2247
2364
|
}
|
|
2248
2365
|
// Task 889 — a webchat-bound admin rc session is the /chat admin surface.
|
|
2249
2366
|
// Inject the authenticated-identity line (resolved name, or the explicit
|
|
@@ -2400,6 +2517,10 @@ export function buildHttpApp(deps) {
|
|
|
2400
2517
|
if (sessionId && deps.webchatChannelStore?.get(sessionId) && webchatPath === null) {
|
|
2401
2518
|
deps.logger(`[rc-spawn] op=webchat-channel-binding-lost sessionId=${sessionId} pid=${pty.pid} reason=resume-without-mcp-config`);
|
|
2402
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
|
+
}
|
|
2403
2524
|
// Tracker registration — closes the leak gap the Task 552 path opened.
|
|
2404
2525
|
// For fresh spawns without a caller-supplied sessionId we register the
|
|
2405
2526
|
// tracker under the unitToken so the entry has a stable key; the
|
|
@@ -2435,7 +2556,7 @@ export function buildHttpApp(deps) {
|
|
|
2435
2556
|
// a failed classification sidecar must not tear it down, so it logs and the
|
|
2436
2557
|
// session keeps running (the pre-fix reader-blind behaviour, no worse).
|
|
2437
2558
|
const classifyChannel = rcRole !== undefined ? adminChannel : undefined;
|
|
2438
|
-
const classifySenderId = adminSenderId ?? effectiveWaChannel?.senderId ?? effectiveWebchatChannel?.key;
|
|
2559
|
+
const classifySenderId = adminSenderId ?? effectiveWaChannel?.senderId ?? effectiveWebchatChannel?.key ?? effectiveTelegramChannel?.senderId;
|
|
2439
2560
|
if (sessionId && classifyChannel && classifySenderId) {
|
|
2440
2561
|
const classifySidecarPath = sidecarPathForSessionId(deps.spawnCwd, sessionId);
|
|
2441
2562
|
if (!existsSync(classifySidecarPath)) {
|
|
@@ -2550,8 +2671,8 @@ export function buildHttpApp(deps) {
|
|
|
2550
2671
|
// no literal spaces, so match on the ANSI/space-stripped letter stream.
|
|
2551
2672
|
// Bounded startup keystroke only — inbound message submission stays a
|
|
2552
2673
|
// channel event (the whole point of the migration). Armed only when a
|
|
2553
|
-
// native channel (waChannel
|
|
2554
|
-
// fired at most once
|
|
2674
|
+
// native channel (waChannel, webchatChannel, or telegramChannel) is
|
|
2675
|
+
// attached, fired at most once. The prompt text is channel-agnostic.
|
|
2555
2676
|
let devChannelConfirmed = false;
|
|
2556
2677
|
let devChannelScan = '';
|
|
2557
2678
|
let resolveStdoutSlug = () => { };
|
|
@@ -2560,7 +2681,7 @@ export function buildHttpApp(deps) {
|
|
|
2560
2681
|
pty.onData((data) => {
|
|
2561
2682
|
stdoutBytes += data.length;
|
|
2562
2683
|
childLog.write(data);
|
|
2563
|
-
if ((effectiveWaChannel || effectiveWebchatChannel) && !devChannelConfirmed) {
|
|
2684
|
+
if ((effectiveWaChannel || effectiveWebchatChannel || effectiveTelegramChannel) && !devChannelConfirmed) {
|
|
2564
2685
|
devChannelScan += data;
|
|
2565
2686
|
if (devChannelScan.length > 4096)
|
|
2566
2687
|
devChannelScan = devChannelScan.slice(devChannelScan.length - 4096);
|