@rubytech/create-maxy-code 0.1.575 → 0.1.577
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/__tests__/preserve-house-cloudflare.test.js +2 -72
- package/dist/index.js +8 -59
- package/dist/preserve-house-cloudflare.js +2 -49
- package/package.json +1 -1
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +32 -1
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
- package/payload/platform/lib/active-rules/src/index.ts +36 -2
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
- package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
- package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
- package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
- package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
- package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -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 +29 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/server.js +670 -155
- package/dist/__tests__/converge-client-admins.test.js +0 -50
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { admitsWebhookPath, botIdFromToken, persistTelegramBot, CANONICAL_TELEGRAM_PREFIX, } from "../lib/account-write.js";
|
|
4
|
+
import { setWebhook, getWebhookInfo, webhookVerdict } from "../lib/telegram.js";
|
|
5
|
+
import { configDirName, resolveSecretFilePath } from "../lib/secret-path.js";
|
|
6
|
+
import { accountDirFor, isActiveAgentSlug } from "../lib/agent-active.js";
|
|
7
|
+
import { buildWebhookUrl } from "../lib/webhook-url.js";
|
|
8
|
+
const TAG = "[telegram-setup]";
|
|
9
|
+
const fail = (text) => ({ text, isError: true });
|
|
10
|
+
/** Register or re-register one bot's webhook.
|
|
11
|
+
*
|
|
12
|
+
* Task 2560 — the order is load-bearing. The path guard runs before Telegram is
|
|
13
|
+
* touched, so a refused path leaves no registration behind. The freshness stamp
|
|
14
|
+
* is taken immediately before setWebhook so the probe can tell an error
|
|
15
|
+
* Telegram saw since THIS registration from one it kept from the previous URL.
|
|
16
|
+
* The persist runs LAST: a registration that cannot be proved reachable leaves
|
|
17
|
+
* nothing on disk. */
|
|
18
|
+
export async function webhookRegister(params, deps) {
|
|
19
|
+
const { botToken, webhookUrl, role, agent, adminUsers, dmPolicy, allowFrom } = params;
|
|
20
|
+
const { platformRoot, accountId, log } = deps;
|
|
21
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
22
|
+
const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1000));
|
|
23
|
+
const secretPathFor = deps.secretPathFor ?? resolveSecretFilePath;
|
|
24
|
+
// Task 2365 — the account comes first, because the secret path and the
|
|
25
|
+
// registered URL are both per-account now and neither can be built without
|
|
26
|
+
// it. This guard used to sit after setWebhook, where a missing ACCOUNT_ID
|
|
27
|
+
// still left a registered webhook behind.
|
|
28
|
+
if (!platformRoot || !accountId) {
|
|
29
|
+
log(`${TAG} op=token-write-rejected reason=no-account-env`);
|
|
30
|
+
return fail(`PLATFORM_ROOT/ACCOUNT_ID not in MCP env — cannot persist the bot token to account.json`);
|
|
31
|
+
}
|
|
32
|
+
// Task 2368 — the bot id is the token's colon-prefix. It keys the secret
|
|
33
|
+
// file, the registered URL and the config entry, so a malformed token has
|
|
34
|
+
// nowhere to land and is refused before anything is written or registered.
|
|
35
|
+
const botId = botIdFromToken(botToken);
|
|
36
|
+
if (!botId) {
|
|
37
|
+
log(`${TAG} op=token-write-rejected reason=malformed-token`);
|
|
38
|
+
return fail(`Bot token is malformed — it must look like 123456:ABC-DEF…`);
|
|
39
|
+
}
|
|
40
|
+
// A public bot must name the agent it answers as. An admin bot names none:
|
|
41
|
+
// it reads agents/admin by construction, so an `agent` field there would be
|
|
42
|
+
// stored and never consumed.
|
|
43
|
+
if (role === "public") {
|
|
44
|
+
if (!agent || agent.length === 0) {
|
|
45
|
+
log(`${TAG} op=token-write-rejected reason=no-agent botId=${botId}`);
|
|
46
|
+
return fail(`A public bot must name the agent it answers as. Pass \`agent\` with one of this account's active agent slugs.`);
|
|
47
|
+
}
|
|
48
|
+
if (!isActiveAgentSlug(accountDirFor(platformRoot, accountId), agent)) {
|
|
49
|
+
log(`${TAG} op=token-write-rejected reason=agent-inactive botId=${botId} agent=${agent}`);
|
|
50
|
+
return fail(`Agent "${agent}" is not an active agent on this account, so a bot cannot answer as it.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Task 2560 — exactly one path reaches the handler. Asserted before Telegram
|
|
54
|
+
// is touched, so a refused path leaves no registration behind.
|
|
55
|
+
const webhookPath = (() => {
|
|
56
|
+
try {
|
|
57
|
+
return new URL(webhookUrl).pathname;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return webhookUrl;
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
const admitted = admitsWebhookPath(webhookPath);
|
|
64
|
+
log(`${TAG} op=webhook-register path=${webhookPath} botId=${botId} account=${accountId} admitted=${admitted}`);
|
|
65
|
+
if (!admitted) {
|
|
66
|
+
return fail(`Webhook path "${webhookPath}" is not ${CANONICAL_TELEGRAM_PREFIX} — that is the only path this ` +
|
|
67
|
+
`install serves, so anything else 404s on every inbound. Use a URL whose path is exactly ${CANONICAL_TELEGRAM_PREFIX}`);
|
|
68
|
+
}
|
|
69
|
+
const secretFilePath = secretPathFor(botId);
|
|
70
|
+
mkdirSync(dirname(secretFilePath), { recursive: true, mode: 0o700 });
|
|
71
|
+
// Task 2368 — the URL Telegram is actually given. buildWebhookUrl only
|
|
72
|
+
// touches the query, so the admitted check above still describes the path
|
|
73
|
+
// that gets registered.
|
|
74
|
+
const registeredUrl = buildWebhookUrl(webhookUrl, botId);
|
|
75
|
+
try {
|
|
76
|
+
// Verify the token is valid by calling getMe
|
|
77
|
+
const meRes = await fetchImpl(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
78
|
+
const meData = (await meRes.json());
|
|
79
|
+
if (!meData.ok) {
|
|
80
|
+
return fail(`Invalid bot token: ${meData.description ?? "getMe failed"}`);
|
|
81
|
+
}
|
|
82
|
+
const botUsername = meData.result?.username ?? "unknown";
|
|
83
|
+
// Stamped immediately before registering: the probe below only treats an
|
|
84
|
+
// error Telegram saw AT OR AFTER this moment as this registration's.
|
|
85
|
+
const registeredAtSec = nowSec();
|
|
86
|
+
const result = await setWebhook(botToken, registeredUrl, secretFilePath, fetchImpl);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
return fail(`Webhook registration failed: ${result.error}`);
|
|
89
|
+
}
|
|
90
|
+
// Task 2560 — prove the URL reaches the handler before anything lands on
|
|
91
|
+
// disk. A probe that THROWS does not fail the registration: a transient
|
|
92
|
+
// network error is not evidence of unreachability. That leaves one blind
|
|
93
|
+
// spot, and probe=failed is its only marker.
|
|
94
|
+
let verifyInfo = "";
|
|
95
|
+
try {
|
|
96
|
+
const info = await getWebhookInfo(botToken, fetchImpl);
|
|
97
|
+
const verdict = webhookVerdict(info, registeredAtSec);
|
|
98
|
+
log(`${TAG} op=webhook-verify path=${webhookPath} botId=${botId} reachable=${verdict.reachable} ` +
|
|
99
|
+
`lastError=${info.lastErrorMessage ?? "none"} lastErrorDate=${info.lastErrorDate} ` +
|
|
100
|
+
`registeredAt=${registeredAtSec} pending=${info.pendingUpdateCount}`);
|
|
101
|
+
if (!verdict.reachable) {
|
|
102
|
+
return fail(`Webhook registered at ${registeredUrl} but Telegram cannot reach it: ${verdict.reason}. ` +
|
|
103
|
+
`Nothing was written to account.json. Fix the tunnel or the app and register again.`);
|
|
104
|
+
}
|
|
105
|
+
verifyInfo = `\nURL: ${info.url}\nPending updates: ${info.pendingUpdateCount}`;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
log(`${TAG} op=webhook-verify path=${webhookPath} botId=${botId} probe=failed ` +
|
|
109
|
+
`reason=${e instanceof Error ? e.message : String(e)}`);
|
|
110
|
+
verifyInfo = "\n(Could not verify — getWebhookInfo failed, but registration succeeded)";
|
|
111
|
+
}
|
|
112
|
+
// Persist this bot's entry into account.json's telegram.bots array, under
|
|
113
|
+
// THIS spawn's account. Task 2368 — the inbound receiver resolves the same
|
|
114
|
+
// entry by the bot id on the registered URL, and the write replaces only
|
|
115
|
+
// the entry with that id, so registering a second bot cannot disturb the
|
|
116
|
+
// first. persistTelegramBot asserts the resolved dir is under the brand
|
|
117
|
+
// install BEFORE writing (non-brand-path is rejected, not written), so a
|
|
118
|
+
// failure here means nothing was persisted.
|
|
119
|
+
const entry = role === "admin"
|
|
120
|
+
? { id: botId, token: botToken, role: "admin", ...(adminUsers ? { adminUsers } : {}) }
|
|
121
|
+
: { id: botId, token: botToken, role: "public", agent: agent, ...(dmPolicy ? { dmPolicy } : {}), ...(allowFrom ? { allowFrom } : {}) };
|
|
122
|
+
const persisted = persistTelegramBot(platformRoot, accountId, entry);
|
|
123
|
+
if (!persisted.ok) {
|
|
124
|
+
log(`${TAG} op=token-write-rejected reason=${persisted.error}${persisted.dir ? ` dir=${persisted.dir}` : ""}`);
|
|
125
|
+
return fail(persisted.error === "non-brand-path"
|
|
126
|
+
? `Refusing to write token outside the brand install: ${persisted.dir}`
|
|
127
|
+
: `Token persist failed: ${persisted.error}`);
|
|
128
|
+
}
|
|
129
|
+
log(`${TAG} op=token-write dir=${persisted.dir} brand=${configDirName()}`);
|
|
130
|
+
// Task 2368 — the pre-registry secret files are removed by the boot
|
|
131
|
+
// converter (server/telegram-migrate.ts), which owns the whole two-slot
|
|
132
|
+
// shape. This tool no longer deletes anything of its own.
|
|
133
|
+
return {
|
|
134
|
+
isError: false,
|
|
135
|
+
text: `Webhook registered for @${botUsername} (bot ${botId}, role ${role}${role === "public" ? `, answering as ${agent}` : ""})` +
|
|
136
|
+
`\nURL: ${registeredUrl}` +
|
|
137
|
+
`\nSecret: generated and saved to ${secretFilePath}` +
|
|
138
|
+
verifyInfo,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return fail(`Webhook registration error: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=webhook-register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-register.js","sourceRoot":"","sources":["../../src/tools/webhook-register.ts"],"names":[],"mappings":"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"}
|
|
@@ -58,7 +58,7 @@ Call `telegram-webhook-register` with:
|
|
|
58
58
|
- admin bot: `adminUsers` — the numeric IDs from Step 2
|
|
59
59
|
- public bot: `agent` — the slug from Step 2, plus `dmPolicy` and, for `allowlist`, `allowFrom`
|
|
60
60
|
|
|
61
|
-
On success the tool has: verified the token, derived the bot id, set the webhook with a generated secret (written to `~/.<brand>/telegram-secrets/<botId>`), persisted the entry into `account.json`'s `telegram.bots` array
|
|
61
|
+
On success the tool has, in this order: verified the token, derived the bot id, set the webhook with a generated secret (written to `~/.<brand>/telegram-secrets/<botId>`), probed Telegram and confirmed no delivery error since the registration, and only then persisted the entry into `account.json`'s `telegram.bots` array. It logs `op=webhook-register admitted=true` + `op=webhook-verify reachable=true` + `op=token-write`. It replaces only the entry with this bot's id, so the account's other bots are untouched. If it returns an error, read it back to the operator verbatim — the common ones are a wrong path (`admitted=false`), a webhook Telegram cannot reach (`op=webhook-verify reachable=false`), a public bot with no or an inactive `agent` (`op=token-write-rejected reason=no-agent|agent-inactive`), or a token write blocked outside the brand install.
|
|
62
62
|
|
|
63
63
|
### Step 5 — Confirm
|
|
64
64
|
|
|
@@ -66,7 +66,9 @@ Tell the operator the bot is connected, which role it has, and that messaging th
|
|
|
66
66
|
|
|
67
67
|
## Failure modes (state these loudly, do not paper over)
|
|
68
68
|
|
|
69
|
-
- **Wrong webhook path** — register returns an error and logs `op=webhook-register admitted=false`.
|
|
69
|
+
- **Wrong webhook path** — register returns an error and logs `op=webhook-register admitted=false`. The path must be **exactly** `/api/telegram/`; a subpath such as `/api/telegram/webhook` is refused because the router leaves every subpath on 404, and `/api/telegram` is refused because the auth gate 401s that shape. Fix the URL and register again.
|
|
70
|
+
- **Registered but unreachable** — the tool returns an error quoting Telegram's own message (commonly `Wrong response from the webhook: 404 Not Found`) and logs `op=webhook-verify reachable=false`. Nothing was written to `account.json`, and the webhook is still registered at Telegram. Fix the tunnel or the app, then register again. Do not retry blindly.
|
|
71
|
+
- **Probe could not run** — `op=webhook-verify probe=failed reason=…` means `getWebhookInfo` itself threw. The registration was kept and the entry persisted, but reachability was never proved. Send the bot one message and confirm `[telegram-inbound] op=received` before telling the operator it is connected.
|
|
70
72
|
- **Token write outside the brand** — `op=token-write-rejected reason=non-brand-path`. The MCP env is misconfigured; do not retry blindly — report it.
|
|
71
73
|
- **Paired agent switched off** — `[telegram-inbound] op=agent … active=false` then `op=reject reason=agent-inactive`, and the standing `[telegram-audit] op=entry-agent-inactive` names the bot. Re-activate that agent or re-register the bot against another. There is no fall back to a different agent: answering as an agent the operator switched off is exactly what this refusal prevents.
|
|
72
74
|
- **Bad secret on inbound** — `[telegram-inbound] op=secret … result=mismatch` means the registered secret and the stored secret diverged; re-run register.
|
|
@@ -46,6 +46,7 @@ export declare function parseSpawnRole(body: {
|
|
|
46
46
|
} | null;
|
|
47
47
|
export declare const SESSION_ID_PATTERN: RegExp;
|
|
48
48
|
export declare const RC_SPAWN_URL_WAIT_DEFAULT_MS = 60000;
|
|
49
|
+
export declare const LOG_FOLLOW_KEEPALIVE_DEFAULT_MS = 30000;
|
|
49
50
|
export interface HttpDeps extends Omit<SpawnDeps, 'onSessionReady' | 'watcher'> {
|
|
50
51
|
/** Admin operator's userId, sourced from users.json at boot (same as
|
|
51
52
|
* RcDaemonDeps.userId). Passed to buildRcChildEnv so the scheduling MCP
|
|
@@ -85,6 +86,10 @@ export interface HttpDeps extends Omit<SpawnDeps, 'onSessionReady' | 'watcher'>
|
|
|
85
86
|
* omits this and gets RC_SPAWN_URL_WAIT_DEFAULT_MS (60s); tests inject a
|
|
86
87
|
* short value to exercise the bind-timeout response without a 60s wait. */
|
|
87
88
|
rcSpawnUrlWaitMs?: number;
|
|
89
|
+
/** Task 2557 — idle gap after which `/log?follow=1` writes a bare newline.
|
|
90
|
+
* Production omits this and gets LOG_FOLLOW_KEEPALIVE_DEFAULT_MS (30s);
|
|
91
|
+
* tests inject a short value to exercise the keepalive without a 30s wait. */
|
|
92
|
+
logFollowKeepaliveMs?: number;
|
|
88
93
|
/** Task 1749 — accessor for the standing PTY census's most recent snapshot,
|
|
89
94
|
* wired from `createPtyCensus(...).latest`. Optional so the pre-1749 test
|
|
90
95
|
* deps factories keep working; production index.ts always injects it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAqB3B,OAAO,KAAK,EAAU,SAAS,EAAW,MAAM,YAAY,CAAA;AAM5D,OAAO,EAkBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAqBzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAoBlD,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAQlE;;;;;;;;;;;mFAWmF;AACnF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GACvB,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,IAAI,CAE/C;AAED;;;;;;;;;;2EAU2E;AAC3E,wBAAgB,cAAc,CAC5B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,CAOlD;AAsFD,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -207,6 +207,17 @@ export const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
|
207
207
|
// Injectable via `HttpDeps.rcSpawnUrlWaitMs` so the timeout path is
|
|
208
208
|
// unit-testable without a real 60s wait.
|
|
209
209
|
export const RC_SPAWN_URL_WAIT_DEFAULT_MS = 60_000;
|
|
210
|
+
// Task 2557 — how long a follow stream may go without writing a byte before it
|
|
211
|
+
// emits a bare newline. The stream writes only when the JSONL grows, and Node's
|
|
212
|
+
// global fetch (undici) kills a response body after 300s with no bytes, so a
|
|
213
|
+
// conversation between messages killed its own reader: 129 of 129
|
|
214
|
+
// log-follow-close lines on laptop 192.168.88.10 read reason=aborted, shortest
|
|
215
|
+
// lifetime 313440ms, and no follower in the whole log history outlived a quiet
|
|
216
|
+
// patch. 30s costs one byte and leaves a tenfold margin against a slow tick.
|
|
217
|
+
// A genuinely stalled manager still trips undici's bound, because a stalled
|
|
218
|
+
// manager writes no keepalive either. Injectable via
|
|
219
|
+
// `HttpDeps.logFollowKeepaliveMs` so the path is testable without a 30s wait.
|
|
220
|
+
export const LOG_FOLLOW_KEEPALIVE_DEFAULT_MS = 30_000;
|
|
210
221
|
const LIST_TAIL_BYTES = 64 * 1024;
|
|
211
222
|
function timed(logger, method, path, status, ms) {
|
|
212
223
|
logger(`api method=${method} path=${path} outcome=${status} ms=${ms}`);
|
|
@@ -2534,6 +2545,10 @@ export function buildHttpApp(deps) {
|
|
|
2534
2545
|
let position = 0;
|
|
2535
2546
|
let linesStreamed = 0;
|
|
2536
2547
|
let closeReason = 'end';
|
|
2548
|
+
// Task 2557 — when this stream last put a byte on the wire. Every real
|
|
2549
|
+
// write stamps it; the poll loop below sends a keepalive once the gap
|
|
2550
|
+
// reaches the threshold, so an idle session never looks dead to undici.
|
|
2551
|
+
let lastWriteAt = Date.now();
|
|
2537
2552
|
const countNewlines = (buf) => {
|
|
2538
2553
|
let n = 0;
|
|
2539
2554
|
for (let i = 0; i < buf.length; i++)
|
|
@@ -2548,6 +2563,7 @@ export function buildHttpApp(deps) {
|
|
|
2548
2563
|
const buf = chunk;
|
|
2549
2564
|
linesStreamed += countNewlines(buf);
|
|
2550
2565
|
await out.write(buf);
|
|
2566
|
+
lastWriteAt = Date.now();
|
|
2551
2567
|
}
|
|
2552
2568
|
position = initialSize;
|
|
2553
2569
|
}
|
|
@@ -2587,12 +2603,25 @@ export function buildHttpApp(deps) {
|
|
|
2587
2603
|
const buf = chunk;
|
|
2588
2604
|
linesStreamed += countNewlines(buf);
|
|
2589
2605
|
void out.write(buf);
|
|
2606
|
+
lastWriteAt = Date.now();
|
|
2590
2607
|
}
|
|
2591
2608
|
});
|
|
2592
2609
|
};
|
|
2593
2610
|
watchFile(jsonlPath, { interval: 250 }, listener);
|
|
2611
|
+
const keepaliveMs = deps.logFollowKeepaliveMs ?? LOG_FOLLOW_KEEPALIVE_DEFAULT_MS;
|
|
2594
2612
|
while (active) {
|
|
2595
2613
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
2614
|
+
if (!active)
|
|
2615
|
+
break;
|
|
2616
|
+
if (Date.now() - lastWriteAt < keepaliveMs)
|
|
2617
|
+
continue;
|
|
2618
|
+
// Task 2557 — a bare newline, not a file line: it is excluded from
|
|
2619
|
+
// `linesStreamed` because that counter is what this stream carried from
|
|
2620
|
+
// the JSONL, and the emitter drops it at its own `if (!line) continue`.
|
|
2621
|
+
// Its only job is to be a byte, so undici's bodyTimeout never elapses
|
|
2622
|
+
// on a conversation that is merely quiet.
|
|
2623
|
+
await out.write(Buffer.from('\n'));
|
|
2624
|
+
lastWriteAt = Date.now();
|
|
2596
2625
|
}
|
|
2597
2626
|
deps.logger(`log-follow-close sessionId=${sessionId} reason=${closeReason} linesStreamed=${linesStreamed} ms=${Date.now() - followStreamStart}`);
|
|
2598
2627
|
});
|