viveworker 0.8.1 → 0.8.3
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 +64 -4
- 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/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/viveworker-bridge.mjs +1909 -183
- package/web/app.css +147 -1
- package/web/app.js +1205 -131
- package/web/build-id.js +1 -1
- package/web/i18n.js +147 -17
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +250 -13
- 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";
|
|
@@ -21,7 +21,7 @@ import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "
|
|
|
21
21
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
22
22
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
23
23
|
import { registerWithRelay, startRelayPolling, stopRelayPolling, postRelayResult, getRelayStatus, updatePublicTasksFlag } from "./a2a-relay-client.mjs";
|
|
24
|
-
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems } from "./moltbook-api.mjs";
|
|
24
|
+
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems, updateInboxStatus, getMoltbookReplyQuotaState } from "./moltbook-api.mjs";
|
|
25
25
|
import { startRemotePairingRelay, DEFAULT_RELAY_URL } from "./lib/remote-pairing/orchestrator.mjs";
|
|
26
26
|
import { restartRemotePairingRelay, persistRemotePairingEnv, getRemotePairingStatus } from "./lib/remote-pairing/control.mjs";
|
|
27
27
|
import { appendRemotePairingAuditEvent, readRemotePairingAuditEvents } from "./lib/remote-pairing/audit.mjs";
|
|
@@ -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, "activity_status", "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,10 +64,13 @@ 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;
|
|
73
|
+
const TIMELINE_ACTIVITY_TTL_MS = 2 * 60 * 1000;
|
|
71
74
|
|
|
72
75
|
// Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
|
|
73
76
|
// per tracked repo (`git diff --name-status`, `git status --porcelain`,
|
|
@@ -263,6 +266,7 @@ const runtime = {
|
|
|
263
266
|
completionDetailsByToken: new Map(),
|
|
264
267
|
moltbookItemsByToken: new Map(),
|
|
265
268
|
moltbookDraftsByToken: new Map(),
|
|
269
|
+
moltbookReplyQuotaQueue: Promise.resolve(),
|
|
266
270
|
a2aTasksByToken: new Map(),
|
|
267
271
|
threadSharesByToken: new Map(),
|
|
268
272
|
threadRegistry: new Map(),
|
|
@@ -270,6 +274,16 @@ const runtime = {
|
|
|
270
274
|
recentHistoryItems: [],
|
|
271
275
|
recentTimelineEntries: [],
|
|
272
276
|
recentCodeEvents: [],
|
|
277
|
+
activeTimelineActivitiesByKey: new Map(),
|
|
278
|
+
timelineBus: null,
|
|
279
|
+
timelineRevision: 0,
|
|
280
|
+
timelineLiveWatchers: [],
|
|
281
|
+
timelineLiveScanTimer: null,
|
|
282
|
+
timelineLiveScanInFlight: false,
|
|
283
|
+
timelineLiveScanReschedule: false,
|
|
284
|
+
timelineLiveScanReasons: new Set(),
|
|
285
|
+
timelineLiveClaudeFiles: new Set(),
|
|
286
|
+
timelineLiveRolloutFiles: new Set(),
|
|
273
287
|
pairingAttemptsByRemoteAddress: new Map(),
|
|
274
288
|
ipcClient: null,
|
|
275
289
|
remotePairingHandle: null,
|
|
@@ -335,6 +349,58 @@ setInterval(async () => {
|
|
|
335
349
|
}
|
|
336
350
|
}
|
|
337
351
|
}, 60_000).unref?.();
|
|
352
|
+
|
|
353
|
+
async function withMoltbookReplyQuotaLock(fn) {
|
|
354
|
+
const previous = runtime.moltbookReplyQuotaQueue || Promise.resolve();
|
|
355
|
+
let release;
|
|
356
|
+
const next = new Promise((resolve) => { release = resolve; });
|
|
357
|
+
runtime.moltbookReplyQuotaQueue = previous.then(() => next, () => next);
|
|
358
|
+
await previous.catch(() => {});
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} finally {
|
|
362
|
+
release();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function reserveMoltbookReplyQuotaSlot(draft, maxDaily = 5) {
|
|
367
|
+
if (!draft || draft.draftType === "original_post") return { reserved: false };
|
|
368
|
+
return withMoltbookReplyQuotaLock(async () => {
|
|
369
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
370
|
+
if ((Number(scoutState.sentToday) || 0) >= maxDaily) {
|
|
371
|
+
return {
|
|
372
|
+
reserved: false,
|
|
373
|
+
quotaReached: true,
|
|
374
|
+
day: scoutState.day,
|
|
375
|
+
sentToday: Number(scoutState.sentToday) || 0,
|
|
376
|
+
maxDaily,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
scoutState.sentToday = (Number(scoutState.sentToday) || 0) + 1;
|
|
380
|
+
markPostSeen(scoutState, draft.postId, "reserved");
|
|
381
|
+
await writeScoutState(scoutState);
|
|
382
|
+
draft.replyQuotaReserved = true;
|
|
383
|
+
draft.replyQuotaReservedDay = scoutState.day;
|
|
384
|
+
draft.replyQuotaReservedAtMs = Date.now();
|
|
385
|
+
return {
|
|
386
|
+
reserved: true,
|
|
387
|
+
day: scoutState.day,
|
|
388
|
+
sentToday: scoutState.sentToday,
|
|
389
|
+
maxDaily,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function releaseMoltbookReplyQuotaSlot(draft) {
|
|
395
|
+
if (!draft?.replyQuotaReserved || draft.draftType === "original_post") return;
|
|
396
|
+
await withMoltbookReplyQuotaLock(async () => {
|
|
397
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
398
|
+
if (draft.replyQuotaReservedDay && scoutState.day !== draft.replyQuotaReservedDay) return;
|
|
399
|
+
scoutState.sentToday = Math.max(0, (Number(scoutState.sentToday) || 0) - 1);
|
|
400
|
+
await writeScoutState(scoutState);
|
|
401
|
+
});
|
|
402
|
+
draft.replyQuotaReserved = false;
|
|
403
|
+
}
|
|
338
404
|
const initialHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
339
405
|
const initialTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
340
406
|
const normalizedHistoryStateChanged =
|
|
@@ -503,6 +569,8 @@ function kindTitle(locale, kind) {
|
|
|
503
569
|
return t(locale, "server.title.assistantCommentary");
|
|
504
570
|
case "assistant_final":
|
|
505
571
|
return t(locale, "server.title.assistantFinal");
|
|
572
|
+
case "activity_status":
|
|
573
|
+
return t(locale, "server.title.activityStatus");
|
|
506
574
|
case "ambient_suggestions":
|
|
507
575
|
return t(locale, "server.title.ambientSuggestions");
|
|
508
576
|
case "approval":
|
|
@@ -517,6 +585,8 @@ function kindTitle(locale, kind) {
|
|
|
517
585
|
return t(locale, "server.title.complete");
|
|
518
586
|
case "file_event":
|
|
519
587
|
return t(locale, "common.fileEvent");
|
|
588
|
+
case "command_event":
|
|
589
|
+
return t(locale, "common.commandEvent");
|
|
520
590
|
case "diff_thread":
|
|
521
591
|
return t(locale, "common.diff");
|
|
522
592
|
case "a2a_task":
|
|
@@ -672,6 +742,118 @@ function formatLocalizedTitle(locale, baseKeyOrTitle, threadLabel) {
|
|
|
672
742
|
return formatTitle(baseTitle, threadLabel);
|
|
673
743
|
}
|
|
674
744
|
|
|
745
|
+
function localizedThreadSharePushContent(locale, { sourceLabel = "", sourceTool = "", targetLabel = "", targetTool = "", body = "" } = {}) {
|
|
746
|
+
const source = cleanText(sourceLabel || sourceTool || "agent");
|
|
747
|
+
const target = cleanText(targetLabel || targetTool || "target");
|
|
748
|
+
return {
|
|
749
|
+
title: t(locale, "server.push.threadShare.title", { source, target }),
|
|
750
|
+
body: pushBodySnippet(body),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function localizedThreadShareFallbackPushContent(locale, target) {
|
|
755
|
+
const normalizedTarget = cleanText(target || "");
|
|
756
|
+
return {
|
|
757
|
+
title: t(locale, "server.push.threadShareFallback.title"),
|
|
758
|
+
body: t(locale, "server.push.threadShareFallback.body", { target: normalizedTarget }),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function localizedMcpNotifyTitle(locale, title) {
|
|
763
|
+
const normalizedTitle = cleanText(title || "");
|
|
764
|
+
if (!normalizedTitle || normalizedTitle === "MCP") {
|
|
765
|
+
return t(locale, "server.push.mcpNotify.title");
|
|
766
|
+
}
|
|
767
|
+
return normalizedTitle;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function localizedMcpApprovalPushContent(locale, approval) {
|
|
771
|
+
const kind = cleanText(approval?.kind || "mcp");
|
|
772
|
+
const title = cleanText(approval?.title || "");
|
|
773
|
+
const body = cleanText(approval?.messageText || "");
|
|
774
|
+
if (kind === "question") {
|
|
775
|
+
return {
|
|
776
|
+
title: t(locale, "server.push.mcpQuestion.title"),
|
|
777
|
+
body: pushBodySnippet(body),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (kind === "file_share") {
|
|
781
|
+
const filePath = compactPath(normalizeTimelineFileRefs(approval?.fileRefs ?? [])[0] || t(locale, "common.fileEvent"));
|
|
782
|
+
const sizeMatch = body.match(/^\s*Size:\s*([^\n]+)$/imu);
|
|
783
|
+
return {
|
|
784
|
+
title: t(locale, "server.push.mcpShareFile.title"),
|
|
785
|
+
body: t(locale, "server.push.mcpShareFile.body", {
|
|
786
|
+
path: filePath,
|
|
787
|
+
size: cleanText(sizeMatch?.[1] || ""),
|
|
788
|
+
}),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
if (kind === "a2a_task") {
|
|
792
|
+
const target = cleanText(title.match(/^Send A2A task to\s+(.+)$/iu)?.[1] || "A2A target");
|
|
793
|
+
return {
|
|
794
|
+
title: t(locale, "server.push.mcpA2aTask.title", { target }),
|
|
795
|
+
body: t(locale, "server.push.mcpA2aTask.body", { target }),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (!title || title === "MCP approval") {
|
|
799
|
+
return {
|
|
800
|
+
title: t(locale, "server.push.mcpApproval.title"),
|
|
801
|
+
body: pushBodySnippet(body),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
return { title, body: pushBodySnippet(body) };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function localizedPaymentApprovalPushContent(locale, approval) {
|
|
808
|
+
const payment = isPlainObject(approval?.rawParams) ? approval.rawParams : {};
|
|
809
|
+
const amount = cleanText(payment.amountUsdc || payment.amountAtomic || "");
|
|
810
|
+
const network = cleanText(payment.network || "");
|
|
811
|
+
const resource = cleanText(payment.resource || payment.url || "");
|
|
812
|
+
const titleKey = cleanText(approval?.kind || "") === "hazbase_wallet_payment"
|
|
813
|
+
? "server.push.hazbasePayment.title"
|
|
814
|
+
: "server.push.paymentApproval.title";
|
|
815
|
+
return {
|
|
816
|
+
title: t(locale, titleKey, { amount }),
|
|
817
|
+
body: t(locale, "server.push.paymentApproval.body", { amount, network, resource }),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function localizedMoltbookReplyPushContent(locale, item) {
|
|
822
|
+
const title = cleanText(item?.title || "");
|
|
823
|
+
return {
|
|
824
|
+
title: !title || title === "Moltbook reply" ? t(locale, "server.push.moltbookReply.title") : title,
|
|
825
|
+
body: cleanText(item?.summary || "") || t(locale, "server.push.moltbookReply.body"),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function localizedMoltbookDraftPushTitle(locale, { draftType = "", postTitle = "", slot = "" } = {}) {
|
|
830
|
+
const normalizedPostTitle = cleanText(postTitle || "");
|
|
831
|
+
if (draftType !== "original_post") {
|
|
832
|
+
return t(locale, "server.push.moltbookDraft.replyTitle", { postTitle: normalizedPostTitle });
|
|
833
|
+
}
|
|
834
|
+
switch (cleanText(slot || "")) {
|
|
835
|
+
case "morning":
|
|
836
|
+
return t(locale, "server.push.moltbookDraft.originalMorning");
|
|
837
|
+
case "noon":
|
|
838
|
+
return t(locale, "server.push.moltbookDraft.originalNoon");
|
|
839
|
+
case "evening":
|
|
840
|
+
return t(locale, "server.push.moltbookDraft.originalEvening");
|
|
841
|
+
default:
|
|
842
|
+
return normalizedPostTitle || t(locale, "server.push.moltbookDraft.originalTitle");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function localizedMoltbookPostedBody(locale, draft, finalTitle) {
|
|
847
|
+
if (cleanText(draft?.draftType || "") === "original_post") {
|
|
848
|
+
return t(locale, "server.push.moltbookDraft.postedOriginalBody", { title: cleanText(finalTitle || draft?.postTitle || "") });
|
|
849
|
+
}
|
|
850
|
+
return t(locale, "server.push.moltbookDraft.postedReplyBody", { postTitle: cleanText(draft?.postTitle || "") });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function pushBodySnippet(value, maxChars = 160) {
|
|
854
|
+
return truncate(singleLine(value || ""), maxChars);
|
|
855
|
+
}
|
|
856
|
+
|
|
675
857
|
function notificationIconPrefix(kind) {
|
|
676
858
|
switch (kind) {
|
|
677
859
|
case "approval":
|
|
@@ -704,13 +886,19 @@ function normalizeTimelineOutcome(value) {
|
|
|
704
886
|
|
|
705
887
|
function normalizeTimelineFileEventType(value) {
|
|
706
888
|
const normalized = cleanText(value || "").toLowerCase();
|
|
707
|
-
return ["read", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
889
|
+
return ["read", "search", "git", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
708
890
|
}
|
|
709
891
|
|
|
710
892
|
function fileEventTitle(locale, fileEventType) {
|
|
711
893
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
712
894
|
case "read":
|
|
713
895
|
return t(locale, "fileEvent.read");
|
|
896
|
+
case "search":
|
|
897
|
+
return t(locale, "fileEvent.search");
|
|
898
|
+
case "git":
|
|
899
|
+
return t(locale, "fileEvent.command");
|
|
900
|
+
case "command":
|
|
901
|
+
return t(locale, "fileEvent.command");
|
|
714
902
|
case "write":
|
|
715
903
|
return t(locale, "fileEvent.write");
|
|
716
904
|
case "create":
|
|
@@ -729,6 +917,12 @@ function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
|
729
917
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
730
918
|
case "read":
|
|
731
919
|
return t(locale, "detail.fileEvent.read", vars);
|
|
920
|
+
case "search":
|
|
921
|
+
return t(locale, "detail.fileEvent.search", vars);
|
|
922
|
+
case "git":
|
|
923
|
+
return t(locale, "detail.fileEvent.command", vars);
|
|
924
|
+
case "command":
|
|
925
|
+
return t(locale, "detail.fileEvent.command", vars);
|
|
732
926
|
case "write":
|
|
733
927
|
return t(locale, "detail.fileEvent.write", vars);
|
|
734
928
|
case "create":
|
|
@@ -1398,7 +1592,7 @@ function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
|
|
|
1398
1592
|
return null;
|
|
1399
1593
|
}
|
|
1400
1594
|
|
|
1401
|
-
const command =
|
|
1595
|
+
const command = commandBaseName(tokens[0]);
|
|
1402
1596
|
if (!command || /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(command)) {
|
|
1403
1597
|
return null;
|
|
1404
1598
|
}
|
|
@@ -1436,6 +1630,86 @@ function extractCommandLineFromFunctionOutput(outputText) {
|
|
|
1436
1630
|
return unwrapShellCommand(match?.[1] || "");
|
|
1437
1631
|
}
|
|
1438
1632
|
|
|
1633
|
+
function parseToolArgumentsJson(value) {
|
|
1634
|
+
if (isPlainObject(value)) {
|
|
1635
|
+
return value;
|
|
1636
|
+
}
|
|
1637
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
const parsed = safeJsonParse(value);
|
|
1641
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function commandBaseName(command) {
|
|
1645
|
+
const normalized = cleanText(command || "");
|
|
1646
|
+
if (!normalized) {
|
|
1647
|
+
return "";
|
|
1648
|
+
}
|
|
1649
|
+
return path.basename(normalized);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function classifyTimelineCommand(commandText) {
|
|
1653
|
+
const normalizedCommand = unwrapShellCommand(commandText);
|
|
1654
|
+
const tokens = tokenizeShellWords(normalizedCommand);
|
|
1655
|
+
if (tokens.length === 0) {
|
|
1656
|
+
return {
|
|
1657
|
+
fileEventType: "command",
|
|
1658
|
+
command: "",
|
|
1659
|
+
commandText: normalizedCommand,
|
|
1660
|
+
fileRefs: [],
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const command = commandBaseName(tokens[0]);
|
|
1665
|
+
const gitSubcommand = command === "git" ? cleanText(tokens[1] || "") : "";
|
|
1666
|
+
let fileEventType = "command";
|
|
1667
|
+
if (["rg", "grep", "ag", "ack", "fd", "find"].includes(command) || gitSubcommand === "grep") {
|
|
1668
|
+
fileEventType = "search";
|
|
1669
|
+
} else if (
|
|
1670
|
+
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command)
|
|
1671
|
+
) {
|
|
1672
|
+
fileEventType = "read";
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return {
|
|
1676
|
+
fileEventType,
|
|
1677
|
+
command,
|
|
1678
|
+
commandText: normalizedCommand,
|
|
1679
|
+
fileRefs: extractReadFileRefsFromCommand(normalizedCommand),
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function redactTimelineCommandText(commandText) {
|
|
1684
|
+
return cleanText(commandText || "")
|
|
1685
|
+
.replace(/((?:--)?(?:api[-_]?key|token|secret|password|credential)(?:=|\s+))(["']?)[^\s"']+\2/giu, "$1[redacted]")
|
|
1686
|
+
.replace(/\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*=)(["']?)[^\s"']+\2/giu, "$1[redacted]");
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function timelineCommandMessage(locale, { commandText = "", toolName = "", fileRefs = [] } = {}) {
|
|
1690
|
+
const commandBlock = commandText
|
|
1691
|
+
? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${redactTimelineCommandText(commandText)}\n\`\`\``
|
|
1692
|
+
: "";
|
|
1693
|
+
const toolBlock = !commandBlock && toolName
|
|
1694
|
+
? `${t(locale, "server.message.toolLabel")}\n\`\`\`text\n${cleanText(toolName)}\n\`\`\``
|
|
1695
|
+
: "";
|
|
1696
|
+
const files = normalizeTimelineFileRefs(fileRefs);
|
|
1697
|
+
const filesBlock = files.length
|
|
1698
|
+
? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${files.join("\n")}\n\`\`\``
|
|
1699
|
+
: "";
|
|
1700
|
+
return [commandBlock || toolBlock, filesBlock].filter(Boolean).join("\n\n");
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function timelineCommandSummary(commandText, fallback = "") {
|
|
1704
|
+
const redacted = redactTimelineCommandText(commandText);
|
|
1705
|
+
return truncate(singleLine(redacted || fallback || ""), 180);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function firstMarkdownCodeFenceText(text) {
|
|
1709
|
+
const match = String(text || "").match(/```(?:\w+)?\n([\s\S]*?)\n```/u);
|
|
1710
|
+
return cleanText(match?.[1] || "");
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1439
1713
|
function extractReadFileRefsFromCommand(commandText) {
|
|
1440
1714
|
const normalizedCommand = unwrapShellCommand(commandText);
|
|
1441
1715
|
const tokens = tokenizeShellWords(normalizedCommand);
|
|
@@ -1454,15 +1728,19 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1454
1728
|
return [];
|
|
1455
1729
|
}
|
|
1456
1730
|
|
|
1457
|
-
if (command === "cat" || command === "nl") {
|
|
1458
|
-
return normalizeTimelineFileRefs(tokens
|
|
1731
|
+
if (command === "cat" || command === "nl" || command === "wc" || command === "less" || command === "more" || command === "ls") {
|
|
1732
|
+
return normalizeTimelineFileRefs(readCommandPathArgs(tokens));
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (command === "head" || command === "tail") {
|
|
1736
|
+
return normalizeTimelineFileRefs(readCommandPathArgs(tokens, { valueOptions: new Set(["-n", "--lines", "-c", "--bytes"]) }));
|
|
1459
1737
|
}
|
|
1460
1738
|
|
|
1461
1739
|
if (command === "sed") {
|
|
1462
1740
|
if (!tokens.includes("-n")) {
|
|
1463
1741
|
return [];
|
|
1464
1742
|
}
|
|
1465
|
-
return normalizeTimelineFileRefs(tokens
|
|
1743
|
+
return normalizeTimelineFileRefs(sedCommandPathArgs(tokens));
|
|
1466
1744
|
}
|
|
1467
1745
|
|
|
1468
1746
|
if (command === "rg") {
|
|
@@ -1493,6 +1771,57 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1493
1771
|
return [];
|
|
1494
1772
|
}
|
|
1495
1773
|
|
|
1774
|
+
function readCommandPathArgs(tokens, { valueOptions = new Set() } = {}) {
|
|
1775
|
+
const args = [];
|
|
1776
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1777
|
+
const token = cleanText(tokens[index] || "");
|
|
1778
|
+
if (!token) {
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (token === "--") {
|
|
1782
|
+
args.push(...tokens.slice(index + 1));
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (token.startsWith("-")) {
|
|
1786
|
+
const optionName = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
|
|
1787
|
+
if (valueOptions.has(optionName) && !token.includes("=")) {
|
|
1788
|
+
index += 1;
|
|
1789
|
+
}
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
args.push(token);
|
|
1793
|
+
}
|
|
1794
|
+
return args;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function sedCommandPathArgs(tokens) {
|
|
1798
|
+
const args = [];
|
|
1799
|
+
let scriptSeen = false;
|
|
1800
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1801
|
+
const token = cleanText(tokens[index] || "");
|
|
1802
|
+
if (!token) {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (token === "--") {
|
|
1806
|
+
args.push(...tokens.slice(index + 1));
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
if (token.startsWith("-")) {
|
|
1810
|
+
if (token === "-e" || token === "-f") {
|
|
1811
|
+
index += 1;
|
|
1812
|
+
scriptSeen = true;
|
|
1813
|
+
}
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (!scriptSeen) {
|
|
1817
|
+
scriptSeen = true;
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
args.push(token);
|
|
1821
|
+
}
|
|
1822
|
+
return args;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1496
1825
|
function autoPilotApprovalMessage(locale, commandText) {
|
|
1497
1826
|
const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
|
|
1498
1827
|
const commandBlock = cleanText(commandText || "")
|
|
@@ -1664,6 +1993,36 @@ function rememberApplyPatchInput(fileState, payload, createdAtMs = Date.now()) {
|
|
|
1664
1993
|
}
|
|
1665
1994
|
}
|
|
1666
1995
|
|
|
1996
|
+
function rememberToolEventInput(fileState, payload, createdAtMs = Date.now()) {
|
|
1997
|
+
const callId = cleanText(payload?.call_id || payload?.callId || payload?.id || "");
|
|
1998
|
+
if (!callId) {
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
if (!(fileState.toolEventInputsByCallId instanceof Map)) {
|
|
2002
|
+
fileState.toolEventInputsByCallId = new Map();
|
|
2003
|
+
}
|
|
2004
|
+
const stored = {
|
|
2005
|
+
callId,
|
|
2006
|
+
name: cleanText(payload?.name || ""),
|
|
2007
|
+
arguments: parseToolArgumentsJson(payload?.arguments),
|
|
2008
|
+
createdAtMs: Number(createdAtMs) || Date.now(),
|
|
2009
|
+
};
|
|
2010
|
+
fileState.toolEventInputsByCallId.set(callId, stored);
|
|
2011
|
+
while (fileState.toolEventInputsByCallId.size > 128) {
|
|
2012
|
+
const oldestKey = fileState.toolEventInputsByCallId.keys().next().value;
|
|
2013
|
+
fileState.toolEventInputsByCallId.delete(oldestKey);
|
|
2014
|
+
}
|
|
2015
|
+
return stored;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function findStoredToolEventInput(fileState, callId) {
|
|
2019
|
+
const normalizedCallId = cleanText(callId || "");
|
|
2020
|
+
if (!normalizedCallId || !(fileState?.toolEventInputsByCallId instanceof Map)) {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
return fileState.toolEventInputsByCallId.get(normalizedCallId) || null;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1667
2026
|
async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath }) {
|
|
1668
2027
|
const normalizedCallId = cleanText(callId || "");
|
|
1669
2028
|
if (!normalizedCallId) {
|
|
@@ -2068,6 +2427,7 @@ async function ensureRolloutFileState(runtime, threadId, rolloutFilePath) {
|
|
|
2068
2427
|
threadId: cleanText(threadId || ""),
|
|
2069
2428
|
cwd: await findRolloutThreadCwd(runtime, threadId || ""),
|
|
2070
2429
|
applyPatchInputsByCallId: new Map(),
|
|
2430
|
+
toolEventInputsByCallId: new Map(),
|
|
2071
2431
|
startupCutoffMs: 0,
|
|
2072
2432
|
skipPartialLine: false,
|
|
2073
2433
|
};
|
|
@@ -3132,6 +3492,8 @@ function migrateRecentCodeEventsState({ config, runtime, state }) {
|
|
|
3132
3492
|
|
|
3133
3493
|
function timelineKindSortPriority(kind) {
|
|
3134
3494
|
switch (cleanText(kind || "")) {
|
|
3495
|
+
case "activity_status":
|
|
3496
|
+
return 90;
|
|
3135
3497
|
case "completion":
|
|
3136
3498
|
return 70;
|
|
3137
3499
|
case "approval":
|
|
@@ -3141,6 +3503,8 @@ function timelineKindSortPriority(kind) {
|
|
|
3141
3503
|
return 60;
|
|
3142
3504
|
case "ambient_suggestions":
|
|
3143
3505
|
return 52;
|
|
3506
|
+
case "command_event":
|
|
3507
|
+
return 46;
|
|
3144
3508
|
case "file_event":
|
|
3145
3509
|
return 45;
|
|
3146
3510
|
case "assistant_final":
|
|
@@ -3163,7 +3527,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
3163
3527
|
}
|
|
3164
3528
|
|
|
3165
3529
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
3166
|
-
const
|
|
3530
|
+
const rawKind = cleanText(raw.kind ?? "");
|
|
3531
|
+
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3532
|
+
const kind = rawKind === "file_event" && fileEventType === "command" ? "command_event" : rawKind;
|
|
3167
3533
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
3168
3534
|
if (!stableId || !timelineKinds.has(kind)) {
|
|
3169
3535
|
return null;
|
|
@@ -3172,7 +3538,6 @@ function normalizeTimelineEntry(raw) {
|
|
|
3172
3538
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
3173
3539
|
const rawMessageText = raw.messageText ?? "";
|
|
3174
3540
|
const messageText = normalizeTimelineMessageText(rawMessageText);
|
|
3175
|
-
const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
|
|
3176
3541
|
const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
|
|
3177
3542
|
const diffSource = normalizeTimelineDiffSource(raw.diffSource ?? "");
|
|
3178
3543
|
const diffCounts = diffLineCounts(diffText);
|
|
@@ -3242,6 +3607,8 @@ function normalizeTimelineEntry(raw) {
|
|
|
3242
3607
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
3243
3608
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
3244
3609
|
provider: normalizeProvider(raw.provider),
|
|
3610
|
+
...(raw.activityPhase != null ? { activityPhase: cleanText(raw.activityPhase) } : {}),
|
|
3611
|
+
...(raw.commandText != null ? { commandText: cleanText(raw.commandText) } : {}),
|
|
3245
3612
|
// Moltbook-specific fields — preserved for detail view fallback after
|
|
3246
3613
|
// the in-memory draft/item expires.
|
|
3247
3614
|
...(raw.draftText != null ? { draftText: cleanText(raw.draftText) } : {}),
|
|
@@ -3264,107 +3631,408 @@ function normalizeTimelineEntry(raw) {
|
|
|
3264
3631
|
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
3265
3632
|
}
|
|
3266
3633
|
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
return false;
|
|
3634
|
+
class TimelineBus {
|
|
3635
|
+
constructor() {
|
|
3636
|
+
this.clients = new Set();
|
|
3271
3637
|
}
|
|
3272
3638
|
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
// line-count fields (`diffAddedLines` / `diffRemovedLines`) are a sufficient
|
|
3279
|
-
// proxy for "diff content changed" and avoid materialising a megabyte-class
|
|
3280
|
-
// intermediate string on every record call.
|
|
3281
|
-
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3282
|
-
"stableId",
|
|
3283
|
-
"title",
|
|
3284
|
-
"createdAtMs",
|
|
3285
|
-
"diffAvailable",
|
|
3286
|
-
"diffSource",
|
|
3287
|
-
"diffAddedLines",
|
|
3288
|
-
"diffRemovedLines",
|
|
3289
|
-
"previousFileRefs",
|
|
3290
|
-
"cwd",
|
|
3291
|
-
]);
|
|
3292
|
-
runtime.recentTimelineEntries = nextItems;
|
|
3293
|
-
state.recentTimelineEntries = nextItems;
|
|
3294
|
-
return changed;
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
function recordCodeEvent({ config, runtime, state, entry }) {
|
|
3298
|
-
if (!isCodeEventEntry(entry)) {
|
|
3299
|
-
return false;
|
|
3639
|
+
addClient(client) {
|
|
3640
|
+
this.clients.add(client);
|
|
3641
|
+
return () => {
|
|
3642
|
+
this.clients.delete(client);
|
|
3643
|
+
};
|
|
3300
3644
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3645
|
+
|
|
3646
|
+
emit(eventName, payload) {
|
|
3647
|
+
for (const client of [...this.clients]) {
|
|
3648
|
+
try {
|
|
3649
|
+
client.send(eventName, payload);
|
|
3650
|
+
} catch {
|
|
3651
|
+
try {
|
|
3652
|
+
client.close?.();
|
|
3653
|
+
} catch {}
|
|
3654
|
+
this.clients.delete(client);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3304
3657
|
}
|
|
3305
3658
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
"stableId",
|
|
3314
|
-
"title",
|
|
3315
|
-
"createdAtMs",
|
|
3316
|
-
"diffAvailable",
|
|
3317
|
-
"diffSource",
|
|
3318
|
-
"diffAddedLines",
|
|
3319
|
-
"diffRemovedLines",
|
|
3320
|
-
"previousFileRefs",
|
|
3321
|
-
"cwd",
|
|
3322
|
-
]);
|
|
3323
|
-
runtime.recentCodeEvents = nextItems;
|
|
3324
|
-
state.recentCodeEvents = nextItems;
|
|
3325
|
-
if (changed) {
|
|
3326
|
-
invalidateDiffThreadGroupsCache();
|
|
3659
|
+
closeAll() {
|
|
3660
|
+
for (const client of [...this.clients]) {
|
|
3661
|
+
try {
|
|
3662
|
+
client.close?.();
|
|
3663
|
+
} catch {}
|
|
3664
|
+
}
|
|
3665
|
+
this.clients.clear();
|
|
3327
3666
|
}
|
|
3328
|
-
return changed;
|
|
3329
3667
|
}
|
|
3330
3668
|
|
|
3331
|
-
function
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
return false;
|
|
3669
|
+
function ensureTimelineBus(runtime) {
|
|
3670
|
+
if (!runtime.timelineBus) {
|
|
3671
|
+
runtime.timelineBus = new TimelineBus();
|
|
3335
3672
|
}
|
|
3673
|
+
return runtime.timelineBus;
|
|
3674
|
+
}
|
|
3336
3675
|
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
"diffRemovedLines",
|
|
3356
|
-
"previousFileRefs",
|
|
3357
|
-
"cwd",
|
|
3358
|
-
]);
|
|
3359
|
-
runtime.recentCodeEvents = nextItems;
|
|
3360
|
-
state.recentCodeEvents = nextItems;
|
|
3361
|
-
if (changed) {
|
|
3362
|
-
invalidateDiffThreadGroupsCache();
|
|
3676
|
+
function buildTimelineUpdatePayload({ runtime, entry, source = "unknown" }) {
|
|
3677
|
+
runtime.timelineRevision = (Number(runtime.timelineRevision) || 0) + 1;
|
|
3678
|
+
return {
|
|
3679
|
+
revision: runtime.timelineRevision,
|
|
3680
|
+
token: cleanText(entry?.token || historyToken(entry?.stableId || entry?.createdAtMs || "")).slice(0, 120),
|
|
3681
|
+
stableId: cleanText(entry?.stableId || "").slice(0, 180),
|
|
3682
|
+
kind: cleanText(entry?.kind || "").slice(0, 80),
|
|
3683
|
+
provider: cleanText(entry?.provider || "").slice(0, 40),
|
|
3684
|
+
threadId: cleanText(entry?.threadId || "").slice(0, 120),
|
|
3685
|
+
createdAtMs: Number(entry?.createdAtMs) || 0,
|
|
3686
|
+
source: cleanText(source || "unknown").slice(0, 80),
|
|
3687
|
+
ingestAtMs: Date.now(),
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
function publishTimelineUpdate({ config, runtime, entry, source = "unknown" }) {
|
|
3692
|
+
if (!config?.timelineLiveSync || !runtime || !entry) {
|
|
3693
|
+
return;
|
|
3363
3694
|
}
|
|
3364
|
-
|
|
3695
|
+
const payload = buildTimelineUpdatePayload({ runtime, entry, source });
|
|
3696
|
+
console.log(
|
|
3697
|
+
`[timeline-ingest] at=${new Date(payload.ingestAtMs).toISOString()} ` +
|
|
3698
|
+
`provider=${payload.provider || "unknown"} kind=${payload.kind || "unknown"} ` +
|
|
3699
|
+
`token=${payload.token || "none"} createdAtMs=${payload.createdAtMs || 0} ` +
|
|
3700
|
+
`source=${payload.source || "unknown"} revision=${payload.revision}`
|
|
3701
|
+
);
|
|
3702
|
+
ensureTimelineBus(runtime).emit("timeline:update", payload);
|
|
3365
3703
|
}
|
|
3366
3704
|
|
|
3367
|
-
function
|
|
3705
|
+
function timelineActivityKey(provider, threadId) {
|
|
3706
|
+
const normalizedProvider = normalizeProvider(provider) || "codex";
|
|
3707
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
3708
|
+
if (!normalizedThreadId) {
|
|
3709
|
+
return "";
|
|
3710
|
+
}
|
|
3711
|
+
return `${normalizedProvider}:${normalizedThreadId}`;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
function timelineActivityTitle(locale, phase) {
|
|
3715
|
+
switch (cleanText(phase || "")) {
|
|
3716
|
+
case "thinking":
|
|
3717
|
+
return t(locale, "server.activity.thinking");
|
|
3718
|
+
case "reading":
|
|
3719
|
+
return t(locale, "server.activity.reading");
|
|
3720
|
+
case "searching":
|
|
3721
|
+
return t(locale, "server.activity.searching");
|
|
3722
|
+
case "editing":
|
|
3723
|
+
return t(locale, "server.activity.editing");
|
|
3724
|
+
case "running_command":
|
|
3725
|
+
return t(locale, "server.activity.runningCommand");
|
|
3726
|
+
case "awaiting_approval":
|
|
3727
|
+
return t(locale, "server.activity.awaitingApproval");
|
|
3728
|
+
default:
|
|
3729
|
+
return t(locale, "server.activity.working");
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
function timelineActivitySummary(activity) {
|
|
3734
|
+
const phase = cleanText(activity?.phase || "");
|
|
3735
|
+
const refs = normalizeTimelineFileRefs(activity?.fileRefs || []);
|
|
3736
|
+
if (refs.length === 1 && ["reading", "searching", "editing"].includes(phase)) {
|
|
3737
|
+
return compactPath(refs[0]);
|
|
3738
|
+
}
|
|
3739
|
+
if (refs.length > 1 && ["reading", "searching", "editing"].includes(phase)) {
|
|
3740
|
+
return `${refs.length} files`;
|
|
3741
|
+
}
|
|
3742
|
+
const commandText = cleanText(activity?.commandText || "");
|
|
3743
|
+
if (commandText && phase !== "running_command") {
|
|
3744
|
+
return timelineCommandSummary(commandText);
|
|
3745
|
+
}
|
|
3746
|
+
return cleanText(activity?.summary || "");
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
function buildTimelineActivityEntry(activity, locale = DEFAULT_LOCALE) {
|
|
3750
|
+
if (!isPlainObject(activity)) {
|
|
3751
|
+
return null;
|
|
3752
|
+
}
|
|
3753
|
+
const provider = normalizeProvider(activity.provider);
|
|
3754
|
+
const threadId = cleanText(activity.threadId || "");
|
|
3755
|
+
const key = cleanText(activity.key || timelineActivityKey(provider, threadId));
|
|
3756
|
+
if (!key || !threadId) {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
const phase = cleanText(activity.phase || "working");
|
|
3760
|
+
const updatedAtMs = Number(activity.updatedAtMs) || Date.now();
|
|
3761
|
+
return normalizeTimelineEntry({
|
|
3762
|
+
stableId: `activity_status:${key}`,
|
|
3763
|
+
token: historyToken(`activity_status:${key}`),
|
|
3764
|
+
kind: "activity_status",
|
|
3765
|
+
threadId,
|
|
3766
|
+
threadLabel: cleanText(activity.threadLabel || ""),
|
|
3767
|
+
title: timelineActivityTitle(locale, phase),
|
|
3768
|
+
summary: timelineActivitySummary(activity),
|
|
3769
|
+
messageText: cleanText(activity.commandText || activity.summary || ""),
|
|
3770
|
+
fileRefs: normalizeTimelineFileRefs(activity.fileRefs || []),
|
|
3771
|
+
commandText: cleanText(activity.commandText || ""),
|
|
3772
|
+
activityPhase: phase,
|
|
3773
|
+
createdAtMs: updatedAtMs,
|
|
3774
|
+
cwd: cleanText(activity.cwd || ""),
|
|
3775
|
+
readOnly: true,
|
|
3776
|
+
provider,
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function publishTimelineActivityUpdate({ config, runtime, activity, source = "activity-status" }) {
|
|
3781
|
+
const entry = buildTimelineActivityEntry(activity, DEFAULT_LOCALE);
|
|
3782
|
+
if (entry) {
|
|
3783
|
+
publishTimelineUpdate({ config, runtime, entry, source });
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
function upsertTimelineActivity({
|
|
3788
|
+
config,
|
|
3789
|
+
runtime,
|
|
3790
|
+
provider,
|
|
3791
|
+
threadId,
|
|
3792
|
+
threadLabel = "",
|
|
3793
|
+
phase = "working",
|
|
3794
|
+
summary = "",
|
|
3795
|
+
commandText = "",
|
|
3796
|
+
fileRefs = [],
|
|
3797
|
+
cwd = "",
|
|
3798
|
+
source = "activity-status",
|
|
3799
|
+
atMs = Date.now(),
|
|
3800
|
+
}) {
|
|
3801
|
+
if (!config?.webUiEnabled || !runtime) {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
if (!(runtime.activeTimelineActivitiesByKey instanceof Map)) {
|
|
3805
|
+
runtime.activeTimelineActivitiesByKey = new Map();
|
|
3806
|
+
}
|
|
3807
|
+
const normalizedProvider = normalizeProvider(provider) || "codex";
|
|
3808
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
3809
|
+
const key = timelineActivityKey(normalizedProvider, normalizedThreadId);
|
|
3810
|
+
if (!key) {
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
const next = {
|
|
3814
|
+
key,
|
|
3815
|
+
provider: normalizedProvider,
|
|
3816
|
+
threadId: normalizedThreadId,
|
|
3817
|
+
threadLabel: cleanText(threadLabel || ""),
|
|
3818
|
+
phase: cleanText(phase || "working"),
|
|
3819
|
+
summary: cleanText(summary || ""),
|
|
3820
|
+
commandText: cleanText(commandText || ""),
|
|
3821
|
+
fileRefs: normalizeTimelineFileRefs(fileRefs),
|
|
3822
|
+
cwd: cleanText(cwd || ""),
|
|
3823
|
+
updatedAtMs: Number(atMs) || Date.now(),
|
|
3824
|
+
};
|
|
3825
|
+
const previous = runtime.activeTimelineActivitiesByKey.get(key);
|
|
3826
|
+
const changed =
|
|
3827
|
+
!previous ||
|
|
3828
|
+
previous.phase !== next.phase ||
|
|
3829
|
+
previous.summary !== next.summary ||
|
|
3830
|
+
previous.commandText !== next.commandText ||
|
|
3831
|
+
previous.threadLabel !== next.threadLabel ||
|
|
3832
|
+
previous.cwd !== next.cwd ||
|
|
3833
|
+
JSON.stringify(previous.fileRefs || []) !== JSON.stringify(next.fileRefs || []);
|
|
3834
|
+
runtime.activeTimelineActivitiesByKey.set(key, next);
|
|
3835
|
+
if (changed) {
|
|
3836
|
+
publishTimelineActivityUpdate({ config, runtime, activity: next, source });
|
|
3837
|
+
}
|
|
3838
|
+
return changed;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function clearTimelineActivity({ config, runtime, provider, threadId, source = "activity-clear", atMs = Date.now() }) {
|
|
3842
|
+
if (!runtime?.activeTimelineActivitiesByKey) {
|
|
3843
|
+
return false;
|
|
3844
|
+
}
|
|
3845
|
+
const key = timelineActivityKey(provider, threadId);
|
|
3846
|
+
if (!key || !runtime.activeTimelineActivitiesByKey.has(key)) {
|
|
3847
|
+
return false;
|
|
3848
|
+
}
|
|
3849
|
+
const previous = runtime.activeTimelineActivitiesByKey.get(key);
|
|
3850
|
+
runtime.activeTimelineActivitiesByKey.delete(key);
|
|
3851
|
+
publishTimelineActivityUpdate({
|
|
3852
|
+
config,
|
|
3853
|
+
runtime,
|
|
3854
|
+
activity: {
|
|
3855
|
+
...(isPlainObject(previous) ? previous : {}),
|
|
3856
|
+
key,
|
|
3857
|
+
provider: normalizeProvider(provider),
|
|
3858
|
+
threadId: cleanText(threadId || ""),
|
|
3859
|
+
phase: "working",
|
|
3860
|
+
updatedAtMs: Number(atMs) || Date.now(),
|
|
3861
|
+
},
|
|
3862
|
+
source,
|
|
3863
|
+
});
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
function timelineActivityFromCommand({ commandText, cwd = "" }) {
|
|
3868
|
+
const classified = classifyTimelineCommand(commandText);
|
|
3869
|
+
const fileEventType = cleanText(classified.fileEventType || "");
|
|
3870
|
+
const phase = fileEventType === "read"
|
|
3871
|
+
? "reading"
|
|
3872
|
+
: fileEventType === "search"
|
|
3873
|
+
? "searching"
|
|
3874
|
+
: "running_command";
|
|
3875
|
+
return {
|
|
3876
|
+
phase,
|
|
3877
|
+
commandText: classified.commandText || commandText,
|
|
3878
|
+
fileRefs: classified.fileRefs || [],
|
|
3879
|
+
cwd,
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function timelineActivityFromClaudeTool(toolName, input = {}) {
|
|
3884
|
+
const lowerToolName = cleanText(toolName || "").toLowerCase();
|
|
3885
|
+
if (lowerToolName === "bash") {
|
|
3886
|
+
return timelineActivityFromCommand({ commandText: cleanText(input.command || ""), cwd: cleanText(input.cwd || "") });
|
|
3887
|
+
}
|
|
3888
|
+
if (["read", "ls"].includes(lowerToolName)) {
|
|
3889
|
+
return {
|
|
3890
|
+
phase: "reading",
|
|
3891
|
+
summary: cleanText(toolName || ""),
|
|
3892
|
+
fileRefs: normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean)),
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
if (["grep", "glob", "websearch", "webfetch"].includes(lowerToolName)) {
|
|
3896
|
+
return {
|
|
3897
|
+
phase: "searching",
|
|
3898
|
+
summary: cleanText(input.pattern || input.query || input.url || toolName || ""),
|
|
3899
|
+
fileRefs: normalizeTimelineFileRefs([input.path || input.file_path || input.filePath].filter(Boolean)),
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
if (["write", "edit", "multiedit", "todowrite"].includes(lowerToolName)) {
|
|
3903
|
+
return {
|
|
3904
|
+
phase: "editing",
|
|
3905
|
+
summary: cleanText(toolName || ""),
|
|
3906
|
+
fileRefs: normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean)),
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
if (["askuserquestion", "exitplanmode"].includes(lowerToolName)) {
|
|
3910
|
+
return { phase: "awaiting_approval", summary: cleanText(toolName || "") };
|
|
3911
|
+
}
|
|
3912
|
+
return { phase: "working", summary: cleanText(toolName || "") };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
function timelineActivityFromClaudeContent(content) {
|
|
3916
|
+
if (!Array.isArray(content)) {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
for (const block of content) {
|
|
3920
|
+
if (!isPlainObject(block) || block.type !== "tool_use") {
|
|
3921
|
+
continue;
|
|
3922
|
+
}
|
|
3923
|
+
return timelineActivityFromClaudeTool(block.name || "", isPlainObject(block.input) ? block.input : {});
|
|
3924
|
+
}
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
function recordTimelineEntry({ config, runtime, state, entry, source = "unknown" }) {
|
|
3929
|
+
const normalized = normalizeTimelineEntry(entry);
|
|
3930
|
+
if (!normalized) {
|
|
3931
|
+
return false;
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
const nextItems = normalizeTimelineEntries(
|
|
3935
|
+
[normalized, ...runtime.recentTimelineEntries.filter((item) => item.stableId !== normalized.stableId)],
|
|
3936
|
+
config.maxTimelineEntries
|
|
3937
|
+
);
|
|
3938
|
+
// Note: diffText is intentionally omitted from the projection — the
|
|
3939
|
+
// line-count fields (`diffAddedLines` / `diffRemovedLines`) are a sufficient
|
|
3940
|
+
// proxy for "diff content changed" and avoid materialising a megabyte-class
|
|
3941
|
+
// intermediate string on every record call.
|
|
3942
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3943
|
+
"stableId",
|
|
3944
|
+
"title",
|
|
3945
|
+
"summary",
|
|
3946
|
+
"messageText",
|
|
3947
|
+
"fileEventType",
|
|
3948
|
+
"fileRefs",
|
|
3949
|
+
"createdAtMs",
|
|
3950
|
+
"diffAvailable",
|
|
3951
|
+
"diffSource",
|
|
3952
|
+
"diffAddedLines",
|
|
3953
|
+
"diffRemovedLines",
|
|
3954
|
+
"previousFileRefs",
|
|
3955
|
+
"cwd",
|
|
3956
|
+
]);
|
|
3957
|
+
runtime.recentTimelineEntries = nextItems;
|
|
3958
|
+
state.recentTimelineEntries = nextItems;
|
|
3959
|
+
if (changed) {
|
|
3960
|
+
publishTimelineUpdate({ config, runtime, entry: normalized, source });
|
|
3961
|
+
}
|
|
3962
|
+
return changed;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
function recordCodeEvent({ config, runtime, state, entry }) {
|
|
3966
|
+
if (!isCodeEventEntry(entry)) {
|
|
3967
|
+
return false;
|
|
3968
|
+
}
|
|
3969
|
+
const normalized = normalizeTimelineEntry(entry);
|
|
3970
|
+
if (!normalized) {
|
|
3971
|
+
return false;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
const nextItems = normalizeCodeEvents(
|
|
3975
|
+
[normalized, ...runtime.recentCodeEvents.filter((item) => item.stableId !== normalized.stableId)],
|
|
3976
|
+
config.maxCodeEvents
|
|
3977
|
+
);
|
|
3978
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText` from
|
|
3979
|
+
// the projection — same megabyte-stringify hot path; same line-count proxy.
|
|
3980
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
3981
|
+
"stableId",
|
|
3982
|
+
"title",
|
|
3983
|
+
"createdAtMs",
|
|
3984
|
+
"diffAvailable",
|
|
3985
|
+
"diffSource",
|
|
3986
|
+
"diffAddedLines",
|
|
3987
|
+
"diffRemovedLines",
|
|
3988
|
+
"previousFileRefs",
|
|
3989
|
+
"cwd",
|
|
3990
|
+
]);
|
|
3991
|
+
runtime.recentCodeEvents = nextItems;
|
|
3992
|
+
state.recentCodeEvents = nextItems;
|
|
3993
|
+
if (changed) {
|
|
3994
|
+
invalidateDiffThreadGroupsCache();
|
|
3995
|
+
}
|
|
3996
|
+
return changed;
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
|
|
4000
|
+
const timelineCodeEvents = normalizeCodeEvents(runtime.recentTimelineEntries, config.maxCodeEvents);
|
|
4001
|
+
if (timelineCodeEvents.length === 0 && runtime.recentCodeEvents.length === 0) {
|
|
4002
|
+
return false;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
const nextItems = normalizeCodeEvents(
|
|
4006
|
+
[
|
|
4007
|
+
...timelineCodeEvents,
|
|
4008
|
+
...runtime.recentCodeEvents.filter(
|
|
4009
|
+
(entry) => !timelineCodeEvents.some((timelineEntry) => timelineEntry.stableId === entry.stableId)
|
|
4010
|
+
),
|
|
4011
|
+
],
|
|
4012
|
+
config.maxCodeEvents
|
|
4013
|
+
);
|
|
4014
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText`.
|
|
4015
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
4016
|
+
"stableId",
|
|
4017
|
+
"title",
|
|
4018
|
+
"createdAtMs",
|
|
4019
|
+
"threadLabel",
|
|
4020
|
+
"diffAvailable",
|
|
4021
|
+
"diffSource",
|
|
4022
|
+
"diffAddedLines",
|
|
4023
|
+
"diffRemovedLines",
|
|
4024
|
+
"previousFileRefs",
|
|
4025
|
+
"cwd",
|
|
4026
|
+
]);
|
|
4027
|
+
runtime.recentCodeEvents = nextItems;
|
|
4028
|
+
state.recentCodeEvents = nextItems;
|
|
4029
|
+
if (changed) {
|
|
4030
|
+
invalidateDiffThreadGroupsCache();
|
|
4031
|
+
}
|
|
4032
|
+
return changed;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function timelineEntryByToken(runtime, token, kind = "") {
|
|
3368
4036
|
const normalizedToken = cleanText(token ?? "");
|
|
3369
4037
|
const normalizedKind = cleanText(kind ?? "");
|
|
3370
4038
|
return runtime.recentTimelineEntries.find(
|
|
@@ -3411,6 +4079,18 @@ function historyItemFromEvent(event) {
|
|
|
3411
4079
|
});
|
|
3412
4080
|
}
|
|
3413
4081
|
|
|
4082
|
+
function completionPushContentDedupeId(event) {
|
|
4083
|
+
if (!event || event.kind !== "task_complete") {
|
|
4084
|
+
return "";
|
|
4085
|
+
}
|
|
4086
|
+
const threadId = cleanText(event.threadId ?? event.conversationId ?? "unknown") || "unknown";
|
|
4087
|
+
const messageText = normalizeLongText(event.detailText || event.message || "");
|
|
4088
|
+
if (!messageText) {
|
|
4089
|
+
return "";
|
|
4090
|
+
}
|
|
4091
|
+
return `task_complete_content:${threadId}:${historyToken(messageText)}`;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
3414
4094
|
function recordHistoryItem({ config, runtime, state, item }) {
|
|
3415
4095
|
const normalized = normalizeHistoryItem(item);
|
|
3416
4096
|
if (!normalized) {
|
|
@@ -3573,6 +4253,7 @@ function recordActionHistoryItem({
|
|
|
3573
4253
|
runtime,
|
|
3574
4254
|
state,
|
|
3575
4255
|
entry: item,
|
|
4256
|
+
source: "event",
|
|
3576
4257
|
});
|
|
3577
4258
|
return historyChanged || timelineChanged;
|
|
3578
4259
|
}
|
|
@@ -3732,7 +4413,20 @@ function pushDeliveryKey(deviceId, stableId) {
|
|
|
3732
4413
|
return `${cleanText(deviceId || "")}:${cleanText(stableId || "")}`;
|
|
3733
4414
|
}
|
|
3734
4415
|
|
|
3735
|
-
async function deliverWebPushItem({
|
|
4416
|
+
async function deliverWebPushItem({
|
|
4417
|
+
config,
|
|
4418
|
+
state,
|
|
4419
|
+
kind,
|
|
4420
|
+
token,
|
|
4421
|
+
stableId,
|
|
4422
|
+
title,
|
|
4423
|
+
body,
|
|
4424
|
+
tab = "",
|
|
4425
|
+
subtab = "",
|
|
4426
|
+
buildLocalizedContent = null,
|
|
4427
|
+
dedupeId = "",
|
|
4428
|
+
dedupeWindowMs = 0,
|
|
4429
|
+
}) {
|
|
3736
4430
|
if (!config.webPushEnabled || config.dryRun) {
|
|
3737
4431
|
return false;
|
|
3738
4432
|
}
|
|
@@ -3749,8 +4443,18 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3749
4443
|
let changed = false;
|
|
3750
4444
|
|
|
3751
4445
|
for (const subscription of subscriptions) {
|
|
3752
|
-
const
|
|
3753
|
-
|
|
4446
|
+
const now = Date.now();
|
|
4447
|
+
const stableDeliveryKey = pushDeliveryKey(subscription.deviceId, stableId);
|
|
4448
|
+
const dedupeDeliveryKey = pushDeliveryKey(subscription.deviceId, dedupeId || stableId);
|
|
4449
|
+
const stableDeliveredAt = Number(state.pushDeliveries[stableDeliveryKey]) || 0;
|
|
4450
|
+
const dedupeDeliveredAt = Number(state.pushDeliveries[dedupeDeliveryKey]) || 0;
|
|
4451
|
+
if (stableDeliveredAt) {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (
|
|
4455
|
+
dedupeDeliveredAt &&
|
|
4456
|
+
(!dedupeWindowMs || now - dedupeDeliveredAt <= Math.max(0, Number(dedupeWindowMs) || 0))
|
|
4457
|
+
) {
|
|
3754
4458
|
continue;
|
|
3755
4459
|
}
|
|
3756
4460
|
|
|
@@ -3778,8 +4482,10 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3778
4482
|
},
|
|
3779
4483
|
payload
|
|
3780
4484
|
);
|
|
3781
|
-
|
|
3782
|
-
|
|
4485
|
+
state.pushDeliveries[stableDeliveryKey] = now;
|
|
4486
|
+
if (dedupeDeliveryKey !== stableDeliveryKey) {
|
|
4487
|
+
state.pushDeliveries[dedupeDeliveryKey] = now;
|
|
4488
|
+
}
|
|
3783
4489
|
trimSeenEvents(state.pushDeliveries, config.maxSeenEvents * 4);
|
|
3784
4490
|
const stored = normalizePushSubscriptionRecord(state.pushSubscriptions?.[subscription.id]);
|
|
3785
4491
|
if (stored) {
|
|
@@ -3931,6 +4637,207 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3931
4637
|
return dirty;
|
|
3932
4638
|
}
|
|
3933
4639
|
|
|
4640
|
+
function isPathWithin(parentDir, candidatePath) {
|
|
4641
|
+
const parent = path.resolve(parentDir || "");
|
|
4642
|
+
const candidate = path.resolve(candidatePath || "");
|
|
4643
|
+
if (!parent || !candidate) return false;
|
|
4644
|
+
return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function isClaudeTranscriptPath(config, filePath) {
|
|
4648
|
+
return Boolean(
|
|
4649
|
+
filePath &&
|
|
4650
|
+
filePath.endsWith(".jsonl") &&
|
|
4651
|
+
isPathWithin(config.claudeProjectsDir, filePath)
|
|
4652
|
+
);
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
function isRolloutFilePath(filePath) {
|
|
4656
|
+
const base = path.basename(filePath || "");
|
|
4657
|
+
return base.startsWith("rollout-") && base.endsWith(".jsonl");
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
function watchedEventPath(root, filename) {
|
|
4661
|
+
if (!filename) return root;
|
|
4662
|
+
const text = Buffer.isBuffer(filename) ? filename.toString("utf8") : String(filename);
|
|
4663
|
+
if (!text) return root;
|
|
4664
|
+
return path.isAbsolute(text) ? text : path.join(root, text);
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
function addTimelineLiveWatcher({ runtime, label, targetPath, options = {}, onChange }) {
|
|
4668
|
+
if (!targetPath) return;
|
|
4669
|
+
try {
|
|
4670
|
+
const watcher = watchFs(targetPath, options, (_eventType, filename) => {
|
|
4671
|
+
onChange(watchedEventPath(targetPath, filename));
|
|
4672
|
+
});
|
|
4673
|
+
watcher.on?.("error", (err) => {
|
|
4674
|
+
console.warn(`[timeline-live-watch-error] ${label} ${err?.message || err}`);
|
|
4675
|
+
});
|
|
4676
|
+
runtime.timelineLiveWatchers.push(watcher);
|
|
4677
|
+
console.log(`[timeline-live-watch] ${label} ${targetPath}`);
|
|
4678
|
+
} catch (err) {
|
|
4679
|
+
console.warn(`[timeline-live-watch-skip] ${label} ${targetPath} ${err?.message || err}`);
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
function scheduleTimelineLiveScan({ config, runtime, state, reason = "unknown", filePath = "" }) {
|
|
4684
|
+
if (!config.timelineLiveSync || runtime.stopping) {
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
runtime.timelineLiveScanReasons.add(cleanText(reason || "unknown").slice(0, 80));
|
|
4688
|
+
const normalizedPath = cleanText(filePath || "");
|
|
4689
|
+
if (isClaudeTranscriptPath(config, normalizedPath)) {
|
|
4690
|
+
runtime.timelineLiveClaudeFiles.add(normalizedPath);
|
|
4691
|
+
} else if (isRolloutFilePath(normalizedPath)) {
|
|
4692
|
+
runtime.timelineLiveRolloutFiles.add(normalizedPath);
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
if (runtime.timelineLiveScanTimer) {
|
|
4696
|
+
return;
|
|
4697
|
+
}
|
|
4698
|
+
runtime.timelineLiveScanTimer = setTimeout(() => {
|
|
4699
|
+
runtime.timelineLiveScanTimer = null;
|
|
4700
|
+
runPendingTimelineLiveScan({ config, runtime, state }).catch((err) => {
|
|
4701
|
+
console.warn(`[timeline-live-scan-error] ${err?.message || err}`);
|
|
4702
|
+
});
|
|
4703
|
+
}, Math.max(25, Number(config.timelineLiveDebounceMs) || 150));
|
|
4704
|
+
runtime.timelineLiveScanTimer.unref?.();
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
async function runPendingTimelineLiveScan({ config, runtime, state }) {
|
|
4708
|
+
if (runtime.timelineLiveScanInFlight) {
|
|
4709
|
+
runtime.timelineLiveScanReschedule = true;
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
runtime.timelineLiveScanInFlight = true;
|
|
4713
|
+
const reasons = new Set(runtime.timelineLiveScanReasons);
|
|
4714
|
+
const claudeFiles = [...runtime.timelineLiveClaudeFiles];
|
|
4715
|
+
const rolloutFiles = [...runtime.timelineLiveRolloutFiles];
|
|
4716
|
+
runtime.timelineLiveScanReasons.clear();
|
|
4717
|
+
runtime.timelineLiveClaudeFiles.clear();
|
|
4718
|
+
runtime.timelineLiveRolloutFiles.clear();
|
|
4719
|
+
|
|
4720
|
+
let dirty = false;
|
|
4721
|
+
const now = Date.now();
|
|
4722
|
+
try {
|
|
4723
|
+
if (reasons.has("codex-home") || reasons.has("codex-history") || reasons.has("codex-logs")) {
|
|
4724
|
+
if (!config.codexLogsDbFile) {
|
|
4725
|
+
const latestLogsDbFile = await findLatestCodexLogsDbFile(config.codexHome);
|
|
4726
|
+
if (latestLogsDbFile && latestLogsDbFile !== runtime.logsDbFile) {
|
|
4727
|
+
runtime.logsDbFile = latestLogsDbFile;
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
dirty = (await processHistoryTimelineFile({ config, runtime, state, now })) || dirty;
|
|
4731
|
+
if (config.notifyCompletions || config.webUiEnabled) {
|
|
4732
|
+
dirty = (await processSqliteCompletionLog({ config, runtime, state, now })) || dirty;
|
|
4733
|
+
}
|
|
4734
|
+
if (config.webUiEnabled) {
|
|
4735
|
+
dirty = (await processSqliteTimelineLog({ config, runtime, state, now })) || dirty;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
for (const filePath of rolloutFiles.slice(0, 12)) {
|
|
4740
|
+
if (!runtime.knownFiles.includes(filePath)) {
|
|
4741
|
+
runtime.knownFiles.push(filePath);
|
|
4742
|
+
runtime.knownFiles.sort();
|
|
4743
|
+
}
|
|
4744
|
+
dirty = (await processRolloutFile({ filePath, config, runtime, state, now })) || dirty;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
let claudeTranscriptChanged = false;
|
|
4748
|
+
let claudeTargets = claudeFiles;
|
|
4749
|
+
if (reasons.has("claude-dir") && claudeTargets.length === 0) {
|
|
4750
|
+
claudeTargets = await listClaudeTranscriptFiles(config.claudeProjectsDir, config.claudeTranscriptMaxAgeMs);
|
|
4751
|
+
}
|
|
4752
|
+
for (const filePath of claudeTargets.slice(0, 24)) {
|
|
4753
|
+
if (!runtime.claudeKnownFiles.includes(filePath)) {
|
|
4754
|
+
runtime.claudeKnownFiles.push(filePath);
|
|
4755
|
+
}
|
|
4756
|
+
const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
|
|
4757
|
+
claudeTranscriptChanged = claudeTranscriptChanged || changed;
|
|
4758
|
+
dirty = dirty || changed;
|
|
4759
|
+
}
|
|
4760
|
+
if (claudeTranscriptChanged) {
|
|
4761
|
+
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
if (dirty) {
|
|
4765
|
+
scheduleSaveState(config, state);
|
|
4766
|
+
}
|
|
4767
|
+
} finally {
|
|
4768
|
+
runtime.timelineLiveScanInFlight = false;
|
|
4769
|
+
if (runtime.timelineLiveScanReschedule || runtime.timelineLiveScanReasons.size > 0) {
|
|
4770
|
+
runtime.timelineLiveScanReschedule = false;
|
|
4771
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "reschedule" });
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
function startTimelineLiveSync({ config, runtime, state }) {
|
|
4777
|
+
if (config.dryRun || !config.webUiEnabled || !config.timelineLiveSync) {
|
|
4778
|
+
return null;
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
addTimelineLiveWatcher({
|
|
4782
|
+
runtime,
|
|
4783
|
+
label: "codex-home",
|
|
4784
|
+
targetPath: config.codexHome,
|
|
4785
|
+
onChange: (filePath) => {
|
|
4786
|
+
const base = path.basename(filePath || "");
|
|
4787
|
+
if (filePath === config.historyFile || base === path.basename(config.historyFile || "")) {
|
|
4788
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-history", filePath });
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
if (/^logs(?:_\d+)?\.sqlite(?:-wal|-shm)?$/u.test(base)) {
|
|
4792
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-logs", filePath });
|
|
4793
|
+
}
|
|
4794
|
+
},
|
|
4795
|
+
});
|
|
4796
|
+
|
|
4797
|
+
addTimelineLiveWatcher({
|
|
4798
|
+
runtime,
|
|
4799
|
+
label: "codex-sessions",
|
|
4800
|
+
targetPath: config.sessionsDir,
|
|
4801
|
+
options: { recursive: true },
|
|
4802
|
+
onChange: (filePath) => {
|
|
4803
|
+
if (isRolloutFilePath(filePath)) {
|
|
4804
|
+
scheduleTimelineLiveScan({ config, runtime, state, reason: "codex-rollout", filePath });
|
|
4805
|
+
}
|
|
4806
|
+
},
|
|
4807
|
+
});
|
|
4808
|
+
|
|
4809
|
+
addTimelineLiveWatcher({
|
|
4810
|
+
runtime,
|
|
4811
|
+
label: "claude-projects",
|
|
4812
|
+
targetPath: config.claudeProjectsDir,
|
|
4813
|
+
options: { recursive: true },
|
|
4814
|
+
onChange: (filePath) => {
|
|
4815
|
+
scheduleTimelineLiveScan({
|
|
4816
|
+
config,
|
|
4817
|
+
runtime,
|
|
4818
|
+
state,
|
|
4819
|
+
reason: isClaudeTranscriptPath(config, filePath) ? "claude-file" : "claude-dir",
|
|
4820
|
+
filePath,
|
|
4821
|
+
});
|
|
4822
|
+
},
|
|
4823
|
+
});
|
|
4824
|
+
|
|
4825
|
+
return {
|
|
4826
|
+
close() {
|
|
4827
|
+
if (runtime.timelineLiveScanTimer) {
|
|
4828
|
+
clearTimeout(runtime.timelineLiveScanTimer);
|
|
4829
|
+
runtime.timelineLiveScanTimer = null;
|
|
4830
|
+
}
|
|
4831
|
+
for (const watcher of runtime.timelineLiveWatchers.splice(0)) {
|
|
4832
|
+
try {
|
|
4833
|
+
watcher.close();
|
|
4834
|
+
} catch {}
|
|
4835
|
+
}
|
|
4836
|
+
runtime.timelineBus?.closeAll?.();
|
|
4837
|
+
},
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
|
|
3934
4841
|
async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
3935
4842
|
let stat;
|
|
3936
4843
|
try {
|
|
@@ -3954,6 +4861,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
3954
4861
|
threadId: extractThreadIdFromRolloutPath(filePath),
|
|
3955
4862
|
cwd: null,
|
|
3956
4863
|
applyPatchInputsByCallId: new Map(),
|
|
4864
|
+
toolEventInputsByCallId: new Map(),
|
|
3957
4865
|
startupCutoffMs:
|
|
3958
4866
|
typeof restoredOffset === "number" ? 0 : now - config.replaySeconds * 1000,
|
|
3959
4867
|
skipPartialLine:
|
|
@@ -4031,7 +4939,19 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4031
4939
|
runtime,
|
|
4032
4940
|
state,
|
|
4033
4941
|
entry: timelineEntry,
|
|
4942
|
+
source: "codex-rollout",
|
|
4034
4943
|
}) || dirty;
|
|
4944
|
+
upsertTimelineActivity({
|
|
4945
|
+
config,
|
|
4946
|
+
runtime,
|
|
4947
|
+
provider: "codex",
|
|
4948
|
+
threadId: timelineEntry.threadId,
|
|
4949
|
+
threadLabel: timelineEntry.threadLabel,
|
|
4950
|
+
phase: "thinking",
|
|
4951
|
+
cwd: fileState.cwd || "",
|
|
4952
|
+
source: "codex-user-message",
|
|
4953
|
+
atMs: Date.parse(record.timestamp ?? "") || Date.now(),
|
|
4954
|
+
});
|
|
4035
4955
|
}
|
|
4036
4956
|
|
|
4037
4957
|
const fileTimelineEntries = await buildRolloutFileTimelineEntries({
|
|
@@ -4048,6 +4968,7 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4048
4968
|
runtime,
|
|
4049
4969
|
state,
|
|
4050
4970
|
entry: fileTimelineEntry,
|
|
4971
|
+
source: "codex-rollout",
|
|
4051
4972
|
}) || dirty;
|
|
4052
4973
|
dirty =
|
|
4053
4974
|
recordCodeEvent({
|
|
@@ -4234,6 +5155,109 @@ async function listClaudeTranscriptFiles(claudeProjectsDir, maxAgeMs = 0) {
|
|
|
4234
5155
|
}
|
|
4235
5156
|
}
|
|
4236
5157
|
|
|
5158
|
+
function buildClaudeToolTimelineEntries({ content, threadId, threadLabel, uuid, createdAtMs, cwd }) {
|
|
5159
|
+
if (!Array.isArray(content) || !threadId) {
|
|
5160
|
+
return [];
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
const entries = [];
|
|
5164
|
+
for (const block of content) {
|
|
5165
|
+
if (!isPlainObject(block) || block.type !== "tool_use") {
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
const toolName = cleanText(block.name || "");
|
|
5169
|
+
const input = isPlainObject(block.input) ? block.input : {};
|
|
5170
|
+
const toolId = cleanText(block.id || block.tool_use_id || "") || historyToken(JSON.stringify(block));
|
|
5171
|
+
const callId = `${uuid || createdAtMs}:${toolId}`;
|
|
5172
|
+
const lowerToolName = toolName.toLowerCase();
|
|
5173
|
+
|
|
5174
|
+
if (lowerToolName === "bash") {
|
|
5175
|
+
const commandText = cleanText(input.command || "");
|
|
5176
|
+
if (!commandText) {
|
|
5177
|
+
continue;
|
|
5178
|
+
}
|
|
5179
|
+
const classified = classifyTimelineCommand(commandText);
|
|
5180
|
+
entries.push(
|
|
5181
|
+
buildToolTimelineEntry({
|
|
5182
|
+
provider: "claude",
|
|
5183
|
+
threadId,
|
|
5184
|
+
threadLabel,
|
|
5185
|
+
callId,
|
|
5186
|
+
createdAtMs,
|
|
5187
|
+
fileEventType: classified.fileEventType,
|
|
5188
|
+
commandText: classified.commandText || commandText,
|
|
5189
|
+
fileRefs: classified.fileRefs,
|
|
5190
|
+
cwd,
|
|
5191
|
+
})
|
|
5192
|
+
);
|
|
5193
|
+
continue;
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
if (["write", "edit", "multiedit", "todowrite", "exitplanmode", "askuserquestion"].includes(lowerToolName)) {
|
|
5197
|
+
continue;
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
if (lowerToolName === "read") {
|
|
5201
|
+
const fileRefs = normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean));
|
|
5202
|
+
entries.push(
|
|
5203
|
+
buildToolTimelineEntry({
|
|
5204
|
+
provider: "claude",
|
|
5205
|
+
threadId,
|
|
5206
|
+
threadLabel,
|
|
5207
|
+
callId,
|
|
5208
|
+
createdAtMs,
|
|
5209
|
+
fileEventType: "read",
|
|
5210
|
+
toolName,
|
|
5211
|
+
summaryText: fileRefs[0] ? `${toolName}: ${fileRefs[0]}` : toolName,
|
|
5212
|
+
fileRefs,
|
|
5213
|
+
cwd,
|
|
5214
|
+
})
|
|
5215
|
+
);
|
|
5216
|
+
continue;
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
if (["grep", "glob", "websearch", "webfetch"].includes(lowerToolName)) {
|
|
5220
|
+
const query = cleanText(input.pattern || input.query || input.url || "");
|
|
5221
|
+
const fileRefs = normalizeTimelineFileRefs([input.path || input.file_path || input.filePath].filter(Boolean));
|
|
5222
|
+
entries.push(
|
|
5223
|
+
buildToolTimelineEntry({
|
|
5224
|
+
provider: "claude",
|
|
5225
|
+
threadId,
|
|
5226
|
+
threadLabel,
|
|
5227
|
+
callId,
|
|
5228
|
+
createdAtMs,
|
|
5229
|
+
fileEventType: "search",
|
|
5230
|
+
toolName,
|
|
5231
|
+
summaryText: query ? `${toolName}: ${query}` : toolName,
|
|
5232
|
+
fileRefs,
|
|
5233
|
+
cwd,
|
|
5234
|
+
})
|
|
5235
|
+
);
|
|
5236
|
+
continue;
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
if (lowerToolName === "ls") {
|
|
5240
|
+
const fileRefs = normalizeTimelineFileRefs([input.path].filter(Boolean));
|
|
5241
|
+
entries.push(
|
|
5242
|
+
buildToolTimelineEntry({
|
|
5243
|
+
provider: "claude",
|
|
5244
|
+
threadId,
|
|
5245
|
+
threadLabel,
|
|
5246
|
+
callId,
|
|
5247
|
+
createdAtMs,
|
|
5248
|
+
fileEventType: "read",
|
|
5249
|
+
toolName,
|
|
5250
|
+
summaryText: fileRefs[0] ? `${toolName}: ${fileRefs[0]}` : toolName,
|
|
5251
|
+
fileRefs,
|
|
5252
|
+
cwd,
|
|
5253
|
+
})
|
|
5254
|
+
);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
return entries.filter(Boolean);
|
|
5259
|
+
}
|
|
5260
|
+
|
|
4237
5261
|
async function processClaudeTranscriptFile({ filePath, config, runtime, state, now }) {
|
|
4238
5262
|
let fileState = runtime.claudeFileStates.get(filePath);
|
|
4239
5263
|
if (!fileState) {
|
|
@@ -4349,6 +5373,37 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4349
5373
|
const claudeTitle = threadId ? runtime.claudeSessionTitles.get(threadId) || "" : "";
|
|
4350
5374
|
const threadLabel = claudeTitle || fileState.threadLabel || "";
|
|
4351
5375
|
|
|
5376
|
+
if (type === "assistant" && Array.isArray(content)) {
|
|
5377
|
+
const toolEntries = buildClaudeToolTimelineEntries({
|
|
5378
|
+
content,
|
|
5379
|
+
threadId,
|
|
5380
|
+
threadLabel,
|
|
5381
|
+
uuid,
|
|
5382
|
+
createdAtMs,
|
|
5383
|
+
cwd: fileState.cwd,
|
|
5384
|
+
});
|
|
5385
|
+
for (const toolEntry of toolEntries) {
|
|
5386
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry: toolEntry, source: "claude-tool" }) || dirty;
|
|
5387
|
+
}
|
|
5388
|
+
const toolActivity = timelineActivityFromClaudeContent(content);
|
|
5389
|
+
if (toolActivity) {
|
|
5390
|
+
upsertTimelineActivity({
|
|
5391
|
+
config,
|
|
5392
|
+
runtime,
|
|
5393
|
+
provider: "claude",
|
|
5394
|
+
threadId,
|
|
5395
|
+
threadLabel,
|
|
5396
|
+
phase: toolActivity.phase,
|
|
5397
|
+
summary: toolActivity.summary,
|
|
5398
|
+
commandText: toolActivity.commandText,
|
|
5399
|
+
fileRefs: toolActivity.fileRefs,
|
|
5400
|
+
cwd: fileState.cwd,
|
|
5401
|
+
source: "claude-tool-start",
|
|
5402
|
+
atMs: createdAtMs,
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
|
|
4352
5407
|
let text = "";
|
|
4353
5408
|
if (typeof content === "string") {
|
|
4354
5409
|
text = content;
|
|
@@ -4386,6 +5441,28 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4386
5441
|
: stopReason === "end_turn"
|
|
4387
5442
|
? "assistant_final"
|
|
4388
5443
|
: "assistant_commentary";
|
|
5444
|
+
if (kind === "user_message") {
|
|
5445
|
+
upsertTimelineActivity({
|
|
5446
|
+
config,
|
|
5447
|
+
runtime,
|
|
5448
|
+
provider: "claude",
|
|
5449
|
+
threadId,
|
|
5450
|
+
threadLabel,
|
|
5451
|
+
phase: "thinking",
|
|
5452
|
+
cwd: fileState.cwd,
|
|
5453
|
+
source: "claude-user-message",
|
|
5454
|
+
atMs: createdAtMs,
|
|
5455
|
+
});
|
|
5456
|
+
} else if (kind === "assistant_final") {
|
|
5457
|
+
clearTimelineActivity({
|
|
5458
|
+
config,
|
|
5459
|
+
runtime,
|
|
5460
|
+
provider: "claude",
|
|
5461
|
+
threadId,
|
|
5462
|
+
source: "claude-final",
|
|
5463
|
+
atMs: createdAtMs,
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
4389
5466
|
// Use kind-independent stableId so re-scans with corrected classification
|
|
4390
5467
|
// replace old entries rather than creating duplicates.
|
|
4391
5468
|
const stableId = `claude_msg:${threadId}:${uuid}`;
|
|
@@ -4404,7 +5481,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
4404
5481
|
provider: "claude",
|
|
4405
5482
|
});
|
|
4406
5483
|
if (entry) {
|
|
4407
|
-
dirty = recordTimelineEntry({ config, runtime, state, entry }) || dirty;
|
|
5484
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry, source: "claude-transcript" }) || dirty;
|
|
4408
5485
|
|
|
4409
5486
|
// Also record Claude assistant_final as a history item for the completed list
|
|
4410
5487
|
if (kind === "assistant_final") {
|
|
@@ -4713,6 +5790,7 @@ async function processSqliteTimelineLog({ config, runtime, state, now }) {
|
|
|
4713
5790
|
runtime,
|
|
4714
5791
|
state,
|
|
4715
5792
|
entry,
|
|
5793
|
+
source: "codex-sqlite",
|
|
4716
5794
|
}) || dirty;
|
|
4717
5795
|
|
|
4718
5796
|
// Also record Codex assistant_final as a history item so it appears
|
|
@@ -4864,6 +5942,7 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
|
|
|
4864
5942
|
runtime,
|
|
4865
5943
|
state,
|
|
4866
5944
|
entry,
|
|
5945
|
+
source: "codex-history",
|
|
4867
5946
|
}) || dirty;
|
|
4868
5947
|
}
|
|
4869
5948
|
|
|
@@ -5038,6 +6117,7 @@ async function backfillRecentTimelineEntryDiffs({ config, runtime, state }) {
|
|
|
5038
6117
|
threadId: cleanText(entry?.threadId || ""),
|
|
5039
6118
|
cwd: await findRolloutThreadCwd(runtime, entry?.threadId || ""),
|
|
5040
6119
|
applyPatchInputsByCallId: new Map(),
|
|
6120
|
+
toolEventInputsByCallId: new Map(),
|
|
5041
6121
|
startupCutoffMs: 0,
|
|
5042
6122
|
skipPartialLine: false,
|
|
5043
6123
|
};
|
|
@@ -5610,6 +6690,47 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
5610
6690
|
});
|
|
5611
6691
|
}
|
|
5612
6692
|
|
|
6693
|
+
function buildToolTimelineEntry({
|
|
6694
|
+
provider,
|
|
6695
|
+
threadId,
|
|
6696
|
+
threadLabel,
|
|
6697
|
+
callId,
|
|
6698
|
+
createdAtMs,
|
|
6699
|
+
fileEventType,
|
|
6700
|
+
commandText = "",
|
|
6701
|
+
toolName = "",
|
|
6702
|
+
summaryText = "",
|
|
6703
|
+
fileRefs = [],
|
|
6704
|
+
cwd = "",
|
|
6705
|
+
}) {
|
|
6706
|
+
const normalizedType = normalizeTimelineFileEventType(fileEventType) || "command";
|
|
6707
|
+
const normalizedRefs = normalizeTimelineFileRefs(fileRefs);
|
|
6708
|
+
const kind = normalizedType === "command" ? "command_event" : "file_event";
|
|
6709
|
+
const stableKey = callId || historyToken(`${threadId}:${createdAtMs}:${normalizedType}:${commandText || toolName}:${normalizedRefs.join("|")}`);
|
|
6710
|
+
const messageText = timelineCommandMessage(DEFAULT_LOCALE, {
|
|
6711
|
+
commandText,
|
|
6712
|
+
toolName,
|
|
6713
|
+
fileRefs: normalizedRefs,
|
|
6714
|
+
});
|
|
6715
|
+
const summary = timelineCommandSummary(commandText, summaryText || toolName) || fileEventTitle(DEFAULT_LOCALE, normalizedType);
|
|
6716
|
+
return normalizeTimelineEntry({
|
|
6717
|
+
stableId: `${kind}:${normalizedType}:${threadId}:${stableKey}`,
|
|
6718
|
+
token: historyToken(`${kind}:${normalizedType}:${threadId}:${stableKey}`),
|
|
6719
|
+
kind,
|
|
6720
|
+
fileEventType: normalizedType,
|
|
6721
|
+
threadId,
|
|
6722
|
+
threadLabel,
|
|
6723
|
+
title: kind === "command_event" ? kindTitle(DEFAULT_LOCALE, "command_event") : fileEventTitle(DEFAULT_LOCALE, normalizedType),
|
|
6724
|
+
summary,
|
|
6725
|
+
messageText,
|
|
6726
|
+
fileRefs: normalizedRefs,
|
|
6727
|
+
createdAtMs,
|
|
6728
|
+
cwd,
|
|
6729
|
+
readOnly: true,
|
|
6730
|
+
provider,
|
|
6731
|
+
});
|
|
6732
|
+
}
|
|
6733
|
+
|
|
5613
6734
|
async function buildRolloutFileTimelineEntries({ config, record, fileState, runtime, rolloutFilePath = "" }) {
|
|
5614
6735
|
if (!isPlainObject(record) || cleanText(record.type) !== "response_item") {
|
|
5615
6736
|
return [];
|
|
@@ -5630,30 +6751,102 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
5630
6751
|
cwd: fileState.cwd || "",
|
|
5631
6752
|
});
|
|
5632
6753
|
|
|
6754
|
+
if (payloadType === "function_call" && cleanText(payload?.name || "") === "exec_command") {
|
|
6755
|
+
const stored = rememberToolEventInput(fileState, payload, createdAtMs);
|
|
6756
|
+
const args = stored?.arguments || {};
|
|
6757
|
+
const commandText = cleanText(args.cmd || args.command || "");
|
|
6758
|
+
if (!commandText) {
|
|
6759
|
+
return [];
|
|
6760
|
+
}
|
|
6761
|
+
const classified = classifyTimelineCommand(commandText);
|
|
6762
|
+
const activity = timelineActivityFromCommand({ commandText, cwd: cleanText(args.workdir || fileState.cwd || "") });
|
|
6763
|
+
upsertTimelineActivity({
|
|
6764
|
+
config,
|
|
6765
|
+
runtime,
|
|
6766
|
+
provider: "codex",
|
|
6767
|
+
threadId,
|
|
6768
|
+
threadLabel,
|
|
6769
|
+
phase: activity.phase,
|
|
6770
|
+
commandText: activity.commandText,
|
|
6771
|
+
fileRefs: activity.fileRefs,
|
|
6772
|
+
cwd: activity.cwd,
|
|
6773
|
+
source: "codex-tool-start",
|
|
6774
|
+
atMs: createdAtMs,
|
|
6775
|
+
});
|
|
6776
|
+
return [
|
|
6777
|
+
buildToolTimelineEntry({
|
|
6778
|
+
provider: "codex",
|
|
6779
|
+
threadId,
|
|
6780
|
+
threadLabel,
|
|
6781
|
+
callId,
|
|
6782
|
+
createdAtMs,
|
|
6783
|
+
fileEventType: classified.fileEventType,
|
|
6784
|
+
commandText: classified.commandText || commandText,
|
|
6785
|
+
fileRefs: classified.fileRefs,
|
|
6786
|
+
cwd: cleanText(args.workdir || fileState.cwd || ""),
|
|
6787
|
+
}),
|
|
6788
|
+
].filter(Boolean);
|
|
6789
|
+
}
|
|
6790
|
+
|
|
5633
6791
|
if (payloadType === "custom_tool_call") {
|
|
5634
6792
|
rememberApplyPatchInput(fileState, payload, createdAtMs);
|
|
6793
|
+
upsertTimelineActivity({
|
|
6794
|
+
config,
|
|
6795
|
+
runtime,
|
|
6796
|
+
provider: "codex",
|
|
6797
|
+
threadId,
|
|
6798
|
+
threadLabel,
|
|
6799
|
+
phase: "editing",
|
|
6800
|
+
summary: cleanText(payload?.name || "apply_patch"),
|
|
6801
|
+
cwd: fileState.cwd || "",
|
|
6802
|
+
source: "codex-tool-start",
|
|
6803
|
+
atMs: createdAtMs,
|
|
6804
|
+
});
|
|
5635
6805
|
return [];
|
|
5636
6806
|
}
|
|
5637
6807
|
|
|
5638
6808
|
if (payloadType === "function_call_output") {
|
|
6809
|
+
if (findStoredToolEventInput(fileState, callId)) {
|
|
6810
|
+
upsertTimelineActivity({
|
|
6811
|
+
config,
|
|
6812
|
+
runtime,
|
|
6813
|
+
provider: "codex",
|
|
6814
|
+
threadId,
|
|
6815
|
+
threadLabel,
|
|
6816
|
+
phase: "thinking",
|
|
6817
|
+
cwd: fileState.cwd || "",
|
|
6818
|
+
source: "codex-tool-output",
|
|
6819
|
+
atMs: createdAtMs,
|
|
6820
|
+
});
|
|
6821
|
+
return [];
|
|
6822
|
+
}
|
|
5639
6823
|
const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
|
|
5640
|
-
|
|
5641
|
-
if (fileRefs.length === 0) {
|
|
6824
|
+
if (!commandText) {
|
|
5642
6825
|
return [];
|
|
5643
6826
|
}
|
|
6827
|
+
const classified = classifyTimelineCommand(commandText);
|
|
6828
|
+
upsertTimelineActivity({
|
|
6829
|
+
config,
|
|
6830
|
+
runtime,
|
|
6831
|
+
provider: "codex",
|
|
6832
|
+
threadId,
|
|
6833
|
+
threadLabel,
|
|
6834
|
+
phase: "thinking",
|
|
6835
|
+
cwd: fileState.cwd || "",
|
|
6836
|
+
source: "codex-tool-output",
|
|
6837
|
+
atMs: createdAtMs,
|
|
6838
|
+
});
|
|
5644
6839
|
return [
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
token: historyToken(`file_event:read:${threadId}:${callId || createdAtMs}`),
|
|
5648
|
-
kind: "file_event",
|
|
5649
|
-
fileEventType: "read",
|
|
6840
|
+
buildToolTimelineEntry({
|
|
6841
|
+
provider: "codex",
|
|
5650
6842
|
threadId,
|
|
5651
6843
|
threadLabel,
|
|
5652
|
-
|
|
5653
|
-
summary: "",
|
|
5654
|
-
fileRefs,
|
|
6844
|
+
callId,
|
|
5655
6845
|
createdAtMs,
|
|
5656
|
-
|
|
6846
|
+
fileEventType: classified.fileEventType,
|
|
6847
|
+
commandText: classified.commandText || commandText,
|
|
6848
|
+
fileRefs: classified.fileRefs,
|
|
6849
|
+
cwd: fileState.cwd || "",
|
|
5657
6850
|
}),
|
|
5658
6851
|
].filter(Boolean);
|
|
5659
6852
|
}
|
|
@@ -5801,6 +6994,18 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
5801
6994
|
fileState.applyPatchInputsByCallId.delete(callId);
|
|
5802
6995
|
}
|
|
5803
6996
|
|
|
6997
|
+
upsertTimelineActivity({
|
|
6998
|
+
config,
|
|
6999
|
+
runtime,
|
|
7000
|
+
provider: "codex",
|
|
7001
|
+
threadId,
|
|
7002
|
+
threadLabel,
|
|
7003
|
+
phase: "thinking",
|
|
7004
|
+
cwd: fileState.cwd || "",
|
|
7005
|
+
source: "codex-tool-output",
|
|
7006
|
+
atMs: createdAtMs,
|
|
7007
|
+
});
|
|
7008
|
+
|
|
5804
7009
|
return entries.filter(Boolean);
|
|
5805
7010
|
}
|
|
5806
7011
|
|
|
@@ -5857,8 +7062,24 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
5857
7062
|
let dirty = false;
|
|
5858
7063
|
|
|
5859
7064
|
if (event.kind === "task_complete") {
|
|
7065
|
+
clearTimelineActivity({
|
|
7066
|
+
config,
|
|
7067
|
+
runtime,
|
|
7068
|
+
provider: "codex",
|
|
7069
|
+
threadId: event.threadId ?? event.conversationId ?? "",
|
|
7070
|
+
source: "codex-task-complete",
|
|
7071
|
+
atMs: event.timestampMs || Date.now(),
|
|
7072
|
+
});
|
|
5860
7073
|
attachCompletionDetails({ config, runtime, event });
|
|
5861
7074
|
} else if (event.kind === "plan_ready") {
|
|
7075
|
+
clearTimelineActivity({
|
|
7076
|
+
config,
|
|
7077
|
+
runtime,
|
|
7078
|
+
provider: "codex",
|
|
7079
|
+
threadId: event.threadId ?? event.conversationId ?? "",
|
|
7080
|
+
source: "codex-plan-ready",
|
|
7081
|
+
atMs: event.timestampMs || Date.now(),
|
|
7082
|
+
});
|
|
5862
7083
|
attachPlanDetails({ config, runtime, event });
|
|
5863
7084
|
}
|
|
5864
7085
|
|
|
@@ -5884,6 +7105,8 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
5884
7105
|
subtab: "completed",
|
|
5885
7106
|
token: historyToken(event.id),
|
|
5886
7107
|
stableId: event.id,
|
|
7108
|
+
dedupeId: completionPushContentDedupeId(event),
|
|
7109
|
+
dedupeWindowMs: COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS,
|
|
5887
7110
|
title: event.title,
|
|
5888
7111
|
body: event.detailText || event.message,
|
|
5889
7112
|
buildLocalizedContent: ({ locale }) => ({
|
|
@@ -6102,6 +7325,8 @@ function buildRolloutEvent({ record, filePath, fileState, sessionIndex, config,
|
|
|
6102
7325
|
threadLabel: context.threadLabel,
|
|
6103
7326
|
message,
|
|
6104
7327
|
detailText,
|
|
7328
|
+
threadId,
|
|
7329
|
+
turnId,
|
|
6105
7330
|
priority: config.completePriority,
|
|
6106
7331
|
tags: config.completeTags,
|
|
6107
7332
|
clickUrl: config.clickUrl,
|
|
@@ -6335,7 +7560,7 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
6335
7560
|
body: approval.messageText,
|
|
6336
7561
|
buildLocalizedContent: ({ locale }) => ({
|
|
6337
7562
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
6338
|
-
body: approval.
|
|
7563
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
6339
7564
|
}),
|
|
6340
7565
|
}) || stateChanged;
|
|
6341
7566
|
}
|
|
@@ -6671,7 +7896,11 @@ async function syncGenericUserInputRequests({
|
|
|
6671
7896
|
userInputRequest.supported ? "server.title.choice" : "server.title.choiceReadOnly",
|
|
6672
7897
|
userInputRequest.threadLabel
|
|
6673
7898
|
),
|
|
6674
|
-
body:
|
|
7899
|
+
body: buildUserInputNotificationText(
|
|
7900
|
+
userInputRequest.questions,
|
|
7901
|
+
userInputRequest.supported,
|
|
7902
|
+
locale
|
|
7903
|
+
),
|
|
6675
7904
|
}),
|
|
6676
7905
|
}) || stateChanged;
|
|
6677
7906
|
}
|
|
@@ -8469,7 +9698,7 @@ class NativeIpcClient {
|
|
|
8469
9698
|
);
|
|
8470
9699
|
}
|
|
8471
9700
|
|
|
8472
|
-
async startTurn(conversationId, turnStartParams, ownerClientId = null) {
|
|
9701
|
+
async startTurn(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8473
9702
|
return this.sendThreadFollowerRequest(
|
|
8474
9703
|
"thread-follower-start-turn",
|
|
8475
9704
|
{
|
|
@@ -8477,11 +9706,12 @@ class NativeIpcClient {
|
|
|
8477
9706
|
turnStartParams,
|
|
8478
9707
|
},
|
|
8479
9708
|
conversationId,
|
|
8480
|
-
ownerClientId
|
|
9709
|
+
ownerClientId,
|
|
9710
|
+
options
|
|
8481
9711
|
);
|
|
8482
9712
|
}
|
|
8483
9713
|
|
|
8484
|
-
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null) {
|
|
9714
|
+
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null, options = {}) {
|
|
8485
9715
|
const targetClientId =
|
|
8486
9716
|
ownerClientId ??
|
|
8487
9717
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
@@ -8489,7 +9719,7 @@ class NativeIpcClient {
|
|
|
8489
9719
|
return this.sendRequest(
|
|
8490
9720
|
"turn/start",
|
|
8491
9721
|
buildDirectTurnStartPayload(conversationId, turnStartParams),
|
|
8492
|
-
{ targetClientId }
|
|
9722
|
+
{ targetClientId, ...options }
|
|
8493
9723
|
);
|
|
8494
9724
|
}
|
|
8495
9725
|
|
|
@@ -8519,12 +9749,13 @@ class NativeIpcClient {
|
|
|
8519
9749
|
);
|
|
8520
9750
|
}
|
|
8521
9751
|
|
|
8522
|
-
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null) {
|
|
9752
|
+
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null, options = {}) {
|
|
8523
9753
|
return this.sendRequest(method, params, {
|
|
8524
9754
|
targetClientId:
|
|
8525
9755
|
ownerClientId ??
|
|
8526
9756
|
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
8527
9757
|
null,
|
|
9758
|
+
...options,
|
|
8528
9759
|
});
|
|
8529
9760
|
}
|
|
8530
9761
|
|
|
@@ -9407,9 +10638,10 @@ function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocal
|
|
|
9407
10638
|
}
|
|
9408
10639
|
|
|
9409
10640
|
function formatCommandApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10641
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
9410
10642
|
const parts = [];
|
|
9411
|
-
const reason = truncate(cleanText(
|
|
9412
|
-
const command = truncate(cleanText(
|
|
10643
|
+
const reason = truncate(cleanText(safeParams.reason ?? safeParams.justification ?? ""), 220);
|
|
10644
|
+
const command = truncate(cleanText(safeParams.command ?? safeParams.cmd ?? ""), 1200);
|
|
9413
10645
|
if (reason) {
|
|
9414
10646
|
parts.push(reason);
|
|
9415
10647
|
} else {
|
|
@@ -9422,21 +10654,41 @@ function formatCommandApprovalMessage(params, locale = config?.defaultLocale ||
|
|
|
9422
10654
|
}
|
|
9423
10655
|
|
|
9424
10656
|
function formatFileApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10657
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
9425
10658
|
const parts = [];
|
|
9426
|
-
const reason = truncate(cleanText(
|
|
10659
|
+
const reason = truncate(cleanText(safeParams.reason ?? ""), 220);
|
|
9427
10660
|
if (reason) {
|
|
9428
10661
|
parts.push(reason);
|
|
9429
10662
|
} else {
|
|
9430
10663
|
parts.push(t(locale, "server.message.fileApprovalNeeded"));
|
|
9431
10664
|
}
|
|
9432
|
-
if (
|
|
9433
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
9434
|
-
} else if (
|
|
9435
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
10665
|
+
if (safeParams.grantRoot) {
|
|
10666
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.grantRoot) }));
|
|
10667
|
+
} else if (safeParams.cwd) {
|
|
10668
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.cwd) }));
|
|
9436
10669
|
}
|
|
9437
10670
|
return truncate(parts.join("\n"), 1024);
|
|
9438
10671
|
}
|
|
9439
10672
|
|
|
10673
|
+
function claudeApprovalRawParams(body, kind) {
|
|
10674
|
+
const toolInput = isPlainObject(body?.toolInput) ? cloneJson(body.toolInput) : {};
|
|
10675
|
+
const rawParams = {
|
|
10676
|
+
...toolInput,
|
|
10677
|
+
...(body?.cwd ? { cwd: String(body.cwd) } : {}),
|
|
10678
|
+
};
|
|
10679
|
+
const messageText = cleanText(body?.messageText || "");
|
|
10680
|
+
if (messageText && !cleanText(rawParams.reason ?? rawParams.justification ?? "")) {
|
|
10681
|
+
rawParams.reason = messageText;
|
|
10682
|
+
}
|
|
10683
|
+
if (kind === "command" && !cleanText(rawParams.command ?? rawParams.cmd ?? "")) {
|
|
10684
|
+
const commandText = cleanText(toolInput.command ?? toolInput.cmd ?? "");
|
|
10685
|
+
if (commandText) {
|
|
10686
|
+
rawParams.command = commandText;
|
|
10687
|
+
}
|
|
10688
|
+
}
|
|
10689
|
+
return rawParams;
|
|
10690
|
+
}
|
|
10691
|
+
|
|
9440
10692
|
function extractApprovalFileRefs(params) {
|
|
9441
10693
|
if (!isPlainObject(params)) {
|
|
9442
10694
|
return [];
|
|
@@ -10866,6 +12118,10 @@ async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
|
10866
12118
|
stableId,
|
|
10867
12119
|
title,
|
|
10868
12120
|
body: messageText,
|
|
12121
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
12122
|
+
title: localizedMcpNotifyTitle(locale, title),
|
|
12123
|
+
body: pushBodySnippet(messageText),
|
|
12124
|
+
}),
|
|
10869
12125
|
}).catch((error) => {
|
|
10870
12126
|
console.error(`[mcp-notify-push] ${error.message}`);
|
|
10871
12127
|
return false;
|
|
@@ -10897,10 +12153,7 @@ async function handleMcpProviderEvent({ config, runtime, state, body, res }) {
|
|
|
10897
12153
|
stableId: pendingApprovalStableId(approval),
|
|
10898
12154
|
title: approval.title || "MCP",
|
|
10899
12155
|
body: approval.messageText,
|
|
10900
|
-
buildLocalizedContent: () => (
|
|
10901
|
-
title: approval.title || "MCP",
|
|
10902
|
-
body: approval.messageText,
|
|
10903
|
-
}),
|
|
12156
|
+
buildLocalizedContent: ({ locale }) => localizedMcpApprovalPushContent(locale, approval),
|
|
10904
12157
|
}).catch((error) => {
|
|
10905
12158
|
console.error(`[mcp-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
10906
12159
|
});
|
|
@@ -11265,6 +12518,17 @@ function historyItemByToken(runtime, kind, token) {
|
|
|
11265
12518
|
) ?? null;
|
|
11266
12519
|
}
|
|
11267
12520
|
|
|
12521
|
+
function approvalDecisionFromHistoryItem(item) {
|
|
12522
|
+
const outcome = cleanText(item?.outcome || "");
|
|
12523
|
+
if (outcome === "approved") {
|
|
12524
|
+
return "accept";
|
|
12525
|
+
}
|
|
12526
|
+
if (outcome === "rejected") {
|
|
12527
|
+
return "decline";
|
|
12528
|
+
}
|
|
12529
|
+
return "";
|
|
12530
|
+
}
|
|
12531
|
+
|
|
11268
12532
|
function listLatestPersistedUserInputRequests({ config, runtime, state }) {
|
|
11269
12533
|
const byToken = new Map();
|
|
11270
12534
|
for (const userInputRequest of runtime.userInputRequestsByToken.values()) {
|
|
@@ -11846,6 +13110,26 @@ function buildTimelineThreads(entries, config) {
|
|
|
11846
13110
|
.slice(0, config.maxTimelineThreads);
|
|
11847
13111
|
}
|
|
11848
13112
|
|
|
13113
|
+
function buildActiveTimelineActivityEntries(runtime, locale) {
|
|
13114
|
+
if (!(runtime?.activeTimelineActivitiesByKey instanceof Map)) {
|
|
13115
|
+
return [];
|
|
13116
|
+
}
|
|
13117
|
+
const now = Date.now();
|
|
13118
|
+
const entries = [];
|
|
13119
|
+
for (const [key, activity] of [...runtime.activeTimelineActivitiesByKey.entries()]) {
|
|
13120
|
+
const updatedAtMs = Number(activity?.updatedAtMs) || 0;
|
|
13121
|
+
if (!updatedAtMs || now - updatedAtMs > TIMELINE_ACTIVITY_TTL_MS) {
|
|
13122
|
+
runtime.activeTimelineActivitiesByKey.delete(key);
|
|
13123
|
+
continue;
|
|
13124
|
+
}
|
|
13125
|
+
const entry = buildTimelineActivityEntry(activity, locale);
|
|
13126
|
+
if (entry) {
|
|
13127
|
+
entries.push(entry);
|
|
13128
|
+
}
|
|
13129
|
+
}
|
|
13130
|
+
return entries;
|
|
13131
|
+
}
|
|
13132
|
+
|
|
11849
13133
|
function buildTimelineResponse(runtime, state, config, locale) {
|
|
11850
13134
|
const messageEntries = normalizeTimelineEntries(
|
|
11851
13135
|
state.recentTimelineEntries ?? runtime.recentTimelineEntries,
|
|
@@ -11853,7 +13137,11 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
11853
13137
|
);
|
|
11854
13138
|
runtime.recentTimelineEntries = messageEntries;
|
|
11855
13139
|
const entries = normalizeTimelineEntries(
|
|
11856
|
-
[
|
|
13140
|
+
[
|
|
13141
|
+
...buildActiveTimelineActivityEntries(runtime, locale),
|
|
13142
|
+
...messageEntries,
|
|
13143
|
+
...buildOperationalTimelineEntries(runtime, state, config, locale),
|
|
13144
|
+
],
|
|
11857
13145
|
config.maxTimelineEntries
|
|
11858
13146
|
).map((entry) => ({
|
|
11859
13147
|
kind: entry.kind,
|
|
@@ -11871,6 +13159,9 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
11871
13159
|
outcome: entry.outcome || "",
|
|
11872
13160
|
createdAtMs: entry.createdAtMs,
|
|
11873
13161
|
provider: normalizeProvider(entry.provider),
|
|
13162
|
+
...(entry.activityPhase != null ? { activityPhase: entry.activityPhase } : {}),
|
|
13163
|
+
...(entry.commandText != null ? { commandText: entry.commandText } : {}),
|
|
13164
|
+
...timelineAutoPilotProjection(entry),
|
|
11874
13165
|
...(entry.draftType != null ? { draftType: entry.draftType } : {}),
|
|
11875
13166
|
}));
|
|
11876
13167
|
|
|
@@ -11880,6 +13171,24 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
11880
13171
|
};
|
|
11881
13172
|
}
|
|
11882
13173
|
|
|
13174
|
+
function timelineAutoPilotProjection(entry) {
|
|
13175
|
+
if (cleanText(entry?.kind || "") !== "approval" || cleanText(entry?.outcome || "") !== "approved") {
|
|
13176
|
+
return {};
|
|
13177
|
+
}
|
|
13178
|
+
const stableId = cleanText(entry?.stableId || "");
|
|
13179
|
+
if (stableId.includes(":autopilot-write")) {
|
|
13180
|
+
const lane = cleanText(stableId.match(/:autopilot-write:([a-z_-]+)$/u)?.[1] || "");
|
|
13181
|
+
return {
|
|
13182
|
+
autoPilotMode: "write",
|
|
13183
|
+
...(lane ? { autoPilotWriteLane: lane } : {}),
|
|
13184
|
+
};
|
|
13185
|
+
}
|
|
13186
|
+
if (stableId.endsWith(":autopilot")) {
|
|
13187
|
+
return { autoPilotMode: "read" };
|
|
13188
|
+
}
|
|
13189
|
+
return {};
|
|
13190
|
+
}
|
|
13191
|
+
|
|
11883
13192
|
function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
11884
13193
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
11885
13194
|
const approvalKind = cleanText(approval.kind || "");
|
|
@@ -12338,6 +13647,48 @@ function findNewerThreadMessageAfterCompletion(runtime, completionItem) {
|
|
|
12338
13647
|
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0] || null;
|
|
12339
13648
|
}
|
|
12340
13649
|
|
|
13650
|
+
function normalizeCompletionReplyComparisonText(value) {
|
|
13651
|
+
return normalizeLongText(value).replace(/\s+/gu, " ").trim();
|
|
13652
|
+
}
|
|
13653
|
+
|
|
13654
|
+
function isMatchingCompletionReplyTimelineMessage(entry, messageText, expectedImageCount = 0) {
|
|
13655
|
+
if (cleanText(entry?.kind || "") !== "user_message") {
|
|
13656
|
+
return false;
|
|
13657
|
+
}
|
|
13658
|
+
const expectedText = normalizeCompletionReplyComparisonText(messageText);
|
|
13659
|
+
const actualText = normalizeCompletionReplyComparisonText(entry?.messageText || entry?.summary || entry?.title || "");
|
|
13660
|
+
if (!expectedText || actualText !== expectedText) {
|
|
13661
|
+
return false;
|
|
13662
|
+
}
|
|
13663
|
+
const entryImageCount = normalizeTimelineImagePaths(entry?.imagePaths || []).length;
|
|
13664
|
+
return expectedImageCount <= 0 || entryImageCount === expectedImageCount;
|
|
13665
|
+
}
|
|
13666
|
+
|
|
13667
|
+
function findMatchingCompletionReplyAfterCompletion(runtime, completionItem, messageText, expectedImageCount = 0) {
|
|
13668
|
+
const itemKind = cleanText(completionItem?.kind || "");
|
|
13669
|
+
if (!runtime || !REPLYABLE_HISTORY_KINDS.has(itemKind)) {
|
|
13670
|
+
return null;
|
|
13671
|
+
}
|
|
13672
|
+
|
|
13673
|
+
const threadId = historyItemThreadId(completionItem);
|
|
13674
|
+
const completionCreatedAtMs = Number(completionItem?.createdAtMs) || 0;
|
|
13675
|
+
if (!threadId || !completionCreatedAtMs) {
|
|
13676
|
+
return null;
|
|
13677
|
+
}
|
|
13678
|
+
|
|
13679
|
+
return runtime.recentTimelineEntries
|
|
13680
|
+
.filter((entry) => {
|
|
13681
|
+
if (cleanText(entry?.threadId || "") !== threadId) {
|
|
13682
|
+
return false;
|
|
13683
|
+
}
|
|
13684
|
+
if (Number(entry?.createdAtMs) <= completionCreatedAtMs) {
|
|
13685
|
+
return false;
|
|
13686
|
+
}
|
|
13687
|
+
return isMatchingCompletionReplyTimelineMessage(entry, messageText, expectedImageCount);
|
|
13688
|
+
})
|
|
13689
|
+
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0] || null;
|
|
13690
|
+
}
|
|
13691
|
+
|
|
12341
13692
|
function buildHistoryDetail(item, locale, runtime = null) {
|
|
12342
13693
|
const threadId = historyItemThreadId(item);
|
|
12343
13694
|
const replyEnabled =
|
|
@@ -12455,6 +13806,13 @@ function buildAmbientSuggestionsDetail(entry, locale) {
|
|
|
12455
13806
|
|
|
12456
13807
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
12457
13808
|
const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
|
|
13809
|
+
const commandText = ["read", "search", "git"].includes(fileEventType)
|
|
13810
|
+
? firstMarkdownCodeFenceText(entry?.messageText ?? "")
|
|
13811
|
+
: "";
|
|
13812
|
+
const detailText = [
|
|
13813
|
+
fileEventDetailCopy(locale, fileEventType, entry?.provider),
|
|
13814
|
+
commandText ? "" : normalizeTimelineMessageText(entry?.messageText ?? "", locale),
|
|
13815
|
+
].filter(Boolean).join("\n\n");
|
|
12458
13816
|
return {
|
|
12459
13817
|
kind: "file_event",
|
|
12460
13818
|
token: entry.token,
|
|
@@ -12463,9 +13821,10 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12463
13821
|
threadLabel: entry.threadLabel || "",
|
|
12464
13822
|
fileEventType,
|
|
12465
13823
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
12466
|
-
messageHtml: renderMessageHtml(
|
|
13824
|
+
messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
12467
13825
|
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
12468
13826
|
previousFileRefs: normalizeTimelineFileRefs(entry.previousFileRefs ?? []),
|
|
13827
|
+
...(commandText ? { commandText } : {}),
|
|
12469
13828
|
diffAvailable: Boolean(entry.diffAvailable),
|
|
12470
13829
|
diffText: normalizeTimelineDiffText(entry.diffText ?? ""),
|
|
12471
13830
|
diffSource: normalizeTimelineDiffSource(entry.diffSource ?? ""),
|
|
@@ -12476,6 +13835,25 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
12476
13835
|
};
|
|
12477
13836
|
}
|
|
12478
13837
|
|
|
13838
|
+
function buildTimelineCommandEventDetail(entry, locale) {
|
|
13839
|
+
const commandText = firstMarkdownCodeFenceText(entry?.messageText ?? "") || cleanText(entry?.summary || "");
|
|
13840
|
+
const detailText = t(locale, "detail.commandEvent.copy", { provider: providerDisplayName(locale, entry?.provider) });
|
|
13841
|
+
return {
|
|
13842
|
+
kind: "command_event",
|
|
13843
|
+
token: entry.token,
|
|
13844
|
+
threadId: cleanText(entry.threadId || ""),
|
|
13845
|
+
title: cleanText(entry.threadLabel || entry.title || "") || kindTitle(locale, "command_event"),
|
|
13846
|
+
threadLabel: entry.threadLabel || "",
|
|
13847
|
+
fileEventType: "command",
|
|
13848
|
+
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
13849
|
+
messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
13850
|
+
commandText,
|
|
13851
|
+
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
13852
|
+
readOnly: true,
|
|
13853
|
+
actions: [],
|
|
13854
|
+
};
|
|
13855
|
+
}
|
|
13856
|
+
|
|
12479
13857
|
function buildDiffThreadDetail(group, locale) {
|
|
12480
13858
|
const changedFileCount = Math.max(0, Number(group.changedFileCount) || 0);
|
|
12481
13859
|
return {
|
|
@@ -12849,6 +14227,25 @@ async function handleCompletionReply({
|
|
|
12849
14227
|
if (!conversationId) {
|
|
12850
14228
|
throw new Error("completion-reply-unavailable");
|
|
12851
14229
|
}
|
|
14230
|
+
console.log(
|
|
14231
|
+
`[completion-reply] received at=${new Date().toISOString()} ` +
|
|
14232
|
+
`token=${cleanText(completionItem?.token || "") || "unknown"} thread=${conversationId} ` +
|
|
14233
|
+
`images=${normalizedLocalImagePaths.length} plan=${planMode ? 1 : 0} force=${force ? 1 : 0}`
|
|
14234
|
+
);
|
|
14235
|
+
|
|
14236
|
+
const alreadyAcceptedReply = findMatchingCompletionReplyAfterCompletion(
|
|
14237
|
+
runtime,
|
|
14238
|
+
completionItem,
|
|
14239
|
+
messageText,
|
|
14240
|
+
normalizedLocalImagePaths.length
|
|
14241
|
+
);
|
|
14242
|
+
if (alreadyAcceptedReply && !force) {
|
|
14243
|
+
console.log(
|
|
14244
|
+
`[completion-reply] idempotent token=${cleanText(completionItem?.token || "") || "unknown"} thread=${conversationId} timeline=${cleanText(alreadyAcceptedReply.stableId || alreadyAcceptedReply.token || "") || "unknown"}`
|
|
14245
|
+
);
|
|
14246
|
+
return { alreadyAccepted: true };
|
|
14247
|
+
}
|
|
14248
|
+
|
|
12852
14249
|
// For assistant_final, check against timeline entries (avoids maxHistoryItems eviction).
|
|
12853
14250
|
// For legacy completion, check against history items.
|
|
12854
14251
|
const itemKind = cleanText(completionItem?.kind || "");
|
|
@@ -12939,30 +14336,32 @@ async function handleCompletionReply({
|
|
|
12939
14336
|
await runtime.ipcClient.startTurnDirect(
|
|
12940
14337
|
conversationId,
|
|
12941
14338
|
candidate.turnStartParams,
|
|
12942
|
-
ownerClientId
|
|
14339
|
+
ownerClientId,
|
|
14340
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12943
14341
|
);
|
|
12944
14342
|
} else {
|
|
12945
14343
|
await runtime.ipcClient.startTurn(
|
|
12946
14344
|
conversationId,
|
|
12947
14345
|
candidate.turnStartParams,
|
|
12948
|
-
ownerClientId
|
|
14346
|
+
ownerClientId,
|
|
14347
|
+
{ timeoutMs: config.completionReplyAckTimeoutMs }
|
|
12949
14348
|
);
|
|
12950
14349
|
}
|
|
12951
14350
|
console.log(
|
|
12952
|
-
`[completion-reply] success candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
14351
|
+
`[completion-reply] success at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
12953
14352
|
);
|
|
12954
14353
|
await finalizeReplyAccepted();
|
|
12955
|
-
return;
|
|
14354
|
+
return { alreadyAccepted: false };
|
|
12956
14355
|
} catch (error) {
|
|
12957
14356
|
if (isStartTurnAckTimeout(error)) {
|
|
12958
14357
|
// Codex can create the turn but fail to send the bridge an ACK before
|
|
12959
14358
|
// its internal follower timeout fires. Retrying risks duplicate user
|
|
12960
14359
|
// turns, so treat this as accepted and let the timeline catch up.
|
|
12961
14360
|
console.log(
|
|
12962
|
-
`[completion-reply] accepted candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(error)}`
|
|
14361
|
+
`[completion-reply] accepted at=${new Date().toISOString()} candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} ack-timeout=${normalizeIpcErrorMessage(error)}`
|
|
12963
14362
|
);
|
|
12964
14363
|
await finalizeReplyAccepted();
|
|
12965
|
-
return;
|
|
14364
|
+
return { alreadyAccepted: false, ackTimeout: true };
|
|
12966
14365
|
}
|
|
12967
14366
|
lastError = error;
|
|
12968
14367
|
console.log(
|
|
@@ -13660,9 +15059,26 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
13660
15059
|
});
|
|
13661
15060
|
} else if (approval.resolveClaudeWaiter) {
|
|
13662
15061
|
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
13663
|
-
// ExitPlanMode
|
|
13664
|
-
//
|
|
13665
|
-
//
|
|
15062
|
+
// ExitPlanMode CANNOT actually be auto-bypassed via the PreToolUse
|
|
15063
|
+
// hook. We tested both "allow" and "deny" verdicts on Claude Desktop:
|
|
15064
|
+
//
|
|
15065
|
+
// - permissionDecision: "allow"
|
|
15066
|
+
// Claude Desktop ignores the verdict and re-shows its native
|
|
15067
|
+
// plan dialog on the Mac, asking for approval a second time
|
|
15068
|
+
// even though the user already tapped on mobile.
|
|
15069
|
+
// - permissionDecision: "deny"
|
|
15070
|
+
// The native dialog is suppressed, but Claude Desktop also
|
|
15071
|
+
// does not exit plan mode internally, so Edit/Write stay
|
|
15072
|
+
// blocked — the session cannot make progress.
|
|
15073
|
+
//
|
|
15074
|
+
// Neither verdict gives us "phone tap → plan mode exits → editing
|
|
15075
|
+
// resumes" by itself. Until we can drive the Mac dialog through an
|
|
15076
|
+
// external mechanism (AppleScript / Accessibility API), the chosen
|
|
15077
|
+
// workaround is "deny + reason": Claude reads the reason as
|
|
15078
|
+
// instructions ("user already approved on mobile, proceed without
|
|
15079
|
+
// calling ExitPlanMode again"). That at least lets the desktop user
|
|
15080
|
+
// finish the flow with one extra Mac click on the native dialog
|
|
15081
|
+
// instead of getting a duplicate dialog from a stale "allow".
|
|
13666
15082
|
approval.resolveClaudeWaiter({
|
|
13667
15083
|
permissionDecision: "deny",
|
|
13668
15084
|
permissionDecisionReason:
|
|
@@ -13718,6 +15134,10 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13718
15134
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13719
15135
|
return entry ? buildTimelineFileEventDetail(entry, locale) : null;
|
|
13720
15136
|
}
|
|
15137
|
+
if (kind === "command_event") {
|
|
15138
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
15139
|
+
return entry ? buildTimelineCommandEventDetail(entry, locale) : null;
|
|
15140
|
+
}
|
|
13721
15141
|
if (kind === "ambient_suggestions") {
|
|
13722
15142
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13723
15143
|
if (entry) {
|
|
@@ -13899,6 +15319,8 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
13899
15319
|
provider: "a2a",
|
|
13900
15320
|
instruction,
|
|
13901
15321
|
messageText: responseText,
|
|
15322
|
+
viveworker: task?.viveworker || entry?.viveworker || {},
|
|
15323
|
+
paidDeliverable: task?.paidDeliverable || entry?.paidDeliverable || null,
|
|
13902
15324
|
taskId: task?.id || "",
|
|
13903
15325
|
taskStatus: task?.status || entry?.taskStatus || (kind === "a2a_task_result" ? "completed" : "submitted"),
|
|
13904
15326
|
callerInfo: task?.callerInfo || {},
|
|
@@ -14168,7 +15590,7 @@ function buildWebAppHtml({ pairToken }) {
|
|
|
14168
15590
|
<link rel="manifest" href="${escapeHtml(manifestHref)}">
|
|
14169
15591
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
14170
15592
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
14171
|
-
<link rel="stylesheet" href="/app.css">
|
|
15593
|
+
<link rel="stylesheet" href="/app.css?v=${escapeHtml(WEB_APP_BUILD_ID)}">
|
|
14172
15594
|
<style>
|
|
14173
15595
|
.boot-splash {
|
|
14174
15596
|
position: fixed;
|
|
@@ -14378,6 +15800,107 @@ function logRemotePairingBootTrace(body, session, req) {
|
|
|
14378
15800
|
}
|
|
14379
15801
|
}
|
|
14380
15802
|
|
|
15803
|
+
function logClientEvent(body, session, req) {
|
|
15804
|
+
const type = cleanText(body?.type || "").slice(0, 80);
|
|
15805
|
+
if (type !== "timeline-render") {
|
|
15806
|
+
return;
|
|
15807
|
+
}
|
|
15808
|
+
|
|
15809
|
+
const route = cleanText(body?.route || "").slice(0, 24) || "unknown";
|
|
15810
|
+
const reason = cleanText(body?.reason || "").slice(0, 80) || "unknown";
|
|
15811
|
+
const latestToken = cleanText(body?.latestToken || "").slice(0, 80) || "none";
|
|
15812
|
+
const latestKind = cleanText(body?.latestKind || "").slice(0, 80) || "unknown";
|
|
15813
|
+
const appBuildId = cleanText(body?.appBuildId || "").slice(0, 80) || "unknown";
|
|
15814
|
+
const currentTab = cleanText(body?.currentTab || "").slice(0, 40) || "unknown";
|
|
15815
|
+
const entryCount = Math.max(0, Math.min(10_000, Math.round(Number(body?.entryCount) || 0)));
|
|
15816
|
+
const latestCreatedAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(body?.latestCreatedAtMs) || 0)));
|
|
15817
|
+
const clientAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(body?.clientAtMs) || 0)));
|
|
15818
|
+
const visible = body?.visible === true;
|
|
15819
|
+
const renderedTokens = Array.isArray(body?.renderedTokens)
|
|
15820
|
+
? body.renderedTokens
|
|
15821
|
+
.slice(0, 5)
|
|
15822
|
+
.map((item) => {
|
|
15823
|
+
const token = cleanText(item?.token || "").slice(0, 80);
|
|
15824
|
+
const kind = cleanText(item?.kind || "").slice(0, 80);
|
|
15825
|
+
const createdAtMs = Math.max(0, Math.min(9_999_999_999_999, Math.round(Number(item?.createdAtMs) || 0)));
|
|
15826
|
+
return token && kind ? `${kind}:${token}:${createdAtMs}` : "";
|
|
15827
|
+
})
|
|
15828
|
+
.filter(Boolean)
|
|
15829
|
+
: [];
|
|
15830
|
+
const deviceId = cleanText(session?.deviceId || "").slice(0, 60) || "unknown";
|
|
15831
|
+
const ua = requestUserAgent(req).slice(0, 90);
|
|
15832
|
+
|
|
15833
|
+
console.log(
|
|
15834
|
+
`[client-event] timeline-render at=${new Date().toISOString()} device=${deviceId} ` +
|
|
15835
|
+
`route=${route} visible=${visible ? 1 : 0} tab=${currentTab} reason=${reason} ` +
|
|
15836
|
+
`latest=${latestKind}:${latestToken} latestCreatedAtMs=${latestCreatedAtMs} ` +
|
|
15837
|
+
`clientAtMs=${clientAtMs} count=${entryCount} build=${appBuildId} ` +
|
|
15838
|
+
`rendered=${renderedTokens.join(",") || "none"} ua=${JSON.stringify(ua)}`
|
|
15839
|
+
);
|
|
15840
|
+
}
|
|
15841
|
+
|
|
15842
|
+
function writeSseEvent(res, eventName, payload) {
|
|
15843
|
+
res.write(`event: ${eventName}\n`);
|
|
15844
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
15845
|
+
}
|
|
15846
|
+
|
|
15847
|
+
function openTimelineStream({ config, runtime, req, res, session }) {
|
|
15848
|
+
if (!config.timelineLiveSync) {
|
|
15849
|
+
return writeJson(res, 404, { error: "timeline-live-sync-disabled" });
|
|
15850
|
+
}
|
|
15851
|
+
res.statusCode = 200;
|
|
15852
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
15853
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
15854
|
+
res.setHeader("Connection", "keep-alive");
|
|
15855
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
15856
|
+
res.flushHeaders?.();
|
|
15857
|
+
|
|
15858
|
+
let closed = false;
|
|
15859
|
+
const deviceId = cleanText(session?.deviceId || "").slice(0, 60) || "unknown";
|
|
15860
|
+
const bus = ensureTimelineBus(runtime);
|
|
15861
|
+
const client = {
|
|
15862
|
+
send(eventName, payload) {
|
|
15863
|
+
if (closed || res.destroyed || res.writableEnded) {
|
|
15864
|
+
throw new Error("timeline-stream-closed");
|
|
15865
|
+
}
|
|
15866
|
+
writeSseEvent(res, eventName, payload);
|
|
15867
|
+
},
|
|
15868
|
+
close() {
|
|
15869
|
+
if (closed) return;
|
|
15870
|
+
closed = true;
|
|
15871
|
+
clearInterval(heartbeat);
|
|
15872
|
+
remove();
|
|
15873
|
+
try {
|
|
15874
|
+
res.end();
|
|
15875
|
+
} catch {}
|
|
15876
|
+
},
|
|
15877
|
+
};
|
|
15878
|
+
const remove = bus.addClient(client);
|
|
15879
|
+
const heartbeat = setInterval(() => {
|
|
15880
|
+
try {
|
|
15881
|
+
client.send("heartbeat", {
|
|
15882
|
+
atMs: Date.now(),
|
|
15883
|
+
revision: Number(runtime.timelineRevision) || 0,
|
|
15884
|
+
});
|
|
15885
|
+
} catch {
|
|
15886
|
+
client.close();
|
|
15887
|
+
}
|
|
15888
|
+
}, 20_000);
|
|
15889
|
+
heartbeat.unref?.();
|
|
15890
|
+
|
|
15891
|
+
req.on("close", () => client.close());
|
|
15892
|
+
req.on("aborted", () => client.close());
|
|
15893
|
+
res.on("error", () => client.close());
|
|
15894
|
+
client.send("hello", {
|
|
15895
|
+
ok: true,
|
|
15896
|
+
revision: Number(runtime.timelineRevision) || 0,
|
|
15897
|
+
appBuildId: WEB_APP_BUILD_ID,
|
|
15898
|
+
deviceId,
|
|
15899
|
+
atMs: Date.now(),
|
|
15900
|
+
});
|
|
15901
|
+
console.log(`[timeline-stream] open device=${deviceId} clients=${bus.clients.size}`);
|
|
15902
|
+
}
|
|
15903
|
+
|
|
14381
15904
|
function recordRemotePairingAudit(event) {
|
|
14382
15905
|
appendRemotePairingAuditEvent({
|
|
14383
15906
|
atMs: Date.now(),
|
|
@@ -14550,7 +16073,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
14550
16073
|
if (action === "approve") {
|
|
14551
16074
|
// Resolve which executor to use.
|
|
14552
16075
|
const available = runtime.a2aAvailableExecutors || { codex: false, claude: false };
|
|
14553
|
-
const preference = requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
16076
|
+
const preference = requestedExecutor || task.viveworker?.requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
14554
16077
|
let executor;
|
|
14555
16078
|
if (preference === "codex" && available.codex) executor = "codex";
|
|
14556
16079
|
else if (preference === "claude" && available.claude) executor = "claude";
|
|
@@ -15459,6 +16982,29 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15459
16982
|
return writeJson(res, 200, { ok: true });
|
|
15460
16983
|
}
|
|
15461
16984
|
|
|
16985
|
+
// POST /api/client-events — local-only diagnostics from the PWA.
|
|
16986
|
+
//
|
|
16987
|
+
// The endpoint deliberately accepts only small sanitized metadata, not
|
|
16988
|
+
// message text, commands, file paths, relay tokens, or public keys. It is
|
|
16989
|
+
// used to correlate "bridge accepted" timings with "the PWA rendered the
|
|
16990
|
+
// latest timeline item" timings while debugging LAN/remote freshness.
|
|
16991
|
+
if (url.pathname === "/api/client-events" && req.method === "POST") {
|
|
16992
|
+
const session = requireApiSession(req, res, config, state);
|
|
16993
|
+
if (!session) return;
|
|
16994
|
+
let body;
|
|
16995
|
+
try {
|
|
16996
|
+
body = await parseJsonBody(req);
|
|
16997
|
+
} catch (err) {
|
|
16998
|
+
return writeJson(res, 400, { error: "invalid-json-body", message: err.message });
|
|
16999
|
+
}
|
|
17000
|
+
try {
|
|
17001
|
+
logClientEvent(body, session, req);
|
|
17002
|
+
} catch (err) {
|
|
17003
|
+
console.warn(`[client-event] failed to log event: ${err?.message}`);
|
|
17004
|
+
}
|
|
17005
|
+
return writeJson(res, 200, { ok: true });
|
|
17006
|
+
}
|
|
17007
|
+
|
|
15462
17008
|
// POST /api/remote-pairing/toggle — flip on/off without restart.
|
|
15463
17009
|
//
|
|
15464
17010
|
// Body: { enabled: boolean }
|
|
@@ -16064,6 +17610,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16064
17610
|
stableId: `thread_share:${shareId}`,
|
|
16065
17611
|
title: `Thread Share: ${sourceLabel || sourceTool || "agent"} → ${targetLabel || targetTool}`,
|
|
16066
17612
|
body: String(content).slice(0, 160),
|
|
17613
|
+
buildLocalizedContent: ({ locale }) => localizedThreadSharePushContent(locale, {
|
|
17614
|
+
sourceLabel,
|
|
17615
|
+
sourceTool,
|
|
17616
|
+
targetLabel,
|
|
17617
|
+
targetTool,
|
|
17618
|
+
body: content,
|
|
17619
|
+
}),
|
|
16067
17620
|
});
|
|
16068
17621
|
} catch (error) {
|
|
16069
17622
|
console.error(`[thread-share-push] ${error.message}`);
|
|
@@ -16163,6 +17716,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16163
17716
|
stableId: `thread_share_fallback:${share.shareId}`,
|
|
16164
17717
|
title: "Thread Share: target unreachable",
|
|
16165
17718
|
body: `Codex thread "${share.targetLabel || share.targetConversationId.slice(0, 8)}" is not connected. Content saved to inbox.`,
|
|
17719
|
+
buildLocalizedContent: ({ locale }) => localizedThreadShareFallbackPushContent(
|
|
17720
|
+
locale,
|
|
17721
|
+
share.targetLabel || share.targetConversationId.slice(0, 8)
|
|
17722
|
+
),
|
|
16166
17723
|
});
|
|
16167
17724
|
} catch (error) {
|
|
16168
17725
|
console.error(`[thread-share-fallback-push] ${error.message}`);
|
|
@@ -16255,7 +17812,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16255
17812
|
rangeDate = d.toISOString().slice(0, 10);
|
|
16256
17813
|
}
|
|
16257
17814
|
|
|
16258
|
-
const relevantKinds = new Set(["file_event", "completion", "plan_ready", "assistant_final"]);
|
|
17815
|
+
const relevantKinds = new Set(["file_event", "command_event", "completion", "plan_ready", "assistant_final"]);
|
|
16259
17816
|
const entries = (runtime.recentTimelineEntries || [])
|
|
16260
17817
|
.filter((e) => e.createdAtMs >= rangeStart && e.createdAtMs < rangeEnd && relevantKinds.has(e.kind))
|
|
16261
17818
|
.map((e) => ({
|
|
@@ -16424,7 +17981,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16424
17981
|
stableId: pendingApprovalStableId(approval),
|
|
16425
17982
|
title: approval.title,
|
|
16426
17983
|
body: approval.messageText,
|
|
16427
|
-
buildLocalizedContent: () => (
|
|
17984
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16428
17985
|
}).catch((error) => {
|
|
16429
17986
|
console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
|
|
16430
17987
|
});
|
|
@@ -16514,10 +18071,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16514
18071
|
stableId: pendingApprovalStableId(approval),
|
|
16515
18072
|
title: approval.title,
|
|
16516
18073
|
body: approval.messageText,
|
|
16517
|
-
buildLocalizedContent: () => (
|
|
16518
|
-
title: approval.title,
|
|
16519
|
-
body: approval.messageText,
|
|
16520
|
-
}),
|
|
18074
|
+
buildLocalizedContent: ({ locale }) => localizedPaymentApprovalPushContent(locale, approval),
|
|
16521
18075
|
}).catch((error) => {
|
|
16522
18076
|
console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
16523
18077
|
});
|
|
@@ -16589,6 +18143,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16589
18143
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
16590
18144
|
console.log(`[claude-stop] threadId=${threadId} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
16591
18145
|
if (threadId) {
|
|
18146
|
+
clearTimelineActivity({
|
|
18147
|
+
config,
|
|
18148
|
+
runtime,
|
|
18149
|
+
provider: "claude",
|
|
18150
|
+
threadId,
|
|
18151
|
+
source: "claude-stop",
|
|
18152
|
+
});
|
|
16592
18153
|
const pending = [];
|
|
16593
18154
|
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
16594
18155
|
if (!approval.resolved && approval.conversationId === threadId) {
|
|
@@ -16659,6 +18220,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16659
18220
|
// for this session/tool as accepted (so iPhone moves it to completed).
|
|
16660
18221
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
16661
18222
|
const toolName = String(body.toolName || "");
|
|
18223
|
+
const eventCwd = String(body.cwd || "");
|
|
18224
|
+
const threadLabel =
|
|
18225
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18226
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18227
|
+
threadId.slice(0, 40);
|
|
18228
|
+
upsertTimelineActivity({
|
|
18229
|
+
config,
|
|
18230
|
+
runtime,
|
|
18231
|
+
provider: "claude",
|
|
18232
|
+
threadId,
|
|
18233
|
+
threadLabel,
|
|
18234
|
+
phase: "thinking",
|
|
18235
|
+
cwd: eventCwd,
|
|
18236
|
+
source: "claude-post-tool",
|
|
18237
|
+
});
|
|
16662
18238
|
const fingerprint = claudeToolFingerprint(toolName, body.toolInput);
|
|
16663
18239
|
console.log(`[claude-post-tool] threadId=${threadId} tool=${toolName} fp=${fingerprint.slice(0, 80)} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
16664
18240
|
const match = findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint);
|
|
@@ -16694,6 +18270,31 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16694
18270
|
}
|
|
16695
18271
|
}
|
|
16696
18272
|
|
|
18273
|
+
if (eventType === "PreToolUse") {
|
|
18274
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
18275
|
+
const eventCwd = String(body.cwd || "");
|
|
18276
|
+
const toolName = String(body.toolName || "");
|
|
18277
|
+
const toolActivity = timelineActivityFromClaudeTool(toolName, isPlainObject(body.toolInput) ? body.toolInput : {});
|
|
18278
|
+
const threadLabel =
|
|
18279
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18280
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18281
|
+
threadId.slice(0, 40);
|
|
18282
|
+
upsertTimelineActivity({
|
|
18283
|
+
config,
|
|
18284
|
+
runtime,
|
|
18285
|
+
provider: "claude",
|
|
18286
|
+
threadId,
|
|
18287
|
+
threadLabel,
|
|
18288
|
+
phase: toolActivity.phase,
|
|
18289
|
+
summary: toolActivity.summary,
|
|
18290
|
+
commandText: toolActivity.commandText,
|
|
18291
|
+
fileRefs: toolActivity.fileRefs,
|
|
18292
|
+
cwd: eventCwd,
|
|
18293
|
+
source: "claude-pre-tool",
|
|
18294
|
+
});
|
|
18295
|
+
return writeJson(res, 200, {});
|
|
18296
|
+
}
|
|
18297
|
+
|
|
16697
18298
|
if (eventType !== "approval_request") {
|
|
16698
18299
|
// Non-approval events (Notification, Stop, etc.) — acknowledge immediately
|
|
16699
18300
|
return writeJson(res, 200, {});
|
|
@@ -16705,6 +18306,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16705
18306
|
const threadId = String(body.threadId || "");
|
|
16706
18307
|
const requestId = String(body.requestId || crypto.randomUUID());
|
|
16707
18308
|
const requestKey = `${threadId}:${requestId}`;
|
|
18309
|
+
const approvalKind = String(body.approvalKind || "command");
|
|
18310
|
+
const rawParams = claudeApprovalRawParams(body, approvalKind);
|
|
16708
18311
|
|
|
16709
18312
|
const approval = {
|
|
16710
18313
|
token,
|
|
@@ -16712,13 +18315,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16712
18315
|
conversationId: threadId,
|
|
16713
18316
|
requestId,
|
|
16714
18317
|
ownerClientId: null,
|
|
16715
|
-
kind:
|
|
18318
|
+
kind: approvalKind,
|
|
16716
18319
|
threadLabel:
|
|
16717
18320
|
runtime.claudeSessionTitles.get(threadId) ||
|
|
16718
18321
|
(body.cwd ? path.basename(String(body.cwd)) : "") ||
|
|
16719
18322
|
String(body.threadId || "").slice(0, 40),
|
|
16720
18323
|
title: config.approvalTitle,
|
|
16721
18324
|
messageText: String(body.messageText || ""),
|
|
18325
|
+
rawParams,
|
|
16722
18326
|
fileRefs: Array.isArray(body.fileRefs) ? body.fileRefs : [],
|
|
16723
18327
|
previousFileRefs: [],
|
|
16724
18328
|
diffText: String(body.diffText || ""),
|
|
@@ -16741,6 +18345,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16741
18345
|
readOnly: body.notifyOnly === true,
|
|
16742
18346
|
};
|
|
16743
18347
|
|
|
18348
|
+
upsertTimelineActivity({
|
|
18349
|
+
config,
|
|
18350
|
+
runtime,
|
|
18351
|
+
provider: "claude",
|
|
18352
|
+
threadId,
|
|
18353
|
+
threadLabel: approval.threadLabel,
|
|
18354
|
+
phase: "awaiting_approval",
|
|
18355
|
+
summary: approval.messageText,
|
|
18356
|
+
fileRefs: approval.fileRefs,
|
|
18357
|
+
cwd: approval.cwd,
|
|
18358
|
+
source: "claude-approval-request",
|
|
18359
|
+
atMs: approval.createdAtMs,
|
|
18360
|
+
});
|
|
18361
|
+
|
|
16744
18362
|
const claudeAutoPilotMatch =
|
|
16745
18363
|
approval.kind === "command"
|
|
16746
18364
|
? maybeAutoApproveTrustedRead({
|
|
@@ -16797,21 +18415,28 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16797
18415
|
runtime.nativeApprovalsByToken.set(token, approval);
|
|
16798
18416
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
16799
18417
|
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
buildLocalizedContent: ({ locale }) => ({
|
|
16811
|
-
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
18418
|
+
try {
|
|
18419
|
+
const pushChanged = await deliverWebPushItem({
|
|
18420
|
+
config,
|
|
18421
|
+
state,
|
|
18422
|
+
kind: "approval",
|
|
18423
|
+
tab: "inbox",
|
|
18424
|
+
subtab: "pending",
|
|
18425
|
+
token: approval.token,
|
|
18426
|
+
stableId: pendingApprovalStableId(approval),
|
|
18427
|
+
title: approval.title,
|
|
16812
18428
|
body: approval.messageText,
|
|
16813
|
-
|
|
16814
|
-
|
|
18429
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18430
|
+
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
18431
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
18432
|
+
}),
|
|
18433
|
+
});
|
|
18434
|
+
if (pushChanged) {
|
|
18435
|
+
await saveState(config.stateFile, state);
|
|
18436
|
+
}
|
|
18437
|
+
} catch (error) {
|
|
18438
|
+
console.error(`[claude-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
18439
|
+
}
|
|
16815
18440
|
|
|
16816
18441
|
// Notify-only path: phone gets a read-only entry + push, but the
|
|
16817
18442
|
// hook does not block on a decision. The PostToolUse handler will
|
|
@@ -16938,6 +18563,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16938
18563
|
stableId: `moltbook_reply:${item.sourceId}`,
|
|
16939
18564
|
title: item.title,
|
|
16940
18565
|
body: item.summary,
|
|
18566
|
+
buildLocalizedContent: ({ locale }) => localizedMoltbookReplyPushContent(locale, item),
|
|
16941
18567
|
});
|
|
16942
18568
|
} catch (error) {
|
|
16943
18569
|
console.error(`[moltbook-push-error] ${error.message}`);
|
|
@@ -17033,6 +18659,22 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17033
18659
|
if (draftType === "reply" && !postId) {
|
|
17034
18660
|
return writeJson(res, 400, { error: "missing-postId-for-reply" });
|
|
17035
18661
|
}
|
|
18662
|
+
if (draftType !== "original_post") {
|
|
18663
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
18664
|
+
const quota = await getMoltbookReplyQuotaState({ state: scoutState, maxDaily: 5 });
|
|
18665
|
+
if (quota.quotaReached) {
|
|
18666
|
+
await writeScoutState(scoutState);
|
|
18667
|
+
return writeJson(res, 409, {
|
|
18668
|
+
error: "moltbook-reply-quota-reached",
|
|
18669
|
+
sentToday: quota.sentToday,
|
|
18670
|
+
pendingToday: quota.pendingToday,
|
|
18671
|
+
usedToday: quota.usedToday,
|
|
18672
|
+
maxDaily: quota.maxDaily,
|
|
18673
|
+
day: quota.day,
|
|
18674
|
+
});
|
|
18675
|
+
}
|
|
18676
|
+
await writeScoutState(scoutState);
|
|
18677
|
+
}
|
|
17036
18678
|
const token = historyToken(`moltbook_draft:${sourceId}`);
|
|
17037
18679
|
const postTitle = cleanText(body.postTitle || "");
|
|
17038
18680
|
const postUrl = cleanText(body.postUrl || `https://www.moltbook.com/post/${postId}`);
|
|
@@ -17097,20 +18739,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17097
18739
|
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
17098
18740
|
}
|
|
17099
18741
|
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
|
-
})();
|
|
18742
|
+
const pushTitle = localizedMoltbookDraftPushTitle(config.defaultLocale, { draftType, postTitle, slot });
|
|
17114
18743
|
await deliverWebPushItem({
|
|
17115
18744
|
config,
|
|
17116
18745
|
state,
|
|
@@ -17121,6 +18750,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17121
18750
|
stableId: `moltbook_draft:${sourceId}`,
|
|
17122
18751
|
title: pushTitle,
|
|
17123
18752
|
body: draftType === "original_post" ? (postTitle || String(draftText).slice(0, 160)) : String(draftText).slice(0, 160),
|
|
18753
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18754
|
+
title: localizedMoltbookDraftPushTitle(locale, { draftType, postTitle, slot }),
|
|
18755
|
+
body: draftType === "original_post" ? (postTitle || pushBodySnippet(draftText)) : pushBodySnippet(draftText),
|
|
18756
|
+
}),
|
|
17124
18757
|
});
|
|
17125
18758
|
} catch (error) {
|
|
17126
18759
|
console.error(`[moltbook-draft-push-error] ${error.message}`);
|
|
@@ -17177,6 +18810,17 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17177
18810
|
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
17178
18811
|
const editedText = cleanText(body.editedText || "");
|
|
17179
18812
|
const editedTitle = cleanText(body.editedTitle || "");
|
|
18813
|
+
if (action === "approve" && draft.draftType !== "original_post") {
|
|
18814
|
+
const quota = await reserveMoltbookReplyQuotaSlot(draft, 5);
|
|
18815
|
+
if (quota.quotaReached) {
|
|
18816
|
+
return writeJson(res, 409, {
|
|
18817
|
+
error: "moltbook-reply-quota-reached",
|
|
18818
|
+
sentToday: quota.sentToday,
|
|
18819
|
+
maxDaily: quota.maxDaily,
|
|
18820
|
+
day: quota.day,
|
|
18821
|
+
});
|
|
18822
|
+
}
|
|
18823
|
+
}
|
|
17180
18824
|
const decision = {
|
|
17181
18825
|
action,
|
|
17182
18826
|
text: action === "approve" ? editedText || draft.draftText : "",
|
|
@@ -17210,6 +18854,15 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17210
18854
|
await executeMoltbookDraftPost(draft, config, runtime, state);
|
|
17211
18855
|
} catch (postError) {
|
|
17212
18856
|
console.error(`[moltbook-draft-post-error] ${postError.message}`);
|
|
18857
|
+
await releaseMoltbookReplyQuotaSlot(draft).catch((error) => console.error(`[moltbook-quota-release] ${error.message}`));
|
|
18858
|
+
if (draft.parentCommentId) {
|
|
18859
|
+
await updateInboxStatus(draft.parentCommentId, "pending", {
|
|
18860
|
+
source: "mobile-draft-approve",
|
|
18861
|
+
draftStatus: "failed",
|
|
18862
|
+
draftToken: draft.token,
|
|
18863
|
+
draftError: String(postError.message || "").slice(0, 500),
|
|
18864
|
+
}).catch((error) => console.error(`[moltbook-inbox-post-failed] ${error.message}`));
|
|
18865
|
+
}
|
|
17213
18866
|
try {
|
|
17214
18867
|
await deliverWebPushItem({
|
|
17215
18868
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
@@ -17218,6 +18871,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17218
18871
|
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
17219
18872
|
title: "Draft post failed",
|
|
17220
18873
|
body: String(postError.message || "").slice(0, 160),
|
|
18874
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18875
|
+
title: t(locale, "server.push.moltbookDraft.failedTitle"),
|
|
18876
|
+
body: pushBodySnippet(postError.message || ""),
|
|
18877
|
+
}),
|
|
17221
18878
|
});
|
|
17222
18879
|
} catch { /* ignore push error */ }
|
|
17223
18880
|
}
|
|
@@ -17225,6 +18882,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17225
18882
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
17226
18883
|
})();
|
|
17227
18884
|
} else {
|
|
18885
|
+
if (draft.parentCommentId) {
|
|
18886
|
+
await updateInboxStatus(draft.parentCommentId, "skipped", {
|
|
18887
|
+
source: "mobile-draft-deny",
|
|
18888
|
+
draftStatus: "denied",
|
|
18889
|
+
draftToken: draft.token,
|
|
18890
|
+
skippedAt: new Date().toISOString(),
|
|
18891
|
+
}).catch((error) => console.error(`[moltbook-inbox-denied] ${error.message}`));
|
|
18892
|
+
}
|
|
17228
18893
|
// Deny — clean up.
|
|
17229
18894
|
await deleteDraft(token).catch(() => {});
|
|
17230
18895
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
@@ -17259,6 +18924,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17259
18924
|
return writeJson(res, 200, buildTimelineResponse(runtime, state, config, locale));
|
|
17260
18925
|
}
|
|
17261
18926
|
|
|
18927
|
+
if (url.pathname === "/api/timeline/stream" && req.method === "GET") {
|
|
18928
|
+
const session = requireApiSession(req, res, config, state);
|
|
18929
|
+
if (!session) {
|
|
18930
|
+
return;
|
|
18931
|
+
}
|
|
18932
|
+
return openTimelineStream({ config, runtime, req, res, session });
|
|
18933
|
+
}
|
|
18934
|
+
|
|
17262
18935
|
const apiTimelineImageMatch = url.pathname.match(/^\/api\/timeline\/([^/]+)\/images\/(\d+)$/u);
|
|
17263
18936
|
if (apiTimelineImageMatch && req.method === "GET") {
|
|
17264
18937
|
const token = decodeURIComponent(apiTimelineImageMatch[1]);
|
|
@@ -17327,7 +19000,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17327
19000
|
const payload = contentType.includes("multipart/form-data")
|
|
17328
19001
|
? await stageCompletionReplyImages(config, req)
|
|
17329
19002
|
: await parseJsonBody(req);
|
|
17330
|
-
await handleCompletionReply({
|
|
19003
|
+
const replyResult = await handleCompletionReply({
|
|
17331
19004
|
config,
|
|
17332
19005
|
runtime,
|
|
17333
19006
|
state,
|
|
@@ -17341,6 +19014,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17341
19014
|
ok: true,
|
|
17342
19015
|
planMode: payload?.planMode === true,
|
|
17343
19016
|
imageCount: Array.isArray(payload?.localImagePaths) ? payload.localImagePaths.length : 0,
|
|
19017
|
+
alreadyAccepted: replyResult?.alreadyAccepted === true,
|
|
19018
|
+
ackTimeout: replyResult?.ackTimeout === true,
|
|
17344
19019
|
});
|
|
17345
19020
|
} catch (error) {
|
|
17346
19021
|
if (error.message === "completion-reply-empty") {
|
|
@@ -17381,6 +19056,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17381
19056
|
const decision = apiApprovalDecisionMatch[2];
|
|
17382
19057
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
17383
19058
|
if (!approval) {
|
|
19059
|
+
const historicalDecision = approvalDecisionFromHistoryItem(historyItemByToken(runtime, "approval", token));
|
|
19060
|
+
if (historicalDecision) {
|
|
19061
|
+
if (historicalDecision === decision) {
|
|
19062
|
+
return writeJson(res, 200, {
|
|
19063
|
+
ok: true,
|
|
19064
|
+
decision,
|
|
19065
|
+
alreadyHandled: true,
|
|
19066
|
+
});
|
|
19067
|
+
}
|
|
19068
|
+
return writeJson(res, 409, {
|
|
19069
|
+
error: "approval-already-handled",
|
|
19070
|
+
decision: historicalDecision,
|
|
19071
|
+
});
|
|
19072
|
+
}
|
|
17384
19073
|
return writeJson(res, 404, { error: "approval-not-found" });
|
|
17385
19074
|
}
|
|
17386
19075
|
if (approval.resolved || approval.resolving) {
|
|
@@ -19283,9 +20972,21 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
19283
20972
|
createdPostId = draft.postId || null;
|
|
19284
20973
|
createdCommentId = comment?.id || null;
|
|
19285
20974
|
console.log(`[moltbook-draft-post] Posted reply (commentId=${comment?.id}) to post ${draft.postId}`);
|
|
20975
|
+
if (draft.parentCommentId) {
|
|
20976
|
+
await updateInboxStatus(draft.parentCommentId, "replied", {
|
|
20977
|
+
source: "mobile-draft-approve",
|
|
20978
|
+
replyText: finalText,
|
|
20979
|
+
replyCommentId: createdCommentId || "",
|
|
20980
|
+
replyVerification: verification || null,
|
|
20981
|
+
draftStatus: "posted",
|
|
20982
|
+
draftToken: draft.token,
|
|
20983
|
+
}).catch((error) => console.error(`[moltbook-inbox-replied] ${error.message}`));
|
|
20984
|
+
}
|
|
19286
20985
|
|
|
19287
20986
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
19288
|
-
|
|
20987
|
+
if (!draft.replyQuotaReserved) {
|
|
20988
|
+
scoutState.sentToday += 1;
|
|
20989
|
+
}
|
|
19289
20990
|
markPostSeen(scoutState, draft.postId, "published");
|
|
19290
20991
|
// Append the reply to `recentComposeTitles` so it shows up in the
|
|
19291
20992
|
// "最近の投稿" list with its reply badge. `recordComposeAttempt` knows
|
|
@@ -19381,6 +21082,10 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
19381
21082
|
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
19382
21083
|
title: "Moltbook posted",
|
|
19383
21084
|
body: truncate(singleLine(pushTitle), 160),
|
|
21085
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
21086
|
+
title: t(locale, "server.push.moltbookDraft.postedTitle"),
|
|
21087
|
+
body: localizedMoltbookPostedBody(locale, draft, finalTitle),
|
|
21088
|
+
}),
|
|
19384
21089
|
});
|
|
19385
21090
|
} catch { /* ignore push error */ }
|
|
19386
21091
|
|
|
@@ -19406,6 +21111,10 @@ function buildConfig(cli) {
|
|
|
19406
21111
|
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
19407
21112
|
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
19408
21113
|
a2aRelayRegisterSecret: cleanText(process.env.A2A_RELAY_REGISTER_SECRET || ""),
|
|
21114
|
+
a2aProModel: cleanText(process.env.A2A_PRO_MODEL || ""),
|
|
21115
|
+
a2aProPrice: cleanText(process.env.A2A_PRO_PRICE || ""),
|
|
21116
|
+
a2aProPayTo: cleanText(process.env.A2A_PRO_PAY_TO || ""),
|
|
21117
|
+
a2aProExpiresDays: cleanText(process.env.A2A_PRO_EXPIRES_DAYS || "7"),
|
|
19409
21118
|
// Remote-pairing relay (PWA off-LAN). Off by default — bridges that
|
|
19410
21119
|
// never leave the trusted LAN don't need to open an outbound connection
|
|
19411
21120
|
// to the relay. Toggle with REMOTE_PAIRING_ENABLED=true in
|
|
@@ -19439,6 +21148,8 @@ function buildConfig(cli) {
|
|
|
19439
21148
|
process.env.TIMELINE_ATTACHMENTS_DIR || path.join(path.dirname(stateFile), "timeline-attachments")
|
|
19440
21149
|
),
|
|
19441
21150
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
21151
|
+
timelineLiveSync: boolEnv("TIMELINE_LIVE_SYNC", true),
|
|
21152
|
+
timelineLiveDebounceMs: numberEnv("TIMELINE_LIVE_DEBOUNCE_MS", 75),
|
|
19442
21153
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
19443
21154
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
19444
21155
|
directoryScanIntervalMs: numberEnv("DIRECTORY_SCAN_INTERVAL_MS", 30000),
|
|
@@ -19497,6 +21208,10 @@ function buildConfig(cli) {
|
|
|
19497
21208
|
ipcSocketPath: resolvePath(process.env.CODEX_IPC_SOCKET_PATH || defaultIpcSocketPath()),
|
|
19498
21209
|
ipcReconnectMs: numberEnv("IPC_RECONNECT_MS", 1500),
|
|
19499
21210
|
ipcRequestTimeoutMs: numberEnv("IPC_REQUEST_TIMEOUT_MS", 12000),
|
|
21211
|
+
completionReplyAckTimeoutMs: numberEnv(
|
|
21212
|
+
"COMPLETION_REPLY_ACK_TIMEOUT_MS",
|
|
21213
|
+
DEFAULT_COMPLETION_REPLY_ACK_TIMEOUT_MS
|
|
21214
|
+
),
|
|
19500
21215
|
choicePageSize: numberEnv("CHOICE_PAGE_SIZE", 5),
|
|
19501
21216
|
completionReplyImageMaxBytes: numberEnv(
|
|
19502
21217
|
"COMPLETION_REPLY_IMAGE_MAX_BYTES",
|
|
@@ -21419,11 +23134,13 @@ async function main() {
|
|
|
21419
23134
|
`notifyPlans=${config.notifyPlans}`,
|
|
21420
23135
|
`nativeApprovalServer=${config.nativeApprovalPublicBaseUrl}`,
|
|
21421
23136
|
`pollMs=${config.pollIntervalMs}`,
|
|
23137
|
+
`timelineLive=${config.timelineLiveSync}`,
|
|
21422
23138
|
`replaySeconds=${config.replaySeconds}`,
|
|
21423
23139
|
].join(" | ")
|
|
21424
23140
|
);
|
|
21425
23141
|
|
|
21426
23142
|
let approvalServer = null;
|
|
23143
|
+
let timelineLiveHandle = null;
|
|
21427
23144
|
|
|
21428
23145
|
process.on("SIGINT", handleSignal);
|
|
21429
23146
|
process.on("SIGTERM", handleSignal);
|
|
@@ -21562,6 +23279,8 @@ async function main() {
|
|
|
21562
23279
|
}
|
|
21563
23280
|
}
|
|
21564
23281
|
|
|
23282
|
+
timelineLiveHandle = startTimelineLiveSync({ config, runtime, state });
|
|
23283
|
+
|
|
21565
23284
|
// --- A2A Relay ---
|
|
21566
23285
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
21567
23286
|
config.a2aAcceptPublicTasks = state.a2aAcceptPublicTasks === true;
|
|
@@ -21694,6 +23413,13 @@ async function main() {
|
|
|
21694
23413
|
} finally {
|
|
21695
23414
|
runtime.stopping = true;
|
|
21696
23415
|
|
|
23416
|
+
if (timelineLiveHandle) {
|
|
23417
|
+
try {
|
|
23418
|
+
timelineLiveHandle.close();
|
|
23419
|
+
} catch {}
|
|
23420
|
+
timelineLiveHandle = null;
|
|
23421
|
+
}
|
|
23422
|
+
|
|
21697
23423
|
stopRelayPolling();
|
|
21698
23424
|
|
|
21699
23425
|
if (runtime.remotePairingHandle) {
|