viveworker 0.8.0 → 0.8.2
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +141 -0
- package/package.json +7 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
- package/scripts/a2a-executor.mjs +261 -7
- package/scripts/a2a-handler.mjs +88 -0
- package/scripts/a2a-relay-client.mjs +6 -0
- package/scripts/lib/markdown-render.mjs +128 -1
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +1504 -128
- package/scripts/viveworker.mjs +262 -1
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +162 -0
- package/web/app.js +1164 -101
- package/web/build-id.js +1 -0
- package/web/i18n.js +123 -15
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +2 -2
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +16 -6
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { createServer as createHttpServer } from "node:http";
|
|
6
6
|
import { createServer as createHttpsServer } from "node:https";
|
|
7
|
-
import { promises as fs, readFileSync, createReadStream } from "node:fs";
|
|
7
|
+
import { promises as fs, readFileSync, createReadStream, watch as watchFs } from "node:fs";
|
|
8
8
|
import net from "node:net";
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import path from "node:path";
|
|
@@ -16,6 +16,7 @@ import zlib from "node:zlib";
|
|
|
16
16
|
import webPush from "web-push";
|
|
17
17
|
import * as hazbaseAuth from "@hazbase/auth";
|
|
18
18
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
19
|
+
import { APP_BUILD_ID as WEB_APP_BUILD_ID } from "../web/build-id.js";
|
|
19
20
|
import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
|
|
20
21
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
21
22
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
@@ -48,13 +49,13 @@ const listSupportedPayments = typeof hazbaseAuth.listSupportedPayments === "func
|
|
|
48
49
|
? hazbaseAuth.listSupportedPayments.bind(hazbaseAuth)
|
|
49
50
|
: async () => ({ networks: [], defaultNetwork: "base-sepolia" });
|
|
50
51
|
const appPackageVersion = readPackageVersion();
|
|
51
|
-
const WEB_APP_BUILD_ID = "20260428-remote-token-refresh";
|
|
52
52
|
const WEB_APP_SCRIPT_URL = `/app.js?v=${WEB_APP_BUILD_ID}`;
|
|
53
|
+
const WEB_APP_BUILD_ID_PLACEHOLDER = "__VIVEWORKER_APP_BUILD_ID__";
|
|
53
54
|
const sessionCookieName = "viveworker_session";
|
|
54
55
|
const deviceCookieName = "viveworker_device";
|
|
55
56
|
const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "approval", "plan", "choice", "info", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
56
57
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
57
|
-
const timelineKinds = new Set([...timelineMessageKinds, "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
58
|
+
const timelineKinds = new Set([...timelineMessageKinds, "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "command_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
58
59
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
59
60
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
60
61
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -63,7 +64,9 @@ const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
|
63
64
|
const REMOTE_PAIRING_RELAY_TOKEN_ROTATION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
64
65
|
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
65
66
|
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
67
|
+
const DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS = 1200;
|
|
66
68
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
69
|
+
const COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS = 2 * 60 * 1000;
|
|
67
70
|
const HAZBASE_METADATA_TIMEOUT_MS = 1500;
|
|
68
71
|
const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
|
|
69
72
|
const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -269,6 +272,15 @@ const runtime = {
|
|
|
269
272
|
recentHistoryItems: [],
|
|
270
273
|
recentTimelineEntries: [],
|
|
271
274
|
recentCodeEvents: [],
|
|
275
|
+
timelineBus: null,
|
|
276
|
+
timelineRevision: 0,
|
|
277
|
+
timelineLiveWatchers: [],
|
|
278
|
+
timelineLiveScanTimer: null,
|
|
279
|
+
timelineLiveScanInFlight: false,
|
|
280
|
+
timelineLiveScanReschedule: false,
|
|
281
|
+
timelineLiveScanReasons: new Set(),
|
|
282
|
+
timelineLiveClaudeFiles: new Set(),
|
|
283
|
+
timelineLiveRolloutFiles: new Set(),
|
|
272
284
|
pairingAttemptsByRemoteAddress: new Map(),
|
|
273
285
|
ipcClient: null,
|
|
274
286
|
remotePairingHandle: null,
|
|
@@ -516,6 +528,8 @@ function kindTitle(locale, kind) {
|
|
|
516
528
|
return t(locale, "server.title.complete");
|
|
517
529
|
case "file_event":
|
|
518
530
|
return t(locale, "common.fileEvent");
|
|
531
|
+
case "command_event":
|
|
532
|
+
return t(locale, "common.commandEvent");
|
|
519
533
|
case "diff_thread":
|
|
520
534
|
return t(locale, "common.diff");
|
|
521
535
|
case "a2a_task":
|
|
@@ -671,6 +685,118 @@ function formatLocalizedTitle(locale, baseKeyOrTitle, threadLabel) {
|
|
|
671
685
|
return formatTitle(baseTitle, threadLabel);
|
|
672
686
|
}
|
|
673
687
|
|
|
688
|
+
function localizedThreadSharePushContent(locale, { sourceLabel = "", sourceTool = "", targetLabel = "", targetTool = "", body = "" } = {}) {
|
|
689
|
+
const source = cleanText(sourceLabel || sourceTool || "agent");
|
|
690
|
+
const target = cleanText(targetLabel || targetTool || "target");
|
|
691
|
+
return {
|
|
692
|
+
title: t(locale, "server.push.threadShare.title", { source, target }),
|
|
693
|
+
body: pushBodySnippet(body),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function localizedThreadShareFallbackPushContent(locale, target) {
|
|
698
|
+
const normalizedTarget = cleanText(target || "");
|
|
699
|
+
return {
|
|
700
|
+
title: t(locale, "server.push.threadShareFallback.title"),
|
|
701
|
+
body: t(locale, "server.push.threadShareFallback.body", { target: normalizedTarget }),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function localizedMcpNotifyTitle(locale, title) {
|
|
706
|
+
const normalizedTitle = cleanText(title || "");
|
|
707
|
+
if (!normalizedTitle || normalizedTitle === "MCP") {
|
|
708
|
+
return t(locale, "server.push.mcpNotify.title");
|
|
709
|
+
}
|
|
710
|
+
return normalizedTitle;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function localizedMcpApprovalPushContent(locale, approval) {
|
|
714
|
+
const kind = cleanText(approval?.kind || "mcp");
|
|
715
|
+
const title = cleanText(approval?.title || "");
|
|
716
|
+
const body = cleanText(approval?.messageText || "");
|
|
717
|
+
if (kind === "question") {
|
|
718
|
+
return {
|
|
719
|
+
title: t(locale, "server.push.mcpQuestion.title"),
|
|
720
|
+
body: pushBodySnippet(body),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (kind === "file_share") {
|
|
724
|
+
const filePath = compactPath(normalizeTimelineFileRefs(approval?.fileRefs ?? [])[0] || t(locale, "common.fileEvent"));
|
|
725
|
+
const sizeMatch = body.match(/^\s*Size:\s*([^\n]+)$/imu);
|
|
726
|
+
return {
|
|
727
|
+
title: t(locale, "server.push.mcpShareFile.title"),
|
|
728
|
+
body: t(locale, "server.push.mcpShareFile.body", {
|
|
729
|
+
path: filePath,
|
|
730
|
+
size: cleanText(sizeMatch?.[1] || ""),
|
|
731
|
+
}),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (kind === "a2a_task") {
|
|
735
|
+
const target = cleanText(title.match(/^Send A2A task to\s+(.+)$/iu)?.[1] || "A2A target");
|
|
736
|
+
return {
|
|
737
|
+
title: t(locale, "server.push.mcpA2aTask.title", { target }),
|
|
738
|
+
body: t(locale, "server.push.mcpA2aTask.body", { target }),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (!title || title === "MCP approval") {
|
|
742
|
+
return {
|
|
743
|
+
title: t(locale, "server.push.mcpApproval.title"),
|
|
744
|
+
body: pushBodySnippet(body),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return { title, body: pushBodySnippet(body) };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function localizedPaymentApprovalPushContent(locale, approval) {
|
|
751
|
+
const payment = isPlainObject(approval?.rawParams) ? approval.rawParams : {};
|
|
752
|
+
const amount = cleanText(payment.amountUsdc || payment.amountAtomic || "");
|
|
753
|
+
const network = cleanText(payment.network || "");
|
|
754
|
+
const resource = cleanText(payment.resource || payment.url || "");
|
|
755
|
+
const titleKey = cleanText(approval?.kind || "") === "hazbase_wallet_payment"
|
|
756
|
+
? "server.push.hazbasePayment.title"
|
|
757
|
+
: "server.push.paymentApproval.title";
|
|
758
|
+
return {
|
|
759
|
+
title: t(locale, titleKey, { amount }),
|
|
760
|
+
body: t(locale, "server.push.paymentApproval.body", { amount, network, resource }),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function localizedMoltbookReplyPushContent(locale, item) {
|
|
765
|
+
const title = cleanText(item?.title || "");
|
|
766
|
+
return {
|
|
767
|
+
title: !title || title === "Moltbook reply" ? t(locale, "server.push.moltbookReply.title") : title,
|
|
768
|
+
body: cleanText(item?.summary || "") || t(locale, "server.push.moltbookReply.body"),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function localizedMoltbookDraftPushTitle(locale, { draftType = "", postTitle = "", slot = "" } = {}) {
|
|
773
|
+
const normalizedPostTitle = cleanText(postTitle || "");
|
|
774
|
+
if (draftType !== "original_post") {
|
|
775
|
+
return t(locale, "server.push.moltbookDraft.replyTitle", { postTitle: normalizedPostTitle });
|
|
776
|
+
}
|
|
777
|
+
switch (cleanText(slot || "")) {
|
|
778
|
+
case "morning":
|
|
779
|
+
return t(locale, "server.push.moltbookDraft.originalMorning");
|
|
780
|
+
case "noon":
|
|
781
|
+
return t(locale, "server.push.moltbookDraft.originalNoon");
|
|
782
|
+
case "evening":
|
|
783
|
+
return t(locale, "server.push.moltbookDraft.originalEvening");
|
|
784
|
+
default:
|
|
785
|
+
return normalizedPostTitle || t(locale, "server.push.moltbookDraft.originalTitle");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function localizedMoltbookPostedBody(locale, draft, finalTitle) {
|
|
790
|
+
if (cleanText(draft?.draftType || "") === "original_post") {
|
|
791
|
+
return t(locale, "server.push.moltbookDraft.postedOriginalBody", { title: cleanText(finalTitle || draft?.postTitle || "") });
|
|
792
|
+
}
|
|
793
|
+
return t(locale, "server.push.moltbookDraft.postedReplyBody", { postTitle: cleanText(draft?.postTitle || "") });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function pushBodySnippet(value, maxChars = 160) {
|
|
797
|
+
return truncate(singleLine(value || ""), maxChars);
|
|
798
|
+
}
|
|
799
|
+
|
|
674
800
|
function notificationIconPrefix(kind) {
|
|
675
801
|
switch (kind) {
|
|
676
802
|
case "approval":
|
|
@@ -703,13 +829,17 @@ function normalizeTimelineOutcome(value) {
|
|
|
703
829
|
|
|
704
830
|
function normalizeTimelineFileEventType(value) {
|
|
705
831
|
const normalized = cleanText(value || "").toLowerCase();
|
|
706
|
-
return ["read", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
832
|
+
return ["read", "search", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
707
833
|
}
|
|
708
834
|
|
|
709
835
|
function fileEventTitle(locale, fileEventType) {
|
|
710
836
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
711
837
|
case "read":
|
|
712
838
|
return t(locale, "fileEvent.read");
|
|
839
|
+
case "search":
|
|
840
|
+
return t(locale, "fileEvent.search");
|
|
841
|
+
case "command":
|
|
842
|
+
return t(locale, "fileEvent.command");
|
|
713
843
|
case "write":
|
|
714
844
|
return t(locale, "fileEvent.write");
|
|
715
845
|
case "create":
|
|
@@ -728,6 +858,10 @@ function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
|
728
858
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
729
859
|
case "read":
|
|
730
860
|
return t(locale, "detail.fileEvent.read", vars);
|
|
861
|
+
case "search":
|
|
862
|
+
return t(locale, "detail.fileEvent.search", vars);
|
|
863
|
+
case "command":
|
|
864
|
+
return t(locale, "detail.fileEvent.command", vars);
|
|
731
865
|
case "write":
|
|
732
866
|
return t(locale, "detail.fileEvent.write", vars);
|
|
733
867
|
case "create":
|
|
@@ -1435,6 +1569,87 @@ function extractCommandLineFromFunctionOutput(outputText) {
|
|
|
1435
1569
|
return unwrapShellCommand(match?.[1] || "");
|
|
1436
1570
|
}
|
|
1437
1571
|
|
|
1572
|
+
function parseToolArgumentsJson(value) {
|
|
1573
|
+
if (isPlainObject(value)) {
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
const parsed = safeJsonParse(value);
|
|
1580
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function commandBaseName(command) {
|
|
1584
|
+
const normalized = cleanText(command || "");
|
|
1585
|
+
if (!normalized) {
|
|
1586
|
+
return "";
|
|
1587
|
+
}
|
|
1588
|
+
return path.basename(normalized);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function classifyTimelineCommand(commandText) {
|
|
1592
|
+
const normalizedCommand = unwrapShellCommand(commandText);
|
|
1593
|
+
const tokens = tokenizeShellWords(normalizedCommand);
|
|
1594
|
+
if (tokens.length === 0) {
|
|
1595
|
+
return {
|
|
1596
|
+
fileEventType: "command",
|
|
1597
|
+
command: "",
|
|
1598
|
+
commandText: normalizedCommand,
|
|
1599
|
+
fileRefs: [],
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const command = commandBaseName(tokens[0]);
|
|
1604
|
+
const gitSubcommand = command === "git" ? cleanText(tokens[1] || "") : "";
|
|
1605
|
+
let fileEventType = "command";
|
|
1606
|
+
if (["rg", "grep", "ag", "ack", "fd", "find"].includes(command) || gitSubcommand === "grep") {
|
|
1607
|
+
fileEventType = "search";
|
|
1608
|
+
} else if (
|
|
1609
|
+
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command) ||
|
|
1610
|
+
(command === "git" && ["status", "diff", "show", "log", "ls-files", "branch"].includes(gitSubcommand))
|
|
1611
|
+
) {
|
|
1612
|
+
fileEventType = "read";
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
return {
|
|
1616
|
+
fileEventType,
|
|
1617
|
+
command,
|
|
1618
|
+
commandText: normalizedCommand,
|
|
1619
|
+
fileRefs: extractReadFileRefsFromCommand(normalizedCommand),
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function redactTimelineCommandText(commandText) {
|
|
1624
|
+
return cleanText(commandText || "")
|
|
1625
|
+
.replace(/((?:--)?(?:api[-_]?key|token|secret|password|credential)(?:=|\s+))(["']?)[^\s"']+\2/giu, "$1[redacted]")
|
|
1626
|
+
.replace(/\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*=)(["']?)[^\s"']+\2/giu, "$1[redacted]");
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function timelineCommandMessage(locale, { commandText = "", toolName = "", fileRefs = [] } = {}) {
|
|
1630
|
+
const commandBlock = commandText
|
|
1631
|
+
? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${redactTimelineCommandText(commandText)}\n\`\`\``
|
|
1632
|
+
: "";
|
|
1633
|
+
const toolBlock = !commandBlock && toolName
|
|
1634
|
+
? `${t(locale, "server.message.toolLabel")}\n\`\`\`text\n${cleanText(toolName)}\n\`\`\``
|
|
1635
|
+
: "";
|
|
1636
|
+
const files = normalizeTimelineFileRefs(fileRefs);
|
|
1637
|
+
const filesBlock = files.length
|
|
1638
|
+
? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${files.join("\n")}\n\`\`\``
|
|
1639
|
+
: "";
|
|
1640
|
+
return [commandBlock || toolBlock, filesBlock].filter(Boolean).join("\n\n");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function timelineCommandSummary(commandText, fallback = "") {
|
|
1644
|
+
const redacted = redactTimelineCommandText(commandText);
|
|
1645
|
+
return truncate(singleLine(redacted || fallback || ""), 180);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function firstMarkdownCodeFenceText(text) {
|
|
1649
|
+
const match = String(text || "").match(/```(?:\w+)?\n([\s\S]*?)\n```/u);
|
|
1650
|
+
return cleanText(match?.[1] || "");
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1438
1653
|
function extractReadFileRefsFromCommand(commandText) {
|
|
1439
1654
|
const normalizedCommand = unwrapShellCommand(commandText);
|
|
1440
1655
|
const tokens = tokenizeShellWords(normalizedCommand);
|
|
@@ -1663,6 +1878,36 @@ function rememberApplyPatchInput(fileState, payload, createdAtMs = Date.now()) {
|
|
|
1663
1878
|
}
|
|
1664
1879
|
}
|
|
1665
1880
|
|
|
1881
|
+
function rememberToolEventInput(fileState, payload, createdAtMs = Date.now()) {
|
|
1882
|
+
const callId = cleanText(payload?.call_id || payload?.callId || payload?.id || "");
|
|
1883
|
+
if (!callId) {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
if (!(fileState.toolEventInputsByCallId instanceof Map)) {
|
|
1887
|
+
fileState.toolEventInputsByCallId = new Map();
|
|
1888
|
+
}
|
|
1889
|
+
const stored = {
|
|
1890
|
+
callId,
|
|
1891
|
+
name: cleanText(payload?.name || ""),
|
|
1892
|
+
arguments: parseToolArgumentsJson(payload?.arguments),
|
|
1893
|
+
createdAtMs: Number(createdAtMs) || Date.now(),
|
|
1894
|
+
};
|
|
1895
|
+
fileState.toolEventInputsByCallId.set(callId, stored);
|
|
1896
|
+
while (fileState.toolEventInputsByCallId.size > 128) {
|
|
1897
|
+
const oldestKey = fileState.toolEventInputsByCallId.keys().next().value;
|
|
1898
|
+
fileState.toolEventInputsByCallId.delete(oldestKey);
|
|
1899
|
+
}
|
|
1900
|
+
return stored;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function findStoredToolEventInput(fileState, callId) {
|
|
1904
|
+
const normalizedCallId = cleanText(callId || "");
|
|
1905
|
+
if (!normalizedCallId || !(fileState?.toolEventInputsByCallId instanceof Map)) {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
return fileState.toolEventInputsByCallId.get(normalizedCallId) || null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1666
1911
|
async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath }) {
|
|
1667
1912
|
const normalizedCallId = cleanText(callId || "");
|
|
1668
1913
|
if (!normalizedCallId) {
|
|
@@ -2067,6 +2312,7 @@ async function ensureRolloutFileState(runtime, threadId, rolloutFilePath) {
|
|
|
2067
2312
|
threadId: cleanText(threadId || ""),
|
|
2068
2313
|
cwd: await findRolloutThreadCwd(runtime, threadId || ""),
|
|
2069
2314
|
applyPatchInputsByCallId: new Map(),
|
|
2315
|
+
toolEventInputsByCallId: new Map(),
|
|
2070
2316
|
startupCutoffMs: 0,
|
|
2071
2317
|
skipPartialLine: false,
|
|
2072
2318
|
};
|
|
@@ -2839,6 +3085,7 @@ function normalizeProvider(value) {
|
|
|
2839
3085
|
if (normalized === "moltbook") return "moltbook";
|
|
2840
3086
|
if (normalized === "a2a") return "a2a";
|
|
2841
3087
|
if (normalized === "viveworker") return "viveworker";
|
|
3088
|
+
if (normalized === "mcp") return "mcp";
|
|
2842
3089
|
return "codex";
|
|
2843
3090
|
}
|
|
2844
3091
|
|
|
@@ -2848,6 +3095,7 @@ function providerDisplayName(locale, provider) {
|
|
|
2848
3095
|
if (p === "moltbook") return "Moltbook";
|
|
2849
3096
|
if (p === "a2a") return "A2A";
|
|
2850
3097
|
if (p === "viveworker") return t(locale, "common.appName");
|
|
3098
|
+
if (p === "mcp") return "MCP";
|
|
2851
3099
|
return t(locale, "common.codex");
|
|
2852
3100
|
}
|
|
2853
3101
|
|
|
@@ -2896,7 +3144,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2896
3144
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
2897
3145
|
const rawThreadLabel = cleanText(raw.threadLabel ?? "");
|
|
2898
3146
|
const historyProvider = normalizeProvider(raw.provider);
|
|
2899
|
-
const skipHistoryThreadLabelRewrite = historyProvider === "moltbook" || historyProvider === "a2a" || historyProvider === "viveworker"
|
|
3147
|
+
const skipHistoryThreadLabelRewrite = historyProvider === "moltbook" || historyProvider === "a2a" || historyProvider === "viveworker" || historyProvider === "mcp"
|
|
2900
3148
|
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
2901
3149
|
const threadLabel = skipHistoryThreadLabelRewrite
|
|
2902
3150
|
? rawThreadLabel
|
|
@@ -3138,6 +3386,8 @@ function timelineKindSortPriority(kind) {
|
|
|
3138
3386
|
return 60;
|
|
3139
3387
|
case "ambient_suggestions":
|
|
3140
3388
|
return 52;
|
|
3389
|
+
case "command_event":
|
|
3390
|
+
return 46;
|
|
3141
3391
|
case "file_event":
|
|
3142
3392
|
return 45;
|
|
3143
3393
|
case "assistant_final":
|
|
@@ -3160,7 +3410,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
3160
3410
|
}
|
|
3161
3411
|
|
|
3162
3412
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
3163
|
-
const
|
|
3413
|
+
const rawKind = cleanText(raw.kind ?? "");
|
|
3414
|
+
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3415
|
+
const kind = rawKind === "file_event" && fileEventType === "command" ? "command_event" : rawKind;
|
|
3164
3416
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
3165
3417
|
if (!stableId || !timelineKinds.has(kind)) {
|
|
3166
3418
|
return null;
|
|
@@ -3169,7 +3421,6 @@ function normalizeTimelineEntry(raw) {
|
|
|
3169
3421
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
3170
3422
|
const rawMessageText = raw.messageText ?? "";
|
|
3171
3423
|
const messageText = normalizeTimelineMessageText(rawMessageText);
|
|
3172
|
-
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3173
3424
|
const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
|
|
3174
3425
|
const diffSource = normalizeTimelineDiffSource(raw.diffSource ?? "");
|
|
3175
3426
|
const diffCounts = diffLineCounts(diffText);
|
|
@@ -3183,7 +3434,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
3183
3434
|
(kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
|
|
3184
3435
|
"";
|
|
3185
3436
|
const rawProvider = normalizeProvider(raw.provider);
|
|
3186
|
-
const skipThreadLabelRewrite = rawProvider === "moltbook" || rawProvider === "a2a" || rawProvider === "viveworker"
|
|
3437
|
+
const skipThreadLabelRewrite = rawProvider === "moltbook" || rawProvider === "a2a" || rawProvider === "viveworker" || rawProvider === "mcp"
|
|
3187
3438
|
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
3188
3439
|
const threadLabel =
|
|
3189
3440
|
skipThreadLabelRewrite
|
|
@@ -3261,7 +3512,78 @@ function normalizeTimelineEntry(raw) {
|
|
|
3261
3512
|
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
3262
3513
|
}
|
|
3263
3514
|
|
|
3264
|
-
|
|
3515
|
+
class TimelineBus {
|
|
3516
|
+
constructor() {
|
|
3517
|
+
this.clients = new Set();
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
addClient(client) {
|
|
3521
|
+
this.clients.add(client);
|
|
3522
|
+
return () => {
|
|
3523
|
+
this.clients.delete(client);
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
emit(eventName, payload) {
|
|
3528
|
+
for (const client of [...this.clients]) {
|
|
3529
|
+
try {
|
|
3530
|
+
client.send(eventName, payload);
|
|
3531
|
+
} catch {
|
|
3532
|
+
try {
|
|
3533
|
+
client.close?.();
|
|
3534
|
+
} catch {}
|
|
3535
|
+
this.clients.delete(client);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
closeAll() {
|
|
3541
|
+
for (const client of [...this.clients]) {
|
|
3542
|
+
try {
|
|
3543
|
+
client.close?.();
|
|
3544
|
+
} catch {}
|
|
3545
|
+
}
|
|
3546
|
+
this.clients.clear();
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
function ensureTimelineBus(runtime) {
|
|
3551
|
+
if (!runtime.timelineBus) {
|
|
3552
|
+
runtime.timelineBus = new TimelineBus();
|
|
3553
|
+
}
|
|
3554
|
+
return runtime.timelineBus;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
function buildTimelineUpdatePayload({ runtime, entry, source = "unknown" }) {
|
|
3558
|
+
runtime.timelineRevision = (Number(runtime.timelineRevision) || 0) + 1;
|
|
3559
|
+
return {
|
|
3560
|
+
revision: runtime.timelineRevision,
|
|
3561
|
+
token: cleanText(entry?.token || historyToken(entry?.stableId || entry?.createdAtMs || "")).slice(0, 120),
|
|
3562
|
+
stableId: cleanText(entry?.stableId || "").slice(0, 180),
|
|
3563
|
+
kind: cleanText(entry?.kind || "").slice(0, 80),
|
|
3564
|
+
provider: cleanText(entry?.provider || "").slice(0, 40),
|
|
3565
|
+
threadId: cleanText(entry?.threadId || "").slice(0, 120),
|
|
3566
|
+
createdAtMs: Number(entry?.createdAtMs) || 0,
|
|
3567
|
+
source: cleanText(source || "unknown").slice(0, 80),
|
|
3568
|
+
ingestAtMs: Date.now(),
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function publishTimelineUpdate({ config, runtime, entry, source = "unknown" }) {
|
|
3573
|
+
if (!config?.timelineLiveSync || !runtime || !entry) {
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
const payload = buildTimelineUpdatePayload({ runtime, entry, source });
|
|
3577
|
+
console.log(
|
|
3578
|
+
`[timeline-ingest] at=${new Date(payload.ingestAtMs).toISOString()} ` +
|
|
3579
|
+
`provider=${payload.provider || "unknown"} kind=${payload.kind || "unknown"} ` +
|
|
3580
|
+
`token=${payload.token || "none"} createdAtMs=${payload.createdAtMs || 0} ` +
|
|
3581
|
+
`source=${payload.source || "unknown"} revision=${payload.revision}`
|
|
3582
|
+
);
|
|
3583
|
+
ensureTimelineBus(runtime).emit("timeline:update", payload);
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
function recordTimelineEntry({ config, runtime, state, entry, source = "unknown" }) {
|
|
3265
3587
|
const normalized = normalizeTimelineEntry(entry);
|
|
3266
3588
|
if (!normalized) {
|
|
3267
3589
|
return false;
|
|
@@ -3278,6 +3600,10 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3278
3600
|
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3279
3601
|
"stableId",
|
|
3280
3602
|
"title",
|
|
3603
|
+
"summary",
|
|
3604
|
+
"messageText",
|
|
3605
|
+
"fileEventType",
|
|
3606
|
+
"fileRefs",
|
|
3281
3607
|
"createdAtMs",
|
|
3282
3608
|
"diffAvailable",
|
|
3283
3609
|
"diffSource",
|
|
@@ -3288,6 +3614,9 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3288
3614
|
]);
|
|
3289
3615
|
runtime.recentTimelineEntries = nextItems;
|
|
3290
3616
|
state.recentTimelineEntries = nextItems;
|
|
3617
|
+
if (changed) {
|
|
3618
|
+
publishTimelineUpdate({ config, runtime, entry: normalized, source });
|
|
3619
|
+
}
|
|
3291
3620
|
return changed;
|
|
3292
3621
|
}
|
|
3293
3622
|
|
|
@@ -3408,6 +3737,18 @@ function historyItemFromEvent(event) {
|
|
|
3408
3737
|
});
|
|
3409
3738
|
}
|
|
3410
3739
|
|
|
3740
|
+
function completionPushContentDedupeId(event) {
|
|
3741
|
+
if (!event || event.kind !== "task_complete") {
|
|
3742
|
+
return "";
|
|
3743
|
+
}
|
|
3744
|
+
const threadId = cleanText(event.threadId ?? event.conversationId ?? "unknown") || "unknown";
|
|
3745
|
+
const messageText = normalizeLongText(event.detailText || event.message || "");
|
|
3746
|
+
if (!messageText) {
|
|
3747
|
+
return "";
|
|
3748
|
+
}
|
|
3749
|
+
return `task_complete_content:${threadId}:${historyToken(messageText)}`;
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3411
3752
|
function recordHistoryItem({ config, runtime, state, item }) {
|
|
3412
3753
|
const normalized = normalizeHistoryItem(item);
|
|
3413
3754
|
if (!normalized) {
|
|
@@ -3570,6 +3911,7 @@ function recordActionHistoryItem({
|
|
|
3570
3911
|
runtime,
|
|
3571
3912
|
state,
|
|
3572
3913
|
entry: item,
|
|
3914
|
+
source: "event",
|
|
3573
3915
|
});
|
|
3574
3916
|
return historyChanged || timelineChanged;
|
|
3575
3917
|
}
|
|
@@ -3729,7 +4071,20 @@ function pushDeliveryKey(deviceId, stableId) {
|
|
|
3729
4071
|
return `${cleanText(deviceId || "")}:${cleanText(stableId || "")}`;
|
|
3730
4072
|
}
|
|
3731
4073
|
|
|
3732
|
-
async function deliverWebPushItem({
|
|
4074
|
+
async function deliverWebPushItem({
|
|
4075
|
+
config,
|
|
4076
|
+
state,
|
|
4077
|
+
kind,
|
|
4078
|
+
token,
|
|
4079
|
+
stableId,
|
|
4080
|
+
title,
|
|
4081
|
+
body,
|
|
4082
|
+
tab = "",
|
|
4083
|
+
subtab = "",
|
|
4084
|
+
buildLocalizedContent = null,
|
|
4085
|
+
dedupeId = "",
|
|
4086
|
+
dedupeWindowMs = 0,
|
|
4087
|
+
}) {
|
|
3733
4088
|
if (!config.webPushEnabled || config.dryRun) {
|
|
3734
4089
|
return false;
|
|
3735
4090
|
}
|
|
@@ -3746,8 +4101,18 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3746
4101
|
let changed = false;
|
|
3747
4102
|
|
|
3748
4103
|
for (const subscription of subscriptions) {
|
|
3749
|
-
const
|
|
3750
|
-
|
|
4104
|
+
const now = Date.now();
|
|
4105
|
+
const stableDeliveryKey = pushDeliveryKey(subscription.deviceId, stableId);
|
|
4106
|
+
const dedupeDeliveryKey = pushDeliveryKey(subscription.deviceId, dedupeId || stableId);
|
|
4107
|
+
const stableDeliveredAt = Number(state.pushDeliveries[stableDeliveryKey]) || 0;
|
|
4108
|
+
const dedupeDeliveredAt = Number(state.pushDeliveries[dedupeDeliveryKey]) || 0;
|
|
4109
|
+
if (stableDeliveredAt) {
|
|
4110
|
+
continue;
|
|
4111
|
+
}
|
|
4112
|
+
if (
|
|
4113
|
+
dedupeDeliveredAt &&
|
|
4114
|
+
(!dedupeWindowMs || now - dedupeDeliveredAt <= Math.max(0, Number(dedupeWindowMs) || 0))
|
|
4115
|
+
) {
|
|
3751
4116
|
continue;
|
|
3752
4117
|
}
|
|
3753
4118
|
|
|
@@ -3775,8 +4140,10 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3775
4140
|
},
|
|
3776
4141
|
payload
|
|
3777
4142
|
);
|
|
3778
|
-
|
|
3779
|
-
|
|
4143
|
+
state.pushDeliveries[stableDeliveryKey] = now;
|
|
4144
|
+
if (dedupeDeliveryKey !== stableDeliveryKey) {
|
|
4145
|
+
state.pushDeliveries[dedupeDeliveryKey] = now;
|
|
4146
|
+
}
|
|
3780
4147
|
trimSeenEvents(state.pushDeliveries, config.maxSeenEvents * 4);
|
|
3781
4148
|
const stored = normalizePushSubscriptionRecord(state.pushSubscriptions?.[subscription.id]);
|
|
3782
4149
|
if (stored) {
|
|
@@ -3928,6 +4295,207 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3928
4295
|
return dirty;
|
|
3929
4296
|
}
|
|
3930
4297
|
|
|
4298
|
+
function isPathWithin(parentDir, candidatePath) {
|
|
4299
|
+
const parent = path.resolve(parentDir || "");
|
|
4300
|
+
const candidate = path.resolve(candidatePath || "");
|
|
4301
|
+
if (!parent || !candidate) return false;
|
|
4302
|
+
return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
function isClaudeTranscriptPath(config, filePath) {
|
|
4306
|
+
return Boolean(
|
|
4307
|
+
filePath &&
|
|
4308
|
+
filePath.endsWith(".jsonl") &&
|
|
4309
|
+
isPathWithin(config.claudeProjectsDir, filePath)
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
function isRolloutFilePath(filePath) {
|
|
4314
|
+
const base = path.basename(filePath || "");
|
|
4315
|
+
return base.startsWith("rollout-") && base.endsWith(".jsonl");
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
function watchedEventPath(root, filename) {
|
|
4319
|
+
if (!filename) return root;
|
|
4320
|
+
const text = Buffer.isBuffer(filename) ? filename.toString("utf8") : String(filename);
|
|
4321
|
+
if (!text) return root;
|
|
4322
|
+
return path.isAbsolute(text) ? text : path.join(root, text);
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
function addTimelineLiveWatcher({ runtime, label, targetPath, options = {}, onChange }) {
|
|
4326
|
+
if (!targetPath) return;
|
|
4327
|
+
try {
|
|
4328
|
+
const watcher = watchFs(targetPath, options, (_eventType, filename) => {
|
|
4329
|
+
onChange(watchedEventPath(targetPath, filename));
|
|
4330
|
+
});
|
|
4331
|
+
watcher.on?.("error", (err) => {
|
|
4332
|
+
console.warn(`[timeline-live-watch-error] ${label} ${err?.message || err}`);
|
|
4333
|
+
});
|
|
4334
|
+
runtime.timelineLiveWatchers.push(watcher);
|
|
4335
|
+
console.log(`[timeline-live-watch] ${label} ${targetPath}`);
|
|
4336
|
+
} catch (err) {
|
|
4337
|
+
console.warn(`[timeline-live-watch-skip] ${label} ${targetPath} ${err?.message || err}`);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
function scheduleTimelineLiveScan({ config, runtime, state, reason = "unknown", filePath = "" }) {
|
|
4342
|
+
if (!config.timelineLiveSync || runtime.stopping) {
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
runtime.timelineLiveScanReasons.add(cleanText(reason || "unknown").slice(0, 80));
|
|
4346
|
+
const normalizedPath = cleanText(filePath || "");
|
|
4347
|
+
if (isClaudeTranscriptPath(config, normalizedPath)) {
|
|
4348
|
+
runtime.timelineLiveClaudeFiles.add(normalizedPath);
|
|
4349
|
+
} else if (isRolloutFilePath(normalizedPath)) {
|
|
4350
|
+
runtime.timelineLiveRolloutFiles.add(normalizedPath);
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
if (runtime.timelineLiveScanTimer) {
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
runtime.timelineLiveScanTimer = setTimeout(() => {
|
|
4357
|
+
runtime.timelineLiveScanTimer = null;
|
|
4358
|
+
runPendingTimelineLiveScan({ config, runtime, state }).catch((err) => {
|
|
4359
|
+
console.warn(`[timeline-live-scan-error] ${err?.message || err}`);
|
|
4360
|
+
});
|
|
4361
|
+
}, Math.max(25, Number(config.timelineLiveDebounceMs) || 150));
|
|
4362
|
+
runtime.timelineLiveScanTimer.unref?.();
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
async function runPendingTimelineLiveScan({ config, runtime, state }) {
|
|
4366
|
+
if (runtime.timelineLiveScanInFlight) {
|
|
4367
|
+
runtime.timelineLiveScanReschedule = true;
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
runtime.timelineLiveScanInFlight = true;
|
|
4371
|
+
const reasons = new Set(runtime.timelineLiveScanReasons);
|
|
4372
|
+
const claudeFiles = [...runtime.timelineLiveClaudeFiles];
|
|
4373
|
+
const rolloutFiles = [...runtime.timelineLiveRolloutFiles];
|
|
4374
|
+
runtime.timelineLiveScanReasons.clear();
|
|
4375
|
+
runtime.timelineLiveClaudeFiles.clear();
|
|
4376
|
+
runtime.timelineLiveRolloutFiles.clear();
|
|
4377
|
+
|
|
4378
|
+
let dirty = false;
|
|
4379
|
+
const now = Date.now();
|
|
4380
|
+
try {
|
|
4381
|
+
if (reasons.has("codex-home") || reasons.has("codex-history") || reasons.has("codex-logs")) {
|
|
4382
|
+
if (!config.codexLogsDbFile) {
|
|
4383
|
+
const latestLogsDbFile = await findLatestCodexLogsDbFile(config.codexHome);
|
|
4384
|
+
if (latestLogsDbFile && latestLogsDbFile !== runtime.logsDbFile) {
|
|
4385
|
+
runtime.logsDbFile = latestLogsDbFile;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
dirty = (await processHistoryTimelineFile({ config, runtime, state, now })) || dirty;
|
|
4389
|
+
if (config.notifyCompletions || config.webUiEnabled) {
|
|
4390
|
+
dirty = (await processSqliteCompletionLog({ config, runtime, state, now })) || dirty;
|
|
4391
|
+
}
|
|
4392
|
+
if (config.webUiEnabled) {
|
|
4393
|
+
dirty = (await processSqliteTimelineLog({ config, runtime, state, now })) || dirty;
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
for (const filePath of rolloutFiles.slice(0, 12)) {
|
|
4398
|
+
if (!runtime.knownFiles.includes(filePath)) {
|
|
4399
|
+
runtime.knownFiles.push(filePath);
|
|
4400
|
+
runtime.knownFiles.sort();
|
|
4401
|
+
}
|
|
4402
|
+
dirty = (await processRolloutFile({ filePath, config, runtime, state, now })) || dirty;
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
let claudeTranscriptChanged = false;
|
|
4406
|
+
let claudeTargets = claudeFiles;
|
|
4407
|
+
if (reasons.has("claude-dir") && claudeTargets.length === 0) {
|
|
4408
|
+
claudeTargets = await listClaudeTranscriptFiles(config.claudeProjectsDir, config.claudeTranscriptMaxAgeMs);
|
|
4409
|
+
}
|
|
4410
|
+
for (const filePath of claudeTargets.slice(0, 24)) {
|
|
4411
|
+
if (!runtime.claudeKnownFiles.includes(filePath)) {
|
|
4412
|
+
runtime.claudeKnownFiles.push(filePath);
|
|
4413
|
+
}
|
|
4414
|
+
const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
|
|
4415
|
+
claudeTranscriptChanged = claudeTranscriptChanged || changed;
|
|
4416
|
+
dirty = dirty || changed;
|
|
4417
|
+
}
|
|
4418
|
+
if (claudeTranscriptChanged) {
|
|
4419
|
+
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
if (dirty) {
|
|
4423
|
+
scheduleSaveState(config, state);
|
|
4424
|
+
}
|
|
4425
|
+
} finally {
|
|
4426
|
+
runtime.timelineLiveScanInFlight = false;
|
|
4427
|
+
if (runtime.timelineLiveScanReschedule || runtime.timelineLiveScanReasons.size > 0) {
|
|
4428
|
+
runtime.timelineLiveScanReschedule = false;
|
|
4429
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "reschedule" });
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
function startTimelineLiveSync({ config, runtime, state }) {
|
|
4435
|
+
if (config.dryRun || !config.webUiEnabled || !config.timelineLiveSync) {
|
|
4436
|
+
return null;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
addTimelineLiveWatcher({
|
|
4440
|
+
runtime,
|
|
4441
|
+
label: "codex-home",
|
|
4442
|
+
targetPath: config.codexHome,
|
|
4443
|
+
onChange: (filePath) => {
|
|
4444
|
+
const base = path.basename(filePath || "");
|
|
4445
|
+
if (filePath === config.historyFile || base === path.basename(config.historyFile || "")) {
|
|
4446
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-history", filePath });
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
if (/^logs(?:_\d+)?\.sqlite(?:-wal|-shm)?$/u.test(base)) {
|
|
4450
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-logs", filePath });
|
|
4451
|
+
}
|
|
4452
|
+
},
|
|
4453
|
+
});
|
|
4454
|
+
|
|
4455
|
+
addTimelineLiveWatcher({
|
|
4456
|
+
runtime,
|
|
4457
|
+
label: "codex-sessions",
|
|
4458
|
+
targetPath: config.sessionsDir,
|
|
4459
|
+
options: { recursive: true },
|
|
4460
|
+
onChange: (filePath) => {
|
|
4461
|
+
if (isRolloutFilePath(filePath)) {
|
|
4462
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-rollout", filePath });
|
|
4463
|
+
}
|
|
4464
|
+
},
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
addTimelineLiveWatcher({
|
|
4468
|
+
runtime,
|
|
4469
|
+
label: "claude-projects",
|
|
4470
|
+
targetPath: config.claudeProjectsDir,
|
|
4471
|
+
options: { recursive: true },
|
|
4472
|
+
onChange: (filePath) => {
|
|
4473
|
+
scheduleTimelineLiveScan({
|
|
4474
|
+
config,
|
|
4475
|
+
runtime,
|
|
4476
|
+
state,
|
|
4477
|
+
reason: isClaudeTranscriptPath(config, filePath) ? "claude-file" : "claude-dir",
|
|
4478
|
+
filePath,
|
|
4479
|
+
});
|
|
4480
|
+
},
|
|
4481
|
+
});
|
|
4482
|
+
|
|
4483
|
+
return {
|
|
4484
|
+
close() {
|
|
4485
|
+
if (runtime.timelineLiveScanTimer) {
|
|
4486
|
+
clearTimeout(runtime.timelineLiveScanTimer);
|
|
4487
|
+
runtime.timelineLiveScanTimer = null;
|
|
4488
|
+
}
|
|
4489
|
+
for (const watcher of runtime.timelineLiveWatchers.splice(0)) {
|
|
4490
|
+
try {
|
|
4491
|
+
watcher.close();
|
|
4492
|
+
} catch {}
|
|
4493
|
+
}
|
|
4494
|
+
runtime.timelineBus?.closeAll?.();
|
|
4495
|
+
},
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4498
|
+
|
|
3931
4499
|
async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
3932
4500
|
let stat;
|
|
3933
4501
|
try {
|
|
@@ -3951,6 +4519,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
3951
4519
|
threadId: extractThreadIdFromRolloutPath(filePath),
|
|
3952
4520
|
cwd: null,
|
|
3953
4521
|
applyPatchInputsByCallId: new Map(),
|
|
4522
|
+
toolEventInputsByCallId: new Map(),
|
|
3954
4523
|
startupCutoffMs:
|
|
3955
4524
|
typeof restoredOffset === "number" ? 0 : now - config.replaySeconds * 1000,
|
|
3956
4525
|
skipPartialLine:
|
|
@@ -4028,6 +4597,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4028
4597
|
runtime,
|
|
4029
4598
|
state,
|
|
4030
4599
|
entry: timelineEntry,
|
|
4600
|
+
source: "codex-rollout",
|
|
4031
4601
|
}) || dirty;
|
|
4032
4602
|
}
|
|
4033
4603
|
|
|
@@ -4045,6 +4615,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4045
4615
|
runtime,
|
|
4046
4616
|
state,
|
|
4047
4617
|
entry: fileTimelineEntry,
|
|
4618
|
+
source: "codex-rollout",
|
|
4048
4619
|
}) || dirty;
|
|
4049
4620
|
dirty =
|
|
4050
4621
|
recordCodeEvent({
|
|
@@ -4231,6 +4802,109 @@ async function listClaudeTranscriptFiles(claudeProjectsDir, maxAgeMs = 0) {
|
|
|
4231
4802
|
}
|
|
4232
4803
|
}
|
|
4233
4804
|
|
|
4805
|
+
function buildClaudeToolTimelineEntries({ content, threadId, threadLabel, uuid, createdAtMs, cwd }) {
|
|
4806
|
+
if (!Array.isArray(content) || !threadId) {
|
|
4807
|
+
return [];
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
const entries = [];
|
|
4811
|
+
for (const block of content) {
|
|
4812
|
+
if (!isPlainObject(block) || block.type !== "tool_use") {
|
|
4813
|
+
continue;
|
|
4814
|
+
}
|
|
4815
|
+
const toolName = cleanText(block.name || "");
|
|
4816
|
+
const input = isPlainObject(block.input) ? block.input : {};
|
|
4817
|
+
const toolId = cleanText(block.id || block.tool_use_id || "") || historyToken(JSON.stringify(block));
|
|
4818
|
+
const callId = `${uuid || createdAtMs}:${toolId}`;
|
|
4819
|
+
const lowerToolName = toolName.toLowerCase();
|
|
4820
|
+
|
|
4821
|
+
if (lowerToolName === "bash") {
|
|
4822
|
+
const commandText = cleanText(input.command || "");
|
|
4823
|
+
if (!commandText) {
|
|
4824
|
+
continue;
|
|
4825
|
+
}
|
|
4826
|
+
const classified = classifyTimelineCommand(commandText);
|
|
4827
|
+
entries.push(
|
|
4828
|
+
buildToolTimelineEntry({
|
|
4829
|
+
provider: "claude",
|
|
4830
|
+
threadId,
|
|
4831
|
+
threadLabel,
|
|
4832
|
+
callId,
|
|
4833
|
+
createdAtMs,
|
|
4834
|
+
fileEventType: classified.fileEventType,
|
|
4835
|
+
commandText: classified.commandText || commandText,
|
|
4836
|
+
fileRefs: classified.fileRefs,
|
|
4837
|
+
cwd,
|
|
4838
|
+
})
|
|
4839
|
+
);
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
if (["write", "edit", "multiedit", "todowrite", "exitplanmode", "askuserquestion"].includes(lowerToolName)) {
|
|
4844
|
+
continue;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
if (lowerToolName === "read") {
|
|
4848
|
+
const fileRefs = normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean));
|
|
4849
|
+
entries.push(
|
|
4850
|
+
buildToolTimelineEntry({
|
|
4851
|
+
provider: "claude",
|
|
4852
|
+
threadId,
|
|
4853
|
+
threadLabel,
|
|
4854
|
+
callId,
|
|
4855
|
+
createdAtMs,
|
|
4856
|
+
fileEventType: "read",
|
|
4857
|
+
toolName,
|
|
4858
|
+
summaryText: fileRefs[0] ? `${toolName}: ${fileRefs[0]}` : toolName,
|
|
4859
|
+
fileRefs,
|
|
4860
|
+
cwd,
|
|
4861
|
+
})
|
|
4862
|
+
);
|
|
4863
|
+
continue;
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
if (["grep", "glob", "websearch", "webfetch"].includes(lowerToolName)) {
|
|
4867
|
+
const query = cleanText(input.pattern || input.query || input.url || "");
|
|
4868
|
+
const fileRefs = normalizeTimelineFileRefs([input.path || input.file_path || input.filePath].filter(Boolean));
|
|
4869
|
+
entries.push(
|
|
4870
|
+
buildToolTimelineEntry({
|
|
4871
|
+
provider: "claude",
|
|
4872
|
+
threadId,
|
|
4873
|
+
threadLabel,
|
|
4874
|
+
callId,
|
|
4875
|
+
createdAtMs,
|
|
4876
|
+
fileEventType: "search",
|
|
4877
|
+
toolName,
|
|
4878
|
+
summaryText: query ? `${toolName}: ${query}` : toolName,
|
|
4879
|
+
fileRefs,
|
|
4880
|
+
cwd,
|
|
4881
|
+
})
|
|
4882
|
+
);
|
|
4883
|
+
continue;
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
if (lowerToolName === "ls") {
|
|
4887
|
+
const fileRefs = normalizeTimelineFileRefs([input.path].filter(Boolean));
|
|
4888
|
+
entries.push(
|
|
4889
|
+
buildToolTimelineEntry({
|
|
4890
|
+
provider: "claude",
|
|
4891
|
+
threadId,
|
|
4892
|
+
threadLabel,
|
|
4893
|
+
callId,
|
|
4894
|
+
createdAtMs,
|
|
4895
|
+
fileEventType: "read",
|
|
4896
|
+
toolName,
|
|
4897
|
+
summaryText: fileRefs[0] ? `${toolName}: ${fileRefs[0]}` : toolName,
|
|
4898
|
+
fileRefs,
|
|
4899
|
+
cwd,
|
|
4900
|
+
})
|
|
4901
|
+
);
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
return entries.filter(Boolean);
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4234
4908
|
async function processClaudeTranscriptFile({ filePath, config, runtime, state, now }) {
|
|
4235
4909
|
let fileState = runtime.claudeFileStates.get(filePath);
|
|
4236
4910
|
if (!fileState) {
|
|
@@ -4346,6 +5020,20 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4346
5020
|
const claudeTitle = threadId ? runtime.claudeSessionTitles.get(threadId) || "" : "";
|
|
4347
5021
|
const threadLabel = claudeTitle || fileState.threadLabel || "";
|
|
4348
5022
|
|
|
5023
|
+
if (type === "assistant" && Array.isArray(content)) {
|
|
5024
|
+
const toolEntries = buildClaudeToolTimelineEntries({
|
|
5025
|
+
content,
|
|
5026
|
+
threadId,
|
|
5027
|
+
threadLabel,
|
|
5028
|
+
uuid,
|
|
5029
|
+
createdAtMs,
|
|
5030
|
+
cwd: fileState.cwd,
|
|
5031
|
+
});
|
|
5032
|
+
for (const toolEntry of toolEntries) {
|
|
5033
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry: toolEntry, source: "claude-tool" }) || dirty;
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4349
5037
|
let text = "";
|
|
4350
5038
|
if (typeof content === "string") {
|
|
4351
5039
|
text = content;
|
|
@@ -4354,7 +5042,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4354
5042
|
if (block?.type === "text" && block.text) text += block.text;
|
|
4355
5043
|
}
|
|
4356
5044
|
}
|
|
4357
|
-
|
|
5045
|
+
// Preserve paragraph structure for the renderer — `cleanText` collapses
|
|
5046
|
+
// every \s+ (newlines included) into a single space, which would flatten
|
|
5047
|
+
// markdown tables / code fences / lists into a single illegible line.
|
|
5048
|
+
text = normalizeLongText(text);
|
|
4358
5049
|
if (!text) continue;
|
|
4359
5050
|
if (type === "user" && record.entrypoint === "sdk-cli") {
|
|
4360
5051
|
const derivedThreadLabel = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
@@ -4398,7 +5089,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4398
5089
|
provider: "claude",
|
|
4399
5090
|
});
|
|
4400
5091
|
if (entry) {
|
|
4401
|
-
dirty = recordTimelineEntry({ config, runtime, state, entry }) || dirty;
|
|
5092
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry, source: "claude-transcript" }) || dirty;
|
|
4402
5093
|
|
|
4403
5094
|
// Also record Claude assistant_final as a history item for the completed list
|
|
4404
5095
|
if (kind === "assistant_final") {
|
|
@@ -4707,6 +5398,7 @@ async function processSqliteTimelineLog({ config, runtime, state, now }) {
|
|
|
4707
5398
|
runtime,
|
|
4708
5399
|
state,
|
|
4709
5400
|
entry,
|
|
5401
|
+
source: "codex-sqlite",
|
|
4710
5402
|
}) || dirty;
|
|
4711
5403
|
|
|
4712
5404
|
// Also record Codex assistant_final as a history item so it appears
|
|
@@ -4858,6 +5550,7 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
|
|
|
4858
5550
|
runtime,
|
|
4859
5551
|
state,
|
|
4860
5552
|
entry,
|
|
5553
|
+
source: "codex-history",
|
|
4861
5554
|
}) || dirty;
|
|
4862
5555
|
}
|
|
4863
5556
|
|
|
@@ -5032,6 +5725,7 @@ async function backfillRecentTimelineEntryDiffs({ config, runtime, state }) {
|
|
|
5032
5725
|
threadId: cleanText(entry?.threadId || ""),
|
|
5033
5726
|
cwd: await findRolloutThreadCwd(runtime, entry?.threadId || ""),
|
|
5034
5727
|
applyPatchInputsByCallId: new Map(),
|
|
5728
|
+
toolEventInputsByCallId: new Map(),
|
|
5035
5729
|
startupCutoffMs: 0,
|
|
5036
5730
|
skipPartialLine: false,
|
|
5037
5731
|
};
|
|
@@ -5604,6 +6298,47 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
5604
6298
|
});
|
|
5605
6299
|
}
|
|
5606
6300
|
|
|
6301
|
+
function buildToolTimelineEntry({
|
|
6302
|
+
provider,
|
|
6303
|
+
threadId,
|
|
6304
|
+
threadLabel,
|
|
6305
|
+
callId,
|
|
6306
|
+
createdAtMs,
|
|
6307
|
+
fileEventType,
|
|
6308
|
+
commandText = "",
|
|
6309
|
+
toolName = "",
|
|
6310
|
+
summaryText = "",
|
|
6311
|
+
fileRefs = [],
|
|
6312
|
+
cwd = "",
|
|
6313
|
+
}) {
|
|
6314
|
+
const normalizedType = normalizeTimelineFileEventType(fileEventType) || "command";
|
|
6315
|
+
const normalizedRefs = normalizeTimelineFileRefs(fileRefs);
|
|
6316
|
+
const kind = normalizedType === "command" ? "command_event" : "file_event";
|
|
6317
|
+
const stableKey = callId || historyToken(`${threadId}:${createdAtMs}:${normalizedType}:${commandText || toolName}:${normalizedRefs.join("|")}`);
|
|
6318
|
+
const messageText = timelineCommandMessage(DEFAULT_LOCALE, {
|
|
6319
|
+
commandText,
|
|
6320
|
+
toolName,
|
|
6321
|
+
fileRefs: normalizedRefs,
|
|
6322
|
+
});
|
|
6323
|
+
const summary = timelineCommandSummary(commandText, summaryText || toolName) || fileEventTitle(DEFAULT_LOCALE, normalizedType);
|
|
6324
|
+
return normalizeTimelineEntry({
|
|
6325
|
+
stableId: `${kind}:${normalizedType}:${threadId}:${stableKey}`,
|
|
6326
|
+
token: historyToken(`${kind}:${normalizedType}:${threadId}:${stableKey}`),
|
|
6327
|
+
kind,
|
|
6328
|
+
fileEventType: normalizedType,
|
|
6329
|
+
threadId,
|
|
6330
|
+
threadLabel,
|
|
6331
|
+
title: kind === "command_event" ? kindTitle(DEFAULT_LOCALE, "command_event") : fileEventTitle(DEFAULT_LOCALE, normalizedType),
|
|
6332
|
+
summary,
|
|
6333
|
+
messageText,
|
|
6334
|
+
fileRefs: normalizedRefs,
|
|
6335
|
+
createdAtMs,
|
|
6336
|
+
cwd,
|
|
6337
|
+
readOnly: true,
|
|
6338
|
+
provider,
|
|
6339
|
+
});
|
|
6340
|
+
}
|
|
6341
|
+
|
|
5607
6342
|
async function buildRolloutFileTimelineEntries({ config, record, fileState, runtime, rolloutFilePath = "" }) {
|
|
5608
6343
|
if (!isPlainObject(record) || cleanText(record.type) !== "response_item") {
|
|
5609
6344
|
return [];
|
|
@@ -5624,30 +6359,54 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
5624
6359
|
cwd: fileState.cwd || "",
|
|
5625
6360
|
});
|
|
5626
6361
|
|
|
6362
|
+
if (payloadType === "function_call" && cleanText(payload?.name || "") === "exec_command") {
|
|
6363
|
+
const stored = rememberToolEventInput(fileState, payload, createdAtMs);
|
|
6364
|
+
const args = stored?.arguments || {};
|
|
6365
|
+
const commandText = cleanText(args.cmd || args.command || "");
|
|
6366
|
+
if (!commandText) {
|
|
6367
|
+
return [];
|
|
6368
|
+
}
|
|
6369
|
+
const classified = classifyTimelineCommand(commandText);
|
|
6370
|
+
return [
|
|
6371
|
+
buildToolTimelineEntry({
|
|
6372
|
+
provider: "codex",
|
|
6373
|
+
threadId,
|
|
6374
|
+
threadLabel,
|
|
6375
|
+
callId,
|
|
6376
|
+
createdAtMs,
|
|
6377
|
+
fileEventType: classified.fileEventType,
|
|
6378
|
+
commandText: classified.commandText || commandText,
|
|
6379
|
+
fileRefs: classified.fileRefs,
|
|
6380
|
+
cwd: cleanText(args.workdir || fileState.cwd || ""),
|
|
6381
|
+
}),
|
|
6382
|
+
].filter(Boolean);
|
|
6383
|
+
}
|
|
6384
|
+
|
|
5627
6385
|
if (payloadType === "custom_tool_call") {
|
|
5628
6386
|
rememberApplyPatchInput(fileState, payload, createdAtMs);
|
|
5629
6387
|
return [];
|
|
5630
6388
|
}
|
|
5631
6389
|
|
|
5632
6390
|
if (payloadType === "function_call_output") {
|
|
6391
|
+
if (findStoredToolEventInput(fileState, callId)) {
|
|
6392
|
+
return [];
|
|
6393
|
+
}
|
|
5633
6394
|
const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
|
|
5634
|
-
|
|
5635
|
-
if (fileRefs.length === 0) {
|
|
6395
|
+
if (!commandText) {
|
|
5636
6396
|
return [];
|
|
5637
6397
|
}
|
|
6398
|
+
const classified = classifyTimelineCommand(commandText);
|
|
5638
6399
|
return [
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
token: historyToken(`file_event:read:${threadId}:${callId || createdAtMs}`),
|
|
5642
|
-
kind: "file_event",
|
|
5643
|
-
fileEventType: "read",
|
|
6400
|
+
buildToolTimelineEntry({
|
|
6401
|
+
provider: "codex",
|
|
5644
6402
|
threadId,
|
|
5645
6403
|
threadLabel,
|
|
5646
|
-
|
|
5647
|
-
summary: "",
|
|
5648
|
-
fileRefs,
|
|
6404
|
+
callId,
|
|
5649
6405
|
createdAtMs,
|
|
5650
|
-
|
|
6406
|
+
fileEventType: classified.fileEventType,
|
|
6407
|
+
commandText: classified.commandText || commandText,
|
|
6408
|
+
fileRefs: classified.fileRefs,
|
|
6409
|
+
cwd: fileState.cwd || "",
|
|
5651
6410
|
}),
|
|
5652
6411
|
].filter(Boolean);
|
|
5653
6412
|
}
|
|
@@ -5878,6 +6637,8 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
5878
6637
|
subtab: "completed",
|
|
5879
6638
|
token: historyToken(event.id),
|
|
5880
6639
|
stableId: event.id,
|
|
6640
|
+
dedupeId: completionPushContentDedupeId(event),
|
|
6641
|
+
dedupeWindowMs: COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS,
|
|
5881
6642
|
title: event.title,
|
|
5882
6643
|
body: event.detailText || event.message,
|
|
5883
6644
|
buildLocalizedContent: ({ locale }) => ({
|
|
@@ -6096,6 +6857,8 @@ function buildRolloutEvent({ record, filePath, fileState, sessionIndex, config,
|
|
|
6096
6857
|
threadLabel: context.threadLabel,
|
|
6097
6858
|
message,
|
|
6098
6859
|
detailText,
|
|
6860
|
+
threadId,
|
|
6861
|
+
turnId,
|
|
6099
6862
|
priority: config.completePriority,
|
|
6100
6863
|
tags: config.completeTags,
|
|
6101
6864
|
clickUrl: config.clickUrl,
|
|
@@ -6329,7 +7092,7 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
6329
7092
|
body: approval.messageText,
|
|
6330
7093
|
buildLocalizedContent: ({ locale }) => ({
|
|
6331
7094
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
6332
|
-
body: approval.
|
|
7095
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
6333
7096
|
}),
|
|
6334
7097
|
}) || stateChanged;
|
|
6335
7098
|
}
|
|
@@ -6665,7 +7428,11 @@ async function syncGenericUserInputRequests({
|
|
|
6665
7428
|
userInputRequest.supported ? "server.title.choice" : "server.title.choiceReadOnly",
|
|
6666
7429
|
userInputRequest.threadLabel
|
|
6667
7430
|
),
|
|
6668
|
-
body:
|
|
7431
|
+
body: buildUserInputNotificationText(
|
|
7432
|
+
userInputRequest.questions,
|
|
7433
|
+
userInputRequest.supported,
|
|
7434
|
+
locale
|
|
7435
|
+
),
|
|
6669
7436
|
}),
|
|
6670
7437
|
}) || stateChanged;
|
|
6671
7438
|
}
|
|
@@ -8463,7 +9230,7 @@ class NativeIpcClient {
|
|
|
8463
9230
|
);
|
|
8464
9231
|
}
|
|
8465
9232
|
|
|
8466
|
-
async startTurn(conversationId, turnStartParams, ownerClientId = null) {
|
|
9233
|
+
async startTurn(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8467
9234
|
return this.sendThreadFollowerRequest(
|
|
8468
9235
|
"thread-follower-start-turn",
|
|
8469
9236
|
{
|
|
@@ -8471,11 +9238,12 @@ class NativeIpcClient {
|
|
|
8471
9238
|
turnStartParams,
|
|
8472
9239
|
},
|
|
8473
9240
|
conversationId,
|
|
8474
|
-
ownerClientId
|
|
9241
|
+
ownerClientId,
|
|
9242
|
+
options
|
|
8475
9243
|
);
|
|
8476
9244
|
}
|
|
8477
9245
|
|
|
8478
|
-
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null) {
|
|
9246
|
+
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8479
9247
|
const targetClientId =
|
|
8480
9248
|
ownerClientId ??
|
|
8481
9249
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
@@ -8483,7 +9251,7 @@ class NativeIpcClient {
|
|
|
8483
9251
|
return this.sendRequest(
|
|
8484
9252
|
"turn/start",
|
|
8485
9253
|
buildDirectTurnStartPayload(conversationId, turnStartParams),
|
|
8486
|
-
{ targetClientId }
|
|
9254
|
+
{ targetClientId, ...options }
|
|
8487
9255
|
);
|
|
8488
9256
|
}
|
|
8489
9257
|
|
|
@@ -8513,12 +9281,13 @@ class NativeIpcClient {
|
|
|
8513
9281
|
);
|
|
8514
9282
|
}
|
|
8515
9283
|
|
|
8516
|
-
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null) {
|
|
9284
|
+
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null, options = {}) {
|
|
8517
9285
|
return this.sendRequest(method, params, {
|
|
8518
9286
|
targetClientId:
|
|
8519
9287
|
ownerClientId ??
|
|
8520
9288
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
8521
9289
|
null,
|
|
9290
|
+
...options,
|
|
8522
9291
|
});
|
|
8523
9292
|
}
|
|
8524
9293
|
|
|
@@ -10593,55 +11362,323 @@ function setPairingCookies(res, config, existingDeviceId = "") {
|
|
|
10593
11362
|
]);
|
|
10594
11363
|
}
|
|
10595
11364
|
|
|
10596
|
-
function clearAuthCookies(res, config) {
|
|
10597
|
-
const secure = config.nativeApprovalPublicBaseUrl.startsWith("https://");
|
|
10598
|
-
res.setHeader("Set-Cookie", [
|
|
10599
|
-
buildCookieHeader(sessionCookieName, { value: "", maxAgeSecs: 0, secure }),
|
|
10600
|
-
buildCookieHeader(deviceCookieName, { value: "", maxAgeSecs: 0, secure }),
|
|
10601
|
-
]);
|
|
11365
|
+
function clearAuthCookies(res, config) {
|
|
11366
|
+
const secure = config.nativeApprovalPublicBaseUrl.startsWith("https://");
|
|
11367
|
+
res.setHeader("Set-Cookie", [
|
|
11368
|
+
buildCookieHeader(sessionCookieName, { value: "", maxAgeSecs: 0, secure }),
|
|
11369
|
+
buildCookieHeader(deviceCookieName, { value: "", maxAgeSecs: 0, secure }),
|
|
11370
|
+
]);
|
|
11371
|
+
}
|
|
11372
|
+
|
|
11373
|
+
function normalizePushSubscriptionBody(payload, deviceId) {
|
|
11374
|
+
const endpoint = cleanText(payload?.subscription?.endpoint ?? payload?.endpoint ?? "");
|
|
11375
|
+
const p256dh = cleanText(
|
|
11376
|
+
payload?.subscription?.keys?.p256dh ??
|
|
11377
|
+
payload?.keys?.p256dh ??
|
|
11378
|
+
payload?.p256dh ??
|
|
11379
|
+
""
|
|
11380
|
+
);
|
|
11381
|
+
const auth = cleanText(
|
|
11382
|
+
payload?.subscription?.keys?.auth ??
|
|
11383
|
+
payload?.keys?.auth ??
|
|
11384
|
+
payload?.auth ??
|
|
11385
|
+
""
|
|
11386
|
+
);
|
|
11387
|
+
if (!endpoint || !p256dh || !auth || !deviceId) {
|
|
11388
|
+
return null;
|
|
11389
|
+
}
|
|
11390
|
+
return normalizePushSubscriptionRecord({
|
|
11391
|
+
id: pushSubscriptionId(endpoint),
|
|
11392
|
+
endpoint,
|
|
11393
|
+
keys: { p256dh, auth },
|
|
11394
|
+
deviceId,
|
|
11395
|
+
userAgent: cleanText(payload?.userAgent ?? ""),
|
|
11396
|
+
standalone: payload?.standalone === true,
|
|
11397
|
+
createdAtMs: Date.now(),
|
|
11398
|
+
updatedAtMs: Date.now(),
|
|
11399
|
+
});
|
|
11400
|
+
}
|
|
11401
|
+
|
|
11402
|
+
function buildPushStatusResponse(config, state, session) {
|
|
11403
|
+
const current = getPushSubscriptionForDevice(state, session.deviceId);
|
|
11404
|
+
return {
|
|
11405
|
+
enabled: config.webPushEnabled,
|
|
11406
|
+
secureOrigin: config.nativeApprovalPublicBaseUrl.startsWith("https://"),
|
|
11407
|
+
vapidPublicKey: config.webPushEnabled ? config.webPushVapidPublicKey : "",
|
|
11408
|
+
subject: config.webPushEnabled ? config.webPushSubject : "",
|
|
11409
|
+
deviceId: session.deviceId || null,
|
|
11410
|
+
subscribed: Boolean(current),
|
|
11411
|
+
subscriptionId: current?.id || null,
|
|
11412
|
+
lastSuccessfulDeliveryAtMs: Number(current?.lastSuccessfulDeliveryAtMs) || 0,
|
|
11413
|
+
};
|
|
11414
|
+
}
|
|
11415
|
+
|
|
11416
|
+
async function buildMcpStatusResponse(config, runtime, state) {
|
|
11417
|
+
const remote = getRemotePairingStatus(runtime);
|
|
11418
|
+
let remotePairings = 0;
|
|
11419
|
+
try {
|
|
11420
|
+
remotePairings = (await loadPairings()).length;
|
|
11421
|
+
} catch {
|
|
11422
|
+
remotePairings = 0;
|
|
11423
|
+
}
|
|
11424
|
+
const now = Date.now();
|
|
11425
|
+
const pairedDevices = isPlainObject(state.pairedDevices) ? state.pairedDevices : {};
|
|
11426
|
+
const trustedDevices = Object.values(pairedDevices).filter((raw) => {
|
|
11427
|
+
const record = normalizeDeviceTrustRecord(raw, { trustTtlMs: config.deviceTrustTtlMs, now });
|
|
11428
|
+
return record && !record.revokedAtMs && Number(record.trustedUntilMs) > now;
|
|
11429
|
+
}).length;
|
|
11430
|
+
const a2aRelay = getRelayStatus();
|
|
11431
|
+
return {
|
|
11432
|
+
ok: true,
|
|
11433
|
+
bridge: {
|
|
11434
|
+
running: true,
|
|
11435
|
+
publicBaseUrl: config.nativeApprovalPublicBaseUrl,
|
|
11436
|
+
locale: config.defaultLocale,
|
|
11437
|
+
codexConnected: Boolean(runtime.ipcClient?.clientId),
|
|
11438
|
+
},
|
|
11439
|
+
pairing: {
|
|
11440
|
+
trustedDevices,
|
|
11441
|
+
webPushEnabled: Boolean(config.webPushEnabled),
|
|
11442
|
+
pushSubscriptions: listPushSubscriptions(state).length,
|
|
11443
|
+
},
|
|
11444
|
+
remote: {
|
|
11445
|
+
enabled: Boolean(remote.enabled),
|
|
11446
|
+
relayUrl: remote.relayUrl || config.remotePairingRelayUrl || "",
|
|
11447
|
+
identityFingerprint: remote.identityFingerprint || null,
|
|
11448
|
+
pairings: remotePairings,
|
|
11449
|
+
sessions: Array.isArray(remote.sessions)
|
|
11450
|
+
? remote.sessions.map((session) => ({
|
|
11451
|
+
state: cleanText(session?.state || ""),
|
|
11452
|
+
lastRoute: cleanText(session?.lastRoute || ""),
|
|
11453
|
+
connected: cleanText(session?.state || "") === "connected",
|
|
11454
|
+
phoneFingerprint: cleanText(session?.phoneFingerprint || ""),
|
|
11455
|
+
lastSeenAtMs: Number(session?.lastSeenAtMs) || 0,
|
|
11456
|
+
}))
|
|
11457
|
+
: [],
|
|
11458
|
+
},
|
|
11459
|
+
a2a: {
|
|
11460
|
+
enabled: Boolean(config.a2aApiKey),
|
|
11461
|
+
relayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
11462
|
+
relayConnected: Boolean(a2aRelay.polling && a2aRelay.lastPollOk),
|
|
11463
|
+
userIdConfigured: Boolean(config.a2aRelayUserId),
|
|
11464
|
+
},
|
|
11465
|
+
share: {
|
|
11466
|
+
enabled: Boolean(config.a2aRelayUserId && config.a2aApiKey),
|
|
11467
|
+
shareUrl: config.a2aShareUrl || "",
|
|
11468
|
+
},
|
|
11469
|
+
moltbook: {
|
|
11470
|
+
enabled: Boolean(config.moltbookApiKey),
|
|
11471
|
+
},
|
|
11472
|
+
};
|
|
11473
|
+
}
|
|
11474
|
+
|
|
11475
|
+
function mcpTimeoutMs(value) {
|
|
11476
|
+
const n = Number(value);
|
|
11477
|
+
if (!Number.isFinite(n)) return 600_000;
|
|
11478
|
+
return Math.max(10_000, Math.min(900_000, Math.floor(n)));
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11481
|
+
function mcpText(value, maxChars = 50_000) {
|
|
11482
|
+
const text = String(value ?? "");
|
|
11483
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}\n\n[truncated by viveworker MCP]` : text;
|
|
11484
|
+
}
|
|
11485
|
+
|
|
11486
|
+
function normalizeMcpStringArray(value, maxItems = 20) {
|
|
11487
|
+
if (!Array.isArray(value)) return [];
|
|
11488
|
+
return value
|
|
11489
|
+
.map((item) => cleanText(item ?? ""))
|
|
11490
|
+
.filter(Boolean)
|
|
11491
|
+
.slice(0, maxItems);
|
|
11492
|
+
}
|
|
11493
|
+
|
|
11494
|
+
function normalizeMcpQuestionOptions(value) {
|
|
11495
|
+
if (!Array.isArray(value)) return [];
|
|
11496
|
+
return value
|
|
11497
|
+
.map((option, index) => {
|
|
11498
|
+
if (typeof option === "string") {
|
|
11499
|
+
const label = cleanText(option);
|
|
11500
|
+
return label ? { label } : null;
|
|
11501
|
+
}
|
|
11502
|
+
if (!isPlainObject(option)) return null;
|
|
11503
|
+
const label = cleanText(option.label ?? option.title ?? `Option ${index + 1}`);
|
|
11504
|
+
if (!label) return null;
|
|
11505
|
+
const description = cleanText(option.description ?? option.detail ?? "");
|
|
11506
|
+
return { label, ...(description ? { description } : {}) };
|
|
11507
|
+
})
|
|
11508
|
+
.filter(Boolean)
|
|
11509
|
+
.slice(0, 10);
|
|
11510
|
+
}
|
|
11511
|
+
|
|
11512
|
+
function normalizeMcpApprovalKind(value) {
|
|
11513
|
+
const normalized = cleanText(value || "mcp").toLowerCase();
|
|
11514
|
+
if (!/^[a-z0-9_-]{1,40}$/u.test(normalized)) {
|
|
11515
|
+
return "mcp";
|
|
11516
|
+
}
|
|
11517
|
+
// Keep MCP in the normal approval lane. These kinds have provider-specific
|
|
11518
|
+
// UI/actions elsewhere and should not be spoofable from a generic MCP client.
|
|
11519
|
+
if (normalized === "payment" || normalized === "hazbase_wallet_payment" || normalized === "plan" || normalized === "question") {
|
|
11520
|
+
return "mcp";
|
|
11521
|
+
}
|
|
11522
|
+
return normalized;
|
|
11523
|
+
}
|
|
11524
|
+
|
|
11525
|
+
function createMcpPendingApproval(body, kind) {
|
|
11526
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
11527
|
+
const requestId = cleanText(body.requestId || crypto.randomUUID());
|
|
11528
|
+
const title = cleanText(body.title || (kind === "question" ? "MCP question" : "MCP approval"));
|
|
11529
|
+
const messageText = kind === "question"
|
|
11530
|
+
? mcpText(body.question || body.message || "")
|
|
11531
|
+
: mcpText(body.messageText || body.message || "");
|
|
11532
|
+
const requestKey = `mcp:${requestId}`;
|
|
11533
|
+
const approval = {
|
|
11534
|
+
token,
|
|
11535
|
+
requestKey,
|
|
11536
|
+
conversationId: "mcp",
|
|
11537
|
+
requestId,
|
|
11538
|
+
ownerClientId: null,
|
|
11539
|
+
kind,
|
|
11540
|
+
threadLabel: title || "MCP",
|
|
11541
|
+
title,
|
|
11542
|
+
messageText,
|
|
11543
|
+
fileRefs: normalizeMcpStringArray(body.fileRefs),
|
|
11544
|
+
previousFileRefs: [],
|
|
11545
|
+
diffText: kind === "question" ? "" : mcpText(body.diffText || "", 100_000),
|
|
11546
|
+
diffAvailable: kind !== "question" && Boolean(body.diffText),
|
|
11547
|
+
diffSource: kind === "question" ? "" : "mcp",
|
|
11548
|
+
diffAddedLines: 0,
|
|
11549
|
+
diffRemovedLines: 0,
|
|
11550
|
+
cwd: "",
|
|
11551
|
+
workspaceRoot: "",
|
|
11552
|
+
createdAtMs: Date.now(),
|
|
11553
|
+
resolved: false,
|
|
11554
|
+
resolving: false,
|
|
11555
|
+
resolveMcpWaiter: null,
|
|
11556
|
+
provider: "mcp",
|
|
11557
|
+
planText: "",
|
|
11558
|
+
questions: [],
|
|
11559
|
+
notifyOnly: false,
|
|
11560
|
+
readOnly: false,
|
|
11561
|
+
};
|
|
11562
|
+
if (kind === "question") {
|
|
11563
|
+
approval.questions = [
|
|
11564
|
+
{
|
|
11565
|
+
question: mcpText(body.question || body.message || "", 2000),
|
|
11566
|
+
options: normalizeMcpQuestionOptions(body.options),
|
|
11567
|
+
multiSelect: body.multiSelect === true,
|
|
11568
|
+
},
|
|
11569
|
+
];
|
|
11570
|
+
}
|
|
11571
|
+
return approval;
|
|
11572
|
+
}
|
|
11573
|
+
|
|
11574
|
+
async function waitForMcpApprovalResult(runtime, approval, timeoutMs) {
|
|
11575
|
+
const waitMs = mcpTimeoutMs(timeoutMs);
|
|
11576
|
+
const result = await new Promise((resolve) => {
|
|
11577
|
+
const timer = setTimeout(() => {
|
|
11578
|
+
resolve({ approved: false, decision: "timeout" });
|
|
11579
|
+
}, waitMs);
|
|
11580
|
+
approval.resolveMcpWaiter = (value) => {
|
|
11581
|
+
clearTimeout(timer);
|
|
11582
|
+
resolve(value);
|
|
11583
|
+
};
|
|
11584
|
+
});
|
|
11585
|
+
if (!approval.resolved) {
|
|
11586
|
+
approval.resolved = true;
|
|
11587
|
+
approval.resolving = false;
|
|
11588
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
11589
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
11590
|
+
}
|
|
11591
|
+
return result;
|
|
10602
11592
|
}
|
|
10603
11593
|
|
|
10604
|
-
function
|
|
10605
|
-
const
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
payload?.keys?.p256dh ??
|
|
10609
|
-
payload?.p256dh ??
|
|
10610
|
-
""
|
|
10611
|
-
);
|
|
10612
|
-
const auth = cleanText(
|
|
10613
|
-
payload?.subscription?.keys?.auth ??
|
|
10614
|
-
payload?.keys?.auth ??
|
|
10615
|
-
payload?.auth ??
|
|
10616
|
-
""
|
|
10617
|
-
);
|
|
10618
|
-
if (!endpoint || !p256dh || !auth || !deviceId) {
|
|
10619
|
-
return null;
|
|
11594
|
+
async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
11595
|
+
const eventType = cleanText(body.eventType || "");
|
|
11596
|
+
if (eventType === "status") {
|
|
11597
|
+
return writeJson(res, 200, await buildMcpStatusResponse(config, runtime, state));
|
|
10620
11598
|
}
|
|
10621
|
-
return normalizePushSubscriptionRecord({
|
|
10622
|
-
id: pushSubscriptionId(endpoint),
|
|
10623
|
-
endpoint,
|
|
10624
|
-
keys: { p256dh, auth },
|
|
10625
|
-
deviceId,
|
|
10626
|
-
userAgent: cleanText(payload?.userAgent ?? ""),
|
|
10627
|
-
standalone: payload?.standalone === true,
|
|
10628
|
-
createdAtMs: Date.now(),
|
|
10629
|
-
updatedAtMs: Date.now(),
|
|
10630
|
-
});
|
|
10631
|
-
}
|
|
10632
11599
|
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
11600
|
+
if (eventType === "notify") {
|
|
11601
|
+
const title = cleanText(body.title || "MCP");
|
|
11602
|
+
const messageText = mcpText(body.message || body.messageText || "");
|
|
11603
|
+
if (!messageText) {
|
|
11604
|
+
return writeJson(res, 400, { error: "missing-message" });
|
|
11605
|
+
}
|
|
11606
|
+
const stableId = `mcp_notify:${historyToken(`${title}:${messageText}:${Date.now()}`)}`;
|
|
11607
|
+
const token = historyToken(stableId);
|
|
11608
|
+
const entry = {
|
|
11609
|
+
stableId,
|
|
11610
|
+
token,
|
|
11611
|
+
kind: "assistant_commentary",
|
|
11612
|
+
threadId: "mcp",
|
|
11613
|
+
threadLabel: cleanText(body.threadLabel || "MCP"),
|
|
11614
|
+
title,
|
|
11615
|
+
summary: formatNotificationBody(messageText, 160) || messageText,
|
|
11616
|
+
messageText,
|
|
11617
|
+
createdAtMs: Date.now(),
|
|
11618
|
+
readOnly: true,
|
|
11619
|
+
provider: "mcp",
|
|
11620
|
+
};
|
|
11621
|
+
const timelineChanged = recordTimelineEntry({ config, runtime, state, entry });
|
|
11622
|
+
const historyChanged = recordHistoryItem({ config, runtime, state, item: entry });
|
|
11623
|
+
const pushChanged = await deliverWebPushItem({
|
|
11624
|
+
config,
|
|
11625
|
+
state,
|
|
11626
|
+
kind: "assistant_commentary",
|
|
11627
|
+
tab: "timeline",
|
|
11628
|
+
token,
|
|
11629
|
+
stableId,
|
|
11630
|
+
title,
|
|
11631
|
+
body: messageText,
|
|
11632
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
11633
|
+
title: localizedMcpNotifyTitle(locale, title),
|
|
11634
|
+
body: pushBodySnippet(messageText),
|
|
11635
|
+
}),
|
|
11636
|
+
}).catch((error) => {
|
|
11637
|
+
console.error(`[mcp-notify-push] ${error.message}`);
|
|
11638
|
+
return false;
|
|
11639
|
+
});
|
|
11640
|
+
if (timelineChanged || historyChanged || pushChanged) {
|
|
11641
|
+
await saveState(config.stateFile, state);
|
|
11642
|
+
}
|
|
11643
|
+
return writeJson(res, 200, { ok: true, token });
|
|
11644
|
+
}
|
|
11645
|
+
|
|
11646
|
+
if (eventType === "approval_request" || eventType === "choice_request") {
|
|
11647
|
+
const kind = eventType === "choice_request" ? "question" : normalizeMcpApprovalKind(body.approvalKind);
|
|
11648
|
+
if (kind !== "question" && !mcpText(body.message || body.messageText || "")) {
|
|
11649
|
+
return writeJson(res, 400, { error: "missing-message" });
|
|
11650
|
+
}
|
|
11651
|
+
if (kind === "question" && !mcpText(body.question || body.message || "")) {
|
|
11652
|
+
return writeJson(res, 400, { error: "missing-question" });
|
|
11653
|
+
}
|
|
11654
|
+
const approval = createMcpPendingApproval(body, kind);
|
|
11655
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
11656
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
11657
|
+
deliverWebPushItem({
|
|
11658
|
+
config,
|
|
11659
|
+
state,
|
|
11660
|
+
kind: "approval",
|
|
11661
|
+
tab: "inbox",
|
|
11662
|
+
subtab: "pending",
|
|
11663
|
+
token: approval.token,
|
|
11664
|
+
stableId: pendingApprovalStableId(approval),
|
|
11665
|
+
title: approval.title || "MCP",
|
|
11666
|
+
body: approval.messageText,
|
|
11667
|
+
buildLocalizedContent: ({ locale }) => localizedMcpApprovalPushContent(locale, approval),
|
|
11668
|
+
}).catch((error) => {
|
|
11669
|
+
console.error(`[mcp-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
11670
|
+
});
|
|
11671
|
+
const result = await waitForMcpApprovalResult(runtime, approval, body.timeoutMs);
|
|
11672
|
+
return writeJson(res, 200, {
|
|
11673
|
+
ok: true,
|
|
11674
|
+
approved: result?.approved === true || result?.decision === "accept",
|
|
11675
|
+
decision: result?.decision || (result?.approved ? "accept" : "reject"),
|
|
11676
|
+
...(result?.answerText ? { answerText: result.answerText } : {}),
|
|
11677
|
+
...(Array.isArray(result?.answers) ? { answers: result.answers } : {}),
|
|
11678
|
+
});
|
|
11679
|
+
}
|
|
11680
|
+
|
|
11681
|
+
return writeJson(res, 400, { error: "unsupported-mcp-event" });
|
|
10645
11682
|
}
|
|
10646
11683
|
|
|
10647
11684
|
function requestUserAgent(req) {
|
|
@@ -10992,6 +12029,17 @@ function historyItemByToken(runtime, kind, token) {
|
|
|
10992
12029
|
) ?? null;
|
|
10993
12030
|
}
|
|
10994
12031
|
|
|
12032
|
+
function approvalDecisionFromHistoryItem(item) {
|
|
12033
|
+
const outcome = cleanText(item?.outcome || "");
|
|
12034
|
+
if (outcome === "approved") {
|
|
12035
|
+
return "accept";
|
|
12036
|
+
}
|
|
12037
|
+
if (outcome === "rejected") {
|
|
12038
|
+
return "decline";
|
|
12039
|
+
}
|
|
12040
|
+
return "";
|
|
12041
|
+
}
|
|
12042
|
+
|
|
10995
12043
|
function listLatestPersistedUserInputRequests({ config, runtime, state }) {
|
|
10996
12044
|
const byToken = new Map();
|
|
10997
12045
|
for (const userInputRequest of runtime.userInputRequestsByToken.values()) {
|
|
@@ -12065,6 +13113,48 @@ function findNewerThreadMessageAfterCompletion(runtime, completionItem) {
|
|
|
12065
13113
|
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0] || null;
|
|
12066
13114
|
}
|
|
12067
13115
|
|
|
13116
|
+
function normalizeCompletionReplyComparisonText(value) {
|
|
13117
|
+
return normalizeLongText(value).replace(/\s+/gu, " ").trim();
|
|
13118
|
+
}
|
|
13119
|
+
|
|
13120
|
+
function isMatchingCompletionReplyTimelineMessage(entry, messageText, expectedImageCount = 0) {
|
|
13121
|
+
if (cleanText(entry?.kind || "") !== "user_message") {
|
|
13122
|
+
return false;
|
|
13123
|
+
}
|
|
13124
|
+
const expectedText = normalizeCompletionReplyComparisonText(messageText);
|
|
13125
|
+
const actualText = normalizeCompletionReplyComparisonText(entry?.messageText || entry?.summary || entry?.title || "");
|
|
13126
|
+
if (!expectedText || actualText !== expectedText) {
|
|
13127
|
+
return false;
|
|
13128
|
+
}
|
|
13129
|
+
const entryImageCount = normalizeTimelineImagePaths(entry?.imagePaths || []).length;
|
|
13130
|
+
return expectedImageCount <= 0 || entryImageCount === expectedImageCount;
|
|
13131
|
+
}
|
|
13132
|
+
|
|
13133
|
+
function findMatchingCompletionReplyAfterCompletion(runtime, completionItem, messageText, expectedImageCount = 0) {
|
|
13134
|
+
const itemKind = cleanText(completionItem?.kind || "");
|
|
13135
|
+
if (!runtime || !REPLYABLE_HISTORY_KINDS.has(itemKind)) {
|
|
13136
|
+
return null;
|
|
13137
|
+
}
|
|
13138
|
+
|
|
13139
|
+
const threadId = historyItemThreadId(completionItem);
|
|
13140
|
+
const completionCreatedAtMs = Number(completionItem?.createdAtMs) || 0;
|
|
13141
|
+
if (!threadId || !completionCreatedAtMs) {
|
|
13142
|
+
return null;
|
|
13143
|
+
}
|
|
13144
|
+
|
|
13145
|
+
return runtime.recentTimelineEntries
|
|
13146
|
+
.filter((entry) => {
|
|
13147
|
+
if (cleanText(entry?.threadId || "") !== threadId) {
|
|
13148
|
+
return false;
|
|
13149
|
+
}
|
|
13150
|
+
if (Number(entry?.createdAtMs) <= completionCreatedAtMs) {
|
|
13151
|
+
return false;
|
|
13152
|
+
}
|
|
13153
|
+
return isMatchingCompletionReplyTimelineMessage(entry, messageText, expectedImageCount);
|
|
13154
|
+
})
|
|
13155
|
+
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0] || null;
|
|
13156
|
+
}
|
|
13157
|
+
|
|
12068
13158
|
function buildHistoryDetail(item, locale, runtime = null) {
|
|
12069
13159
|
const threadId = historyItemThreadId(item);
|
|
12070
13160
|
const replyEnabled =
|
|
@@ -12182,6 +13272,13 @@ function buildAmbientSuggestionsDetail(entry, locale) {
|
|
|
12182
13272
|
|
|
12183
13273
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
12184
13274
|
const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
|
|
13275
|
+
const commandText = ["read", "search"].includes(fileEventType)
|
|
13276
|
+
? firstMarkdownCodeFenceText(entry?.messageText ?? "")
|
|
13277
|
+
: "";
|
|
13278
|
+
const detailText = [
|
|
13279
|
+
fileEventDetailCopy(locale, fileEventType, entry?.provider),
|
|
13280
|
+
commandText ? "" : normalizeTimelineMessageText(entry?.messageText ?? "", locale),
|
|
13281
|
+
].filter(Boolean).join("\n\n");
|
|
12185
13282
|
return {
|
|
12186
13283
|
kind: "file_event",
|
|
12187
13284
|
token: entry.token,
|
|
@@ -12190,9 +13287,10 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12190
13287
|
threadLabel: entry.threadLabel || "",
|
|
12191
13288
|
fileEventType,
|
|
12192
13289
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
12193
|
-
messageHtml: renderMessageHtml(
|
|
13290
|
+
messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
12194
13291
|
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
12195
13292
|
previousFileRefs: normalizeTimelineFileRefs(entry.previousFileRefs ?? []),
|
|
13293
|
+
...(commandText ? { commandText } : {}),
|
|
12196
13294
|
diffAvailable: Boolean(entry.diffAvailable),
|
|
12197
13295
|
diffText: normalizeTimelineDiffText(entry.diffText ?? ""),
|
|
12198
13296
|
diffSource: normalizeTimelineDiffSource(entry.diffSource ?? ""),
|
|
@@ -12203,6 +13301,25 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12203
13301
|
};
|
|
12204
13302
|
}
|
|
12205
13303
|
|
|
13304
|
+
function buildTimelineCommandEventDetail(entry, locale) {
|
|
13305
|
+
const commandText = firstMarkdownCodeFenceText(entry?.messageText ?? "") || cleanText(entry?.summary || "");
|
|
13306
|
+
const detailText = t(locale, "detail.commandEvent.copy", { provider: providerDisplayName(locale, entry?.provider) });
|
|
13307
|
+
return {
|
|
13308
|
+
kind: "command_event",
|
|
13309
|
+
token: entry.token,
|
|
13310
|
+
threadId: cleanText(entry.threadId || ""),
|
|
13311
|
+
title: cleanText(entry.threadLabel || entry.title || "") || kindTitle(locale, "command_event"),
|
|
13312
|
+
threadLabel: entry.threadLabel || "",
|
|
13313
|
+
fileEventType: "command",
|
|
13314
|
+
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
13315
|
+
messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
13316
|
+
commandText,
|
|
13317
|
+
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
13318
|
+
readOnly: true,
|
|
13319
|
+
actions: [],
|
|
13320
|
+
};
|
|
13321
|
+
}
|
|
13322
|
+
|
|
12206
13323
|
function buildDiffThreadDetail(group, locale) {
|
|
12207
13324
|
const changedFileCount = Math.max(0, Number(group.changedFileCount) || 0);
|
|
12208
13325
|
return {
|
|
@@ -12576,6 +13693,25 @@ async function handleCompletionReply({
|
|
|
12576
13693
|
if (!conversationId) {
|
|
12577
13694
|
throw new Error("completion-reply-unavailable");
|
|
12578
13695
|
}
|
|
13696
|
+
console.log(
|
|
13697
|
+
`[completion-reply] received at=${new Date().toISOString()} ` +
|
|
13698
|
+
`token=${cleanText(completionItem?.token || "") || "unknown"} thread=${conversationId} ` +
|
|
13699
|
+
`images=${normalizedLocalImagePaths.length} plan=${planMode ? 1 : 0} force=${force ? 1 : 0}`
|
|
13700
|
+
);
|
|
13701
|
+
|
|
13702
|
+
const alreadyAcceptedReply = findMatchingCompletionReplyAfterCompletion(
|
|
13703
|
+
runtime,
|
|
13704
|
+
completionItem,
|
|
13705
|
+
messageText,
|
|
13706
|
+
normalizedLocalImagePaths.length
|
|
13707
|
+
);
|
|
13708
|
+
if (alreadyAcceptedReply && !force) {
|
|
13709
|
+
console.log(
|
|
13710
|
+
`[completion-reply] idempotent token=${cleanText(completionItem?.token || "") || "unknown"} thread=${conversationId} timeline=${cleanText(alreadyAcceptedReply.stableId || alreadyAcceptedReply.token || "") || "unknown"}`
|
|
13711
|
+
);
|
|
13712
|
+
return { alreadyAccepted: true };
|
|
13713
|
+
}
|
|
13714
|
+
|
|
12579
13715
|
// For assistant_final, check against timeline entries (avoids maxHistoryItems eviction).
|
|
12580
13716
|
// For legacy completion, check against history items.
|
|
12581
13717
|
const itemKind = cleanText(completionItem?.kind || "");
|
|
@@ -12666,30 +13802,32 @@ async function handleCompletionReply({
|
|
|
12666
13802
|
await runtime.ipcClient.startTurnDirect(
|
|
12667
13803
|
conversationId,
|
|
12668
13804
|
candidate.turnStartParams,
|
|
12669
|
-
ownerClientId
|
|
13805
|
+
ownerClientId,
|
|
13806
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12670
13807
|
);
|
|
12671
13808
|
} else {
|
|
12672
13809
|
await runtime.ipcClient.startTurn(
|
|
12673
13810
|
conversationId,
|
|
12674
13811
|
candidate.turnStartParams,
|
|
12675
|
-
ownerClientId
|
|
13812
|
+
ownerClientId,
|
|
13813
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12676
13814
|
);
|
|
12677
13815
|
}
|
|
12678
13816
|
console.log(
|
|
12679
|
-
`[completion-reply] success candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
13817
|
+
`[completion-reply] success at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
12680
13818
|
);
|
|
12681
13819
|
await finalizeReplyAccepted();
|
|
12682
|
-
return;
|
|
13820
|
+
return { alreadyAccepted: false };
|
|
12683
13821
|
} catch (error) {
|
|
12684
13822
|
if (isStartTurnAckTimeout(error)) {
|
|
12685
13823
|
// Codex can create the turn but fail to send the bridge an ACK before
|
|
12686
13824
|
// its internal follower timeout fires. Retrying risks duplicate user
|
|
12687
13825
|
// turns, so treat this as accepted and let the timeline catch up.
|
|
12688
13826
|
console.log(
|
|
12689
|
-
`[completion-reply] accepted candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(error)}`
|
|
13827
|
+
`[completion-reply] accepted at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(error)}`
|
|
12690
13828
|
);
|
|
12691
13829
|
await finalizeReplyAccepted();
|
|
12692
|
-
return;
|
|
13830
|
+
return { alreadyAccepted: false, ackTimeout: true };
|
|
12693
13831
|
}
|
|
12694
13832
|
lastError = error;
|
|
12695
13833
|
console.log(
|
|
@@ -13380,7 +14518,12 @@ async function autoApproveTrustedWrite({
|
|
|
13380
14518
|
}
|
|
13381
14519
|
|
|
13382
14520
|
async function handleNativeApprovalDecision({ config, runtime, state, approval, decision }) {
|
|
13383
|
-
if (approval.
|
|
14521
|
+
if (approval.resolveMcpWaiter) {
|
|
14522
|
+
approval.resolveMcpWaiter({
|
|
14523
|
+
approved: decision === "accept",
|
|
14524
|
+
decision,
|
|
14525
|
+
});
|
|
14526
|
+
} else if (approval.resolveClaudeWaiter) {
|
|
13384
14527
|
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
13385
14528
|
// ExitPlanMode cannot be truly auto-approved via permissionDecision: "allow"
|
|
13386
14529
|
// (Claude still shows the native PC plan dialog). Instead, deny the tool
|
|
@@ -13440,6 +14583,10 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13440
14583
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13441
14584
|
return entry ? buildTimelineFileEventDetail(entry, locale) : null;
|
|
13442
14585
|
}
|
|
14586
|
+
if (kind === "command_event") {
|
|
14587
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
14588
|
+
return entry ? buildTimelineCommandEventDetail(entry, locale) : null;
|
|
14589
|
+
}
|
|
13443
14590
|
if (kind === "ambient_suggestions") {
|
|
13444
14591
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13445
14592
|
if (entry) {
|
|
@@ -13621,6 +14768,8 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13621
14768
|
provider: "a2a",
|
|
13622
14769
|
instruction,
|
|
13623
14770
|
messageText: responseText,
|
|
14771
|
+
viveworker: task?.viveworker || entry?.viveworker || {},
|
|
14772
|
+
paidDeliverable: task?.paidDeliverable || entry?.paidDeliverable || null,
|
|
13624
14773
|
taskId: task?.id || "",
|
|
13625
14774
|
taskStatus: task?.status || entry?.taskStatus || (kind === "a2a_task_result" ? "completed" : "submitted"),
|
|
13626
14775
|
callerInfo: task?.callerInfo || {},
|
|
@@ -13751,13 +14900,32 @@ const GZIPPABLE_EXTENSIONS = new Set([
|
|
|
13751
14900
|
]);
|
|
13752
14901
|
const MIN_GZIP_BYTES = 512;
|
|
13753
14902
|
|
|
14903
|
+
function renderWebAssetBuffer(filePath, raw) {
|
|
14904
|
+
const basename = path.basename(filePath);
|
|
14905
|
+
if (basename !== "index.html" && basename !== "sw.js" && basename !== "app.js") {
|
|
14906
|
+
return raw;
|
|
14907
|
+
}
|
|
14908
|
+
|
|
14909
|
+
const text = raw.toString("utf8");
|
|
14910
|
+
if (!text.includes(WEB_APP_BUILD_ID_PLACEHOLDER)) {
|
|
14911
|
+
return raw;
|
|
14912
|
+
}
|
|
14913
|
+
|
|
14914
|
+
return Buffer.from(text.replaceAll(WEB_APP_BUILD_ID_PLACEHOLDER, WEB_APP_BUILD_ID), "utf8");
|
|
14915
|
+
}
|
|
14916
|
+
|
|
13754
14917
|
async function loadWebAssetEntry(filePath) {
|
|
13755
14918
|
const stat = await fs.stat(filePath);
|
|
13756
14919
|
const cached = WEB_ASSET_CACHE.get(filePath);
|
|
13757
|
-
if (
|
|
14920
|
+
if (
|
|
14921
|
+
cached &&
|
|
14922
|
+
cached.mtimeMs === stat.mtimeMs &&
|
|
14923
|
+
cached.sourceSize === stat.size &&
|
|
14924
|
+
cached.buildId === WEB_APP_BUILD_ID
|
|
14925
|
+
) {
|
|
13758
14926
|
return cached;
|
|
13759
14927
|
}
|
|
13760
|
-
const raw = await fs.readFile(filePath);
|
|
14928
|
+
const raw = renderWebAssetBuffer(filePath, await fs.readFile(filePath));
|
|
13761
14929
|
const extension = path.extname(filePath).toLowerCase();
|
|
13762
14930
|
let gzip = null;
|
|
13763
14931
|
if (GZIPPABLE_EXTENSIONS.has(extension) && raw.length >= MIN_GZIP_BYTES) {
|
|
@@ -13771,7 +14939,8 @@ async function loadWebAssetEntry(filePath) {
|
|
|
13771
14939
|
raw,
|
|
13772
14940
|
gzip,
|
|
13773
14941
|
mtimeMs: stat.mtimeMs,
|
|
13774
|
-
|
|
14942
|
+
sourceSize: stat.size,
|
|
14943
|
+
buildId: WEB_APP_BUILD_ID,
|
|
13775
14944
|
contentType: contentTypeForFile(filePath),
|
|
13776
14945
|
};
|
|
13777
14946
|
WEB_ASSET_CACHE.set(filePath, entry);
|
|
@@ -13870,7 +15039,7 @@ function buildWebAppHtml({ pairToken }) {
|
|
|
13870
15039
|
<link rel="manifest" href="${escapeHtml(manifestHref)}">
|
|
13871
15040
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
13872
15041
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
13873
|
-
<link rel="stylesheet" href="/app.css">
|
|
15042
|
+
<link rel="stylesheet" href="/app.css?v=${escapeHtml(WEB_APP_BUILD_ID)}">
|
|
13874
15043
|
<style>
|
|
13875
15044
|
.boot-splash {
|
|
13876
15045
|
position: fixed;
|
|
@@ -14080,6 +15249,107 @@ function logRemotePairingBootTrace(body, session, req) {
|
|
|
14080
15249
|
}
|
|
14081
15250
|
}
|
|
14082
15251
|
|
|
15252
|
+
function logClientEvent(body, session, req) {
|
|
15253
|
+
const type = cleanText(body?.type || "").slice(0, 80);
|
|
15254
|
+
if (type !== "timeline-render") {
|
|
15255
|
+
return;
|
|
15256
|
+
}
|
|
15257
|
+
|
|
15258
|
+
const route = cleanText(body?.route || "").slice(0, 24) || "unknown";
|
|
15259
|
+
const reason = cleanText(body?.reason || "").slice(0, 80) || "unknown";
|
|
15260
|
+
const latestToken = cleanText(body?.latestToken || "").slice(0, 80) || "none";
|
|
15261
|
+
const latestKind = cleanText(body?.latestKind || "").slice(0, 80) || "unknown";
|
|
15262
|
+
const appBuildId = cleanText(body?.appBuildId || "").slice(0, 80) || "unknown";
|
|
15263
|
+
const currentTab = cleanText(body?.currentTab || "").slice(0, 40) || "unknown";
|
|
15264
|
+
const entryCount = Math.max(0, Math.min(10_000, Math.round(Number(body?.entryCount) || 0)));
|
|
15265
|
+
const latestCreatedAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(body?.latestCreatedAtMs) || 0)));
|
|
15266
|
+
const clientAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(body?.clientAtMs) || 0)));
|
|
15267
|
+
const visible = body?.visible === true;
|
|
15268
|
+
const renderedTokens = Array.isArray(body?.renderedTokens)
|
|
15269
|
+
? body.renderedTokens
|
|
15270
|
+
.slice(0, 5)
|
|
15271
|
+
.map((item) => {
|
|
15272
|
+
const token = cleanText(item?.token || "").slice(0, 80);
|
|
15273
|
+
const kind = cleanText(item?.kind || "").slice(0, 80);
|
|
15274
|
+
const createdAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(item?.createdAtMs) || 0)));
|
|
15275
|
+
return token && kind ? `${kind}:${token}:${createdAtMs}` : "";
|
|
15276
|
+
})
|
|
15277
|
+
.filter(Boolean)
|
|
15278
|
+
: [];
|
|
15279
|
+
const deviceId = cleanText(session?.deviceId || "").slice(0, 60) || "unknown";
|
|
15280
|
+
const ua = requestUserAgent(req).slice(0, 90);
|
|
15281
|
+
|
|
15282
|
+
console.log(
|
|
15283
|
+
`[client-event] timeline-render at=${new Date().toISOString()} device=${deviceId} ` +
|
|
15284
|
+
`route=${route} visible=${visible ? 1 : 0} tab=${currentTab} reason=${reason} ` +
|
|
15285
|
+
`latest=${latestKind}:${latestToken} latestCreatedAtMs=${latestCreatedAtMs} ` +
|
|
15286
|
+
`clientAtMs=${clientAtMs} count=${entryCount} build=${appBuildId} ` +
|
|
15287
|
+
`rendered=${renderedTokens.join(",") || "none"} ua=${JSON.stringify(ua)}`
|
|
15288
|
+
);
|
|
15289
|
+
}
|
|
15290
|
+
|
|
15291
|
+
function writeSseEvent(res, eventName, payload) {
|
|
15292
|
+
res.write(`event: ${eventName}\n`);
|
|
15293
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
15294
|
+
}
|
|
15295
|
+
|
|
15296
|
+
function openTimelineStream({ config, runtime, req, res, session }) {
|
|
15297
|
+
if (!config.timelineLiveSync) {
|
|
15298
|
+
return writeJson(res, 404, { error: "timeline-live-sync-disabled" });
|
|
15299
|
+
}
|
|
15300
|
+
res.statusCode = 200;
|
|
15301
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
15302
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
15303
|
+
res.setHeader("Connection", "keep-alive");
|
|
15304
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
15305
|
+
res.flushHeaders?.();
|
|
15306
|
+
|
|
15307
|
+
let closed = false;
|
|
15308
|
+
const deviceId = cleanText(session?.deviceId || "").slice(0, 60) || "unknown";
|
|
15309
|
+
const bus = ensureTimelineBus(runtime);
|
|
15310
|
+
const client = {
|
|
15311
|
+
send(eventName, payload) {
|
|
15312
|
+
if (closed || res.destroyed || res.writableEnded) {
|
|
15313
|
+
throw new Error("timeline-stream-closed");
|
|
15314
|
+
}
|
|
15315
|
+
writeSseEvent(res, eventName, payload);
|
|
15316
|
+
},
|
|
15317
|
+
close() {
|
|
15318
|
+
if (closed) return;
|
|
15319
|
+
closed = true;
|
|
15320
|
+
clearInterval(heartbeat);
|
|
15321
|
+
remove();
|
|
15322
|
+
try {
|
|
15323
|
+
res.end();
|
|
15324
|
+
} catch {}
|
|
15325
|
+
},
|
|
15326
|
+
};
|
|
15327
|
+
const remove = bus.addClient(client);
|
|
15328
|
+
const heartbeat = setInterval(() => {
|
|
15329
|
+
try {
|
|
15330
|
+
client.send("heartbeat", {
|
|
15331
|
+
atMs: Date.now(),
|
|
15332
|
+
revision: Number(runtime.timelineRevision) || 0,
|
|
15333
|
+
});
|
|
15334
|
+
} catch {
|
|
15335
|
+
client.close();
|
|
15336
|
+
}
|
|
15337
|
+
}, 20_000);
|
|
15338
|
+
heartbeat.unref?.();
|
|
15339
|
+
|
|
15340
|
+
req.on("close", () => client.close());
|
|
15341
|
+
req.on("aborted", () => client.close());
|
|
15342
|
+
res.on("error", () => client.close());
|
|
15343
|
+
client.send("hello", {
|
|
15344
|
+
ok: true,
|
|
15345
|
+
revision: Number(runtime.timelineRevision) || 0,
|
|
15346
|
+
appBuildId: WEB_APP_BUILD_ID,
|
|
15347
|
+
deviceId,
|
|
15348
|
+
atMs: Date.now(),
|
|
15349
|
+
});
|
|
15350
|
+
console.log(`[timeline-stream] open device=${deviceId} clients=${bus.clients.size}`);
|
|
15351
|
+
}
|
|
15352
|
+
|
|
14083
15353
|
function recordRemotePairingAudit(event) {
|
|
14084
15354
|
appendRemotePairingAuditEvent({
|
|
14085
15355
|
atMs: Date.now(),
|
|
@@ -14173,6 +15443,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
14173
15443
|
if (
|
|
14174
15444
|
url.pathname === "/app.js" ||
|
|
14175
15445
|
url.pathname === "/app.css" ||
|
|
15446
|
+
url.pathname === "/build-id.js" ||
|
|
14176
15447
|
url.pathname === "/i18n.js" ||
|
|
14177
15448
|
url.pathname === "/hazbase-passkey.js" ||
|
|
14178
15449
|
url.pathname === "/sw.js" ||
|
|
@@ -14251,7 +15522,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
14251
15522
|
if (action === "approve") {
|
|
14252
15523
|
// Resolve which executor to use.
|
|
14253
15524
|
const available = runtime.a2aAvailableExecutors || { codex: false, claude: false };
|
|
14254
|
-
const preference = requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
15525
|
+
const preference = requestedExecutor || task.viveworker?.requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
14255
15526
|
let executor;
|
|
14256
15527
|
if (preference === "codex" && available.codex) executor = "codex";
|
|
14257
15528
|
else if (preference === "claude" && available.claude) executor = "claude";
|
|
@@ -15160,6 +16431,29 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15160
16431
|
return writeJson(res, 200, { ok: true });
|
|
15161
16432
|
}
|
|
15162
16433
|
|
|
16434
|
+
// POST /api/client-events — local-only diagnostics from the PWA.
|
|
16435
|
+
//
|
|
16436
|
+
// The endpoint deliberately accepts only small sanitized metadata, not
|
|
16437
|
+
// message text, commands, file paths, relay tokens, or public keys. It is
|
|
16438
|
+
// used to correlate "bridge accepted" timings with "the PWA rendered the
|
|
16439
|
+
// latest timeline item" timings while debugging LAN/remote freshness.
|
|
16440
|
+
if (url.pathname === "/api/client-events" && req.method === "POST") {
|
|
16441
|
+
const session = requireApiSession(req, res, config, state);
|
|
16442
|
+
if (!session) return;
|
|
16443
|
+
let body;
|
|
16444
|
+
try {
|
|
16445
|
+
body = await parseJsonBody(req);
|
|
16446
|
+
} catch (err) {
|
|
16447
|
+
return writeJson(res, 400, { error: "invalid-json-body", message: err.message });
|
|
16448
|
+
}
|
|
16449
|
+
try {
|
|
16450
|
+
logClientEvent(body, session, req);
|
|
16451
|
+
} catch (err) {
|
|
16452
|
+
console.warn(`[client-event] failed to log event: ${err?.message}`);
|
|
16453
|
+
}
|
|
16454
|
+
return writeJson(res, 200, { ok: true });
|
|
16455
|
+
}
|
|
16456
|
+
|
|
15163
16457
|
// POST /api/remote-pairing/toggle — flip on/off without restart.
|
|
15164
16458
|
//
|
|
15165
16459
|
// Body: { enabled: boolean }
|
|
@@ -15748,7 +17042,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15748
17042
|
messageText: content,
|
|
15749
17043
|
createdAtMs: share.createdAtMs,
|
|
15750
17044
|
readOnly: false,
|
|
15751
|
-
provider: "viveworker",
|
|
17045
|
+
provider: sourceTool === "mcp" ? "mcp" : "viveworker",
|
|
15752
17046
|
},
|
|
15753
17047
|
});
|
|
15754
17048
|
await saveState(config.stateFile, state);
|
|
@@ -15765,6 +17059,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15765
17059
|
stableId: `thread_share:${shareId}`,
|
|
15766
17060
|
title: `Thread Share: ${sourceLabel || sourceTool || "agent"} → ${targetLabel || targetTool}`,
|
|
15767
17061
|
body: String(content).slice(0, 160),
|
|
17062
|
+
buildLocalizedContent: ({ locale }) => localizedThreadSharePushContent(locale, {
|
|
17063
|
+
sourceLabel,
|
|
17064
|
+
sourceTool,
|
|
17065
|
+
targetLabel,
|
|
17066
|
+
targetTool,
|
|
17067
|
+
body: content,
|
|
17068
|
+
}),
|
|
15768
17069
|
});
|
|
15769
17070
|
} catch (error) {
|
|
15770
17071
|
console.error(`[thread-share-push] ${error.message}`);
|
|
@@ -15864,6 +17165,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15864
17165
|
stableId: `thread_share_fallback:${share.shareId}`,
|
|
15865
17166
|
title: "Thread Share: target unreachable",
|
|
15866
17167
|
body: `Codex thread "${share.targetLabel || share.targetConversationId.slice(0, 8)}" is not connected. Content saved to inbox.`,
|
|
17168
|
+
buildLocalizedContent: ({ locale }) => localizedThreadShareFallbackPushContent(
|
|
17169
|
+
locale,
|
|
17170
|
+
share.targetLabel || share.targetConversationId.slice(0, 8)
|
|
17171
|
+
),
|
|
15867
17172
|
});
|
|
15868
17173
|
} catch (error) {
|
|
15869
17174
|
console.error(`[thread-share-fallback-push] ${error.message}`);
|
|
@@ -15956,7 +17261,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15956
17261
|
rangeDate = d.toISOString().slice(0, 10);
|
|
15957
17262
|
}
|
|
15958
17263
|
|
|
15959
|
-
const relevantKinds = new Set(["file_event", "completion", "plan_ready", "assistant_final"]);
|
|
17264
|
+
const relevantKinds = new Set(["file_event", "command_event", "completion", "plan_ready", "assistant_final"]);
|
|
15960
17265
|
const entries = (runtime.recentTimelineEntries || [])
|
|
15961
17266
|
.filter((e) => e.createdAtMs >= rangeStart && e.createdAtMs < rangeEnd && relevantKinds.has(e.kind))
|
|
15962
17267
|
.map((e) => ({
|
|
@@ -16125,7 +17430,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16125
17430
|
stableId: pendingApprovalStableId(approval),
|
|
16126
17431
|
title: approval.title,
|
|
16127
17432
|
body: approval.messageText,
|
|
16128
|
-
buildLocalizedContent: () => (
|
|
17433
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16129
17434
|
}).catch((error) => {
|
|
16130
17435
|
console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
|
|
16131
17436
|
});
|
|
@@ -16215,10 +17520,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16215
17520
|
stableId: pendingApprovalStableId(approval),
|
|
16216
17521
|
title: approval.title,
|
|
16217
17522
|
body: approval.messageText,
|
|
16218
|
-
buildLocalizedContent: () => (
|
|
16219
|
-
title: approval.title,
|
|
16220
|
-
body: approval.messageText,
|
|
16221
|
-
}),
|
|
17523
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16222
17524
|
}).catch((error) => {
|
|
16223
17525
|
console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
16224
17526
|
});
|
|
@@ -16252,6 +17554,22 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16252
17554
|
return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
|
|
16253
17555
|
}
|
|
16254
17556
|
|
|
17557
|
+
if (url.pathname === "/api/providers/mcp/events" && req.method === "POST") {
|
|
17558
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
17559
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
17560
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
17561
|
+
}
|
|
17562
|
+
|
|
17563
|
+
let body;
|
|
17564
|
+
try {
|
|
17565
|
+
body = await parseJsonBody(req);
|
|
17566
|
+
} catch {
|
|
17567
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
17568
|
+
}
|
|
17569
|
+
|
|
17570
|
+
return handleMcpProviderEvent({ config, runtime, state, body, res });
|
|
17571
|
+
}
|
|
17572
|
+
|
|
16255
17573
|
if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
|
|
16256
17574
|
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
16257
17575
|
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
@@ -16494,7 +17812,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16494
17812
|
body: approval.messageText,
|
|
16495
17813
|
buildLocalizedContent: ({ locale }) => ({
|
|
16496
17814
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
16497
|
-
body: approval.
|
|
17815
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
16498
17816
|
}),
|
|
16499
17817
|
}).catch(() => {});
|
|
16500
17818
|
|
|
@@ -16623,6 +17941,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16623
17941
|
stableId: `moltbook_reply:${item.sourceId}`,
|
|
16624
17942
|
title: item.title,
|
|
16625
17943
|
body: item.summary,
|
|
17944
|
+
buildLocalizedContent: ({ locale }) => localizedMoltbookReplyPushContent(locale, item),
|
|
16626
17945
|
});
|
|
16627
17946
|
} catch (error) {
|
|
16628
17947
|
console.error(`[moltbook-push-error] ${error.message}`);
|
|
@@ -16782,20 +18101,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16782
18101
|
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
16783
18102
|
}
|
|
16784
18103
|
try {
|
|
16785
|
-
const pushTitle = (
|
|
16786
|
-
if (draftType !== "original_post") {
|
|
16787
|
-
return postTitle ? `draft → ${postTitle}` : "Moltbook draft";
|
|
16788
|
-
}
|
|
16789
|
-
const greetings = {
|
|
16790
|
-
morning: { en: "Good morning! Share yesterday's highlights?", ja: "おはようございます!昨日の成果をシェアしませんか?" },
|
|
16791
|
-
noon: { en: "Nice progress! Ready to share your morning work?", ja: "午前中お疲れ様でした!進捗をシェアしませんか?" },
|
|
16792
|
-
evening: { en: "Great work today! Let's share what you built.", ja: "今日もお疲れ様でした!成果をシェアしませんか?" },
|
|
16793
|
-
};
|
|
16794
|
-
const locale = config.locale || "en";
|
|
16795
|
-
const lang = locale.startsWith("ja") ? "ja" : "en";
|
|
16796
|
-
const g = greetings[slot];
|
|
16797
|
-
return g ? g[lang] : (postTitle || "Moltbook new post");
|
|
16798
|
-
})();
|
|
18104
|
+
const pushTitle = localizedMoltbookDraftPushTitle(config.defaultLocale, { draftType, postTitle, slot });
|
|
16799
18105
|
await deliverWebPushItem({
|
|
16800
18106
|
config,
|
|
16801
18107
|
state,
|
|
@@ -16806,6 +18112,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16806
18112
|
stableId: `moltbook_draft:${sourceId}`,
|
|
16807
18113
|
title: pushTitle,
|
|
16808
18114
|
body: draftType === "original_post" ? (postTitle || String(draftText).slice(0, 160)) : String(draftText).slice(0, 160),
|
|
18115
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18116
|
+
title: localizedMoltbookDraftPushTitle(locale, { draftType, postTitle, slot }),
|
|
18117
|
+
body: draftType === "original_post" ? (postTitle || pushBodySnippet(draftText)) : pushBodySnippet(draftText),
|
|
18118
|
+
}),
|
|
16809
18119
|
});
|
|
16810
18120
|
} catch (error) {
|
|
16811
18121
|
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
@@ -16903,6 +18213,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16903
18213
|
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
16904
18214
|
title: "Draft post failed",
|
|
16905
18215
|
body: String(postError.message || "").slice(0, 160),
|
|
18216
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18217
|
+
title: t(locale, "server.push.moltbookDraft.failedTitle"),
|
|
18218
|
+
body: pushBodySnippet(postError.message || ""),
|
|
18219
|
+
}),
|
|
16906
18220
|
});
|
|
16907
18221
|
} catch { /* ignore push error */ }
|
|
16908
18222
|
}
|
|
@@ -16944,6 +18258,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16944
18258
|
return writeJson(res, 200, buildTimelineResponse(runtime, state, config, locale));
|
|
16945
18259
|
}
|
|
16946
18260
|
|
|
18261
|
+
if (url.pathname === "/api/timeline/stream" && req.method === "GET") {
|
|
18262
|
+
const session = requireApiSession(req, res, config, state);
|
|
18263
|
+
if (!session) {
|
|
18264
|
+
return;
|
|
18265
|
+
}
|
|
18266
|
+
return openTimelineStream({ config, runtime, req, res, session });
|
|
18267
|
+
}
|
|
18268
|
+
|
|
16947
18269
|
const apiTimelineImageMatch = url.pathname.match(/^\/api\/timeline\/([^/]+)\/images\/(\d+)$/u);
|
|
16948
18270
|
if (apiTimelineImageMatch && req.method === "GET") {
|
|
16949
18271
|
const token = decodeURIComponent(apiTimelineImageMatch[1]);
|
|
@@ -17012,7 +18334,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17012
18334
|
const payload = contentType.includes("multipart/form-data")
|
|
17013
18335
|
? await stageCompletionReplyImages(config, req)
|
|
17014
18336
|
: await parseJsonBody(req);
|
|
17015
|
-
await handleCompletionReply({
|
|
18337
|
+
const replyResult = await handleCompletionReply({
|
|
17016
18338
|
config,
|
|
17017
18339
|
runtime,
|
|
17018
18340
|
state,
|
|
@@ -17026,6 +18348,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17026
18348
|
ok: true,
|
|
17027
18349
|
planMode: payload?.planMode === true,
|
|
17028
18350
|
imageCount: Array.isArray(payload?.localImagePaths) ? payload.localImagePaths.length : 0,
|
|
18351
|
+
alreadyAccepted: replyResult?.alreadyAccepted === true,
|
|
18352
|
+
ackTimeout: replyResult?.ackTimeout === true,
|
|
17029
18353
|
});
|
|
17030
18354
|
} catch (error) {
|
|
17031
18355
|
if (error.message === "completion-reply-empty") {
|
|
@@ -17066,6 +18390,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17066
18390
|
const decision = apiApprovalDecisionMatch[2];
|
|
17067
18391
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
17068
18392
|
if (!approval) {
|
|
18393
|
+
const historicalDecision = approvalDecisionFromHistoryItem(historyItemByToken(runtime, "approval", token));
|
|
18394
|
+
if (historicalDecision) {
|
|
18395
|
+
if (historicalDecision === decision) {
|
|
18396
|
+
return writeJson(res, 200, {
|
|
18397
|
+
ok: true,
|
|
18398
|
+
decision,
|
|
18399
|
+
alreadyHandled: true,
|
|
18400
|
+
});
|
|
18401
|
+
}
|
|
18402
|
+
return writeJson(res, 409, {
|
|
18403
|
+
error: "approval-already-handled",
|
|
18404
|
+
decision: historicalDecision,
|
|
18405
|
+
});
|
|
18406
|
+
}
|
|
17069
18407
|
return writeJson(res, 404, { error: "approval-not-found" });
|
|
17070
18408
|
}
|
|
17071
18409
|
if (approval.resolved || approval.resolving) {
|
|
@@ -17127,7 +18465,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17127
18465
|
|
|
17128
18466
|
approval.resolving = true;
|
|
17129
18467
|
try {
|
|
17130
|
-
if (approval.
|
|
18468
|
+
if (approval.resolveMcpWaiter) {
|
|
18469
|
+
approval.resolveMcpWaiter({
|
|
18470
|
+
approved: true,
|
|
18471
|
+
decision: "answered",
|
|
18472
|
+
answers,
|
|
18473
|
+
answerText: reasonText,
|
|
18474
|
+
});
|
|
18475
|
+
} else if (approval.resolveClaudeWaiter) {
|
|
17131
18476
|
approval.resolveClaudeWaiter({
|
|
17132
18477
|
permissionDecision: "deny",
|
|
17133
18478
|
permissionDecisionReason: reasonText,
|
|
@@ -17163,8 +18508,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17163
18508
|
if (stateChanged) {
|
|
17164
18509
|
await saveState(config.stateFile, state);
|
|
17165
18510
|
}
|
|
17166
|
-
console.log(`[claude-question-answer] ${approval.requestKey}`);
|
|
17167
|
-
|
|
18511
|
+
console.log(`[${approval.provider === "mcp" ? "mcp" : "claude"}-question-answer] ${approval.requestKey}`);
|
|
18512
|
+
if (approval.provider === "claude") {
|
|
18513
|
+
activateClaudeDesktopIfMac(req);
|
|
18514
|
+
}
|
|
17168
18515
|
return writeJson(res, 200, { ok: true });
|
|
17169
18516
|
} catch (error) {
|
|
17170
18517
|
approval.resolving = false;
|
|
@@ -19057,6 +20404,10 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
19057
20404
|
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
19058
20405
|
title: "Moltbook posted",
|
|
19059
20406
|
body: truncate(singleLine(pushTitle), 160),
|
|
20407
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
20408
|
+
title: t(locale, "server.push.moltbookDraft.postedTitle"),
|
|
20409
|
+
body: localizedMoltbookPostedBody(locale, draft, finalTitle),
|
|
20410
|
+
}),
|
|
19060
20411
|
});
|
|
19061
20412
|
} catch { /* ignore push error */ }
|
|
19062
20413
|
|
|
@@ -19082,6 +20433,10 @@ function buildConfig(cli) {
|
|
|
19082
20433
|
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
19083
20434
|
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
19084
20435
|
a2aRelayRegisterSecret: cleanText(process.env.A2A_RELAY_REGISTER_SECRET || ""),
|
|
20436
|
+
a2aProModel: cleanText(process.env.A2A_PRO_MODEL || ""),
|
|
20437
|
+
a2aProPrice: cleanText(process.env.A2A_PRO_PRICE || ""),
|
|
20438
|
+
a2aProPayTo: cleanText(process.env.A2A_PRO_PAY_TO || ""),
|
|
20439
|
+
a2aProExpiresDays: cleanText(process.env.A2A_PRO_EXPIRES_DAYS || "7"),
|
|
19085
20440
|
// Remote-pairing relay (PWA off-LAN). Off by default — bridges that
|
|
19086
20441
|
// never leave the trusted LAN don't need to open an outbound connection
|
|
19087
20442
|
// to the relay. Toggle with REMOTE_PAIRING_ENABLED=true in
|
|
@@ -19115,6 +20470,8 @@ function buildConfig(cli) {
|
|
|
19115
20470
|
process.env.TIMELINE_ATTACHMENTS_DIR || path.join(path.dirname(stateFile), "timeline-attachments")
|
|
19116
20471
|
),
|
|
19117
20472
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
20473
|
+
timelineLiveSync: boolEnv("TIMELINE_LIVE_SYNC", true),
|
|
20474
|
+
timelineLiveDebounceMs: numberEnv("TIMELINE_LIVE_DEBOUNCE_MS", 75),
|
|
19118
20475
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
19119
20476
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
19120
20477
|
directoryScanIntervalMs: numberEnv("DIRECTORY_SCAN_INTERVAL_MS", 30000),
|
|
@@ -19173,6 +20530,10 @@ function buildConfig(cli) {
|
|
|
19173
20530
|
ipcSocketPath: resolvePath(process.env.CODEX_IPC_SOCKET_PATH || defaultIpcSocketPath()),
|
|
19174
20531
|
ipcReconnectMs: numberEnv("IPC_RECONNECT_MS", 1500),
|
|
19175
20532
|
ipcRequestTimeoutMs: numberEnv("IPC_REQUEST_TIMEOUT_MS", 12000),
|
|
20533
|
+
completionReplyAckTimeoutMs: numberEnv(
|
|
20534
|
+
"COMPLETION_REPLY_ACK_TIMEOUT_MS",
|
|
20535
|
+
DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS
|
|
20536
|
+
),
|
|
19176
20537
|
choicePageSize: numberEnv("CHOICE_PAGE_SIZE", 5),
|
|
19177
20538
|
completionReplyImageMaxBytes: numberEnv(
|
|
19178
20539
|
"COMPLETION_REPLY_IMAGE_MAX_BYTES",
|
|
@@ -20109,16 +21470,20 @@ function extractRolloutMessageText(content) {
|
|
|
20109
21470
|
if (!Array.isArray(content)) {
|
|
20110
21471
|
return "";
|
|
20111
21472
|
}
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
21473
|
+
// The per-entry text is already passed through `normalizeTimelineMessageText`
|
|
21474
|
+
// (which preserves newlines for the markdown renderer). The final wrap used
|
|
21475
|
+
// to be `cleanText`, which collapses every \s+ — newlines included — into
|
|
21476
|
+
// a single space, flattening tables / code fences / lists into one
|
|
21477
|
+
// illegible line. Use a plain trim instead so the structure survives.
|
|
21478
|
+
return content
|
|
21479
|
+
.map((entry) =>
|
|
21480
|
+
isPlainObject(entry) && (entry.type === "input_text" || entry.type === "output_text")
|
|
21481
|
+
? normalizeTimelineMessageText(entry.text ?? "")
|
|
21482
|
+
: ""
|
|
21483
|
+
)
|
|
21484
|
+
.filter(Boolean)
|
|
21485
|
+
.join("\n")
|
|
21486
|
+
.trim();
|
|
20122
21487
|
}
|
|
20123
21488
|
|
|
20124
21489
|
function extractTitleOnlyJsonTitleFromRolloutContent(content) {
|
|
@@ -21091,11 +22456,13 @@ async function main() {
|
|
|
21091
22456
|
`notifyPlans=${config.notifyPlans}`,
|
|
21092
22457
|
`nativeApprovalServer=${config.nativeApprovalPublicBaseUrl}`,
|
|
21093
22458
|
`pollMs=${config.pollIntervalMs}`,
|
|
22459
|
+
`timelineLive=${config.timelineLiveSync}`,
|
|
21094
22460
|
`replaySeconds=${config.replaySeconds}`,
|
|
21095
22461
|
].join(" | ")
|
|
21096
22462
|
);
|
|
21097
22463
|
|
|
21098
22464
|
let approvalServer = null;
|
|
22465
|
+
let timelineLiveHandle = null;
|
|
21099
22466
|
|
|
21100
22467
|
process.on("SIGINT", handleSignal);
|
|
21101
22468
|
process.on("SIGTERM", handleSignal);
|
|
@@ -21234,6 +22601,8 @@ async function main() {
|
|
|
21234
22601
|
}
|
|
21235
22602
|
}
|
|
21236
22603
|
|
|
22604
|
+
timelineLiveHandle = startTimelineLiveSync({ config, runtime, state });
|
|
22605
|
+
|
|
21237
22606
|
// --- A2A Relay ---
|
|
21238
22607
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
21239
22608
|
config.a2aAcceptPublicTasks = state.a2aAcceptPublicTasks === true;
|
|
@@ -21366,6 +22735,13 @@ async function main() {
|
|
|
21366
22735
|
} finally {
|
|
21367
22736
|
runtime.stopping = true;
|
|
21368
22737
|
|
|
22738
|
+
if (timelineLiveHandle) {
|
|
22739
|
+
try {
|
|
22740
|
+
timelineLiveHandle.close();
|
|
22741
|
+
} catch {}
|
|
22742
|
+
timelineLiveHandle = null;
|
|
22743
|
+
}
|
|
22744
|
+
|
|
21369
22745
|
stopRelayPolling();
|
|
21370
22746
|
|
|
21371
22747
|
if (runtime.remotePairingHandle) {
|