sensorium-mcp 3.0.75 โ†’ 3.0.77

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 (31) hide show
  1. package/dist/dashboard/log-parser.d.ts +51 -0
  2. package/dist/dashboard/log-parser.d.ts.map +1 -0
  3. package/dist/dashboard/log-parser.js +140 -0
  4. package/dist/dashboard/log-parser.js.map +1 -0
  5. package/dist/dashboard/routes/copilot.d.ts +7 -4
  6. package/dist/dashboard/routes/copilot.d.ts.map +1 -1
  7. package/dist/dashboard/routes/copilot.js +47 -13
  8. package/dist/dashboard/routes/copilot.js.map +1 -1
  9. package/dist/dashboard/routes/threads.d.ts +22 -0
  10. package/dist/dashboard/routes/threads.d.ts.map +1 -1
  11. package/dist/dashboard/routes/threads.js +193 -3
  12. package/dist/dashboard/routes/threads.js.map +1 -1
  13. package/dist/dashboard/routes.d.ts.map +1 -1
  14. package/dist/dashboard/routes.js +9 -3
  15. package/dist/dashboard/routes.js.map +1 -1
  16. package/dist/dashboard/spa.html +7 -7
  17. package/dist/response-builders.d.ts.map +1 -1
  18. package/dist/response-builders.js +12 -5
  19. package/dist/response-builders.js.map +1 -1
  20. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  21. package/dist/services/worker-cleanup.service.js +8 -3
  22. package/dist/services/worker-cleanup.service.js.map +1 -1
  23. package/dist/tools/wait/message-delivery.d.ts.map +1 -1
  24. package/dist/tools/wait/message-delivery.js +7 -2
  25. package/dist/tools/wait/message-delivery.js.map +1 -1
  26. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  27. package/dist/tools/wait/poll-loop.js +5 -4
  28. package/dist/tools/wait/poll-loop.js.map +1 -1
  29. package/package.json +2 -2
  30. package/templates/playwright.default.md +30 -0
  31. package/templates/sensorium-repair.default.md +99 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Parser for Claude CLI stream-json thread logs (newline-delimited JSON).
3
+ *
4
+ * The agent's stdout is captured as one JSON event per line. This turns that
5
+ * raw event stream into readable, condensed "turns" for display over Telegram.
6
+ * A turn is one assistant message (its text + any tool calls) together with the
7
+ * tool results that immediately follow it.
8
+ *
9
+ * Assistant text is shown in full; tool inputs and tool results are previewed
10
+ * (collapsed to a single line and capped) because they are machine output, not
11
+ * the agent's reasoning, and would otherwise drown the useful signal.
12
+ */
13
+ export interface LogTurn {
14
+ /** Rendered lines for this turn, in display order. */
15
+ lines: string[];
16
+ }
17
+ export declare const DEFAULT_PREVIEW_CHARS = 300;
18
+ /**
19
+ * Parse a raw stream-json log into chronological turns. Malformed lines and
20
+ * noise events (system, rate_limit_event) are skipped.
21
+ */
22
+ export declare function parseClaudeStreamLog(raw: string, previewChars?: number): LogTurn[];
23
+ /**
24
+ * Render turns to a single string. With tailFirst (default) the most recent
25
+ * turn appears first, so a reader paging from the top sees latest activity.
26
+ */
27
+ export declare function renderThreadLog(turns: LogTurn[], tailFirst?: boolean): string;
28
+ /** Default character window per page โ€” matches Telegram's ~4096-char message cap. */
29
+ export declare const DEFAULT_LOG_PAGE_CHARS = 4000;
30
+ export interface LogPage {
31
+ /** The character window for this page. */
32
+ text: string;
33
+ /** Start index of this page within the rendered log. */
34
+ offset: number;
35
+ /** Start index of the next page (feeds a "next chunk" button). */
36
+ nextOffset: number;
37
+ /** Whether more text remains after this page. */
38
+ hasMore: boolean;
39
+ /** Total length of the rendered log. */
40
+ totalLength: number;
41
+ }
42
+ /**
43
+ * Extract a single character window from a rendered log for paginated display.
44
+ *
45
+ * The digest uses astral emoji markers (๐Ÿค–๐Ÿ”งโœ…โ€ฆ), so a naive .slice() could cut
46
+ * between the two UTF-16 code units of a surrogate pair and emit a lone
47
+ * surrogate that renders as ๏ฟฝ. When the window would end mid-pair we snap the
48
+ * end back one unit; that code unit becomes the start of the next page.
49
+ */
50
+ export declare function sliceLogWindow(rendered: string, offsetRaw: number, limitRaw: number): LogPage;
51
+ //# sourceMappingURL=log-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-parser.d.ts","sourceRoot":"","sources":["../../src/dashboard/log-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkBH,MAAM,WAAW,OAAO;IACpB,sDAAsD;IACtD,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,qBAAqB,MAAM,CAAC;AA+BzC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAA8B,GAAG,OAAO,EAAE,CAoCzG;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,SAAS,GAAE,OAAc,GAAG,MAAM,CAGnF;AAED,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,MAAM,WAAW,OAAO;IACpB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;CACvB;AAcD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAY7F"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Parser for Claude CLI stream-json thread logs (newline-delimited JSON).
3
+ *
4
+ * The agent's stdout is captured as one JSON event per line. This turns that
5
+ * raw event stream into readable, condensed "turns" for display over Telegram.
6
+ * A turn is one assistant message (its text + any tool calls) together with the
7
+ * tool results that immediately follow it.
8
+ *
9
+ * Assistant text is shown in full; tool inputs and tool results are previewed
10
+ * (collapsed to a single line and capped) because they are machine output, not
11
+ * the agent's reasoning, and would otherwise drown the useful signal.
12
+ */
13
+ export const DEFAULT_PREVIEW_CHARS = 300;
14
+ /** Collapse whitespace and cap length, appending an ellipsis when truncated. */
15
+ function preview(s, max) {
16
+ const oneLine = s.replace(/\s+/g, " ").trim();
17
+ return oneLine.length > max ? oneLine.slice(0, max) + "โ€ฆ" : oneLine;
18
+ }
19
+ function compactArgs(input, max) {
20
+ let s;
21
+ try {
22
+ s = typeof input === "string" ? input : JSON.stringify(input ?? {});
23
+ }
24
+ catch {
25
+ s = String(input);
26
+ }
27
+ return preview(s, max);
28
+ }
29
+ /** Extract readable text from a tool_result's content (string, or array of text blocks). */
30
+ function resultText(content) {
31
+ if (typeof content === "string")
32
+ return content;
33
+ if (Array.isArray(content)) {
34
+ return content
35
+ .map((c) => {
36
+ if (c && typeof c === "object" && typeof c.text === "string") {
37
+ return c.text;
38
+ }
39
+ return JSON.stringify(c);
40
+ })
41
+ .join(" ");
42
+ }
43
+ return JSON.stringify(content ?? "");
44
+ }
45
+ /**
46
+ * Parse a raw stream-json log into chronological turns. Malformed lines and
47
+ * noise events (system, rate_limit_event) are skipped.
48
+ */
49
+ export function parseClaudeStreamLog(raw, previewChars = DEFAULT_PREVIEW_CHARS) {
50
+ const turns = [];
51
+ let current = null;
52
+ for (const line of raw.split("\n")) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed)
55
+ continue;
56
+ let ev;
57
+ try {
58
+ ev = JSON.parse(trimmed);
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ if (ev.type === "assistant" && ev.message?.content) {
64
+ current = { lines: [] };
65
+ turns.push(current);
66
+ for (const c of ev.message.content) {
67
+ if (c.type === "text" && typeof c.text === "string") {
68
+ const t = c.text.trim();
69
+ if (t)
70
+ current.lines.push(`๐Ÿค– ${t}`);
71
+ }
72
+ else if (c.type === "tool_use") {
73
+ const name = typeof c.name === "string" ? c.name : "tool";
74
+ current.lines.push(`๐Ÿ”ง ${name}(${compactArgs(c.input, previewChars)})`);
75
+ }
76
+ }
77
+ }
78
+ else if (ev.type === "user" && ev.message?.content) {
79
+ for (const c of ev.message.content) {
80
+ if (c.type !== "tool_result")
81
+ continue;
82
+ if (!current) {
83
+ current = { lines: [] };
84
+ turns.push(current);
85
+ }
86
+ const marker = c.is_error ? "โš ๏ธ" : "โ†ณ";
87
+ current.lines.push(` ${marker} ${preview(resultText(c.content), previewChars)}`);
88
+ }
89
+ }
90
+ else if (ev.type === "result") {
91
+ const marker = ev.is_error ? "โŒ" : "โœ…";
92
+ turns.push({ lines: [`${marker} ${preview(typeof ev.result === "string" ? ev.result : "", previewChars)}`] });
93
+ current = null;
94
+ }
95
+ }
96
+ return turns;
97
+ }
98
+ /**
99
+ * Render turns to a single string. With tailFirst (default) the most recent
100
+ * turn appears first, so a reader paging from the top sees latest activity.
101
+ */
102
+ export function renderThreadLog(turns, tailFirst = true) {
103
+ const ordered = tailFirst ? [...turns].reverse() : turns;
104
+ return ordered.map((t) => t.lines.join("\n")).join("\n\n");
105
+ }
106
+ /** Default character window per page โ€” matches Telegram's ~4096-char message cap. */
107
+ export const DEFAULT_LOG_PAGE_CHARS = 4000;
108
+ /** Clamp an offset to [0, max]; non-finite or non-positive values become 0. */
109
+ function clampOffset(n, max) {
110
+ if (!Number.isFinite(n) || n <= 0)
111
+ return 0;
112
+ return Math.min(Math.floor(n), max);
113
+ }
114
+ /** Clamp a page size to (0, DEFAULT_LOG_PAGE_CHARS]; invalid values default to the cap. */
115
+ function clampLimit(n) {
116
+ if (!Number.isFinite(n) || n <= 0)
117
+ return DEFAULT_LOG_PAGE_CHARS;
118
+ return Math.min(Math.floor(n), DEFAULT_LOG_PAGE_CHARS);
119
+ }
120
+ /**
121
+ * Extract a single character window from a rendered log for paginated display.
122
+ *
123
+ * The digest uses astral emoji markers (๐Ÿค–๐Ÿ”งโœ…โ€ฆ), so a naive .slice() could cut
124
+ * between the two UTF-16 code units of a surrogate pair and emit a lone
125
+ * surrogate that renders as ๏ฟฝ. When the window would end mid-pair we snap the
126
+ * end back one unit; that code unit becomes the start of the next page.
127
+ */
128
+ export function sliceLogWindow(rendered, offsetRaw, limitRaw) {
129
+ const totalLength = rendered.length;
130
+ const offset = clampOffset(offsetRaw, totalLength);
131
+ const limit = clampLimit(limitRaw);
132
+ let end = Math.min(offset + limit, totalLength);
133
+ if (end < totalLength && end - 1 > offset) {
134
+ const lastCode = rendered.charCodeAt(end - 1);
135
+ if (lastCode >= 0xd800 && lastCode <= 0xdbff)
136
+ end -= 1;
137
+ }
138
+ return { text: rendered.slice(offset, end), offset, nextOffset: end, hasMore: end < totalLength, totalLength };
139
+ }
140
+ //# sourceMappingURL=log-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-parser.js","sourceRoot":"","sources":["../../src/dashboard/log-parser.ts"],"names":[],"mappings":"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"}
@@ -9,9 +9,12 @@ import { type RouteHandler } from "./types.js";
9
9
  export declare const handleGetCopilot: RouteHandler;
10
10
  export declare const handlePostCopilot: RouteHandler;
11
11
  /**
12
- * Verify the BYOK connection by listing models from the configured provider's
13
- * OpenAI-compatible `{baseUrl}/models` endpoint using the stored API key.
14
- * Returns model ids on success, a descriptive error otherwise.
12
+ * Verify the BYOK connection by listing models from the provider, using the
13
+ * values POSTed from the form (baseUrl, providerType, apiKey) so the user can
14
+ * test BEFORE saving. Any field omitted or blank falls back to the stored
15
+ * config โ€” an empty apiKey keeps the previously saved key so it isn't echoed to
16
+ * the browser. Request shape and headers are provider-aware (openai/azure/
17
+ * anthropic). Returns model ids on success, a descriptive error otherwise.
15
18
  */
16
- export declare const handleGetCopilotModels: RouteHandler;
19
+ export declare const handlePostCopilotModels: RouteHandler;
17
20
  //# sourceMappingURL=copilot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAuBxE,eAAO,MAAM,gBAAgB,EAAE,YAG9B,CAAC;AAIF,eAAO,MAAM,iBAAiB,EAAE,YAiF/B,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,YAgCpC,CAAC"}
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAkDxE,eAAO,MAAM,gBAAgB,EAAE,YAG9B,CAAC;AAIF,eAAO,MAAM,iBAAiB,EAAE,YAiF/B,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,YA2CrC,CAAC"}
@@ -9,6 +9,28 @@ import { getCopilotModel, setCopilotModel, getCopilotByokConfig, setCopilotByokC
9
9
  import { readBody, safeParseJSON } from "./types.js";
10
10
  import { errorMessage } from "../../utils.js";
11
11
  const MODELS_FETCH_TIMEOUT_MS = 10_000;
12
+ /**
13
+ * Build the provider-specific "list models" request. Each provider type speaks a
14
+ * different wire protocol, so the models endpoint path and auth header differ:
15
+ * - openai: GET {base}/models Authorization: Bearer <key>
16
+ * - anthropic: GET {base}/v1/models x-api-key + anthropic-version
17
+ * - azure: GET {base}/models?api-version=โ€ฆ api-key: <key>
18
+ * All three return an OpenAI-style { data: [{ id }] } body.
19
+ */
20
+ function buildModelsRequest(providerType, baseUrl, apiKey) {
21
+ const base = baseUrl.replace(/\/+$/, "");
22
+ switch (providerType) {
23
+ case "anthropic": {
24
+ const url = /\/v1$/.test(base) ? `${base}/models` : `${base}/v1/models`;
25
+ return { url, headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } };
26
+ }
27
+ case "azure":
28
+ return { url: `${base}/models?api-version=2024-10-21`, headers: { "api-key": apiKey } };
29
+ case "openai":
30
+ default:
31
+ return { url: `${base}/models`, headers: { Authorization: `Bearer ${apiKey}` } };
32
+ }
33
+ }
12
34
  /** Public (key-masked) view of the Copilot settings for the dashboard. */
13
35
  function publicCopilotSettings() {
14
36
  const byok = getCopilotByokConfig();
@@ -109,27 +131,39 @@ export const handlePostCopilot = ({ req, json }) => {
109
131
  })();
110
132
  return true;
111
133
  };
112
- // โ”€โ”€โ”€ GET /api/settings/copilot/models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
134
+ // โ”€โ”€โ”€ POST /api/settings/copilot/models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
113
135
  /**
114
- * Verify the BYOK connection by listing models from the configured provider's
115
- * OpenAI-compatible `{baseUrl}/models` endpoint using the stored API key.
116
- * Returns model ids on success, a descriptive error otherwise.
136
+ * Verify the BYOK connection by listing models from the provider, using the
137
+ * values POSTed from the form (baseUrl, providerType, apiKey) so the user can
138
+ * test BEFORE saving. Any field omitted or blank falls back to the stored
139
+ * config โ€” an empty apiKey keeps the previously saved key so it isn't echoed to
140
+ * the browser. Request shape and headers are provider-aware (openai/azure/
141
+ * anthropic). Returns model ids on success, a descriptive error otherwise.
117
142
  */
118
- export const handleGetCopilotModels = ({ json }) => {
143
+ export const handlePostCopilotModels = ({ req, json }) => {
119
144
  void (async () => {
120
- const byok = getCopilotByokConfig();
121
- if (!byok.baseUrl || !byok.apiKey) {
122
- json({ ok: false, error: "Set a provider baseUrl and API key first" }, 400);
145
+ const stored = getCopilotByokConfig();
146
+ let body = {};
147
+ try {
148
+ const raw = await readBody(req);
149
+ const parsed = raw ? safeParseJSON(raw) : null;
150
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
151
+ body = parsed;
152
+ }
153
+ }
154
+ catch { /* no/invalid body โ€” fall back entirely to stored config */ }
155
+ const baseUrl = (typeof body.baseUrl === "string" && body.baseUrl.trim() ? body.baseUrl.trim() : stored.baseUrl);
156
+ const apiKey = (typeof body.apiKey === "string" && body.apiKey.length > 0 ? body.apiKey : stored.apiKey);
157
+ const providerType = isValidCopilotProviderType(body.providerType) ? body.providerType : stored.providerType;
158
+ if (!baseUrl || !apiKey) {
159
+ json({ ok: false, error: "Enter a base URL and API key first (or save them)." }, 400);
123
160
  return;
124
161
  }
125
- const url = `${byok.baseUrl.replace(/\/+$/, "")}/models`;
162
+ const { url, headers } = buildModelsRequest(providerType, baseUrl, apiKey);
126
163
  const controller = new AbortController();
127
164
  const timer = setTimeout(() => controller.abort(), MODELS_FETCH_TIMEOUT_MS);
128
165
  try {
129
- const resp = await fetch(url, {
130
- headers: { Authorization: `Bearer ${byok.apiKey}` },
131
- signal: controller.signal,
132
- });
166
+ const resp = await fetch(url, { headers, signal: controller.signal });
133
167
  if (!resp.ok) {
134
168
  json({ ok: false, error: `Provider returned HTTP ${resp.status} ${resp.statusText}` }, 502);
135
169
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"copilot.js","sourceRoot":"","sources":["../../../src/dashboard/routes/copilot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"copilot.js","sourceRoot":"","sources":["../../../src/dashboard/routes/copilot.ts"],"names":[],"mappings":"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"}
@@ -10,12 +10,34 @@ export declare const handleGetArchivedThreads: RouteHandler;
10
10
  export declare function handleUnarchiveThread(args: RouteArgs, threadId: number): boolean;
11
11
  export declare function handleGenerateSummary(args: RouteArgs, threadId: number): boolean;
12
12
  export declare const handleCreateThread: RouteHandler;
13
+ /**
14
+ * Create a brand-new root thread: opens a Telegram forum topic and registers
15
+ * the thread. Unlike POST /api/threads (which only registers a caller-supplied
16
+ * topic id), this owns topic creation so callers without Telegram access (e.g.
17
+ * the supervisor) can spin up threads. keepAlive + dailyRotation default on for
18
+ * root threads via registerThread.
19
+ */
20
+ export declare const handleCreateRoot: RouteHandler;
21
+ /**
22
+ * Create a branch thread under an existing root: opens a Telegram forum topic
23
+ * and registers it with type=branch. Working directory and agent type are
24
+ * inherited from the parent root unless overridden.
25
+ */
26
+ export declare const handleCreateBranch: RouteHandler;
13
27
  /** GET /api/threads/:threadId โ€” get single thread */
14
28
  export declare function handleGetThread(args: RouteArgs, threadId: number): boolean;
15
29
  /** GET /api/threads/:threadId/running โ€” check if an agent process is alive for this thread */
16
30
  export declare function handleGetThreadRunning(args: RouteArgs, threadId: number): boolean;
17
31
  /** GET /api/threads/:threadId/heartbeat โ€” last MCP activity timestamp for stuck-process detection */
18
32
  export declare function handleGetThreadHeartbeat(args: RouteArgs, threadId: number): boolean;
33
+ /**
34
+ * GET /api/threads/:threadId/logs โ€” a readable digest of the agent's raw stdout.
35
+ *
36
+ * Parses the newest stream-json log file into tail-first turns, then returns a
37
+ * single character window (default 4000, Telegram-sized). Callers page deeper
38
+ * into history with ?offset=; nextOffset/hasMore drive a "next chunk" button.
39
+ */
40
+ export declare function handleGetThreadLogs(args: RouteArgs, threadId: number): boolean;
19
41
  /** GET /api/threads/:threadId/children โ€” get children of a root */
20
42
  export declare function handleGetThreadChildren(args: RouteArgs, rootThreadId: number): boolean;
21
43
  /** PATCH /api/threads/:threadId โ€” update a thread */
@@ -1 +1 @@
1
- {"version":3,"file":"threads.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/threads.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkCH,OAAO,EAA2B,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAsFxF,eAAO,MAAM,gBAAgB,EAAE,YAI9B,CAAC;AAIF,eAAO,MAAM,oBAAoB,EAAE,YAGlC,CAAC;AAIF,eAAO,MAAM,yBAAyB,EAAE,YAGvC,CAAC;AAIF,eAAO,MAAM,wBAAwB,EAAE,YAItC,CAAC;AAIF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAchF;AAID,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA2ChF;AAID,eAAO,MAAM,kBAAkB,EAAE,YAmEhC,CAAC;AAIF,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQ1E;AAED,8FAA8F;AAC9F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIjF;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAInF;AAED,mEAAmE;AACnE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAItF;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAwC7E;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsC7E;AAYD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA8B5E;AAID,6FAA6F;AAC7F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA6B9E"}
1
+ {"version":3,"file":"threads.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/threads.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmCH,OAAO,EAA2B,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AA0FxF,eAAO,MAAM,gBAAgB,EAAE,YAI9B,CAAC;AAIF,eAAO,MAAM,oBAAoB,EAAE,YAGlC,CAAC;AAIF,eAAO,MAAM,yBAAyB,EAAE,YAGvC,CAAC;AAIF,eAAO,MAAM,wBAAwB,EAAE,YAItC,CAAC;AAIF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAchF;AAID,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA2ChF;AAID,eAAO,MAAM,kBAAkB,EAAE,YAmEhC,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAuC9B,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,YAiDhC,CAAC;AAcF,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQ1E;AAED,8FAA8F;AAC9F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIjF;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAInF;AAqBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAkB9E;AAED,mEAAmE;AACnE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAItF;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAwC7E;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsC7E;AAYD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA8B5E;AAID,6FAA6F;AAC7F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA6B9E"}
@@ -3,14 +3,18 @@
3
3
  * Covers: listing, creating, updating, archiving, and deleting threads.
4
4
  */
5
5
  import { registerThread, getThread, getRootThreads, getKeepAliveThreads, getActiveThreads, getArchivedThreads, getDashboardThreads, getThreadsByRoot, updateThread, archiveThread, unarchiveThread, deleteThread, getExplicitTelegramTopicId, resolveTelegramTopicId, } from "../../data/memory/thread-registry.js";
6
- import { config, setKeepAliveEnabled, setKeepAliveThreadId, setKeepAliveClient, setKeepAliveMaxRetries, setKeepAliveCooldownMs, setThreadKeepAlive, removeThreadKeepAlive, isValidKeeperClient, } from "../../config.js";
6
+ import { config, getAgentType, setKeepAliveEnabled, setKeepAliveThreadId, setKeepAliveClient, setKeepAliveMaxRetries, setKeepAliveCooldownMs, setThreadKeepAlive, removeThreadKeepAlive, isValidKeeperClient, } from "../../config.js";
7
7
  import { readBody, safeParseJSON } from "./types.js";
8
8
  import { errorMessage } from "../../utils.js";
9
9
  import { log } from "../../logger.js";
10
- import { isThreadRunning } from "../../services/process.service.js";
10
+ import { isThreadRunning, THREAD_LOGS_DIR } from "../../services/process.service.js";
11
11
  import { readThreadHeartbeat } from "../../data/file-storage.js";
12
- import { deleteTelegramTopicByBotApi } from "../../services/topic.service.js";
12
+ import { parseClaudeStreamLog, renderThreadLog, sliceLogWindow } from "../log-parser.js";
13
+ import { readdirSync, statSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { createManagedTopic, deleteTelegramTopicByBotApi } from "../../services/topic.service.js";
13
16
  import { dispatchSpawn } from "../../services/agent-spawn.service.js";
17
+ import { TelegramClient } from "../../telegram.js";
14
18
  // โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
19
  const VALID_STATUSES = ["active", "archived", "expired", "exited"];
16
20
  const VALID_CLIENTS = ["claude", "copilot", "codex", "openai_codex"];
@@ -233,6 +237,141 @@ export const handleCreateThread = ({ req, json, db }) => {
233
237
  })();
234
238
  return true;
235
239
  };
240
+ // โ”€โ”€โ”€ POST /api/threads/create-root โ€” create a fresh topic + root thread โ”€โ”€โ”€โ”€โ”€
241
+ /**
242
+ * Create a brand-new root thread: opens a Telegram forum topic and registers
243
+ * the thread. Unlike POST /api/threads (which only registers a caller-supplied
244
+ * topic id), this owns topic creation so callers without Telegram access (e.g.
245
+ * the supervisor) can spin up threads. keepAlive + dailyRotation default on for
246
+ * root threads via registerThread.
247
+ */
248
+ export const handleCreateRoot = ({ req, json, db, ctx }) => {
249
+ void (async () => {
250
+ try {
251
+ const raw = await readBody(req);
252
+ const body = safeParseJSON(raw);
253
+ if (!body || typeof body !== "object") {
254
+ json({ error: "Invalid request body" }, 400);
255
+ return;
256
+ }
257
+ const name = typeof body.name === "string" ? body.name.trim() : "";
258
+ if (!name) {
259
+ json({ error: "name is required" }, 400);
260
+ return;
261
+ }
262
+ const agentType = resolveAgentType(body.agentType);
263
+ if (agentType === INVALID_AGENT) {
264
+ json({ error: `agentType must be one of: ${VALID_CLIENTS.join(", ")}` }, 400);
265
+ return;
266
+ }
267
+ const workingDirectory = typeof body.workingDirectory === "string" ? body.workingDirectory.trim() : "";
268
+ if (!workingDirectory) {
269
+ json({ error: "workingDirectory is required" }, 400);
270
+ return;
271
+ }
272
+ const { TELEGRAM_TOKEN, TELEGRAM_CHAT_ID } = config;
273
+ if (!TELEGRAM_TOKEN || !TELEGRAM_CHAT_ID) {
274
+ json({ error: "Telegram is not configured" }, 500);
275
+ return;
276
+ }
277
+ const telegram = new TelegramClient(TELEGRAM_TOKEN);
278
+ const threadId = await createManagedTopic(telegram, TELEGRAM_CHAT_ID, name);
279
+ const thread = ctx.threadLifecycle.registerThread(db, {
280
+ threadId,
281
+ name,
282
+ type: "root",
283
+ client: agentType ?? getAgentType(),
284
+ workingDirectory,
285
+ chatId: TELEGRAM_CHAT_ID,
286
+ telegramTopicId: threadId,
287
+ });
288
+ json({ ok: true, threadId, thread }, 201);
289
+ syncKeepAliveToSettings(db);
290
+ }
291
+ catch (err) {
292
+ json({ error: errorMessage(err) }, 500);
293
+ }
294
+ })();
295
+ return true;
296
+ };
297
+ // โ”€โ”€โ”€ POST /api/threads/create-branch โ€” create a fresh topic + branch thread โ”€โ”€
298
+ /**
299
+ * Create a branch thread under an existing root: opens a Telegram forum topic
300
+ * and registers it with type=branch. Working directory and agent type are
301
+ * inherited from the parent root unless overridden.
302
+ */
303
+ export const handleCreateBranch = ({ req, json, db, ctx }) => {
304
+ void (async () => {
305
+ try {
306
+ const raw = await readBody(req);
307
+ const body = safeParseJSON(raw);
308
+ if (!body || typeof body !== "object") {
309
+ json({ error: "Invalid request body" }, 400);
310
+ return;
311
+ }
312
+ const rootThreadId = body.rootThreadId;
313
+ if (typeof rootThreadId !== "number" || !Number.isInteger(rootThreadId) || rootThreadId <= 0) {
314
+ json({ error: "rootThreadId must be a positive integer" }, 400);
315
+ return;
316
+ }
317
+ const name = typeof body.name === "string" ? body.name.trim() : "";
318
+ if (!name) {
319
+ json({ error: "name is required" }, 400);
320
+ return;
321
+ }
322
+ const parent = getThread(db, rootThreadId);
323
+ if (!parent) {
324
+ json({ error: `Root thread ${rootThreadId} not found` }, 404);
325
+ return;
326
+ }
327
+ if (parent.type === "worker") {
328
+ json({ error: "Cannot branch from a worker thread" }, 400);
329
+ return;
330
+ }
331
+ const agentType = resolveAgentType(body.agentType);
332
+ if (agentType === INVALID_AGENT) {
333
+ json({ error: `agentType must be one of: ${VALID_CLIENTS.join(", ")}` }, 400);
334
+ return;
335
+ }
336
+ const workingDirectoryOverride = typeof body.workingDirectory === "string" && body.workingDirectory.trim()
337
+ ? body.workingDirectory.trim()
338
+ : undefined;
339
+ const { TELEGRAM_TOKEN, TELEGRAM_CHAT_ID } = config;
340
+ if (!TELEGRAM_TOKEN || !TELEGRAM_CHAT_ID) {
341
+ json({ error: "Telegram is not configured" }, 500);
342
+ return;
343
+ }
344
+ const telegram = new TelegramClient(TELEGRAM_TOKEN);
345
+ const threadId = await createManagedTopic(telegram, TELEGRAM_CHAT_ID, name);
346
+ const thread = ctx.threadLifecycle.registerThread(db, {
347
+ threadId,
348
+ name,
349
+ type: "branch",
350
+ rootThreadId,
351
+ client: agentType ?? parent.client ?? getAgentType(),
352
+ workingDirectory: workingDirectoryOverride ?? parent.workingDirectory ?? undefined,
353
+ chatId: TELEGRAM_CHAT_ID,
354
+ telegramTopicId: threadId,
355
+ });
356
+ json({ ok: true, threadId, thread }, 201);
357
+ syncKeepAliveToSettings(db);
358
+ }
359
+ catch (err) {
360
+ json({ error: errorMessage(err) }, 500);
361
+ }
362
+ })();
363
+ return true;
364
+ };
365
+ /** Sentinel returned by resolveAgentType when an explicit agentType is invalid. */
366
+ const INVALID_AGENT = Symbol("invalid-agent");
367
+ /** Validate an optional agentType field: undefined โ†’ undefined, valid โ†’ the value, invalid โ†’ INVALID_AGENT. */
368
+ function resolveAgentType(value) {
369
+ if (value === undefined)
370
+ return undefined;
371
+ if (typeof value === "string" && VALID_CLIENTS.includes(value))
372
+ return value;
373
+ return INVALID_AGENT;
374
+ }
236
375
  // โ”€โ”€โ”€ Dynamic handlers (threadId extracted by caller) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
237
376
  /** GET /api/threads/:threadId โ€” get single thread */
238
377
  export function handleGetThread(args, threadId) {
@@ -257,6 +396,57 @@ export function handleGetThreadHeartbeat(args, threadId) {
257
396
  args.json({ threadId, lastActivityMs });
258
397
  return true;
259
398
  }
399
+ /** Locate the most-recently-modified log file for a thread, or null if none exist. */
400
+ function findLatestThreadLogFile(threadId) {
401
+ let files;
402
+ try {
403
+ files = readdirSync(THREAD_LOGS_DIR);
404
+ }
405
+ catch {
406
+ return null;
407
+ }
408
+ const suffixRe = new RegExp(`_${threadId}_\\d{4}-\\d{2}-\\d{2}\\.jsonl?$`);
409
+ let best = null;
410
+ for (const file of files) {
411
+ if (!suffixRe.test(file))
412
+ continue;
413
+ const full = join(THREAD_LOGS_DIR, file);
414
+ try {
415
+ const mtimeMs = statSync(full).mtimeMs;
416
+ if (!best || mtimeMs > best.mtimeMs)
417
+ best = { path: full, mtimeMs };
418
+ }
419
+ catch { /* file vanished between readdir and stat */ }
420
+ }
421
+ return best?.path ?? null;
422
+ }
423
+ /**
424
+ * GET /api/threads/:threadId/logs โ€” a readable digest of the agent's raw stdout.
425
+ *
426
+ * Parses the newest stream-json log file into tail-first turns, then returns a
427
+ * single character window (default 4000, Telegram-sized). Callers page deeper
428
+ * into history with ?offset=; nextOffset/hasMore drive a "next chunk" button.
429
+ */
430
+ export function handleGetThreadLogs(args, threadId) {
431
+ const { json, url } = args;
432
+ const logFile = findLatestThreadLogFile(threadId);
433
+ if (!logFile) {
434
+ json({ threadId, text: "", offset: 0, nextOffset: 0, hasMore: false, totalLength: 0 });
435
+ return true;
436
+ }
437
+ let raw;
438
+ try {
439
+ raw = readFileSync(logFile, "utf-8");
440
+ }
441
+ catch (err) {
442
+ json({ error: errorMessage(err) }, 500);
443
+ return true;
444
+ }
445
+ const rendered = renderThreadLog(parseClaudeStreamLog(raw), true);
446
+ const page = sliceLogWindow(rendered, Number(url.searchParams.get("offset")), Number(url.searchParams.get("limit")));
447
+ json({ threadId, ...page });
448
+ return true;
449
+ }
260
450
  /** GET /api/threads/:threadId/children โ€” get children of a root */
261
451
  export function handleGetThreadChildren(args, rootThreadId) {
262
452
  const children = getThreadsByRoot(args.db, rootThreadId);