viveworker 0.8.9 → 0.9.0

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.
@@ -12,16 +12,20 @@ import process from "node:process";
12
12
  import { createInterface } from "node:readline";
13
13
  import { inspect } from "node:util";
14
14
  import { fileURLToPath } from "node:url";
15
+ import { Worker } from "node:worker_threads";
15
16
  import zlib from "node:zlib";
16
17
  import webPush from "web-push";
17
18
  import * as hazbaseAuth from "@hazbase/auth";
18
19
  import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
19
20
  import { APP_BUILD_ID as WEB_APP_BUILD_ID } from "../web/build-id.js";
20
21
  import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
22
+ import { applyJsonPatches } from "./lib/immutable-json-patch.mjs";
23
+ import { LengthPrefixedFrameDecoder } from "./lib/length-prefixed-frame-decoder.mjs";
21
24
  import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
25
+ import { buildWorkItemResponse } from "./lib/work-items.mjs";
22
26
  import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
23
27
  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, updateInboxStatus, getMoltbookReplyQuotaState } from "./moltbook-api.mjs";
28
+ import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems, updateInboxStatus, getMoltbookReplyQuotaState, looksLikeProviderErrorText } from "./moltbook-api.mjs";
25
29
  import { startRemotePairingRelay, DEFAULT_RELAY_URL } from "./lib/remote-pairing/orchestrator.mjs";
26
30
  import { restartRemotePairingRelay, persistRemotePairingEnv, getRemotePairingStatus } from "./lib/remote-pairing/control.mjs";
27
31
  import { appendRemotePairingAuditEvent, readRemotePairingAuditEvents } from "./lib/remote-pairing/audit.mjs";
@@ -58,6 +62,9 @@ const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "ap
58
62
  const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
59
63
  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"]);
60
64
  const SQLITE_COMPLETION_BATCH_SIZE = 200;
65
+ const SQLITE_QUERY_BUSY_TIMEOUT_MS = 1000;
66
+ const SQLITE_QUERY_MAX_ATTEMPTS = 3;
67
+ const SQLITE_QUERY_RETRY_BASE_DELAY_MS = 75;
61
68
  const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
62
69
  const MAX_PAIRED_DEVICES = 200;
63
70
  const PAIRING_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
@@ -73,6 +80,15 @@ const HAZBASE_METADATA_TIMEOUT_MS = 1500;
73
80
  const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
74
81
  const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
75
82
  const TIMELINE_ACTIVITY_TTL_MS = 2 * 60 * 1000;
83
+ const STARTUP_MAINTENANCE_DELAY_MS = 15_000;
84
+ const STARTUP_MAINTENANCE_RETRY_MS = 5_000;
85
+ const INITIAL_SCAN_WARMUP_MS = 1_500;
86
+ const SCAN_COOPERATIVE_YIELD_EVERY = 16;
87
+ const APPLY_PATCH_LOOKBACK_BYTES = 8 * 1024 * 1024;
88
+ const APPLY_PATCH_LOOKUP_MISS_LIMIT = 256;
89
+ const IPC_LARGE_FRAME_LOG_BYTES = 8 * 1024 * 1024;
90
+ const ROLLOUT_LABEL_METADATA_MAX_LINES = 256;
91
+ const ROLLOUT_LABEL_METADATA_MAX_BYTES = 512 * 1024;
76
92
  const PAYMENT_NETWORKS = {
77
93
  base: { family: "evm", chainId: 8453, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base", releaseStatus: "comingSoon" },
78
94
  "base-sepolia": { family: "evm", chainId: 84532, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base Sepolia" },
@@ -254,6 +270,7 @@ const runtime = {
254
270
  sourceFile: "",
255
271
  },
256
272
  rolloutThreadLabels: new Map(),
273
+ rolloutLabelIndexInFlight: false,
257
274
  rolloutThreadCwds: new Map(),
258
275
  threadStates: new Map(),
259
276
  threadOwnerClientIds: new Map(),
@@ -294,6 +311,7 @@ const runtime = {
294
311
  timelineLiveScanReasons: new Set(),
295
312
  timelineLiveClaudeFiles: new Set(),
296
313
  timelineLiveRolloutFiles: new Set(),
314
+ initialScanComplete: false,
297
315
  pairingAttemptsByRemoteAddress: new Map(),
298
316
  ipcClient: null,
299
317
  remotePairingHandle: null,
@@ -337,6 +355,11 @@ try {
337
355
  await deleteDraft(draft.token).catch(() => {});
338
356
  continue;
339
357
  }
358
+ if (looksLikeProviderErrorText(draft.draftText)) {
359
+ await deleteDraft(draft.token).catch(() => {});
360
+ console.warn(`[moltbook-draft-provider-error] Dropped persisted draft ${draft.token}`);
361
+ continue;
362
+ }
340
363
  runtime.moltbookDraftsByToken.set(draft.token, { ...draft, decisionWaiters: [] });
341
364
  }
342
365
  if (runtime.moltbookDraftsByToken.size) {
@@ -351,6 +374,12 @@ setInterval(async () => {
351
374
  const now = Date.now();
352
375
  for (const [token, draft] of runtime.moltbookDraftsByToken) {
353
376
  if (draft.decision) continue;
377
+ if (looksLikeProviderErrorText(draft.draftText)) {
378
+ runtime.moltbookDraftsByToken.delete(token);
379
+ await deleteDraft(token).catch(() => {});
380
+ console.warn(`[moltbook-draft-provider-error] Dropped pending draft ${token}`);
381
+ continue;
382
+ }
354
383
  const age = now - (draft.createdAtMs || 0);
355
384
  if (age > (draft.ttlMs || 86400000)) {
356
385
  runtime.moltbookDraftsByToken.delete(token);
@@ -423,9 +452,6 @@ state.recentHistoryItems = initialHistoryItems;
423
452
  state.recentTimelineEntries = initialTimelineEntries;
424
453
  const backfilledAmbientSuggestionsStateChanged = backfillAmbientSuggestionsState({ config, runtime, state });
425
454
  const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
426
- const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
427
- const backfilledMoltbookInboxChanged = await backfillMoltbookInboxHistory({ config, runtime, state });
428
- const recoveredMissingProviderStateChanged = await recoverMissingProviderStateFromBackup({ config, runtime, state });
429
455
  runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
430
456
  runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
431
457
 
@@ -1803,11 +1829,6 @@ function unwrapShellCommand(commandText) {
1803
1829
  .trim();
1804
1830
  }
1805
1831
 
1806
- function extractCommandLineFromFunctionOutput(outputText) {
1807
- const match = String(outputText || "").match(/^Command:\s+(.+)$/mu);
1808
- return unwrapShellCommand(match?.[1] || "");
1809
- }
1810
-
1811
1832
  function parseToolArgumentsJson(value) {
1812
1833
  if (isPlainObject(value)) {
1813
1834
  return value;
@@ -1864,16 +1885,21 @@ function redactTimelineCommandText(commandText) {
1864
1885
  .replace(/\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*=)(["']?)[^\s"']+\2/giu, "$1[redacted]");
1865
1886
  }
1866
1887
 
1888
+ function escapeMarkdownCodeFenceBody(text) {
1889
+ return cleanText(text || "").replace(/```/gu, "\\`\\`\\`");
1890
+ }
1891
+
1867
1892
  function timelineCommandMessage(locale, { commandText = "", toolName = "", fileRefs = [] } = {}) {
1893
+ const safeCommandText = escapeMarkdownCodeFenceBody(redactTimelineCommandText(commandText));
1868
1894
  const commandBlock = commandText
1869
- ? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${redactTimelineCommandText(commandText)}\n\`\`\``
1895
+ ? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${safeCommandText}\n\`\`\``
1870
1896
  : "";
1871
1897
  const toolBlock = !commandBlock && toolName
1872
- ? `${t(locale, "server.message.toolLabel")}\n\`\`\`text\n${cleanText(toolName)}\n\`\`\``
1898
+ ? `${t(locale, "server.message.toolLabel")}\n\`\`\`text\n${escapeMarkdownCodeFenceBody(toolName)}\n\`\`\``
1873
1899
  : "";
1874
1900
  const files = normalizeTimelineFileRefs(fileRefs);
1875
1901
  const filesBlock = files.length
1876
- ? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${files.join("\n")}\n\`\`\``
1902
+ ? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${files.map(escapeMarkdownCodeFenceBody).join("\n")}\n\`\`\``
1877
1903
  : "";
1878
1904
  return [commandBlock || toolBlock, filesBlock].filter(Boolean).join("\n\n");
1879
1905
  }
@@ -2003,7 +2029,7 @@ function sedCommandPathArgs(tokens) {
2003
2029
  function autoPilotApprovalMessage(locale, commandText) {
2004
2030
  const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
2005
2031
  const commandBlock = cleanText(commandText || "")
2006
- ? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${cleanText(commandText || "")}\n\`\`\``
2032
+ ? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${escapeMarkdownCodeFenceBody(commandText)}\n\`\`\``
2007
2033
  : "";
2008
2034
  return [prefix, commandBlock].filter(Boolean).join("\n\n");
2009
2035
  }
@@ -2162,6 +2188,7 @@ function rememberApplyPatchInput(fileState, payload, createdAtMs = Date.now()) {
2162
2188
  cwd: cleanText(fileState.cwd || ""),
2163
2189
  createdAtMs: Number(createdAtMs) || Date.now(),
2164
2190
  });
2191
+ fileState.applyPatchLookupMisses?.delete(callId);
2165
2192
  while (fileState.applyPatchInputsByCallId.size > 64) {
2166
2193
  const oldestKey = fileState.applyPatchInputsByCallId.keys().next().value;
2167
2194
  if (!oldestKey) {
@@ -2214,6 +2241,9 @@ async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath })
2214
2241
  if (inMemory?.inputText) {
2215
2242
  return inMemory;
2216
2243
  }
2244
+ if (fileState?.applyPatchLookupMisses instanceof Set && fileState.applyPatchLookupMisses.has(normalizedCallId)) {
2245
+ return null;
2246
+ }
2217
2247
 
2218
2248
  const normalizedRolloutFilePath = cleanText(rolloutFilePath || "");
2219
2249
  if (!normalizedRolloutFilePath) {
@@ -2222,14 +2252,40 @@ async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath })
2222
2252
 
2223
2253
  let content = "";
2224
2254
  try {
2225
- content = await fs.readFile(normalizedRolloutFilePath, "utf8");
2255
+ const stat = await fs.stat(normalizedRolloutFilePath);
2256
+ const length = Math.min(stat.size, APPLY_PATCH_LOOKBACK_BYTES);
2257
+ const offset = Math.max(0, stat.size - length);
2258
+ const buffer = Buffer.allocUnsafe(length);
2259
+ const handle = await fs.open(normalizedRolloutFilePath, "r");
2260
+ try {
2261
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
2262
+ content = buffer.subarray(0, bytesRead).toString("utf8");
2263
+ } finally {
2264
+ await handle.close();
2265
+ }
2266
+ if (offset > 0) {
2267
+ const firstNewline = content.indexOf("\n");
2268
+ content = firstNewline >= 0 ? content.slice(firstNewline + 1) : "";
2269
+ }
2226
2270
  } catch {
2227
2271
  return null;
2228
2272
  }
2229
2273
 
2230
- const lines = content.split("\n");
2231
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2232
- const rawLine = lines[index];
2274
+ let cursor = content.length;
2275
+ let scannedLines = 0;
2276
+ while (cursor > 0) {
2277
+ let end = cursor;
2278
+ if (content.charCodeAt(end - 1) === 10) {
2279
+ end -= 1;
2280
+ }
2281
+ const previousNewline = content.lastIndexOf("\n", end - 1);
2282
+ const start = previousNewline + 1;
2283
+ const rawLine = content.slice(start, end);
2284
+ cursor = previousNewline >= 0 ? previousNewline : 0;
2285
+ scannedLines += 1;
2286
+ if (scannedLines % 128 === 0) {
2287
+ await yieldToEventLoop();
2288
+ }
2233
2289
  if (!rawLine.trim()) {
2234
2290
  continue;
2235
2291
  }
@@ -2256,6 +2312,7 @@ async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath })
2256
2312
 
2257
2313
  const inputText = normalizeTimelineDiffText(payload?.input ?? "");
2258
2314
  if (!inputText) {
2315
+ rememberApplyPatchLookupMiss(fileState, normalizedCallId);
2259
2316
  return null;
2260
2317
  }
2261
2318
 
@@ -2268,12 +2325,29 @@ async function findStoredApplyPatchInput({ fileState, callId, rolloutFilePath })
2268
2325
  fileState.applyPatchInputsByCallId = new Map();
2269
2326
  }
2270
2327
  fileState.applyPatchInputsByCallId.set(normalizedCallId, restored);
2328
+ fileState.applyPatchLookupMisses?.delete(normalizedCallId);
2271
2329
  return restored;
2272
2330
  }
2273
2331
 
2332
+ rememberApplyPatchLookupMiss(fileState, normalizedCallId);
2274
2333
  return null;
2275
2334
  }
2276
2335
 
2336
+ function rememberApplyPatchLookupMiss(fileState, callId) {
2337
+ if (!fileState || !callId) {
2338
+ return;
2339
+ }
2340
+ if (!(fileState.applyPatchLookupMisses instanceof Set)) {
2341
+ fileState.applyPatchLookupMisses = new Set();
2342
+ }
2343
+ fileState.applyPatchLookupMisses.add(callId);
2344
+ while (fileState.applyPatchLookupMisses.size > APPLY_PATCH_LOOKUP_MISS_LIMIT) {
2345
+ const oldestKey = fileState.applyPatchLookupMisses.values().next().value;
2346
+ if (!oldestKey) break;
2347
+ fileState.applyPatchLookupMisses.delete(oldestKey);
2348
+ }
2349
+ }
2350
+
2277
2351
  function diffPathForSide(fileRef, side) {
2278
2352
  const normalizedFileRef = cleanText(fileRef || "");
2279
2353
  if (!normalizedFileRef) {
@@ -3399,7 +3473,20 @@ function normalizeHistoryItems(rawItems, maxItems) {
3399
3473
 
3400
3474
  const normalized = rawItems
3401
3475
  .map(normalizeHistoryItem)
3402
- .filter(Boolean)
3476
+ .filter(Boolean);
3477
+ return trimNormalizedHistoryItems(normalized, maxItems);
3478
+ }
3479
+
3480
+ function mergeNormalizedHistoryItems(rawNewItems, existingItems, maxItems) {
3481
+ const additions = (Array.isArray(rawNewItems) ? rawNewItems : [])
3482
+ .map(normalizeHistoryItem)
3483
+ .filter(Boolean);
3484
+ const existing = Array.isArray(existingItems) ? existingItems : [];
3485
+ return trimNormalizedHistoryItems([...additions, ...existing], maxItems);
3486
+ }
3487
+
3488
+ function trimNormalizedHistoryItems(items, maxItems) {
3489
+ const normalized = [...items]
3403
3490
  .sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
3404
3491
  const deduped = [];
3405
3492
  const seen = new Set();
@@ -4792,11 +4879,11 @@ async function scanOnce({ config, runtime, state }) {
4792
4879
  if (knownFilesChanged) {
4793
4880
  runtime.rolloutThreadCwds = new Map();
4794
4881
  }
4795
- runtime.rolloutThreadLabels = await buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex);
4796
- dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
4882
+ refreshRolloutThreadLabelIndexInBackground({ config, runtime, state });
4797
4883
  }
4798
4884
 
4799
- for (const filePath of runtime.knownFiles) {
4885
+ for (let index = 0; index < runtime.knownFiles.length; index += 1) {
4886
+ const filePath = runtime.knownFiles[index];
4800
4887
  const changed = await processRolloutFile({
4801
4888
  filePath,
4802
4889
  config,
@@ -4805,6 +4892,9 @@ async function scanOnce({ config, runtime, state }) {
4805
4892
  now,
4806
4893
  });
4807
4894
  dirty = dirty || changed;
4895
+ if ((index + 1) % 4 === 0) {
4896
+ await yieldToEventLoop();
4897
+ }
4808
4898
  }
4809
4899
 
4810
4900
  if (config.notifyCompletions || config.webUiEnabled) {
@@ -4828,10 +4918,14 @@ async function scanOnce({ config, runtime, state }) {
4828
4918
  claudeSessionTitlesChanged = await refreshClaudeSessionTitles(runtime);
4829
4919
  runtime.lastClaudeSessionTitleScanAt = now;
4830
4920
  }
4831
- for (const filePath of runtime.claudeKnownFiles) {
4921
+ for (let index = 0; index < runtime.claudeKnownFiles.length; index += 1) {
4922
+ const filePath = runtime.claudeKnownFiles[index];
4832
4923
  const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
4833
4924
  claudeTranscriptChanged = claudeTranscriptChanged || changed;
4834
4925
  dirty = dirty || changed;
4926
+ if ((index + 1) % 4 === 0) {
4927
+ await yieldToEventLoop();
4928
+ }
4835
4929
  }
4836
4930
  if (claudeTranscriptChanged || claudeSessionTitlesChanged) {
4837
4931
  dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
@@ -5168,7 +5262,11 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
5168
5262
  state.fileOffsets[filePath] = fileState.offset;
5169
5263
 
5170
5264
  let dirty = true;
5171
- for (const rawLine of lines) {
5265
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
5266
+ if (lineIndex > 0 && lineIndex % SCAN_COOPERATIVE_YIELD_EVERY === 0) {
5267
+ await yieldToEventLoop();
5268
+ }
5269
+ const rawLine = lines[lineIndex];
5172
5270
  if (!rawLine.trim()) {
5173
5271
  continue;
5174
5272
  }
@@ -5581,7 +5679,11 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
5581
5679
  fileState.offset = newOffset;
5582
5680
  let dirty = false;
5583
5681
 
5584
- for (const line of lines) {
5682
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
5683
+ if (lineIndex > 0 && lineIndex % SCAN_COOPERATIVE_YIELD_EVERY === 0) {
5684
+ await yieldToEventLoop();
5685
+ }
5686
+ const line = lines[lineIndex];
5585
5687
  const trimmed = line.trim();
5586
5688
  if (!trimmed) continue;
5587
5689
 
@@ -6172,7 +6274,11 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
6172
6274
  state.historyFileSourceFile = historyFile;
6173
6275
  dirty = true;
6174
6276
 
6175
- for (const rawLine of lines) {
6277
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
6278
+ if (lineIndex > 0 && lineIndex % SCAN_COOPERATIVE_YIELD_EVERY === 0) {
6279
+ await yieldToEventLoop();
6280
+ }
6281
+ const rawLine = lines[lineIndex];
6176
6282
  if (!rawLine.trim()) {
6177
6283
  continue;
6178
6284
  }
@@ -6226,6 +6332,9 @@ async function backfillRecentTimelineEntryImages({ config, runtime, state }) {
6226
6332
  const nextEntries = runtime.recentTimelineEntries.map((entry) => ({ ...entry }));
6227
6333
 
6228
6334
  for (let index = 0; index < nextEntries.length; index += 1) {
6335
+ if (index > 0 && index % SCAN_COOPERATIVE_YIELD_EVERY === 0) {
6336
+ await yieldToEventLoop();
6337
+ }
6229
6338
  const entry = nextEntries[index];
6230
6339
  if (
6231
6340
  cleanText(entry?.kind || "") !== "user_message" ||
@@ -6262,7 +6371,11 @@ async function backfillRecentTimelineEntryImages({ config, runtime, state }) {
6262
6371
  async function backfillPersistedTimelineImagePaths({ config, runtime, state }) {
6263
6372
  let changed = false;
6264
6373
  const nextEntries = [];
6265
- for (const entry of runtime.recentTimelineEntries) {
6374
+ for (let index = 0; index < runtime.recentTimelineEntries.length; index += 1) {
6375
+ if (index > 0 && index % SCAN_COOPERATIVE_YIELD_EVERY === 0) {
6376
+ await yieldToEventLoop();
6377
+ }
6378
+ const entry = runtime.recentTimelineEntries[index];
6266
6379
  const nextImagePaths = await normalizePersistedTimelineImagePaths({
6267
6380
  config,
6268
6381
  state,
@@ -6299,7 +6412,8 @@ async function backfillPersistedTimelineImagePaths({ config, runtime, state }) {
6299
6412
  // watcher push for the same commentId replaces this entry cleanly rather than
6300
6413
  // creating a duplicate.
6301
6414
  async function backfillMoltbookInboxHistory({ config, runtime, state }) {
6302
- let changed = false;
6415
+ const additions = [];
6416
+ const knownStableIds = new Set(runtime.recentHistoryItems.map((entry) => cleanText(entry?.stableId || "")));
6303
6417
  try {
6304
6418
  const inboxItems = await listInboxItems();
6305
6419
  for (const inbox of inboxItems) {
@@ -6309,7 +6423,7 @@ async function backfillMoltbookInboxHistory({ config, runtime, state }) {
6309
6423
  if (!commentId) continue;
6310
6424
  const sourceId = `comment:${commentId}`;
6311
6425
  const stableId = `moltbook_reply:${sourceId}`;
6312
- if (runtime.recentHistoryItems.some((entry) => entry.stableId === stableId)) continue;
6426
+ if (knownStableIds.has(stableId)) continue;
6313
6427
  const token = historyToken(`moltbook:${sourceId}`);
6314
6428
  const createdAtMs = Number(Date.parse(inbox.updatedAt || inbox.createdAt || "")) || Date.now();
6315
6429
  const postTitle = cleanText(inbox.postTitle || "");
@@ -6319,36 +6433,73 @@ async function backfillMoltbookInboxHistory({ config, runtime, state }) {
6319
6433
  const summary = (status === "replied" ? (replyText || contextText) : contextText).slice(0, 160);
6320
6434
  const titlePrefix = status === "replied" ? "replied" : "skipped";
6321
6435
  const title = postTitle ? `${titlePrefix} → ${postTitle}` : (status === "replied" ? "Moltbook reply" : "Moltbook skipped");
6322
- const recorded = recordHistoryItem({
6323
- config,
6324
- runtime,
6325
- state,
6326
- item: {
6327
- stableId,
6328
- token,
6329
- kind: "moltbook_reply",
6330
- threadId: "moltbook",
6331
- threadLabel: postTitle || "Moltbook",
6332
- title,
6333
- summary,
6334
- messageText,
6335
- createdAtMs,
6336
- readOnly: true,
6337
- provider: "moltbook",
6338
- },
6436
+ additions.push({
6437
+ stableId,
6438
+ token,
6439
+ kind: "moltbook_reply",
6440
+ threadId: "moltbook",
6441
+ threadLabel: postTitle || "Moltbook",
6442
+ title,
6443
+ summary,
6444
+ messageText,
6445
+ createdAtMs,
6446
+ readOnly: true,
6447
+ provider: "moltbook",
6339
6448
  });
6340
- if (recorded) changed = true;
6449
+ knownStableIds.add(stableId);
6341
6450
  }
6342
6451
  } catch (error) {
6343
6452
  console.error(`[moltbook-inbox-backfill] ${error.message}`);
6344
6453
  return false;
6345
6454
  }
6455
+ if (additions.length === 0) {
6456
+ return false;
6457
+ }
6458
+
6459
+ const nextItems = mergeNormalizedHistoryItems(
6460
+ additions,
6461
+ runtime.recentHistoryItems,
6462
+ config.maxHistoryItems
6463
+ );
6464
+ const changed = timelineProjectionChanged(nextItems, runtime.recentHistoryItems, [
6465
+ "stableId",
6466
+ "title",
6467
+ "createdAtMs",
6468
+ ]);
6346
6469
  if (changed) {
6470
+ runtime.recentHistoryItems = nextItems;
6471
+ state.recentHistoryItems = nextItems;
6347
6472
  console.log(`[moltbook-inbox-backfill] Synthesized history entries for resolved inbox items`);
6348
6473
  }
6349
6474
  return changed;
6350
6475
  }
6351
6476
 
6477
+ async function runDeferredStartupBackfills({ config, runtime, state }) {
6478
+ const startedAtMs = Date.now();
6479
+ let changed = false;
6480
+ const steps = [
6481
+ ["timeline-images", () => backfillPersistedTimelineImagePaths({ config, runtime, state })],
6482
+ ["moltbook-inbox", () => backfillMoltbookInboxHistory({ config, runtime, state })],
6483
+ ["provider-recovery", () => recoverMissingProviderStateFromBackup({ config, runtime, state })],
6484
+ ];
6485
+
6486
+ for (const [label, run] of steps) {
6487
+ try {
6488
+ changed = (await run()) || changed;
6489
+ } catch (error) {
6490
+ console.error(`[startup] ${label} backfill failed: ${error?.message || error}`);
6491
+ }
6492
+ }
6493
+
6494
+ if (changed && !runtime.stopping) {
6495
+ scheduleSaveState(config, state);
6496
+ }
6497
+ const durationMs = Date.now() - startedAtMs;
6498
+ if (durationMs >= 1_000) {
6499
+ console.log(`[startup] deferred backfills ready durationMs=${durationMs}`);
6500
+ }
6501
+ }
6502
+
6352
6503
  async function backfillRecentTimelineEntryDiffs({ config, runtime, state }) {
6353
6504
  const nextEntries = runtime.recentTimelineEntries.map((entry) => ({ ...entry }));
6354
6505
  let changed = false;
@@ -7080,11 +7231,6 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
7080
7231
  });
7081
7232
  return [];
7082
7233
  }
7083
- const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
7084
- if (!commandText) {
7085
- return [];
7086
- }
7087
- const classified = classifyTimelineCommand(commandText);
7088
7234
  upsertTimelineActivity({
7089
7235
  config,
7090
7236
  runtime,
@@ -7096,19 +7242,7 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
7096
7242
  source: "codex-tool-output",
7097
7243
  atMs: createdAtMs,
7098
7244
  });
7099
- return [
7100
- buildToolTimelineEntry({
7101
- provider: "codex",
7102
- threadId,
7103
- threadLabel,
7104
- callId,
7105
- createdAtMs,
7106
- fileEventType: classified.fileEventType,
7107
- commandText: classified.commandText || commandText,
7108
- fileRefs: classified.fileRefs,
7109
- cwd: fileState.cwd || "",
7110
- }),
7111
- ].filter(Boolean);
7245
+ return [];
7112
7246
  }
7113
7247
 
7114
7248
  if (payloadType === "custom_tool_call_output") {
@@ -7428,9 +7562,34 @@ async function querySqliteCompletionRows({ logsDbFile, cursorId, minTsSec = 0 })
7428
7562
  return runSqliteJsonQuery(logsDbFile, sql);
7429
7563
  }
7430
7564
 
7565
+ function isRetryableSqliteQueryError(error) {
7566
+ const message = cleanText(error?.message ?? error).toLowerCase();
7567
+ return message.includes("database is locked") || message.includes("database is busy") || /sqlite_(?:busy|locked)/u.test(message);
7568
+ }
7569
+
7570
+ function sqliteQueryRetryDelayMs(attempt) {
7571
+ return SQLITE_QUERY_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, Number(attempt) || 0);
7572
+ }
7573
+
7431
7574
  async function runSqliteJsonQuery(dbFile, sql) {
7575
+ let lastError = null;
7576
+ for (let attempt = 0; attempt < SQLITE_QUERY_MAX_ATTEMPTS; attempt += 1) {
7577
+ try {
7578
+ return await runSqliteJsonQueryOnce(dbFile, sql);
7579
+ } catch (error) {
7580
+ lastError = error;
7581
+ if (!isRetryableSqliteQueryError(error) || attempt + 1 >= SQLITE_QUERY_MAX_ATTEMPTS) {
7582
+ throw error;
7583
+ }
7584
+ await sleep(sqliteQueryRetryDelayMs(attempt));
7585
+ }
7586
+ }
7587
+ throw lastError || new Error("sqlite-query-failed");
7588
+ }
7589
+
7590
+ function runSqliteJsonQueryOnce(dbFile, sql) {
7432
7591
  return new Promise((resolve, reject) => {
7433
- const child = spawn("sqlite3", ["-json", dbFile, sql], {
7592
+ const child = spawn("sqlite3", ["-readonly", "-cmd", `.timeout ${SQLITE_QUERY_BUSY_TIMEOUT_MS}`, "-json", dbFile, sql], {
7434
7593
  stdio: ["ignore", "pipe", "pipe"],
7435
7594
  });
7436
7595
 
@@ -9951,13 +10110,28 @@ function buildDefaultCollaborationMode(threadState) {
9951
10110
  return buildRequestedCollaborationMode(threadState, "default");
9952
10111
  }
9953
10112
 
10113
+ function transferableFrameBytes(frame) {
10114
+ if (
10115
+ frame.byteOffset === 0 &&
10116
+ frame.byteLength === frame.buffer.byteLength &&
10117
+ frame.buffer instanceof ArrayBuffer
10118
+ ) {
10119
+ return new Uint8Array(frame.buffer);
10120
+ }
10121
+ const owned = new Uint8Array(frame.byteLength);
10122
+ owned.set(frame);
10123
+ return owned;
10124
+ }
10125
+
9954
10126
  class NativeIpcClient {
9955
10127
  constructor({ config, runtime, onThreadStateChanged, onUserInputRequested }) {
9956
10128
  this.config = config;
9957
10129
  this.runtime = runtime;
9958
10130
  this.onThreadStateChanged = onThreadStateChanged;
9959
10131
  this.onUserInputRequested = onUserInputRequested;
9960
- this.buffer = Buffer.alloc(0);
10132
+ this.frameDecoder = new LengthPrefixedFrameDecoder();
10133
+ this.frameWorker = null;
10134
+ this.messageQueue = Promise.resolve();
9961
10135
  this.socket = null;
9962
10136
  this.clientId = null;
9963
10137
  this.pendingResponses = new Map();
@@ -9966,6 +10140,7 @@ class NativeIpcClient {
9966
10140
  }
9967
10141
 
9968
10142
  start() {
10143
+ this.ensureFrameWorker();
9969
10144
  this.connect();
9970
10145
  }
9971
10146
 
@@ -9987,6 +10162,36 @@ class NativeIpcClient {
9987
10162
  this.socket.destroy();
9988
10163
  this.socket = null;
9989
10164
  }
10165
+ if (this.frameWorker) {
10166
+ void this.frameWorker.terminate();
10167
+ this.frameWorker = null;
10168
+ }
10169
+ }
10170
+
10171
+ ensureFrameWorker() {
10172
+ if (this.frameWorker) {
10173
+ return this.frameWorker;
10174
+ }
10175
+ const worker = new Worker(new URL("./lib/ipc-frame-worker.mjs", import.meta.url), {
10176
+ type: "module",
10177
+ });
10178
+ this.frameWorker = worker;
10179
+ worker.on("message", (payload) => {
10180
+ this.handleFrameWorkerMessage(payload);
10181
+ });
10182
+ worker.on("error", (error) => {
10183
+ console.error(`[ipc-worker-error] ${error.message}`);
10184
+ });
10185
+ worker.on("exit", (code) => {
10186
+ if (this.frameWorker === worker) {
10187
+ this.frameWorker = null;
10188
+ }
10189
+ if (!this.stopped && code !== 0) {
10190
+ console.error(`[ipc-worker-error] exited with code ${code}`);
10191
+ this.socket?.destroy();
10192
+ }
10193
+ });
10194
+ return worker;
9990
10195
  }
9991
10196
 
9992
10197
  async sendApprovalDecision(approval, decision) {
@@ -10128,11 +10333,12 @@ class NativeIpcClient {
10128
10333
  return;
10129
10334
  }
10130
10335
 
10336
+ this.ensureFrameWorker();
10131
10337
  const socket = net.createConnection(this.config.ipcSocketPath);
10132
10338
  this.socket = socket;
10133
10339
 
10134
10340
  socket.on("connect", () => {
10135
- this.buffer = Buffer.alloc(0);
10341
+ this.frameDecoder.reset();
10136
10342
  this.write({
10137
10343
  type: "request",
10138
10344
  requestId: crypto.randomUUID(),
@@ -10142,9 +10348,7 @@ class NativeIpcClient {
10142
10348
  });
10143
10349
 
10144
10350
  socket.on("data", (chunk) => {
10145
- this.handleData(chunk).catch((error) => {
10146
- console.error(`[ipc-error] ${error.message}`);
10147
- });
10351
+ this.handleData(chunk);
10148
10352
  });
10149
10353
 
10150
10354
  socket.on("error", (error) => {
@@ -10178,27 +10382,51 @@ class NativeIpcClient {
10178
10382
  }
10179
10383
  }
10180
10384
 
10181
- async handleData(chunk) {
10182
- this.buffer = Buffer.concat([this.buffer, chunk]);
10183
-
10184
- while (this.buffer.length >= 4) {
10185
- const frameBytes = this.buffer.readUInt32LE(0);
10186
- if (this.buffer.length < 4 + frameBytes) {
10187
- return;
10188
- }
10189
-
10190
- const frame = this.buffer.subarray(4, 4 + frameBytes).toString("utf8");
10191
- this.buffer = this.buffer.subarray(4 + frameBytes);
10385
+ handleData(chunk) {
10386
+ let frames;
10387
+ try {
10388
+ frames = this.frameDecoder.push(chunk);
10389
+ } catch (error) {
10390
+ console.error(`[ipc-error] ${error.message}`);
10391
+ this.socket?.destroy();
10392
+ return this.messageQueue;
10393
+ }
10192
10394
 
10193
- let message;
10395
+ const worker = this.ensureFrameWorker();
10396
+ for (const frame of frames) {
10194
10397
  try {
10195
- message = JSON.parse(frame);
10196
- } catch {
10197
- continue;
10398
+ const transferable = transferableFrameBytes(frame);
10399
+ worker.postMessage(
10400
+ { type: "frame", frame: transferable },
10401
+ [transferable.buffer]
10402
+ );
10403
+ } catch (error) {
10404
+ console.error(`[ipc-worker-error] ${error.message}`);
10405
+ this.socket?.destroy();
10406
+ break;
10198
10407
  }
10408
+ }
10409
+ return this.messageQueue;
10410
+ }
10199
10411
 
10200
- await this.handleMessage(message);
10412
+ handleFrameWorkerMessage(payload) {
10413
+ if (payload?.type === "error") {
10414
+ console.error(`[ipc-worker-error] ${payload.error || "IPC frame parse failed"}`);
10415
+ return;
10416
+ }
10417
+ if (payload?.type !== "message" || !payload.message) {
10418
+ return;
10201
10419
  }
10420
+ if (Number(payload.frameBytes) >= IPC_LARGE_FRAME_LOG_BYTES) {
10421
+ console.log(
10422
+ `[ipc-frame] bytes=${Number(payload.frameBytes) || 0} parseMs=${Number(payload.parseMs) || 0} totalMs=${Number(payload.totalMs) || 0}`
10423
+ );
10424
+ }
10425
+ this.messageQueue = this.messageQueue
10426
+ .then(() => this.handleMessage(payload.message))
10427
+ .catch((error) => {
10428
+ console.error(`[ipc-error] ${error.message}`);
10429
+ });
10202
10430
  }
10203
10431
 
10204
10432
  async handleMessage(message) {
@@ -10267,7 +10495,8 @@ class NativeIpcClient {
10267
10495
  }
10268
10496
 
10269
10497
  const previousState = this.runtime.threadStates.get(normalized.conversationId) ?? { requests: [] };
10270
- let nextState = cloneJson(previousState) ?? { requests: [] };
10498
+ const previousRequests = normalizeNativeRequests(previousState.requests);
10499
+ let nextState = previousState;
10271
10500
 
10272
10501
  if (normalized.state) {
10273
10502
  nextState = mergeConversationState(nextState, normalized.state);
@@ -10281,7 +10510,6 @@ class NativeIpcClient {
10281
10510
  nextState.requests = [];
10282
10511
  }
10283
10512
 
10284
- const previousRequests = normalizeNativeRequests(previousState.requests);
10285
10513
  const nextRequests = normalizeNativeRequests(nextState.requests);
10286
10514
 
10287
10515
  if (previousRequests.length !== nextRequests.length) {
@@ -10449,143 +10677,6 @@ function mergeConversationState(previousState, nextState) {
10449
10677
  return merged;
10450
10678
  }
10451
10679
 
10452
- function applyJsonPatches(document, patches) {
10453
- let next = cloneJson(document) ?? {};
10454
-
10455
- for (const patch of patches) {
10456
- next = applyJsonPatch(next, patch);
10457
- }
10458
-
10459
- return next;
10460
- }
10461
-
10462
- function applyJsonPatch(document, patch) {
10463
- const operation = patch.op;
10464
- const pathSegments = decodeJsonPointer(patch.path);
10465
-
10466
- if (pathSegments.length === 0) {
10467
- if (operation === "remove") {
10468
- return {};
10469
- }
10470
- if (operation === "add" || operation === "replace") {
10471
- return cloneJson(patch.value);
10472
- }
10473
- return document;
10474
- }
10475
-
10476
- const target = cloneJson(document) ?? {};
10477
- if (operation === "remove") {
10478
- removeAtPointer(target, pathSegments);
10479
- return target;
10480
- }
10481
-
10482
- if (operation === "add" || operation === "replace") {
10483
- setAtPointer(target, pathSegments, cloneJson(patch.value), operation === "add");
10484
- return target;
10485
- }
10486
-
10487
- return target;
10488
- }
10489
-
10490
- function decodeJsonPointer(pointer) {
10491
- if (Array.isArray(pointer)) {
10492
- return pointer.map((segment) => String(segment));
10493
- }
10494
-
10495
- if (!pointer || pointer === "/") {
10496
- return [];
10497
- }
10498
-
10499
- return String(pointer)
10500
- .split("/")
10501
- .slice(1)
10502
- .map((segment) => segment.replace(/~1/gu, "/").replace(/~0/gu, "~"));
10503
- }
10504
-
10505
- function setAtPointer(target, segments, value, isAdd) {
10506
- let node = target;
10507
-
10508
- for (let index = 0; index < segments.length - 1; index += 1) {
10509
- const segment = segments[index];
10510
- const nextSegment = segments[index + 1];
10511
-
10512
- if (Array.isArray(node)) {
10513
- const arrayIndex = toArrayIndex(segment, node.length, true);
10514
- if (node[arrayIndex] == null || typeof node[arrayIndex] !== "object") {
10515
- node[arrayIndex] = isArrayPointerSegment(nextSegment) ? [] : {};
10516
- }
10517
- node = node[arrayIndex];
10518
- continue;
10519
- }
10520
-
10521
- if (node[segment] == null || typeof node[segment] !== "object") {
10522
- node[segment] = isArrayPointerSegment(nextSegment) ? [] : {};
10523
- }
10524
- node = node[segment];
10525
- }
10526
-
10527
- const finalSegment = segments.at(-1);
10528
- if (Array.isArray(node)) {
10529
- const index = toArrayIndex(finalSegment, node.length, isAdd);
10530
- if (isAdd && (finalSegment === "-" || index === node.length)) {
10531
- node.push(value);
10532
- return;
10533
- }
10534
- if (isAdd) {
10535
- node.splice(index, 0, value);
10536
- return;
10537
- }
10538
- node[index] = value;
10539
- return;
10540
- }
10541
-
10542
- node[finalSegment] = value;
10543
- }
10544
-
10545
- function removeAtPointer(target, segments) {
10546
- let node = target;
10547
-
10548
- for (let index = 0; index < segments.length - 1; index += 1) {
10549
- const segment = segments[index];
10550
- if (Array.isArray(node)) {
10551
- const arrayIndex = toArrayIndex(segment, node.length, false);
10552
- node = node[arrayIndex];
10553
- } else {
10554
- node = node?.[segment];
10555
- }
10556
-
10557
- if (node == null) {
10558
- return;
10559
- }
10560
- }
10561
-
10562
- const finalSegment = segments.at(-1);
10563
- if (Array.isArray(node)) {
10564
- const arrayIndex = toArrayIndex(finalSegment, node.length, false);
10565
- if (arrayIndex >= 0 && arrayIndex < node.length) {
10566
- node.splice(arrayIndex, 1);
10567
- }
10568
- return;
10569
- }
10570
-
10571
- delete node?.[finalSegment];
10572
- }
10573
-
10574
- function toArrayIndex(segment, length, allowEnd) {
10575
- if (segment === "-" && allowEnd) {
10576
- return length;
10577
- }
10578
- const parsed = Number.parseInt(segment, 10);
10579
- if (Number.isFinite(parsed) && parsed >= 0) {
10580
- return parsed;
10581
- }
10582
- return allowEnd ? length : 0;
10583
- }
10584
-
10585
- function isArrayPointerSegment(segment) {
10586
- return segment === "-" || /^\d+$/u.test(String(segment || ""));
10587
- }
10588
-
10589
10680
  function normalizeNativeRequests(requests) {
10590
10681
  if (!Array.isArray(requests)) {
10591
10682
  return [];
@@ -11033,7 +11124,7 @@ function formatCommandApprovalMessage(params, locale = config?.defaultLocale ||
11033
11124
  parts.push(t(locale, "server.message.commandApprovalNeeded"));
11034
11125
  }
11035
11126
  if (command) {
11036
- parts.push(`${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${command}\n\`\`\``);
11127
+ parts.push(`${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${escapeMarkdownCodeFenceBody(command)}\n\`\`\``);
11037
11128
  }
11038
11129
  return parts.join("\n\n") || t(locale, "server.message.commandApprovalNeeded");
11039
11130
  }
@@ -12986,6 +13077,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
12986
13077
  primaryLabel: t(locale, "server.action.review"),
12987
13078
  createdAtMs: Number(approval.createdAtMs) || now,
12988
13079
  provider: normalizeProvider(approval.provider),
13080
+ approvalKind: cleanText(approval.kind || ""),
12989
13081
  });
12990
13082
  }
12991
13083
 
@@ -13030,6 +13122,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
13030
13122
  primaryLabel: t(locale, userInputRequest.supported ? "server.action.select" : "server.action.detail"),
13031
13123
  createdAtMs: Number(userInputRequest.createdAtMs) || now,
13032
13124
  provider: "codex",
13125
+ supported: userInputRequest.supported !== false,
13033
13126
  });
13034
13127
  }
13035
13128
 
@@ -13091,10 +13184,52 @@ function buildPendingInboxItems(runtime, state, config, locale) {
13091
13184
  return items.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
13092
13185
  }
13093
13186
 
13187
+ function buildInProgressInboxItems(runtime, locale) {
13188
+ const items = buildActiveTimelineActivityEntries(runtime, locale).map((entry) => ({
13189
+ kind: "activity_status",
13190
+ token: entry.token,
13191
+ threadId: cleanText(entry.threadId || ""),
13192
+ threadLabel: cleanText(entry.threadLabel || ""),
13193
+ title: cleanText(entry.threadLabel || entry.title || ""),
13194
+ summary: [cleanText(entry.title || ""), cleanText(entry.summary || "")].filter(Boolean).join(" · "),
13195
+ primaryLabel: t(locale, "server.action.detail"),
13196
+ createdAtMs: Number(entry.createdAtMs) || Date.now(),
13197
+ provider: normalizeProvider(entry.provider),
13198
+ activityPhase: cleanText(entry.activityPhase || "working"),
13199
+ commandText: cleanText(entry.commandText || ""),
13200
+ fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
13201
+ }));
13202
+
13203
+ for (const task of runtime.a2aTasksByToken.values()) {
13204
+ const status = cleanText(task?.status || "");
13205
+ if (status !== "working" && status !== "input-required") {
13206
+ continue;
13207
+ }
13208
+ const instruction = cleanText(task.instruction || "");
13209
+ items.push({
13210
+ kind: "a2a_task",
13211
+ token: task.token,
13212
+ threadId: "a2a",
13213
+ threadLabel: instruction.slice(0, 80),
13214
+ title: instruction.slice(0, 80) || "A2A Task",
13215
+ summary: cleanText(task.statusMessage || instruction).slice(0, 160),
13216
+ primaryLabel: t(locale, "server.action.detail"),
13217
+ createdAtMs: Number(task.updatedAtMs || task.createdAtMs) || Date.now(),
13218
+ provider: "a2a",
13219
+ taskStatus: status,
13220
+ });
13221
+ }
13222
+
13223
+ return items.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
13224
+ }
13225
+
13094
13226
  function buildCompletedInboxItems(runtime, state, config, locale) {
13095
13227
  const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
13096
13228
  runtime.recentHistoryItems = items;
13097
- const completedKinds = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task_result"]);
13229
+ // `completion` remains the notification/history format for the Codex
13230
+ // SQLite completion scanner, so it must stay visible alongside the newer
13231
+ // assistant_final timeline format.
13232
+ const completedKinds = new Set(["completion", "assistant_final", "approval", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task_result"]);
13098
13233
 
13099
13234
  // Infrequent kinds (A2A, thread_share) get evicted from the fixed-size
13100
13235
  // history FIFO by the high volume of approval/assistant_final entries.
@@ -13137,6 +13272,8 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
13137
13272
  createdAtMs: item.createdAtMs,
13138
13273
  provider: normalizeProvider(item.provider),
13139
13274
  ...(item.draftType != null ? { draftType: item.draftType } : {}),
13275
+ ...(item.taskStatus != null ? { taskStatus: item.taskStatus } : {}),
13276
+ ...(item.outcome != null ? { outcome: item.outcome } : {}),
13140
13277
  }));
13141
13278
  }
13142
13279
 
@@ -13306,9 +13443,27 @@ async function buildDiffThreadGroups(runtime, state, config) {
13306
13443
  // is served from `/api/inbox/diff` and fetched separately by the PWA so
13307
13444
  // it doesn't block first paint of the inbox/completed lists.
13308
13445
  function buildInboxFastResponse(runtime, state, config, locale) {
13309
- return {
13446
+ const normalized = buildWorkItemResponse({
13310
13447
  pending: buildPendingInboxItems(runtime, state, config, locale),
13448
+ inProgress: buildInProgressInboxItems(runtime, locale),
13311
13449
  completed: buildCompletedInboxItems(runtime, state, config, locale),
13450
+ });
13451
+ const { pending, inProgress, completed, ...work } = normalized;
13452
+ const attention = [];
13453
+ if (pending.some((item) => item.provider === "codex") && !runtime.ipcClient?.clientId) {
13454
+ attention.push({ source: "codex", code: "connection-required" });
13455
+ }
13456
+ return {
13457
+ pending,
13458
+ inProgress,
13459
+ completed,
13460
+ work: {
13461
+ ...work,
13462
+ health: {
13463
+ checkedAtMs: Date.now(),
13464
+ attention,
13465
+ },
13466
+ },
13312
13467
  };
13313
13468
  }
13314
13469
 
@@ -14174,6 +14329,28 @@ function buildTimelineMessageDetail(entry, locale, runtime = null, config = null
14174
14329
  };
14175
14330
  }
14176
14331
 
14332
+ function buildTimelineActivityDetail(entry, locale) {
14333
+ const detailText = [
14334
+ cleanText(entry?.title || ""),
14335
+ cleanText(entry?.summary || ""),
14336
+ ].filter(Boolean).join("\n\n");
14337
+ return {
14338
+ kind: "activity_status",
14339
+ token: cleanText(entry?.token || ""),
14340
+ threadId: cleanText(entry?.threadId || ""),
14341
+ title: cleanText(entry?.threadLabel || entry?.title || "") || kindTitle(locale, "activity_status"),
14342
+ threadLabel: cleanText(entry?.threadLabel || ""),
14343
+ createdAtMs: Number(entry?.createdAtMs) || 0,
14344
+ messageHtml: renderMessageHtml(detailText, `<p>${escapeHtml(t(locale, "server.activity.working"))}</p>`),
14345
+ fileRefs: normalizeTimelineFileRefs(entry?.fileRefs ?? []),
14346
+ commandText: cleanText(entry?.commandText || ""),
14347
+ activityPhase: cleanText(entry?.activityPhase || "working"),
14348
+ provider: normalizeProvider(entry?.provider),
14349
+ readOnly: true,
14350
+ actions: [],
14351
+ };
14352
+ }
14353
+
14177
14354
  function buildAmbientSuggestionsDetail(entry, locale) {
14178
14355
  const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
14179
14356
  return {
@@ -15548,6 +15725,11 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
15548
15725
  const group = (await getDiffThreadGroupsCached(runtime, state, config)).find((entry) => entry.token === token);
15549
15726
  return group ? buildDiffThreadDetail(group, locale) : null;
15550
15727
  }
15728
+ if (kind === "activity_status") {
15729
+ const entry = buildActiveTimelineActivityEntries(runtime, locale)
15730
+ .find((candidate) => cleanText(candidate?.token || "") === cleanText(token || ""));
15731
+ return entry ? buildTimelineActivityDetail(entry, locale) : null;
15732
+ }
15551
15733
  if (kind === "file_event") {
15552
15734
  const entry = timelineEntryByToken(runtime, token, kind);
15553
15735
  return entry ? buildTimelineFileEventDetail(entry, locale) : null;
@@ -16183,6 +16365,7 @@ function logRemotePairingBootTrace(body, session, req) {
16183
16365
  const traceId = cleanText(body?.traceId || "").slice(0, 80) || "unknown";
16184
16366
  const reason = cleanText(body?.reason || "").slice(0, 80) || "unknown";
16185
16367
  const appBuildId = cleanText(body?.appBuildId || "").slice(0, 80) || "unknown";
16368
+ const origin = cleanText(body?.origin || "").slice(0, 180) || "unknown";
16186
16369
  const totalMs = Math.max(0, Math.min(600_000, Math.round(Number(body?.totalMs) || 0)));
16187
16370
  const remoteRouteSeen = body?.remoteRouteSeen === true;
16188
16371
  const eventList = Array.isArray(body?.events) ? body.events : [];
@@ -16207,7 +16390,7 @@ function logRemotePairingBootTrace(body, session, req) {
16207
16390
  console.log(
16208
16391
  `[remote-pairing-boot] trace=${traceId} device=${deviceId} reason=${reason} ` +
16209
16392
  `total=${totalMs}ms remote=${remoteRouteSeen ? 1 : 0} build=${appBuildId} ` +
16210
- `events=${events.length} ua=${JSON.stringify(ua)}`
16393
+ `origin=${JSON.stringify(origin)} events=${events.length} ua=${JSON.stringify(ua)}`
16211
16394
  );
16212
16395
  if (phases) {
16213
16396
  console.log(`[remote-pairing-boot-phases] trace=${traceId} ${phases}`);
@@ -19235,6 +19418,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
19235
19418
  if (!sourceId || !draftText) {
19236
19419
  return writeJson(res, 400, { error: "missing-sourceId-or-draftText" });
19237
19420
  }
19421
+ if (looksLikeProviderErrorText(draftText)) {
19422
+ return writeJson(res, 422, { error: "moltbook-draft-provider-error" });
19423
+ }
19238
19424
  if (draftType === "reply" && !postId) {
19239
19425
  return writeJson(res, 400, { error: "missing-postId-for-reply" });
19240
19426
  }
@@ -23071,7 +23257,15 @@ async function extractRolloutThreadMetadata(filePath) {
23071
23257
  crlfDelay: Infinity,
23072
23258
  });
23073
23259
 
23260
+ let linesRead = 0;
23261
+ let bytesRead = 0;
23074
23262
  for await (const line of rl) {
23263
+ linesRead += 1;
23264
+ const lineBytes = Buffer.byteLength(line, "utf8");
23265
+ if (linesRead > ROLLOUT_LABEL_METADATA_MAX_LINES || bytesRead + lineBytes > ROLLOUT_LABEL_METADATA_MAX_BYTES) {
23266
+ break;
23267
+ }
23268
+ bytesRead += lineBytes;
23075
23269
  if (!line.trim()) {
23076
23270
  continue;
23077
23271
  }
@@ -23137,11 +23331,16 @@ async function extractRolloutThreadMetadata(filePath) {
23137
23331
 
23138
23332
  async function buildRolloutThreadLabelIndex(knownFiles, sessionIndex) {
23139
23333
  const entries = new Map();
23140
- for (const filePath of Array.isArray(knownFiles) ? knownFiles : []) {
23334
+ const files = Array.isArray(knownFiles) ? knownFiles : [];
23335
+ for (let index = 0; index < files.length; index += 1) {
23336
+ const filePath = files[index];
23141
23337
  const metadata = await extractRolloutThreadMetadata(filePath);
23142
23338
  if (metadata?.threadId) {
23143
23339
  entries.set(metadata.threadId, metadata);
23144
23340
  }
23341
+ if ((index + 1) % 8 === 0) {
23342
+ await yieldToEventLoop();
23343
+ }
23145
23344
  }
23146
23345
 
23147
23346
  const resolved = new Map();
@@ -23201,6 +23400,48 @@ async function buildRolloutThreadLabelIndex(knownFiles, sessionIndex) {
23201
23400
  return resolved;
23202
23401
  }
23203
23402
 
23403
+ function refreshRolloutThreadLabelIndexInBackground({ config, runtime, state }) {
23404
+ if (runtime.rolloutLabelIndexInFlight || runtime.stopping) {
23405
+ return;
23406
+ }
23407
+ runtime.rolloutLabelIndexInFlight = true;
23408
+ const startedAtMs = Date.now();
23409
+ void buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex)
23410
+ .then((labels) => {
23411
+ runtime.rolloutThreadLabels = labels;
23412
+ if (!runtime.stopping && refreshResolvedThreadLabels({ config, runtime, state })) {
23413
+ scheduleSaveState(config, state);
23414
+ }
23415
+ const durationMs = Date.now() - startedAtMs;
23416
+ if (durationMs >= 1_000) {
23417
+ console.log(`[startup] rollout labels ready count=${labels.size} durationMs=${durationMs}`);
23418
+ }
23419
+ })
23420
+ .catch((error) => {
23421
+ console.error(`[startup] rollout label index failed: ${error?.message || error}`);
23422
+ })
23423
+ .finally(() => {
23424
+ runtime.rolloutLabelIndexInFlight = false;
23425
+ });
23426
+ }
23427
+
23428
+ function scheduleStartupMaintenance({ config, runtime, state }) {
23429
+ const run = () => {
23430
+ if (runtime.stopping) {
23431
+ return;
23432
+ }
23433
+ if (!runtime.initialScanComplete) {
23434
+ const retryTimer = setTimeout(run, STARTUP_MAINTENANCE_RETRY_MS);
23435
+ retryTimer.unref?.();
23436
+ return;
23437
+ }
23438
+ refreshRolloutThreadLabelIndexInBackground({ config, runtime, state });
23439
+ void runDeferredStartupBackfills({ config, runtime, state });
23440
+ };
23441
+ const timer = setTimeout(run, STARTUP_MAINTENANCE_DELAY_MS);
23442
+ timer.unref?.();
23443
+ }
23444
+
23204
23445
  async function findLatestCodexLogsDbFile(codexHome) {
23205
23446
  let entries;
23206
23447
  try {
@@ -23824,6 +24065,10 @@ function sleep(ms) {
23824
24065
  return new Promise((resolve) => setTimeout(resolve, ms));
23825
24066
  }
23826
24067
 
24068
+ function yieldToEventLoop() {
24069
+ return new Promise((resolve) => setImmediate(resolve));
24070
+ }
24071
+
23827
24072
  function resolvePath(value) {
23828
24073
  if (!value) {
23829
24074
  return value;
@@ -23929,7 +24174,6 @@ async function main() {
23929
24174
  runtime.knownFiles = await listRolloutFiles(config.sessionsDir);
23930
24175
  runtime.logsDbFile = config.codexLogsDbFile || (await findLatestCodexLogsDbFile(config.codexHome)) || "";
23931
24176
  runtime.lastDirectoryScanAt = Date.now();
23932
- runtime.rolloutThreadLabels = await buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex);
23933
24177
 
23934
24178
  console.log(
23935
24179
  [
@@ -24002,12 +24246,9 @@ async function main() {
24002
24246
  normalizedTimelineStateChanged ||
24003
24247
  migratedPairedDevicesStateChanged ||
24004
24248
  restoredPendingPlanStateChanged ||
24005
- restoredTimelineImagePathsStateChanged ||
24006
24249
  backfilledAmbientSuggestionsStateChanged ||
24007
24250
  migratedRecentCodeEventsStateChanged ||
24008
24251
  restoredPendingUserInputStateChanged ||
24009
- backfilledMoltbookInboxChanged ||
24010
- recoveredMissingProviderStateChanged ||
24011
24252
  refreshResolvedThreadLabels({ config, runtime, state })
24012
24253
  ) {
24013
24254
  await saveState(config.stateFile, state);
@@ -24091,6 +24332,8 @@ async function main() {
24091
24332
  }
24092
24333
  }
24093
24334
 
24335
+ scheduleStartupMaintenance({ config, runtime, state });
24336
+
24094
24337
  timelineLiveHandle = startTimelineLiveSync({ config, runtime, state });
24095
24338
 
24096
24339
  // --- A2A Relay ---
@@ -24144,6 +24387,10 @@ async function main() {
24144
24387
  let lastA2aEnvCheckAt = 0;
24145
24388
  const A2A_ENV_CHECK_INTERVAL_MS = 30_000; // check every 30 seconds
24146
24389
 
24390
+ if (approvalServer) {
24391
+ await sleep(INITIAL_SCAN_WARMUP_MS);
24392
+ }
24393
+
24147
24394
  while (!runtime.stopping) {
24148
24395
  try {
24149
24396
  const dirty = await scanOnce({ config, runtime, state });
@@ -24161,6 +24408,8 @@ async function main() {
24161
24408
  }
24162
24409
  } catch (error) {
24163
24410
  console.error(`[scan-error] ${error.message}`);
24411
+ } finally {
24412
+ runtime.initialScanComplete = true;
24164
24413
  }
24165
24414
 
24166
24415
  // Hot-reload: detect a2a.env changes (new config or updated card)