viveworker 0.8.1 → 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/README.md +59 -0
- package/package.json +1 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +1 -1
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -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/viveworker-bridge.mjs +1113 -65
- package/web/app.css +114 -1
- package/web/app.js +1129 -115
- package/web/build-id.js +1 -1
- package/web/i18n.js +123 -15
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +14 -4
|
@@ -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";
|
|
@@ -55,7 +55,7 @@ const sessionCookieName = "viveworker_session";
|
|
|
55
55
|
const deviceCookieName = "viveworker_device";
|
|
56
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"]);
|
|
57
57
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
58
|
-
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"]);
|
|
59
59
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
60
60
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
61
61
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -64,7 +64,9 @@ const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
|
64
64
|
const REMOTE_PAIRING_RELAY_TOKEN_ROTATION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
65
65
|
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
66
66
|
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
67
|
+
const DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS = 1200;
|
|
67
68
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
69
|
+
const COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS = 2 * 60 * 1000;
|
|
68
70
|
const HAZBASE_METADATA_TIMEOUT_MS = 1500;
|
|
69
71
|
const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
|
|
70
72
|
const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -270,6 +272,15 @@ const runtime = {
|
|
|
270
272
|
recentHistoryItems: [],
|
|
271
273
|
recentTimelineEntries: [],
|
|
272
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(),
|
|
273
284
|
pairingAttemptsByRemoteAddress: new Map(),
|
|
274
285
|
ipcClient: null,
|
|
275
286
|
remotePairingHandle: null,
|
|
@@ -517,6 +528,8 @@ function kindTitle(locale, kind) {
|
|
|
517
528
|
return t(locale, "server.title.complete");
|
|
518
529
|
case "file_event":
|
|
519
530
|
return t(locale, "common.fileEvent");
|
|
531
|
+
case "command_event":
|
|
532
|
+
return t(locale, "common.commandEvent");
|
|
520
533
|
case "diff_thread":
|
|
521
534
|
return t(locale, "common.diff");
|
|
522
535
|
case "a2a_task":
|
|
@@ -672,6 +685,118 @@ function formatLocalizedTitle(locale, baseKeyOrTitle, threadLabel) {
|
|
|
672
685
|
return formatTitle(baseTitle, threadLabel);
|
|
673
686
|
}
|
|
674
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
|
+
|
|
675
800
|
function notificationIconPrefix(kind) {
|
|
676
801
|
switch (kind) {
|
|
677
802
|
case "approval":
|
|
@@ -704,13 +829,17 @@ function normalizeTimelineOutcome(value) {
|
|
|
704
829
|
|
|
705
830
|
function normalizeTimelineFileEventType(value) {
|
|
706
831
|
const normalized = cleanText(value || "").toLowerCase();
|
|
707
|
-
return ["read", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
832
|
+
return ["read", "search", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
708
833
|
}
|
|
709
834
|
|
|
710
835
|
function fileEventTitle(locale, fileEventType) {
|
|
711
836
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
712
837
|
case "read":
|
|
713
838
|
return t(locale, "fileEvent.read");
|
|
839
|
+
case "search":
|
|
840
|
+
return t(locale, "fileEvent.search");
|
|
841
|
+
case "command":
|
|
842
|
+
return t(locale, "fileEvent.command");
|
|
714
843
|
case "write":
|
|
715
844
|
return t(locale, "fileEvent.write");
|
|
716
845
|
case "create":
|
|
@@ -729,6 +858,10 @@ function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
|
729
858
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
730
859
|
case "read":
|
|
731
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);
|
|
732
865
|
case "write":
|
|
733
866
|
return t(locale, "detail.fileEvent.write", vars);
|
|
734
867
|
case "create":
|
|
@@ -1436,6 +1569,87 @@ function extractCommandLineFromFunctionOutput(outputText) {
|
|
|
1436
1569
|
return unwrapShellCommand(match?.[1] || "");
|
|
1437
1570
|
}
|
|
1438
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
|
+
|
|
1439
1653
|
function extractReadFileRefsFromCommand(commandText) {
|
|
1440
1654
|
const normalizedCommand = unwrapShellCommand(commandText);
|
|
1441
1655
|
const tokens = tokenizeShellWords(normalizedCommand);
|
|
@@ -1664,6 +1878,36 @@ function rememberApplyPatchInput(fileState, payload, createdAtMs = Date.now()) {
|
|
|
1664
1878
|
}
|
|
1665
1879
|
}
|
|
1666
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
|
+
|
|
1667
1911
|
async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath }) {
|
|
1668
1912
|
const normalizedCallId = cleanText(callId || "");
|
|
1669
1913
|
if (!normalizedCallId) {
|
|
@@ -2068,6 +2312,7 @@ async function ensureRolloutFileState(runtime, threadId, rolloutFilePath) {
|
|
|
2068
2312
|
threadId: cleanText(threadId || ""),
|
|
2069
2313
|
cwd: await findRolloutThreadCwd(runtime, threadId || ""),
|
|
2070
2314
|
applyPatchInputsByCallId: new Map(),
|
|
2315
|
+
toolEventInputsByCallId: new Map(),
|
|
2071
2316
|
startupCutoffMs: 0,
|
|
2072
2317
|
skipPartialLine: false,
|
|
2073
2318
|
};
|
|
@@ -3141,6 +3386,8 @@ function timelineKindSortPriority(kind) {
|
|
|
3141
3386
|
return 60;
|
|
3142
3387
|
case "ambient_suggestions":
|
|
3143
3388
|
return 52;
|
|
3389
|
+
case "command_event":
|
|
3390
|
+
return 46;
|
|
3144
3391
|
case "file_event":
|
|
3145
3392
|
return 45;
|
|
3146
3393
|
case "assistant_final":
|
|
@@ -3163,7 +3410,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
3163
3410
|
}
|
|
3164
3411
|
|
|
3165
3412
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
3166
|
-
const
|
|
3413
|
+
const rawKind = cleanText(raw.kind ?? "");
|
|
3414
|
+
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3415
|
+
const kind = rawKind === "file_event" && fileEventType === "command" ? "command_event" : rawKind;
|
|
3167
3416
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
3168
3417
|
if (!stableId || !timelineKinds.has(kind)) {
|
|
3169
3418
|
return null;
|
|
@@ -3172,7 +3421,6 @@ function normalizeTimelineEntry(raw) {
|
|
|
3172
3421
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
3173
3422
|
const rawMessageText = raw.messageText ?? "";
|
|
3174
3423
|
const messageText = normalizeTimelineMessageText(rawMessageText);
|
|
3175
|
-
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3176
3424
|
const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
|
|
3177
3425
|
const diffSource = normalizeTimelineDiffSource(raw.diffSource ?? "");
|
|
3178
3426
|
const diffCounts = diffLineCounts(diffText);
|
|
@@ -3264,7 +3512,78 @@ function normalizeTimelineEntry(raw) {
|
|
|
3264
3512
|
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
3265
3513
|
}
|
|
3266
3514
|
|
|
3267
|
-
|
|
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" }) {
|
|
3268
3587
|
const normalized = normalizeTimelineEntry(entry);
|
|
3269
3588
|
if (!normalized) {
|
|
3270
3589
|
return false;
|
|
@@ -3281,6 +3600,10 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3281
3600
|
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3282
3601
|
"stableId",
|
|
3283
3602
|
"title",
|
|
3603
|
+
"summary",
|
|
3604
|
+
"messageText",
|
|
3605
|
+
"fileEventType",
|
|
3606
|
+
"fileRefs",
|
|
3284
3607
|
"createdAtMs",
|
|
3285
3608
|
"diffAvailable",
|
|
3286
3609
|
"diffSource",
|
|
@@ -3291,6 +3614,9 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3291
3614
|
]);
|
|
3292
3615
|
runtime.recentTimelineEntries = nextItems;
|
|
3293
3616
|
state.recentTimelineEntries = nextItems;
|
|
3617
|
+
if (changed) {
|
|
3618
|
+
publishTimelineUpdate({ config, runtime, entry: normalized, source });
|
|
3619
|
+
}
|
|
3294
3620
|
return changed;
|
|
3295
3621
|
}
|
|
3296
3622
|
|
|
@@ -3411,6 +3737,18 @@ function historyItemFromEvent(event) {
|
|
|
3411
3737
|
});
|
|
3412
3738
|
}
|
|
3413
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
|
+
|
|
3414
3752
|
function recordHistoryItem({ config, runtime, state, item }) {
|
|
3415
3753
|
const normalized = normalizeHistoryItem(item);
|
|
3416
3754
|
if (!normalized) {
|
|
@@ -3573,6 +3911,7 @@ function recordActionHistoryItem({
|
|
|
3573
3911
|
runtime,
|
|
3574
3912
|
state,
|
|
3575
3913
|
entry: item,
|
|
3914
|
+
source: "event",
|
|
3576
3915
|
});
|
|
3577
3916
|
return historyChanged || timelineChanged;
|
|
3578
3917
|
}
|
|
@@ -3732,7 +4071,20 @@ function pushDeliveryKey(deviceId, stableId) {
|
|
|
3732
4071
|
return `${cleanText(deviceId || "")}:${cleanText(stableId || "")}`;
|
|
3733
4072
|
}
|
|
3734
4073
|
|
|
3735
|
-
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
|
+
}) {
|
|
3736
4088
|
if (!config.webPushEnabled || config.dryRun) {
|
|
3737
4089
|
return false;
|
|
3738
4090
|
}
|
|
@@ -3749,8 +4101,18 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3749
4101
|
let changed = false;
|
|
3750
4102
|
|
|
3751
4103
|
for (const subscription of subscriptions) {
|
|
3752
|
-
const
|
|
3753
|
-
|
|
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
|
+
) {
|
|
3754
4116
|
continue;
|
|
3755
4117
|
}
|
|
3756
4118
|
|
|
@@ -3778,8 +4140,10 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3778
4140
|
},
|
|
3779
4141
|
payload
|
|
3780
4142
|
);
|
|
3781
|
-
|
|
3782
|
-
|
|
4143
|
+
state.pushDeliveries[stableDeliveryKey] = now;
|
|
4144
|
+
if (dedupeDeliveryKey !== stableDeliveryKey) {
|
|
4145
|
+
state.pushDeliveries[dedupeDeliveryKey] = now;
|
|
4146
|
+
}
|
|
3783
4147
|
trimSeenEvents(state.pushDeliveries, config.maxSeenEvents * 4);
|
|
3784
4148
|
const stored = normalizePushSubscriptionRecord(state.pushSubscriptions?.[subscription.id]);
|
|
3785
4149
|
if (stored) {
|
|
@@ -3931,6 +4295,207 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3931
4295
|
return dirty;
|
|
3932
4296
|
}
|
|
3933
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
|
+
|
|
3934
4499
|
async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
3935
4500
|
let stat;
|
|
3936
4501
|
try {
|
|
@@ -3954,6 +4519,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
3954
4519
|
threadId: extractThreadIdFromRolloutPath(filePath),
|
|
3955
4520
|
cwd: null,
|
|
3956
4521
|
applyPatchInputsByCallId: new Map(),
|
|
4522
|
+
toolEventInputsByCallId: new Map(),
|
|
3957
4523
|
startupCutoffMs:
|
|
3958
4524
|
typeof restoredOffset === "number" ? 0 : now - config.replaySeconds * 1000,
|
|
3959
4525
|
skipPartialLine:
|
|
@@ -4031,6 +4597,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4031
4597
|
runtime,
|
|
4032
4598
|
state,
|
|
4033
4599
|
entry: timelineEntry,
|
|
4600
|
+
source: "codex-rollout",
|
|
4034
4601
|
}) || dirty;
|
|
4035
4602
|
}
|
|
4036
4603
|
|
|
@@ -4048,6 +4615,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4048
4615
|
runtime,
|
|
4049
4616
|
state,
|
|
4050
4617
|
entry: fileTimelineEntry,
|
|
4618
|
+
source: "codex-rollout",
|
|
4051
4619
|
}) || dirty;
|
|
4052
4620
|
dirty =
|
|
4053
4621
|
recordCodeEvent({
|
|
@@ -4234,6 +4802,109 @@ async function listClaudeTranscriptFiles(claudeProjectsDir, maxAgeMs = 0) {
|
|
|
4234
4802
|
}
|
|
4235
4803
|
}
|
|
4236
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
|
+
|
|
4237
4908
|
async function processClaudeTranscriptFile({ filePath, config, runtime, state, now }) {
|
|
4238
4909
|
let fileState = runtime.claudeFileStates.get(filePath);
|
|
4239
4910
|
if (!fileState) {
|
|
@@ -4349,6 +5020,20 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4349
5020
|
const claudeTitle = threadId ? runtime.claudeSessionTitles.get(threadId) || "" : "";
|
|
4350
5021
|
const threadLabel = claudeTitle || fileState.threadLabel || "";
|
|
4351
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
|
+
|
|
4352
5037
|
let text = "";
|
|
4353
5038
|
if (typeof content === "string") {
|
|
4354
5039
|
text = content;
|
|
@@ -4404,7 +5089,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4404
5089
|
provider: "claude",
|
|
4405
5090
|
});
|
|
4406
5091
|
if (entry) {
|
|
4407
|
-
dirty = recordTimelineEntry({ config, runtime, state, entry }) || dirty;
|
|
5092
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry, source: "claude-transcript" }) || dirty;
|
|
4408
5093
|
|
|
4409
5094
|
// Also record Claude assistant_final as a history item for the completed list
|
|
4410
5095
|
if (kind === "assistant_final") {
|
|
@@ -4713,6 +5398,7 @@ async function processSqliteTimelineLog({ config, runtime, state, now }) {
|
|
|
4713
5398
|
runtime,
|
|
4714
5399
|
state,
|
|
4715
5400
|
entry,
|
|
5401
|
+
source: "codex-sqlite",
|
|
4716
5402
|
}) || dirty;
|
|
4717
5403
|
|
|
4718
5404
|
// Also record Codex assistant_final as a history item so it appears
|
|
@@ -4864,6 +5550,7 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
|
|
|
4864
5550
|
runtime,
|
|
4865
5551
|
state,
|
|
4866
5552
|
entry,
|
|
5553
|
+
source: "codex-history",
|
|
4867
5554
|
}) || dirty;
|
|
4868
5555
|
}
|
|
4869
5556
|
|
|
@@ -5038,6 +5725,7 @@ async function backfillRecentTimelineEntryDiffs({ config, runtime, state }) {
|
|
|
5038
5725
|
threadId: cleanText(entry?.threadId || ""),
|
|
5039
5726
|
cwd: await findRolloutThreadCwd(runtime, entry?.threadId || ""),
|
|
5040
5727
|
applyPatchInputsByCallId: new Map(),
|
|
5728
|
+
toolEventInputsByCallId: new Map(),
|
|
5041
5729
|
startupCutoffMs: 0,
|
|
5042
5730
|
skipPartialLine: false,
|
|
5043
5731
|
};
|
|
@@ -5610,6 +6298,47 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
5610
6298
|
});
|
|
5611
6299
|
}
|
|
5612
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
|
+
|
|
5613
6342
|
async function buildRolloutFileTimelineEntries({ config, record, fileState, runtime, rolloutFilePath = "" }) {
|
|
5614
6343
|
if (!isPlainObject(record) || cleanText(record.type) !== "response_item") {
|
|
5615
6344
|
return [];
|
|
@@ -5630,30 +6359,54 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
5630
6359
|
cwd: fileState.cwd || "",
|
|
5631
6360
|
});
|
|
5632
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
|
+
|
|
5633
6385
|
if (payloadType === "custom_tool_call") {
|
|
5634
6386
|
rememberApplyPatchInput(fileState, payload, createdAtMs);
|
|
5635
6387
|
return [];
|
|
5636
6388
|
}
|
|
5637
6389
|
|
|
5638
6390
|
if (payloadType === "function_call_output") {
|
|
6391
|
+
if (findStoredToolEventInput(fileState, callId)) {
|
|
6392
|
+
return [];
|
|
6393
|
+
}
|
|
5639
6394
|
const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
|
|
5640
|
-
|
|
5641
|
-
if (fileRefs.length === 0) {
|
|
6395
|
+
if (!commandText) {
|
|
5642
6396
|
return [];
|
|
5643
6397
|
}
|
|
6398
|
+
const classified = classifyTimelineCommand(commandText);
|
|
5644
6399
|
return [
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
token: historyToken(`file_event:read:${threadId}:${callId || createdAtMs}`),
|
|
5648
|
-
kind: "file_event",
|
|
5649
|
-
fileEventType: "read",
|
|
6400
|
+
buildToolTimelineEntry({
|
|
6401
|
+
provider: "codex",
|
|
5650
6402
|
threadId,
|
|
5651
6403
|
threadLabel,
|
|
5652
|
-
|
|
5653
|
-
summary: "",
|
|
5654
|
-
fileRefs,
|
|
6404
|
+
callId,
|
|
5655
6405
|
createdAtMs,
|
|
5656
|
-
|
|
6406
|
+
fileEventType: classified.fileEventType,
|
|
6407
|
+
commandText: classified.commandText || commandText,
|
|
6408
|
+
fileRefs: classified.fileRefs,
|
|
6409
|
+
cwd: fileState.cwd || "",
|
|
5657
6410
|
}),
|
|
5658
6411
|
].filter(Boolean);
|
|
5659
6412
|
}
|
|
@@ -5884,6 +6637,8 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
5884
6637
|
subtab: "completed",
|
|
5885
6638
|
token: historyToken(event.id),
|
|
5886
6639
|
stableId: event.id,
|
|
6640
|
+
dedupeId: completionPushContentDedupeId(event),
|
|
6641
|
+
dedupeWindowMs: COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS,
|
|
5887
6642
|
title: event.title,
|
|
5888
6643
|
body: event.detailText || event.message,
|
|
5889
6644
|
buildLocalizedContent: ({ locale }) => ({
|
|
@@ -6102,6 +6857,8 @@ function buildRolloutEvent({ record, filePath, fileState, sessionIndex, config,
|
|
|
6102
6857
|
threadLabel: context.threadLabel,
|
|
6103
6858
|
message,
|
|
6104
6859
|
detailText,
|
|
6860
|
+
threadId,
|
|
6861
|
+
turnId,
|
|
6105
6862
|
priority: config.completePriority,
|
|
6106
6863
|
tags: config.completeTags,
|
|
6107
6864
|
clickUrl: config.clickUrl,
|
|
@@ -6335,7 +7092,7 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
6335
7092
|
body: approval.messageText,
|
|
6336
7093
|
buildLocalizedContent: ({ locale }) => ({
|
|
6337
7094
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
6338
|
-
body: approval.
|
|
7095
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
6339
7096
|
}),
|
|
6340
7097
|
}) || stateChanged;
|
|
6341
7098
|
}
|
|
@@ -6671,7 +7428,11 @@ async function syncGenericUserInputRequests({
|
|
|
6671
7428
|
userInputRequest.supported ? "server.title.choice" : "server.title.choiceReadOnly",
|
|
6672
7429
|
userInputRequest.threadLabel
|
|
6673
7430
|
),
|
|
6674
|
-
body:
|
|
7431
|
+
body: buildUserInputNotificationText(
|
|
7432
|
+
userInputRequest.questions,
|
|
7433
|
+
userInputRequest.supported,
|
|
7434
|
+
locale
|
|
7435
|
+
),
|
|
6675
7436
|
}),
|
|
6676
7437
|
}) || stateChanged;
|
|
6677
7438
|
}
|
|
@@ -8469,7 +9230,7 @@ class NativeIpcClient {
|
|
|
8469
9230
|
);
|
|
8470
9231
|
}
|
|
8471
9232
|
|
|
8472
|
-
async startTurn(conversationId, turnStartParams, ownerClientId = null) {
|
|
9233
|
+
async startTurn(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8473
9234
|
return this.sendThreadFollowerRequest(
|
|
8474
9235
|
"thread-follower-start-turn",
|
|
8475
9236
|
{
|
|
@@ -8477,11 +9238,12 @@ class NativeIpcClient {
|
|
|
8477
9238
|
turnStartParams,
|
|
8478
9239
|
},
|
|
8479
9240
|
conversationId,
|
|
8480
|
-
ownerClientId
|
|
9241
|
+
ownerClientId,
|
|
9242
|
+
options
|
|
8481
9243
|
);
|
|
8482
9244
|
}
|
|
8483
9245
|
|
|
8484
|
-
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null) {
|
|
9246
|
+
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8485
9247
|
const targetClientId =
|
|
8486
9248
|
ownerClientId ??
|
|
8487
9249
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
@@ -8489,7 +9251,7 @@ class NativeIpcClient {
|
|
|
8489
9251
|
return this.sendRequest(
|
|
8490
9252
|
"turn/start",
|
|
8491
9253
|
buildDirectTurnStartPayload(conversationId, turnStartParams),
|
|
8492
|
-
{ targetClientId }
|
|
9254
|
+
{ targetClientId, ...options }
|
|
8493
9255
|
);
|
|
8494
9256
|
}
|
|
8495
9257
|
|
|
@@ -8519,12 +9281,13 @@ class NativeIpcClient {
|
|
|
8519
9281
|
);
|
|
8520
9282
|
}
|
|
8521
9283
|
|
|
8522
|
-
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null) {
|
|
9284
|
+
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null, options = {}) {
|
|
8523
9285
|
return this.sendRequest(method, params, {
|
|
8524
9286
|
targetClientId:
|
|
8525
9287
|
ownerClientId ??
|
|
8526
9288
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
8527
9289
|
null,
|
|
9290
|
+
...options,
|
|
8528
9291
|
});
|
|
8529
9292
|
}
|
|
8530
9293
|
|
|
@@ -10866,6 +11629,10 @@ async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
|
10866
11629
|
stableId,
|
|
10867
11630
|
title,
|
|
10868
11631
|
body: messageText,
|
|
11632
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
11633
|
+
title: localizedMcpNotifyTitle(locale, title),
|
|
11634
|
+
body: pushBodySnippet(messageText),
|
|
11635
|
+
}),
|
|
10869
11636
|
}).catch((error) => {
|
|
10870
11637
|
console.error(`[mcp-notify-push] ${error.message}`);
|
|
10871
11638
|
return false;
|
|
@@ -10897,10 +11664,7 @@ async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
|
10897
11664
|
stableId: pendingApprovalStableId(approval),
|
|
10898
11665
|
title: approval.title || "MCP",
|
|
10899
11666
|
body: approval.messageText,
|
|
10900
|
-
buildLocalizedContent: () => (
|
|
10901
|
-
title: approval.title || "MCP",
|
|
10902
|
-
body: approval.messageText,
|
|
10903
|
-
}),
|
|
11667
|
+
buildLocalizedContent: ({ locale }) => localizedMcpApprovalPushContent(locale, approval),
|
|
10904
11668
|
}).catch((error) => {
|
|
10905
11669
|
console.error(`[mcp-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
10906
11670
|
});
|
|
@@ -11265,6 +12029,17 @@ function historyItemByToken(runtime, kind, token) {
|
|
|
11265
12029
|
) ?? null;
|
|
11266
12030
|
}
|
|
11267
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
|
+
|
|
11268
12043
|
function listLatestPersistedUserInputRequests({ config, runtime, state }) {
|
|
11269
12044
|
const byToken = new Map();
|
|
11270
12045
|
for (const userInputRequest of runtime.userInputRequestsByToken.values()) {
|
|
@@ -12338,6 +13113,48 @@ function findNewerThreadMessageAfterCompletion(runtime, completionItem) {
|
|
|
12338
13113
|
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0] || null;
|
|
12339
13114
|
}
|
|
12340
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
|
+
|
|
12341
13158
|
function buildHistoryDetail(item, locale, runtime = null) {
|
|
12342
13159
|
const threadId = historyItemThreadId(item);
|
|
12343
13160
|
const replyEnabled =
|
|
@@ -12455,6 +13272,13 @@ function buildAmbientSuggestionsDetail(entry, locale) {
|
|
|
12455
13272
|
|
|
12456
13273
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
12457
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");
|
|
12458
13282
|
return {
|
|
12459
13283
|
kind: "file_event",
|
|
12460
13284
|
token: entry.token,
|
|
@@ -12463,9 +13287,10 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12463
13287
|
threadLabel: entry.threadLabel || "",
|
|
12464
13288
|
fileEventType,
|
|
12465
13289
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
12466
|
-
messageHtml: renderMessageHtml(
|
|
13290
|
+
messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
12467
13291
|
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
12468
13292
|
previousFileRefs: normalizeTimelineFileRefs(entry.previousFileRefs ?? []),
|
|
13293
|
+
...(commandText ? { commandText } : {}),
|
|
12469
13294
|
diffAvailable: Boolean(entry.diffAvailable),
|
|
12470
13295
|
diffText: normalizeTimelineDiffText(entry.diffText ?? ""),
|
|
12471
13296
|
diffSource: normalizeTimelineDiffSource(entry.diffSource ?? ""),
|
|
@@ -12476,6 +13301,25 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12476
13301
|
};
|
|
12477
13302
|
}
|
|
12478
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
|
+
|
|
12479
13323
|
function buildDiffThreadDetail(group, locale) {
|
|
12480
13324
|
const changedFileCount = Math.max(0, Number(group.changedFileCount) || 0);
|
|
12481
13325
|
return {
|
|
@@ -12849,6 +13693,25 @@ async function handleCompletionReply({
|
|
|
12849
13693
|
if (!conversationId) {
|
|
12850
13694
|
throw new Error("completion-reply-unavailable");
|
|
12851
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
|
+
|
|
12852
13715
|
// For assistant_final, check against timeline entries (avoids maxHistoryItems eviction).
|
|
12853
13716
|
// For legacy completion, check against history items.
|
|
12854
13717
|
const itemKind = cleanText(completionItem?.kind || "");
|
|
@@ -12939,30 +13802,32 @@ async function handleCompletionReply({
|
|
|
12939
13802
|
await runtime.ipcClient.startTurnDirect(
|
|
12940
13803
|
conversationId,
|
|
12941
13804
|
candidate.turnStartParams,
|
|
12942
|
-
ownerClientId
|
|
13805
|
+
ownerClientId,
|
|
13806
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12943
13807
|
);
|
|
12944
13808
|
} else {
|
|
12945
13809
|
await runtime.ipcClient.startTurn(
|
|
12946
13810
|
conversationId,
|
|
12947
13811
|
candidate.turnStartParams,
|
|
12948
|
-
ownerClientId
|
|
13812
|
+
ownerClientId,
|
|
13813
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12949
13814
|
);
|
|
12950
13815
|
}
|
|
12951
13816
|
console.log(
|
|
12952
|
-
`[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")}`
|
|
12953
13818
|
);
|
|
12954
13819
|
await finalizeReplyAccepted();
|
|
12955
|
-
return;
|
|
13820
|
+
return { alreadyAccepted: false };
|
|
12956
13821
|
} catch (error) {
|
|
12957
13822
|
if (isStartTurnAckTimeout(error)) {
|
|
12958
13823
|
// Codex can create the turn but fail to send the bridge an ACK before
|
|
12959
13824
|
// its internal follower timeout fires. Retrying risks duplicate user
|
|
12960
13825
|
// turns, so treat this as accepted and let the timeline catch up.
|
|
12961
13826
|
console.log(
|
|
12962
|
-
`[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)}`
|
|
12963
13828
|
);
|
|
12964
13829
|
await finalizeReplyAccepted();
|
|
12965
|
-
return;
|
|
13830
|
+
return { alreadyAccepted: false, ackTimeout: true };
|
|
12966
13831
|
}
|
|
12967
13832
|
lastError = error;
|
|
12968
13833
|
console.log(
|
|
@@ -13718,6 +14583,10 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13718
14583
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13719
14584
|
return entry ? buildTimelineFileEventDetail(entry, locale) : null;
|
|
13720
14585
|
}
|
|
14586
|
+
if (kind === "command_event") {
|
|
14587
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
14588
|
+
return entry ? buildTimelineCommandEventDetail(entry, locale) : null;
|
|
14589
|
+
}
|
|
13721
14590
|
if (kind === "ambient_suggestions") {
|
|
13722
14591
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13723
14592
|
if (entry) {
|
|
@@ -13899,6 +14768,8 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13899
14768
|
provider: "a2a",
|
|
13900
14769
|
instruction,
|
|
13901
14770
|
messageText: responseText,
|
|
14771
|
+
viveworker: task?.viveworker || entry?.viveworker || {},
|
|
14772
|
+
paidDeliverable: task?.paidDeliverable || entry?.paidDeliverable || null,
|
|
13902
14773
|
taskId: task?.id || "",
|
|
13903
14774
|
taskStatus: task?.status || entry?.taskStatus || (kind === "a2a_task_result" ? "completed" : "submitted"),
|
|
13904
14775
|
callerInfo: task?.callerInfo || {},
|
|
@@ -14168,7 +15039,7 @@ function buildWebAppHtml({ pairToken }) {
|
|
|
14168
15039
|
<link rel="manifest" href="${escapeHtml(manifestHref)}">
|
|
14169
15040
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
14170
15041
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
14171
|
-
<link rel="stylesheet" href="/app.css">
|
|
15042
|
+
<link rel="stylesheet" href="/app.css?v=${escapeHtml(WEB_APP_BUILD_ID)}">
|
|
14172
15043
|
<style>
|
|
14173
15044
|
.boot-splash {
|
|
14174
15045
|
position: fixed;
|
|
@@ -14378,6 +15249,107 @@ function logRemotePairingBootTrace(body, session, req) {
|
|
|
14378
15249
|
}
|
|
14379
15250
|
}
|
|
14380
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
|
+
|
|
14381
15353
|
function recordRemotePairingAudit(event) {
|
|
14382
15354
|
appendRemotePairingAuditEvent({
|
|
14383
15355
|
atMs: Date.now(),
|
|
@@ -14550,7 +15522,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
14550
15522
|
if (action === "approve") {
|
|
14551
15523
|
// Resolve which executor to use.
|
|
14552
15524
|
const available = runtime.a2aAvailableExecutors || { codex: false, claude: false };
|
|
14553
|
-
const preference = requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
15525
|
+
const preference = requestedExecutor || task.viveworker?.requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
14554
15526
|
let executor;
|
|
14555
15527
|
if (preference === "codex" && available.codex) executor = "codex";
|
|
14556
15528
|
else if (preference === "claude" && available.claude) executor = "claude";
|
|
@@ -15459,6 +16431,29 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15459
16431
|
return writeJson(res, 200, { ok: true });
|
|
15460
16432
|
}
|
|
15461
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
|
+
|
|
15462
16457
|
// POST /api/remote-pairing/toggle — flip on/off without restart.
|
|
15463
16458
|
//
|
|
15464
16459
|
// Body: { enabled: boolean }
|
|
@@ -16064,6 +17059,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16064
17059
|
stableId: `thread_share:${shareId}`,
|
|
16065
17060
|
title: `Thread Share: ${sourceLabel || sourceTool || "agent"} → ${targetLabel || targetTool}`,
|
|
16066
17061
|
body: String(content).slice(0, 160),
|
|
17062
|
+
buildLocalizedContent: ({ locale }) => localizedThreadSharePushContent(locale, {
|
|
17063
|
+
sourceLabel,
|
|
17064
|
+
sourceTool,
|
|
17065
|
+
targetLabel,
|
|
17066
|
+
targetTool,
|
|
17067
|
+
body: content,
|
|
17068
|
+
}),
|
|
16067
17069
|
});
|
|
16068
17070
|
} catch (error) {
|
|
16069
17071
|
console.error(`[thread-share-push] ${error.message}`);
|
|
@@ -16163,6 +17165,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16163
17165
|
stableId: `thread_share_fallback:${share.shareId}`,
|
|
16164
17166
|
title: "Thread Share: target unreachable",
|
|
16165
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
|
+
),
|
|
16166
17172
|
});
|
|
16167
17173
|
} catch (error) {
|
|
16168
17174
|
console.error(`[thread-share-fallback-push] ${error.message}`);
|
|
@@ -16255,7 +17261,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16255
17261
|
rangeDate = d.toISOString().slice(0, 10);
|
|
16256
17262
|
}
|
|
16257
17263
|
|
|
16258
|
-
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"]);
|
|
16259
17265
|
const entries = (runtime.recentTimelineEntries || [])
|
|
16260
17266
|
.filter((e) => e.createdAtMs >= rangeStart && e.createdAtMs < rangeEnd && relevantKinds.has(e.kind))
|
|
16261
17267
|
.map((e) => ({
|
|
@@ -16424,7 +17430,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16424
17430
|
stableId: pendingApprovalStableId(approval),
|
|
16425
17431
|
title: approval.title,
|
|
16426
17432
|
body: approval.messageText,
|
|
16427
|
-
buildLocalizedContent: () => (
|
|
17433
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16428
17434
|
}).catch((error) => {
|
|
16429
17435
|
console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
|
|
16430
17436
|
});
|
|
@@ -16514,10 +17520,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16514
17520
|
stableId: pendingApprovalStableId(approval),
|
|
16515
17521
|
title: approval.title,
|
|
16516
17522
|
body: approval.messageText,
|
|
16517
|
-
buildLocalizedContent: () => (
|
|
16518
|
-
title: approval.title,
|
|
16519
|
-
body: approval.messageText,
|
|
16520
|
-
}),
|
|
17523
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16521
17524
|
}).catch((error) => {
|
|
16522
17525
|
console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
16523
17526
|
});
|
|
@@ -16809,7 +17812,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16809
17812
|
body: approval.messageText,
|
|
16810
17813
|
buildLocalizedContent: ({ locale }) => ({
|
|
16811
17814
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
16812
|
-
body: approval.
|
|
17815
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
16813
17816
|
}),
|
|
16814
17817
|
}).catch(() => {});
|
|
16815
17818
|
|
|
@@ -16938,6 +17941,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16938
17941
|
stableId: `moltbook_reply:${item.sourceId}`,
|
|
16939
17942
|
title: item.title,
|
|
16940
17943
|
body: item.summary,
|
|
17944
|
+
buildLocalizedContent: ({ locale }) => localizedMoltbookReplyPushContent(locale, item),
|
|
16941
17945
|
});
|
|
16942
17946
|
} catch (error) {
|
|
16943
17947
|
console.error(`[moltbook-push-error] ${error.message}`);
|
|
@@ -17097,20 +18101,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17097
18101
|
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
17098
18102
|
}
|
|
17099
18103
|
try {
|
|
17100
|
-
const pushTitle = (
|
|
17101
|
-
if (draftType !== "original_post") {
|
|
17102
|
-
return postTitle ? `draft → ${postTitle}` : "Moltbook draft";
|
|
17103
|
-
}
|
|
17104
|
-
const greetings = {
|
|
17105
|
-
morning: { en: "Good morning! Share yesterday's highlights?", ja: "おはようございます!昨日の成果をシェアしませんか?" },
|
|
17106
|
-
noon: { en: "Nice progress! Ready to share your morning work?", ja: "午前中お疲れ様でした!進捗をシェアしませんか?" },
|
|
17107
|
-
evening: { en: "Great work today! Let's share what you built.", ja: "今日もお疲れ様でした!成果をシェアしませんか?" },
|
|
17108
|
-
};
|
|
17109
|
-
const locale = config.locale || "en";
|
|
17110
|
-
const lang = locale.startsWith("ja") ? "ja" : "en";
|
|
17111
|
-
const g = greetings[slot];
|
|
17112
|
-
return g ? g[lang] : (postTitle || "Moltbook new post");
|
|
17113
|
-
})();
|
|
18104
|
+
const pushTitle = localizedMoltbookDraftPushTitle(config.defaultLocale, { draftType, postTitle, slot });
|
|
17114
18105
|
await deliverWebPushItem({
|
|
17115
18106
|
config,
|
|
17116
18107
|
state,
|
|
@@ -17121,6 +18112,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17121
18112
|
stableId: `moltbook_draft:${sourceId}`,
|
|
17122
18113
|
title: pushTitle,
|
|
17123
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
|
+
}),
|
|
17124
18119
|
});
|
|
17125
18120
|
} catch (error) {
|
|
17126
18121
|
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
@@ -17218,6 +18213,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17218
18213
|
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
17219
18214
|
title: "Draft post failed",
|
|
17220
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
|
+
}),
|
|
17221
18220
|
});
|
|
17222
18221
|
} catch { /* ignore push error */ }
|
|
17223
18222
|
}
|
|
@@ -17259,6 +18258,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17259
18258
|
return writeJson(res, 200, buildTimelineResponse(runtime, state, config, locale));
|
|
17260
18259
|
}
|
|
17261
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
|
+
|
|
17262
18269
|
const apiTimelineImageMatch = url.pathname.match(/^\/api\/timeline\/([^/]+)\/images\/(\d+)$/u);
|
|
17263
18270
|
if (apiTimelineImageMatch && req.method === "GET") {
|
|
17264
18271
|
const token = decodeURIComponent(apiTimelineImageMatch[1]);
|
|
@@ -17327,7 +18334,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17327
18334
|
const payload = contentType.includes("multipart/form-data")
|
|
17328
18335
|
? await stageCompletionReplyImages(config, req)
|
|
17329
18336
|
: await parseJsonBody(req);
|
|
17330
|
-
await handleCompletionReply({
|
|
18337
|
+
const replyResult = await handleCompletionReply({
|
|
17331
18338
|
config,
|
|
17332
18339
|
runtime,
|
|
17333
18340
|
state,
|
|
@@ -17341,6 +18348,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17341
18348
|
ok: true,
|
|
17342
18349
|
planMode: payload?.planMode === true,
|
|
17343
18350
|
imageCount: Array.isArray(payload?.localImagePaths) ? payload.localImagePaths.length : 0,
|
|
18351
|
+
alreadyAccepted: replyResult?.alreadyAccepted === true,
|
|
18352
|
+
ackTimeout: replyResult?.ackTimeout === true,
|
|
17344
18353
|
});
|
|
17345
18354
|
} catch (error) {
|
|
17346
18355
|
if (error.message === "completion-reply-empty") {
|
|
@@ -17381,6 +18390,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17381
18390
|
const decision = apiApprovalDecisionMatch[2];
|
|
17382
18391
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
17383
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
|
+
}
|
|
17384
18407
|
return writeJson(res, 404, { error: "approval-not-found" });
|
|
17385
18408
|
}
|
|
17386
18409
|
if (approval.resolved || approval.resolving) {
|
|
@@ -19381,6 +20404,10 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
19381
20404
|
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
19382
20405
|
title: "Moltbook posted",
|
|
19383
20406
|
body: truncate(singleLine(pushTitle), 160),
|
|
20407
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
20408
|
+
title: t(locale, "server.push.moltbookDraft.postedTitle"),
|
|
20409
|
+
body: localizedMoltbookPostedBody(locale, draft, finalTitle),
|
|
20410
|
+
}),
|
|
19384
20411
|
});
|
|
19385
20412
|
} catch { /* ignore push error */ }
|
|
19386
20413
|
|
|
@@ -19406,6 +20433,10 @@ function buildConfig(cli) {
|
|
|
19406
20433
|
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
19407
20434
|
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
19408
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"),
|
|
19409
20440
|
// Remote-pairing relay (PWA off-LAN). Off by default — bridges that
|
|
19410
20441
|
// never leave the trusted LAN don't need to open an outbound connection
|
|
19411
20442
|
// to the relay. Toggle with REMOTE_PAIRING_ENABLED=true in
|
|
@@ -19439,6 +20470,8 @@ function buildConfig(cli) {
|
|
|
19439
20470
|
process.env.TIMELINE_ATTACHMENTS_DIR || path.join(path.dirname(stateFile), "timeline-attachments")
|
|
19440
20471
|
),
|
|
19441
20472
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
20473
|
+
timelineLiveSync: boolEnv("TIMELINE_LIVE_SYNC", true),
|
|
20474
|
+
timelineLiveDebounceMs: numberEnv("TIMELINE_LIVE_DEBOUNCE_MS", 75),
|
|
19442
20475
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
19443
20476
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
19444
20477
|
directoryScanIntervalMs: numberEnv("DIRECTORY_SCAN_INTERVAL_MS", 30000),
|
|
@@ -19497,6 +20530,10 @@ function buildConfig(cli) {
|
|
|
19497
20530
|
ipcSocketPath: resolvePath(process.env.CODEX_IPC_SOCKET_PATH || defaultIpcSocketPath()),
|
|
19498
20531
|
ipcReconnectMs: numberEnv("IPC_RECONNECT_MS", 1500),
|
|
19499
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
|
+
),
|
|
19500
20537
|
choicePageSize: numberEnv("CHOICE_PAGE_SIZE", 5),
|
|
19501
20538
|
completionReplyImageMaxBytes: numberEnv(
|
|
19502
20539
|
"COMPLETION_REPLY_IMAGE_MAX_BYTES",
|
|
@@ -21419,11 +22456,13 @@ async function main() {
|
|
|
21419
22456
|
`notifyPlans=${config.notifyPlans}`,
|
|
21420
22457
|
`nativeApprovalServer=${config.nativeApprovalPublicBaseUrl}`,
|
|
21421
22458
|
`pollMs=${config.pollIntervalMs}`,
|
|
22459
|
+
`timelineLive=${config.timelineLiveSync}`,
|
|
21422
22460
|
`replaySeconds=${config.replaySeconds}`,
|
|
21423
22461
|
].join(" | ")
|
|
21424
22462
|
);
|
|
21425
22463
|
|
|
21426
22464
|
let approvalServer = null;
|
|
22465
|
+
let timelineLiveHandle = null;
|
|
21427
22466
|
|
|
21428
22467
|
process.on("SIGINT", handleSignal);
|
|
21429
22468
|
process.on("SIGTERM", handleSignal);
|
|
@@ -21562,6 +22601,8 @@ async function main() {
|
|
|
21562
22601
|
}
|
|
21563
22602
|
}
|
|
21564
22603
|
|
|
22604
|
+
timelineLiveHandle = startTimelineLiveSync({ config, runtime, state });
|
|
22605
|
+
|
|
21565
22606
|
// --- A2A Relay ---
|
|
21566
22607
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
21567
22608
|
config.a2aAcceptPublicTasks = state.a2aAcceptPublicTasks === true;
|
|
@@ -21694,6 +22735,13 @@ async function main() {
|
|
|
21694
22735
|
} finally {
|
|
21695
22736
|
runtime.stopping = true;
|
|
21696
22737
|
|
|
22738
|
+
if (timelineLiveHandle) {
|
|
22739
|
+
try {
|
|
22740
|
+
timelineLiveHandle.close();
|
|
22741
|
+
} catch {}
|
|
22742
|
+
timelineLiveHandle = null;
|
|
22743
|
+
}
|
|
22744
|
+
|
|
21697
22745
|
stopRelayPolling();
|
|
21698
22746
|
|
|
21699
22747
|
if (runtime.remotePairingHandle) {
|