viveworker 0.8.2 → 0.8.4
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 +7 -4
- package/package.json +1 -1
- 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/share-cli.mjs +127 -54
- package/scripts/viveworker-bridge.mjs +758 -46
- package/web/app.css +33 -0
- package/web/app.js +86 -19
- package/web/build-id.js +1 -1
- package/web/i18n.js +26 -2
- package/web/remote-pairing/api-router.js +168 -15
- package/web/remote-pairing/transport.js +27 -3
|
@@ -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", "command_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;
|
|
@@ -70,6 +70,7 @@ const COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS = 2 * 60 * 1000;
|
|
|
70
70
|
const HAZBASE_METADATA_TIMEOUT_MS = 1500;
|
|
71
71
|
const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
|
|
72
72
|
const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
73
|
+
const TIMELINE_ACTIVITY_TTL_MS = 2 * 60 * 1000;
|
|
73
74
|
|
|
74
75
|
// Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
|
|
75
76
|
// per tracked repo (`git diff --name-status`, `git status --porcelain`,
|
|
@@ -265,6 +266,7 @@ const runtime = {
|
|
|
265
266
|
completionDetailsByToken: new Map(),
|
|
266
267
|
moltbookItemsByToken: new Map(),
|
|
267
268
|
moltbookDraftsByToken: new Map(),
|
|
269
|
+
moltbookReplyQuotaQueue: Promise.resolve(),
|
|
268
270
|
a2aTasksByToken: new Map(),
|
|
269
271
|
threadSharesByToken: new Map(),
|
|
270
272
|
threadRegistry: new Map(),
|
|
@@ -272,6 +274,7 @@ const runtime = {
|
|
|
272
274
|
recentHistoryItems: [],
|
|
273
275
|
recentTimelineEntries: [],
|
|
274
276
|
recentCodeEvents: [],
|
|
277
|
+
activeTimelineActivitiesByKey: new Map(),
|
|
275
278
|
timelineBus: null,
|
|
276
279
|
timelineRevision: 0,
|
|
277
280
|
timelineLiveWatchers: [],
|
|
@@ -346,6 +349,58 @@ setInterval(async () => {
|
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
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
|
+
}
|
|
349
404
|
const initialHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
350
405
|
const initialTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
351
406
|
const normalizedHistoryStateChanged =
|
|
@@ -514,6 +569,8 @@ function kindTitle(locale, kind) {
|
|
|
514
569
|
return t(locale, "server.title.assistantCommentary");
|
|
515
570
|
case "assistant_final":
|
|
516
571
|
return t(locale, "server.title.assistantFinal");
|
|
572
|
+
case "activity_status":
|
|
573
|
+
return t(locale, "server.title.activityStatus");
|
|
517
574
|
case "ambient_suggestions":
|
|
518
575
|
return t(locale, "server.title.ambientSuggestions");
|
|
519
576
|
case "approval":
|
|
@@ -829,7 +886,7 @@ function normalizeTimelineOutcome(value) {
|
|
|
829
886
|
|
|
830
887
|
function normalizeTimelineFileEventType(value) {
|
|
831
888
|
const normalized = cleanText(value || "").toLowerCase();
|
|
832
|
-
return ["read", "search", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
889
|
+
return ["read", "search", "git", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
833
890
|
}
|
|
834
891
|
|
|
835
892
|
function fileEventTitle(locale, fileEventType) {
|
|
@@ -838,6 +895,8 @@ function fileEventTitle(locale, fileEventType) {
|
|
|
838
895
|
return t(locale, "fileEvent.read");
|
|
839
896
|
case "search":
|
|
840
897
|
return t(locale, "fileEvent.search");
|
|
898
|
+
case "git":
|
|
899
|
+
return t(locale, "fileEvent.command");
|
|
841
900
|
case "command":
|
|
842
901
|
return t(locale, "fileEvent.command");
|
|
843
902
|
case "write":
|
|
@@ -860,6 +919,8 @@ function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
|
860
919
|
return t(locale, "detail.fileEvent.read", vars);
|
|
861
920
|
case "search":
|
|
862
921
|
return t(locale, "detail.fileEvent.search", vars);
|
|
922
|
+
case "git":
|
|
923
|
+
return t(locale, "detail.fileEvent.command", vars);
|
|
863
924
|
case "command":
|
|
864
925
|
return t(locale, "detail.fileEvent.command", vars);
|
|
865
926
|
case "write":
|
|
@@ -1531,7 +1592,7 @@ function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
|
|
|
1531
1592
|
return null;
|
|
1532
1593
|
}
|
|
1533
1594
|
|
|
1534
|
-
const command =
|
|
1595
|
+
const command = commandBaseName(tokens[0]);
|
|
1535
1596
|
if (!command || /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(command)) {
|
|
1536
1597
|
return null;
|
|
1537
1598
|
}
|
|
@@ -1606,8 +1667,7 @@ function classifyTimelineCommand(commandText) {
|
|
|
1606
1667
|
if (["rg", "grep", "ag", "ack", "fd", "find"].includes(command) || gitSubcommand === "grep") {
|
|
1607
1668
|
fileEventType = "search";
|
|
1608
1669
|
} else if (
|
|
1609
|
-
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command)
|
|
1610
|
-
(command === "git" && ["status", "diff", "show", "log", "ls-files", "branch"].includes(gitSubcommand))
|
|
1670
|
+
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command)
|
|
1611
1671
|
) {
|
|
1612
1672
|
fileEventType = "read";
|
|
1613
1673
|
}
|
|
@@ -1668,15 +1728,19 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1668
1728
|
return [];
|
|
1669
1729
|
}
|
|
1670
1730
|
|
|
1671
|
-
if (command === "cat" || command === "nl") {
|
|
1672
|
-
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"]) }));
|
|
1673
1737
|
}
|
|
1674
1738
|
|
|
1675
1739
|
if (command === "sed") {
|
|
1676
1740
|
if (!tokens.includes("-n")) {
|
|
1677
1741
|
return [];
|
|
1678
1742
|
}
|
|
1679
|
-
return normalizeTimelineFileRefs(tokens
|
|
1743
|
+
return normalizeTimelineFileRefs(sedCommandPathArgs(tokens));
|
|
1680
1744
|
}
|
|
1681
1745
|
|
|
1682
1746
|
if (command === "rg") {
|
|
@@ -1707,6 +1771,57 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1707
1771
|
return [];
|
|
1708
1772
|
}
|
|
1709
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
|
+
|
|
1710
1825
|
function autoPilotApprovalMessage(locale, commandText) {
|
|
1711
1826
|
const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
|
|
1712
1827
|
const commandBlock = cleanText(commandText || "")
|
|
@@ -3377,6 +3492,8 @@ function migrateRecentCodeEventsState({ config, runtime, state }) {
|
|
|
3377
3492
|
|
|
3378
3493
|
function timelineKindSortPriority(kind) {
|
|
3379
3494
|
switch (cleanText(kind || "")) {
|
|
3495
|
+
case "activity_status":
|
|
3496
|
+
return 90;
|
|
3380
3497
|
case "completion":
|
|
3381
3498
|
return 70;
|
|
3382
3499
|
case "approval":
|
|
@@ -3490,6 +3607,8 @@ function normalizeTimelineEntry(raw) {
|
|
|
3490
3607
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
3491
3608
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
3492
3609
|
provider: normalizeProvider(raw.provider),
|
|
3610
|
+
...(raw.activityPhase != null ? { activityPhase: cleanText(raw.activityPhase) } : {}),
|
|
3611
|
+
...(raw.commandText != null ? { commandText: cleanText(raw.commandText) } : {}),
|
|
3493
3612
|
// Moltbook-specific fields — preserved for detail view fallback after
|
|
3494
3613
|
// the in-memory draft/item expires.
|
|
3495
3614
|
...(raw.draftText != null ? { draftText: cleanText(raw.draftText) } : {}),
|
|
@@ -3583,6 +3702,229 @@ function publishTimelineUpdate({ config, runtime, entry, source = "unknown" }) {
|
|
|
3583
3702
|
ensureTimelineBus(runtime).emit("timeline:update", payload);
|
|
3584
3703
|
}
|
|
3585
3704
|
|
|
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
|
+
|
|
3586
3928
|
function recordTimelineEntry({ config, runtime, state, entry, source = "unknown" }) {
|
|
3587
3929
|
const normalized = normalizeTimelineEntry(entry);
|
|
3588
3930
|
if (!normalized) {
|
|
@@ -4599,6 +4941,17 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4599
4941
|
entry: timelineEntry,
|
|
4600
4942
|
source: "codex-rollout",
|
|
4601
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
|
+
});
|
|
4602
4955
|
}
|
|
4603
4956
|
|
|
4604
4957
|
const fileTimelineEntries = await buildRolloutFileTimelineEntries({
|
|
@@ -5032,6 +5385,23 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
5032
5385
|
for (const toolEntry of toolEntries) {
|
|
5033
5386
|
dirty = recordTimelineEntry({ config, runtime, state, entry: toolEntry, source: "claude-tool" }) || dirty;
|
|
5034
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
|
+
}
|
|
5035
5405
|
}
|
|
5036
5406
|
|
|
5037
5407
|
let text = "";
|
|
@@ -5071,6 +5441,28 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
5071
5441
|
: stopReason === "end_turn"
|
|
5072
5442
|
? "assistant_final"
|
|
5073
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
|
+
}
|
|
5074
5466
|
// Use kind-independent stableId so re-scans with corrected classification
|
|
5075
5467
|
// replace old entries rather than creating duplicates.
|
|
5076
5468
|
const stableId = `claude_msg:${threadId}:${uuid}`;
|
|
@@ -6367,6 +6759,20 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6367
6759
|
return [];
|
|
6368
6760
|
}
|
|
6369
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
|
+
});
|
|
6370
6776
|
return [
|
|
6371
6777
|
buildToolTimelineEntry({
|
|
6372
6778
|
provider: "codex",
|
|
@@ -6384,11 +6790,34 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6384
6790
|
|
|
6385
6791
|
if (payloadType === "custom_tool_call") {
|
|
6386
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
|
+
});
|
|
6387
6805
|
return [];
|
|
6388
6806
|
}
|
|
6389
6807
|
|
|
6390
6808
|
if (payloadType === "function_call_output") {
|
|
6391
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
|
+
});
|
|
6392
6821
|
return [];
|
|
6393
6822
|
}
|
|
6394
6823
|
const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
|
|
@@ -6396,6 +6825,17 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6396
6825
|
return [];
|
|
6397
6826
|
}
|
|
6398
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
|
+
});
|
|
6399
6839
|
return [
|
|
6400
6840
|
buildToolTimelineEntry({
|
|
6401
6841
|
provider: "codex",
|
|
@@ -6554,6 +6994,18 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6554
6994
|
fileState.applyPatchInputsByCallId.delete(callId);
|
|
6555
6995
|
}
|
|
6556
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
|
+
|
|
6557
7009
|
return entries.filter(Boolean);
|
|
6558
7010
|
}
|
|
6559
7011
|
|
|
@@ -6610,8 +7062,24 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
6610
7062
|
let dirty = false;
|
|
6611
7063
|
|
|
6612
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
|
+
});
|
|
6613
7073
|
attachCompletionDetails({ config, runtime, event });
|
|
6614
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
|
+
});
|
|
6615
7083
|
attachPlanDetails({ config, runtime, event });
|
|
6616
7084
|
}
|
|
6617
7085
|
|
|
@@ -9163,6 +9631,15 @@ function isStartTurnAckTimeout(errorValue) {
|
|
|
9163
9631
|
message === "turn/start-timeout";
|
|
9164
9632
|
}
|
|
9165
9633
|
|
|
9634
|
+
function isIpcNoClientFoundError(errorValue) {
|
|
9635
|
+
const message = normalizeIpcErrorMessage(errorValue).toLowerCase();
|
|
9636
|
+
return message === "no-client-found" ||
|
|
9637
|
+
message === "client-not-found" ||
|
|
9638
|
+
message === "target-client-not-found" ||
|
|
9639
|
+
message.includes("no client found") ||
|
|
9640
|
+
message.includes("client not found");
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9166
9643
|
function buildDefaultCollaborationMode(threadState) {
|
|
9167
9644
|
// Fallback turns must leave Plan mode unless the caller explicitly opts in.
|
|
9168
9645
|
return buildRequestedCollaborationMode(threadState, "default");
|
|
@@ -9281,14 +9758,36 @@ class NativeIpcClient {
|
|
|
9281
9758
|
);
|
|
9282
9759
|
}
|
|
9283
9760
|
|
|
9284
|
-
sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null, options = {}) {
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9761
|
+
async sendThreadFollowerRequest(method, params, conversationId, ownerClientId = null, options = {}) {
|
|
9762
|
+
const targetClientId =
|
|
9763
|
+
ownerClientId ??
|
|
9764
|
+
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
9765
|
+
null;
|
|
9766
|
+
|
|
9767
|
+
try {
|
|
9768
|
+
return await this.sendRequest(method, params, {
|
|
9769
|
+
targetClientId,
|
|
9770
|
+
...options,
|
|
9771
|
+
});
|
|
9772
|
+
} catch (error) {
|
|
9773
|
+
if (!targetClientId || !isIpcNoClientFoundError(error)) {
|
|
9774
|
+
throw error;
|
|
9775
|
+
}
|
|
9776
|
+
|
|
9777
|
+
const mappedOwner = this.runtime.threadOwnerClientIds.get(conversationId) ?? null;
|
|
9778
|
+
if (mappedOwner === targetClientId) {
|
|
9779
|
+
this.runtime.threadOwnerClientIds.delete(conversationId);
|
|
9780
|
+
}
|
|
9781
|
+
console.warn(
|
|
9782
|
+
`[ipc] stale owner client for thread=${cleanText(conversationId || "") || "unknown"} ` +
|
|
9783
|
+
`target=${cleanText(targetClientId || "") || "unknown"} method=${cleanText(method || "") || "unknown"}; retrying without target`
|
|
9784
|
+
);
|
|
9785
|
+
|
|
9786
|
+
return this.sendRequest(method, params, {
|
|
9787
|
+
...options,
|
|
9788
|
+
targetClientId: null,
|
|
9789
|
+
});
|
|
9790
|
+
}
|
|
9292
9791
|
}
|
|
9293
9792
|
|
|
9294
9793
|
sendRequest(method, params, options = {}) {
|
|
@@ -10170,9 +10669,10 @@ function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocal
|
|
|
10170
10669
|
}
|
|
10171
10670
|
|
|
10172
10671
|
function formatCommandApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10672
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
10173
10673
|
const parts = [];
|
|
10174
|
-
const reason = truncate(cleanText(
|
|
10175
|
-
const command = truncate(cleanText(
|
|
10674
|
+
const reason = truncate(cleanText(safeParams.reason ?? safeParams.justification ?? ""), 220);
|
|
10675
|
+
const command = truncate(cleanText(safeParams.command ?? safeParams.cmd ?? ""), 1200);
|
|
10176
10676
|
if (reason) {
|
|
10177
10677
|
parts.push(reason);
|
|
10178
10678
|
} else {
|
|
@@ -10185,21 +10685,41 @@ function formatCommandApprovalMessage(params, locale = config?.defaultLocale ||
|
|
|
10185
10685
|
}
|
|
10186
10686
|
|
|
10187
10687
|
function formatFileApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10688
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
10188
10689
|
const parts = [];
|
|
10189
|
-
const reason = truncate(cleanText(
|
|
10690
|
+
const reason = truncate(cleanText(safeParams.reason ?? ""), 220);
|
|
10190
10691
|
if (reason) {
|
|
10191
10692
|
parts.push(reason);
|
|
10192
10693
|
} else {
|
|
10193
10694
|
parts.push(t(locale, "server.message.fileApprovalNeeded"));
|
|
10194
10695
|
}
|
|
10195
|
-
if (
|
|
10196
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
10197
|
-
} else if (
|
|
10198
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
10696
|
+
if (safeParams.grantRoot) {
|
|
10697
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.grantRoot) }));
|
|
10698
|
+
} else if (safeParams.cwd) {
|
|
10699
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.cwd) }));
|
|
10199
10700
|
}
|
|
10200
10701
|
return truncate(parts.join("\n"), 1024);
|
|
10201
10702
|
}
|
|
10202
10703
|
|
|
10704
|
+
function claudeApprovalRawParams(body, kind) {
|
|
10705
|
+
const toolInput = isPlainObject(body?.toolInput) ? cloneJson(body.toolInput) : {};
|
|
10706
|
+
const rawParams = {
|
|
10707
|
+
...toolInput,
|
|
10708
|
+
...(body?.cwd ? { cwd: String(body.cwd) } : {}),
|
|
10709
|
+
};
|
|
10710
|
+
const messageText = cleanText(body?.messageText || "");
|
|
10711
|
+
if (messageText && !cleanText(rawParams.reason ?? rawParams.justification ?? "")) {
|
|
10712
|
+
rawParams.reason = messageText;
|
|
10713
|
+
}
|
|
10714
|
+
if (kind === "command" && !cleanText(rawParams.command ?? rawParams.cmd ?? "")) {
|
|
10715
|
+
const commandText = cleanText(toolInput.command ?? toolInput.cmd ?? "");
|
|
10716
|
+
if (commandText) {
|
|
10717
|
+
rawParams.command = commandText;
|
|
10718
|
+
}
|
|
10719
|
+
}
|
|
10720
|
+
return rawParams;
|
|
10721
|
+
}
|
|
10722
|
+
|
|
10203
10723
|
function extractApprovalFileRefs(params) {
|
|
10204
10724
|
if (!isPlainObject(params)) {
|
|
10205
10725
|
return [];
|
|
@@ -12621,6 +13141,26 @@ function buildTimelineThreads(entries, config) {
|
|
|
12621
13141
|
.slice(0, config.maxTimelineThreads);
|
|
12622
13142
|
}
|
|
12623
13143
|
|
|
13144
|
+
function buildActiveTimelineActivityEntries(runtime, locale) {
|
|
13145
|
+
if (!(runtime?.activeTimelineActivitiesByKey instanceof Map)) {
|
|
13146
|
+
return [];
|
|
13147
|
+
}
|
|
13148
|
+
const now = Date.now();
|
|
13149
|
+
const entries = [];
|
|
13150
|
+
for (const [key, activity] of [...runtime.activeTimelineActivitiesByKey.entries()]) {
|
|
13151
|
+
const updatedAtMs = Number(activity?.updatedAtMs) || 0;
|
|
13152
|
+
if (!updatedAtMs || now - updatedAtMs > TIMELINE_ACTIVITY_TTL_MS) {
|
|
13153
|
+
runtime.activeTimelineActivitiesByKey.delete(key);
|
|
13154
|
+
continue;
|
|
13155
|
+
}
|
|
13156
|
+
const entry = buildTimelineActivityEntry(activity, locale);
|
|
13157
|
+
if (entry) {
|
|
13158
|
+
entries.push(entry);
|
|
13159
|
+
}
|
|
13160
|
+
}
|
|
13161
|
+
return entries;
|
|
13162
|
+
}
|
|
13163
|
+
|
|
12624
13164
|
function buildTimelineResponse(runtime, state, config, locale) {
|
|
12625
13165
|
const messageEntries = normalizeTimelineEntries(
|
|
12626
13166
|
state.recentTimelineEntries ?? runtime.recentTimelineEntries,
|
|
@@ -12628,7 +13168,11 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12628
13168
|
);
|
|
12629
13169
|
runtime.recentTimelineEntries = messageEntries;
|
|
12630
13170
|
const entries = normalizeTimelineEntries(
|
|
12631
|
-
[
|
|
13171
|
+
[
|
|
13172
|
+
...buildActiveTimelineActivityEntries(runtime, locale),
|
|
13173
|
+
...messageEntries,
|
|
13174
|
+
...buildOperationalTimelineEntries(runtime, state, config, locale),
|
|
13175
|
+
],
|
|
12632
13176
|
config.maxTimelineEntries
|
|
12633
13177
|
).map((entry) => ({
|
|
12634
13178
|
kind: entry.kind,
|
|
@@ -12646,6 +13190,9 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12646
13190
|
outcome: entry.outcome || "",
|
|
12647
13191
|
createdAtMs: entry.createdAtMs,
|
|
12648
13192
|
provider: normalizeProvider(entry.provider),
|
|
13193
|
+
...(entry.activityPhase != null ? { activityPhase: entry.activityPhase } : {}),
|
|
13194
|
+
...(entry.commandText != null ? { commandText: entry.commandText } : {}),
|
|
13195
|
+
...timelineAutoPilotProjection(entry),
|
|
12649
13196
|
...(entry.draftType != null ? { draftType: entry.draftType } : {}),
|
|
12650
13197
|
}));
|
|
12651
13198
|
|
|
@@ -12655,6 +13202,24 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12655
13202
|
};
|
|
12656
13203
|
}
|
|
12657
13204
|
|
|
13205
|
+
function timelineAutoPilotProjection(entry) {
|
|
13206
|
+
if (cleanText(entry?.kind || "") !== "approval" || cleanText(entry?.outcome || "") !== "approved") {
|
|
13207
|
+
return {};
|
|
13208
|
+
}
|
|
13209
|
+
const stableId = cleanText(entry?.stableId || "");
|
|
13210
|
+
if (stableId.includes(":autopilot-write")) {
|
|
13211
|
+
const lane = cleanText(stableId.match(/:autopilot-write:([a-z_-]+)$/u)?.[1] || "");
|
|
13212
|
+
return {
|
|
13213
|
+
autoPilotMode: "write",
|
|
13214
|
+
...(lane ? { autoPilotWriteLane: lane } : {}),
|
|
13215
|
+
};
|
|
13216
|
+
}
|
|
13217
|
+
if (stableId.endsWith(":autopilot")) {
|
|
13218
|
+
return { autoPilotMode: "read" };
|
|
13219
|
+
}
|
|
13220
|
+
return {};
|
|
13221
|
+
}
|
|
13222
|
+
|
|
12658
13223
|
function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
12659
13224
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
12660
13225
|
const approvalKind = cleanText(approval.kind || "");
|
|
@@ -13272,7 +13837,7 @@ function buildAmbientSuggestionsDetail(entry, locale) {
|
|
|
13272
13837
|
|
|
13273
13838
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
13274
13839
|
const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
|
|
13275
|
-
const commandText = ["read", "search"].includes(fileEventType)
|
|
13840
|
+
const commandText = ["read", "search", "git"].includes(fileEventType)
|
|
13276
13841
|
? firstMarkdownCodeFenceText(entry?.messageText ?? "")
|
|
13277
13842
|
: "";
|
|
13278
13843
|
const detailText = [
|
|
@@ -14525,9 +15090,26 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
14525
15090
|
});
|
|
14526
15091
|
} else if (approval.resolveClaudeWaiter) {
|
|
14527
15092
|
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
14528
|
-
// ExitPlanMode
|
|
14529
|
-
//
|
|
14530
|
-
//
|
|
15093
|
+
// ExitPlanMode CANNOT actually be auto-bypassed via the PreToolUse
|
|
15094
|
+
// hook. We tested both "allow" and "deny" verdicts on Claude Desktop:
|
|
15095
|
+
//
|
|
15096
|
+
// - permissionDecision: "allow"
|
|
15097
|
+
// Claude Desktop ignores the verdict and re-shows its native
|
|
15098
|
+
// plan dialog on the Mac, asking for approval a second time
|
|
15099
|
+
// even though the user already tapped on mobile.
|
|
15100
|
+
// - permissionDecision: "deny"
|
|
15101
|
+
// The native dialog is suppressed, but Claude Desktop also
|
|
15102
|
+
// does not exit plan mode internally, so Edit/Write stay
|
|
15103
|
+
// blocked — the session cannot make progress.
|
|
15104
|
+
//
|
|
15105
|
+
// Neither verdict gives us "phone tap → plan mode exits → editing
|
|
15106
|
+
// resumes" by itself. Until we can drive the Mac dialog through an
|
|
15107
|
+
// external mechanism (AppleScript / Accessibility API), the chosen
|
|
15108
|
+
// workaround is "deny + reason": Claude reads the reason as
|
|
15109
|
+
// instructions ("user already approved on mobile, proceed without
|
|
15110
|
+
// calling ExitPlanMode again"). That at least lets the desktop user
|
|
15111
|
+
// finish the flow with one extra Mac click on the native dialog
|
|
15112
|
+
// instead of getting a duplicate dialog from a stale "allow".
|
|
14531
15113
|
approval.resolveClaudeWaiter({
|
|
14532
15114
|
permissionDecision: "deny",
|
|
14533
15115
|
permissionDecisionReason:
|
|
@@ -17592,6 +18174,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17592
18174
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
17593
18175
|
console.log(`[claude-stop] threadId=${threadId} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
17594
18176
|
if (threadId) {
|
|
18177
|
+
clearTimelineActivity({
|
|
18178
|
+
config,
|
|
18179
|
+
runtime,
|
|
18180
|
+
provider: "claude",
|
|
18181
|
+
threadId,
|
|
18182
|
+
source: "claude-stop",
|
|
18183
|
+
});
|
|
17595
18184
|
const pending = [];
|
|
17596
18185
|
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
17597
18186
|
if (!approval.resolved && approval.conversationId === threadId) {
|
|
@@ -17662,6 +18251,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17662
18251
|
// for this session/tool as accepted (so iPhone moves it to completed).
|
|
17663
18252
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
17664
18253
|
const toolName = String(body.toolName || "");
|
|
18254
|
+
const eventCwd = String(body.cwd || "");
|
|
18255
|
+
const threadLabel =
|
|
18256
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18257
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18258
|
+
threadId.slice(0, 40);
|
|
18259
|
+
upsertTimelineActivity({
|
|
18260
|
+
config,
|
|
18261
|
+
runtime,
|
|
18262
|
+
provider: "claude",
|
|
18263
|
+
threadId,
|
|
18264
|
+
threadLabel,
|
|
18265
|
+
phase: "thinking",
|
|
18266
|
+
cwd: eventCwd,
|
|
18267
|
+
source: "claude-post-tool",
|
|
18268
|
+
});
|
|
17665
18269
|
const fingerprint = claudeToolFingerprint(toolName, body.toolInput);
|
|
17666
18270
|
console.log(`[claude-post-tool] threadId=${threadId} tool=${toolName} fp=${fingerprint.slice(0, 80)} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
17667
18271
|
const match = findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint);
|
|
@@ -17697,6 +18301,31 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17697
18301
|
}
|
|
17698
18302
|
}
|
|
17699
18303
|
|
|
18304
|
+
if (eventType === "PreToolUse") {
|
|
18305
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
18306
|
+
const eventCwd = String(body.cwd || "");
|
|
18307
|
+
const toolName = String(body.toolName || "");
|
|
18308
|
+
const toolActivity = timelineActivityFromClaudeTool(toolName, isPlainObject(body.toolInput) ? body.toolInput : {});
|
|
18309
|
+
const threadLabel =
|
|
18310
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18311
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18312
|
+
threadId.slice(0, 40);
|
|
18313
|
+
upsertTimelineActivity({
|
|
18314
|
+
config,
|
|
18315
|
+
runtime,
|
|
18316
|
+
provider: "claude",
|
|
18317
|
+
threadId,
|
|
18318
|
+
threadLabel,
|
|
18319
|
+
phase: toolActivity.phase,
|
|
18320
|
+
summary: toolActivity.summary,
|
|
18321
|
+
commandText: toolActivity.commandText,
|
|
18322
|
+
fileRefs: toolActivity.fileRefs,
|
|
18323
|
+
cwd: eventCwd,
|
|
18324
|
+
source: "claude-pre-tool",
|
|
18325
|
+
});
|
|
18326
|
+
return writeJson(res, 200, {});
|
|
18327
|
+
}
|
|
18328
|
+
|
|
17700
18329
|
if (eventType !== "approval_request") {
|
|
17701
18330
|
// Non-approval events (Notification, Stop, etc.) — acknowledge immediately
|
|
17702
18331
|
return writeJson(res, 200, {});
|
|
@@ -17708,6 +18337,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17708
18337
|
const threadId = String(body.threadId || "");
|
|
17709
18338
|
const requestId = String(body.requestId || crypto.randomUUID());
|
|
17710
18339
|
const requestKey = `${threadId}:${requestId}`;
|
|
18340
|
+
const approvalKind = String(body.approvalKind || "command");
|
|
18341
|
+
const rawParams = claudeApprovalRawParams(body, approvalKind);
|
|
17711
18342
|
|
|
17712
18343
|
const approval = {
|
|
17713
18344
|
token,
|
|
@@ -17715,13 +18346,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17715
18346
|
conversationId: threadId,
|
|
17716
18347
|
requestId,
|
|
17717
18348
|
ownerClientId: null,
|
|
17718
|
-
kind:
|
|
18349
|
+
kind: approvalKind,
|
|
17719
18350
|
threadLabel:
|
|
17720
18351
|
runtime.claudeSessionTitles.get(threadId) ||
|
|
17721
18352
|
(body.cwd ? path.basename(String(body.cwd)) : "") ||
|
|
17722
18353
|
String(body.threadId || "").slice(0, 40),
|
|
17723
18354
|
title: config.approvalTitle,
|
|
17724
18355
|
messageText: String(body.messageText || ""),
|
|
18356
|
+
rawParams,
|
|
17725
18357
|
fileRefs: Array.isArray(body.fileRefs) ? body.fileRefs : [],
|
|
17726
18358
|
previousFileRefs: [],
|
|
17727
18359
|
diffText: String(body.diffText || ""),
|
|
@@ -17744,6 +18376,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17744
18376
|
readOnly: body.notifyOnly === true,
|
|
17745
18377
|
};
|
|
17746
18378
|
|
|
18379
|
+
upsertTimelineActivity({
|
|
18380
|
+
config,
|
|
18381
|
+
runtime,
|
|
18382
|
+
provider: "claude",
|
|
18383
|
+
threadId,
|
|
18384
|
+
threadLabel: approval.threadLabel,
|
|
18385
|
+
phase: "awaiting_approval",
|
|
18386
|
+
summary: approval.messageText,
|
|
18387
|
+
fileRefs: approval.fileRefs,
|
|
18388
|
+
cwd: approval.cwd,
|
|
18389
|
+
source: "claude-approval-request",
|
|
18390
|
+
atMs: approval.createdAtMs,
|
|
18391
|
+
});
|
|
18392
|
+
|
|
17747
18393
|
const claudeAutoPilotMatch =
|
|
17748
18394
|
approval.kind === "command"
|
|
17749
18395
|
? maybeAutoApproveTrustedRead({
|
|
@@ -17800,21 +18446,28 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17800
18446
|
runtime.nativeApprovalsByToken.set(token, approval);
|
|
17801
18447
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
17802
18448
|
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
18449
|
+
try {
|
|
18450
|
+
const pushChanged = await deliverWebPushItem({
|
|
18451
|
+
config,
|
|
18452
|
+
state,
|
|
18453
|
+
kind: "approval",
|
|
18454
|
+
tab: "inbox",
|
|
18455
|
+
subtab: "pending",
|
|
18456
|
+
token: approval.token,
|
|
18457
|
+
stableId: pendingApprovalStableId(approval),
|
|
18458
|
+
title: approval.title,
|
|
18459
|
+
body: approval.messageText,
|
|
18460
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18461
|
+
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
18462
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
18463
|
+
}),
|
|
18464
|
+
});
|
|
18465
|
+
if (pushChanged) {
|
|
18466
|
+
await saveState(config.stateFile, state);
|
|
18467
|
+
}
|
|
18468
|
+
} catch (error) {
|
|
18469
|
+
console.error(`[claude-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
18470
|
+
}
|
|
17818
18471
|
|
|
17819
18472
|
// Notify-only path: phone gets a read-only entry + push, but the
|
|
17820
18473
|
// hook does not block on a decision. The PostToolUse handler will
|
|
@@ -18037,6 +18690,22 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18037
18690
|
if (draftType === "reply" && !postId) {
|
|
18038
18691
|
return writeJson(res, 400, { error: "missing-postId-for-reply" });
|
|
18039
18692
|
}
|
|
18693
|
+
if (draftType !== "original_post") {
|
|
18694
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
18695
|
+
const quota = await getMoltbookReplyQuotaState({ state: scoutState, maxDaily: 5 });
|
|
18696
|
+
if (quota.quotaReached) {
|
|
18697
|
+
await writeScoutState(scoutState);
|
|
18698
|
+
return writeJson(res, 409, {
|
|
18699
|
+
error: "moltbook-reply-quota-reached",
|
|
18700
|
+
sentToday: quota.sentToday,
|
|
18701
|
+
pendingToday: quota.pendingToday,
|
|
18702
|
+
usedToday: quota.usedToday,
|
|
18703
|
+
maxDaily: quota.maxDaily,
|
|
18704
|
+
day: quota.day,
|
|
18705
|
+
});
|
|
18706
|
+
}
|
|
18707
|
+
await writeScoutState(scoutState);
|
|
18708
|
+
}
|
|
18040
18709
|
const token = historyToken(`moltbook_draft:${sourceId}`);
|
|
18041
18710
|
const postTitle = cleanText(body.postTitle || "");
|
|
18042
18711
|
const postUrl = cleanText(body.postUrl || `https://www.moltbook.com/post/${postId}`);
|
|
@@ -18172,6 +18841,17 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18172
18841
|
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
18173
18842
|
const editedText = cleanText(body.editedText || "");
|
|
18174
18843
|
const editedTitle = cleanText(body.editedTitle || "");
|
|
18844
|
+
if (action === "approve" && draft.draftType !== "original_post") {
|
|
18845
|
+
const quota = await reserveMoltbookReplyQuotaSlot(draft, 5);
|
|
18846
|
+
if (quota.quotaReached) {
|
|
18847
|
+
return writeJson(res, 409, {
|
|
18848
|
+
error: "moltbook-reply-quota-reached",
|
|
18849
|
+
sentToday: quota.sentToday,
|
|
18850
|
+
maxDaily: quota.maxDaily,
|
|
18851
|
+
day: quota.day,
|
|
18852
|
+
});
|
|
18853
|
+
}
|
|
18854
|
+
}
|
|
18175
18855
|
const decision = {
|
|
18176
18856
|
action,
|
|
18177
18857
|
text: action === "approve" ? editedText || draft.draftText : "",
|
|
@@ -18205,6 +18885,15 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18205
18885
|
await executeMoltbookDraftPost(draft, config, runtime, state);
|
|
18206
18886
|
} catch (postError) {
|
|
18207
18887
|
console.error(`[moltbook-draft-post-error] ${postError.message}`);
|
|
18888
|
+
await releaseMoltbookReplyQuotaSlot(draft).catch((error) => console.error(`[moltbook-quota-release] ${error.message}`));
|
|
18889
|
+
if (draft.parentCommentId) {
|
|
18890
|
+
await updateInboxStatus(draft.parentCommentId, "pending", {
|
|
18891
|
+
source: "mobile-draft-approve",
|
|
18892
|
+
draftStatus: "failed",
|
|
18893
|
+
draftToken: draft.token,
|
|
18894
|
+
draftError: String(postError.message || "").slice(0, 500),
|
|
18895
|
+
}).catch((error) => console.error(`[moltbook-inbox-post-failed] ${error.message}`));
|
|
18896
|
+
}
|
|
18208
18897
|
try {
|
|
18209
18898
|
await deliverWebPushItem({
|
|
18210
18899
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
@@ -18224,6 +18913,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18224
18913
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
18225
18914
|
})();
|
|
18226
18915
|
} else {
|
|
18916
|
+
if (draft.parentCommentId) {
|
|
18917
|
+
await updateInboxStatus(draft.parentCommentId, "skipped", {
|
|
18918
|
+
source: "mobile-draft-deny",
|
|
18919
|
+
draftStatus: "denied",
|
|
18920
|
+
draftToken: draft.token,
|
|
18921
|
+
skippedAt: new Date().toISOString(),
|
|
18922
|
+
}).catch((error) => console.error(`[moltbook-inbox-denied] ${error.message}`));
|
|
18923
|
+
}
|
|
18227
18924
|
// Deny — clean up.
|
|
18228
18925
|
await deleteDraft(token).catch(() => {});
|
|
18229
18926
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
@@ -18375,6 +19072,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18375
19072
|
if (error.message === "codex-ipc-not-connected") {
|
|
18376
19073
|
return writeJson(res, 503, { error: error.message });
|
|
18377
19074
|
}
|
|
19075
|
+
if (isIpcNoClientFoundError(error)) {
|
|
19076
|
+
return writeJson(res, 503, { error: "codex-client-not-found" });
|
|
19077
|
+
}
|
|
18378
19078
|
return writeJson(res, 500, { error: error.message });
|
|
18379
19079
|
}
|
|
18380
19080
|
}
|
|
@@ -20306,9 +21006,21 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
20306
21006
|
createdPostId = draft.postId || null;
|
|
20307
21007
|
createdCommentId = comment?.id || null;
|
|
20308
21008
|
console.log(`[moltbook-draft-post] Posted reply (commentId=${comment?.id}) to post ${draft.postId}`);
|
|
21009
|
+
if (draft.parentCommentId) {
|
|
21010
|
+
await updateInboxStatus(draft.parentCommentId, "replied", {
|
|
21011
|
+
source: "mobile-draft-approve",
|
|
21012
|
+
replyText: finalText,
|
|
21013
|
+
replyCommentId: createdCommentId || "",
|
|
21014
|
+
replyVerification: verification || null,
|
|
21015
|
+
draftStatus: "posted",
|
|
21016
|
+
draftToken: draft.token,
|
|
21017
|
+
}).catch((error) => console.error(`[moltbook-inbox-replied] ${error.message}`));
|
|
21018
|
+
}
|
|
20309
21019
|
|
|
20310
21020
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
20311
|
-
|
|
21021
|
+
if (!draft.replyQuotaReserved) {
|
|
21022
|
+
scoutState.sentToday += 1;
|
|
21023
|
+
}
|
|
20312
21024
|
markPostSeen(scoutState, draft.postId, "published");
|
|
20313
21025
|
// Append the reply to `recentComposeTitles` so it shows up in the
|
|
20314
21026
|
// "最近の投稿" list with its reply badge. `recordComposeAttempt` knows
|