viveworker 0.8.10 → 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
 
@@ -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;
@@ -7411,9 +7562,34 @@ async function querySqliteCompletionRows({ logsDbFile, cursorId, minTsSec = 0 })
7411
7562
  return runSqliteJsonQuery(logsDbFile, sql);
7412
7563
  }
7413
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
+
7414
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) {
7415
7591
  return new Promise((resolve, reject) => {
7416
- const child = spawn("sqlite3", ["-json", dbFile, sql], {
7592
+ const child = spawn("sqlite3", ["-readonly", "-cmd", `.timeout ${SQLITE_QUERY_BUSY_TIMEOUT_MS}`, "-json", dbFile, sql], {
7417
7593
  stdio: ["ignore", "pipe", "pipe"],
7418
7594
  });
7419
7595
 
@@ -9934,13 +10110,28 @@ function buildDefaultCollaborationMode(threadState) {
9934
10110
  return buildRequestedCollaborationMode(threadState, "default");
9935
10111
  }
9936
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
+
9937
10126
  class NativeIpcClient {
9938
10127
  constructor({ config, runtime, onThreadStateChanged, onUserInputRequested }) {
9939
10128
  this.config = config;
9940
10129
  this.runtime = runtime;
9941
10130
  this.onThreadStateChanged = onThreadStateChanged;
9942
10131
  this.onUserInputRequested = onUserInputRequested;
9943
- this.buffer = Buffer.alloc(0);
10132
+ this.frameDecoder = new LengthPrefixedFrameDecoder();
10133
+ this.frameWorker = null;
10134
+ this.messageQueue = Promise.resolve();
9944
10135
  this.socket = null;
9945
10136
  this.clientId = null;
9946
10137
  this.pendingResponses = new Map();
@@ -9949,6 +10140,7 @@ class NativeIpcClient {
9949
10140
  }
9950
10141
 
9951
10142
  start() {
10143
+ this.ensureFrameWorker();
9952
10144
  this.connect();
9953
10145
  }
9954
10146
 
@@ -9970,6 +10162,36 @@ class NativeIpcClient {
9970
10162
  this.socket.destroy();
9971
10163
  this.socket = null;
9972
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;
9973
10195
  }
9974
10196
 
9975
10197
  async sendApprovalDecision(approval, decision) {
@@ -10111,11 +10333,12 @@ class NativeIpcClient {
10111
10333
  return;
10112
10334
  }
10113
10335
 
10336
+ this.ensureFrameWorker();
10114
10337
  const socket = net.createConnection(this.config.ipcSocketPath);
10115
10338
  this.socket = socket;
10116
10339
 
10117
10340
  socket.on("connect", () => {
10118
- this.buffer = Buffer.alloc(0);
10341
+ this.frameDecoder.reset();
10119
10342
  this.write({
10120
10343
  type: "request",
10121
10344
  requestId: crypto.randomUUID(),
@@ -10125,9 +10348,7 @@ class NativeIpcClient {
10125
10348
  });
10126
10349
 
10127
10350
  socket.on("data", (chunk) => {
10128
- this.handleData(chunk).catch((error) => {
10129
- console.error(`[ipc-error] ${error.message}`);
10130
- });
10351
+ this.handleData(chunk);
10131
10352
  });
10132
10353
 
10133
10354
  socket.on("error", (error) => {
@@ -10161,27 +10382,51 @@ class NativeIpcClient {
10161
10382
  }
10162
10383
  }
10163
10384
 
10164
- async handleData(chunk) {
10165
- this.buffer = Buffer.concat([this.buffer, chunk]);
10166
-
10167
- while (this.buffer.length >= 4) {
10168
- const frameBytes = this.buffer.readUInt32LE(0);
10169
- if (this.buffer.length < 4 + frameBytes) {
10170
- return;
10171
- }
10172
-
10173
- const frame = this.buffer.subarray(4, 4 + frameBytes).toString("utf8");
10174
- 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
+ }
10175
10394
 
10176
- let message;
10395
+ const worker = this.ensureFrameWorker();
10396
+ for (const frame of frames) {
10177
10397
  try {
10178
- message = JSON.parse(frame);
10179
- } catch {
10180
- 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;
10181
10407
  }
10408
+ }
10409
+ return this.messageQueue;
10410
+ }
10182
10411
 
10183
- 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;
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
+ );
10184
10424
  }
10425
+ this.messageQueue = this.messageQueue
10426
+ .then(() => this.handleMessage(payload.message))
10427
+ .catch((error) => {
10428
+ console.error(`[ipc-error] ${error.message}`);
10429
+ });
10185
10430
  }
10186
10431
 
10187
10432
  async handleMessage(message) {
@@ -10250,7 +10495,8 @@ class NativeIpcClient {
10250
10495
  }
10251
10496
 
10252
10497
  const previousState = this.runtime.threadStates.get(normalized.conversationId) ?? { requests: [] };
10253
- let nextState = cloneJson(previousState) ?? { requests: [] };
10498
+ const previousRequests = normalizeNativeRequests(previousState.requests);
10499
+ let nextState = previousState;
10254
10500
 
10255
10501
  if (normalized.state) {
10256
10502
  nextState = mergeConversationState(nextState, normalized.state);
@@ -10264,7 +10510,6 @@ class NativeIpcClient {
10264
10510
  nextState.requests = [];
10265
10511
  }
10266
10512
 
10267
- const previousRequests = normalizeNativeRequests(previousState.requests);
10268
10513
  const nextRequests = normalizeNativeRequests(nextState.requests);
10269
10514
 
10270
10515
  if (previousRequests.length !== nextRequests.length) {
@@ -10432,143 +10677,6 @@ function mergeConversationState(previousState, nextState) {
10432
10677
  return merged;
10433
10678
  }
10434
10679
 
10435
- function applyJsonPatches(document, patches) {
10436
- let next = cloneJson(document) ?? {};
10437
-
10438
- for (const patch of patches) {
10439
- next = applyJsonPatch(next, patch);
10440
- }
10441
-
10442
- return next;
10443
- }
10444
-
10445
- function applyJsonPatch(document, patch) {
10446
- const operation = patch.op;
10447
- const pathSegments = decodeJsonPointer(patch.path);
10448
-
10449
- if (pathSegments.length === 0) {
10450
- if (operation === "remove") {
10451
- return {};
10452
- }
10453
- if (operation === "add" || operation === "replace") {
10454
- return cloneJson(patch.value);
10455
- }
10456
- return document;
10457
- }
10458
-
10459
- const target = cloneJson(document) ?? {};
10460
- if (operation === "remove") {
10461
- removeAtPointer(target, pathSegments);
10462
- return target;
10463
- }
10464
-
10465
- if (operation === "add" || operation === "replace") {
10466
- setAtPointer(target, pathSegments, cloneJson(patch.value), operation === "add");
10467
- return target;
10468
- }
10469
-
10470
- return target;
10471
- }
10472
-
10473
- function decodeJsonPointer(pointer) {
10474
- if (Array.isArray(pointer)) {
10475
- return pointer.map((segment) => String(segment));
10476
- }
10477
-
10478
- if (!pointer || pointer === "/") {
10479
- return [];
10480
- }
10481
-
10482
- return String(pointer)
10483
- .split("/")
10484
- .slice(1)
10485
- .map((segment) => segment.replace(/~1/gu, "/").replace(/~0/gu, "~"));
10486
- }
10487
-
10488
- function setAtPointer(target, segments, value, isAdd) {
10489
- let node = target;
10490
-
10491
- for (let index = 0; index < segments.length - 1; index += 1) {
10492
- const segment = segments[index];
10493
- const nextSegment = segments[index + 1];
10494
-
10495
- if (Array.isArray(node)) {
10496
- const arrayIndex = toArrayIndex(segment, node.length, true);
10497
- if (node[arrayIndex] == null || typeof node[arrayIndex] !== "object") {
10498
- node[arrayIndex] = isArrayPointerSegment(nextSegment) ? [] : {};
10499
- }
10500
- node = node[arrayIndex];
10501
- continue;
10502
- }
10503
-
10504
- if (node[segment] == null || typeof node[segment] !== "object") {
10505
- node[segment] = isArrayPointerSegment(nextSegment) ? [] : {};
10506
- }
10507
- node = node[segment];
10508
- }
10509
-
10510
- const finalSegment = segments.at(-1);
10511
- if (Array.isArray(node)) {
10512
- const index = toArrayIndex(finalSegment, node.length, isAdd);
10513
- if (isAdd && (finalSegment === "-" || index === node.length)) {
10514
- node.push(value);
10515
- return;
10516
- }
10517
- if (isAdd) {
10518
- node.splice(index, 0, value);
10519
- return;
10520
- }
10521
- node[index] = value;
10522
- return;
10523
- }
10524
-
10525
- node[finalSegment] = value;
10526
- }
10527
-
10528
- function removeAtPointer(target, segments) {
10529
- let node = target;
10530
-
10531
- for (let index = 0; index < segments.length - 1; index += 1) {
10532
- const segment = segments[index];
10533
- if (Array.isArray(node)) {
10534
- const arrayIndex = toArrayIndex(segment, node.length, false);
10535
- node = node[arrayIndex];
10536
- } else {
10537
- node = node?.[segment];
10538
- }
10539
-
10540
- if (node == null) {
10541
- return;
10542
- }
10543
- }
10544
-
10545
- const finalSegment = segments.at(-1);
10546
- if (Array.isArray(node)) {
10547
- const arrayIndex = toArrayIndex(finalSegment, node.length, false);
10548
- if (arrayIndex >= 0 && arrayIndex < node.length) {
10549
- node.splice(arrayIndex, 1);
10550
- }
10551
- return;
10552
- }
10553
-
10554
- delete node?.[finalSegment];
10555
- }
10556
-
10557
- function toArrayIndex(segment, length, allowEnd) {
10558
- if (segment === "-" && allowEnd) {
10559
- return length;
10560
- }
10561
- const parsed = Number.parseInt(segment, 10);
10562
- if (Number.isFinite(parsed) && parsed >= 0) {
10563
- return parsed;
10564
- }
10565
- return allowEnd ? length : 0;
10566
- }
10567
-
10568
- function isArrayPointerSegment(segment) {
10569
- return segment === "-" || /^\d+$/u.test(String(segment || ""));
10570
- }
10571
-
10572
10680
  function normalizeNativeRequests(requests) {
10573
10681
  if (!Array.isArray(requests)) {
10574
10682
  return [];
@@ -12969,6 +13077,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
12969
13077
  primaryLabel: t(locale, "server.action.review"),
12970
13078
  createdAtMs: Number(approval.createdAtMs) || now,
12971
13079
  provider: normalizeProvider(approval.provider),
13080
+ approvalKind: cleanText(approval.kind || ""),
12972
13081
  });
12973
13082
  }
12974
13083
 
@@ -13013,6 +13122,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
13013
13122
  primaryLabel: t(locale, userInputRequest.supported ? "server.action.select" : "server.action.detail"),
13014
13123
  createdAtMs: Number(userInputRequest.createdAtMs) || now,
13015
13124
  provider: "codex",
13125
+ supported: userInputRequest.supported !== false,
13016
13126
  });
13017
13127
  }
13018
13128
 
@@ -13074,10 +13184,52 @@ function buildPendingInboxItems(runtime, state, config, locale) {
13074
13184
  return items.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
13075
13185
  }
13076
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
+
13077
13226
  function buildCompletedInboxItems(runtime, state, config, locale) {
13078
13227
  const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
13079
13228
  runtime.recentHistoryItems = items;
13080
- 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"]);
13081
13233
 
13082
13234
  // Infrequent kinds (A2A, thread_share) get evicted from the fixed-size
13083
13235
  // history FIFO by the high volume of approval/assistant_final entries.
@@ -13120,6 +13272,8 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
13120
13272
  createdAtMs: item.createdAtMs,
13121
13273
  provider: normalizeProvider(item.provider),
13122
13274
  ...(item.draftType != null ? { draftType: item.draftType } : {}),
13275
+ ...(item.taskStatus != null ? { taskStatus: item.taskStatus } : {}),
13276
+ ...(item.outcome != null ? { outcome: item.outcome } : {}),
13123
13277
  }));
13124
13278
  }
13125
13279
 
@@ -13289,9 +13443,27 @@ async function buildDiffThreadGroups(runtime, state, config) {
13289
13443
  // is served from `/api/inbox/diff` and fetched separately by the PWA so
13290
13444
  // it doesn't block first paint of the inbox/completed lists.
13291
13445
  function buildInboxFastResponse(runtime, state, config, locale) {
13292
- return {
13446
+ const normalized = buildWorkItemResponse({
13293
13447
  pending: buildPendingInboxItems(runtime, state, config, locale),
13448
+ inProgress: buildInProgressInboxItems(runtime, locale),
13294
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
+ },
13295
13467
  };
13296
13468
  }
13297
13469
 
@@ -14157,6 +14329,28 @@ function buildTimelineMessageDetail(entry, locale, runtime = null, config = null
14157
14329
  };
14158
14330
  }
14159
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
+
14160
14354
  function buildAmbientSuggestionsDetail(entry, locale) {
14161
14355
  const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
14162
14356
  return {
@@ -15531,6 +15725,11 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
15531
15725
  const group = (await getDiffThreadGroupsCached(runtime, state, config)).find((entry) => entry.token === token);
15532
15726
  return group ? buildDiffThreadDetail(group, locale) : null;
15533
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
+ }
15534
15733
  if (kind === "file_event") {
15535
15734
  const entry = timelineEntryByToken(runtime, token, kind);
15536
15735
  return entry ? buildTimelineFileEventDetail(entry, locale) : null;
@@ -16166,6 +16365,7 @@ function logRemotePairingBootTrace(body, session, req) {
16166
16365
  const traceId = cleanText(body?.traceId || "").slice(0, 80) || "unknown";
16167
16366
  const reason = cleanText(body?.reason || "").slice(0, 80) || "unknown";
16168
16367
  const appBuildId = cleanText(body?.appBuildId || "").slice(0, 80) || "unknown";
16368
+ const origin = cleanText(body?.origin || "").slice(0, 180) || "unknown";
16169
16369
  const totalMs = Math.max(0, Math.min(600_000, Math.round(Number(body?.totalMs) || 0)));
16170
16370
  const remoteRouteSeen = body?.remoteRouteSeen === true;
16171
16371
  const eventList = Array.isArray(body?.events) ? body.events : [];
@@ -16190,7 +16390,7 @@ function logRemotePairingBootTrace(body, session, req) {
16190
16390
  console.log(
16191
16391
  `[remote-pairing-boot] trace=${traceId} device=${deviceId} reason=${reason} ` +
16192
16392
  `total=${totalMs}ms remote=${remoteRouteSeen ? 1 : 0} build=${appBuildId} ` +
16193
- `events=${events.length} ua=${JSON.stringify(ua)}`
16393
+ `origin=${JSON.stringify(origin)} events=${events.length} ua=${JSON.stringify(ua)}`
16194
16394
  );
16195
16395
  if (phases) {
16196
16396
  console.log(`[remote-pairing-boot-phases] trace=${traceId} ${phases}`);
@@ -19218,6 +19418,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
19218
19418
  if (!sourceId || !draftText) {
19219
19419
  return writeJson(res, 400, { error: "missing-sourceId-or-draftText" });
19220
19420
  }
19421
+ if (looksLikeProviderErrorText(draftText)) {
19422
+ return writeJson(res, 422, { error: "moltbook-draft-provider-error" });
19423
+ }
19221
19424
  if (draftType === "reply" && !postId) {
19222
19425
  return writeJson(res, 400, { error: "missing-postId-for-reply" });
19223
19426
  }
@@ -23054,7 +23257,15 @@ async function extractRolloutThreadMetadata(filePath) {
23054
23257
  crlfDelay: Infinity,
23055
23258
  });
23056
23259
 
23260
+ let linesRead = 0;
23261
+ let bytesRead = 0;
23057
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;
23058
23269
  if (!line.trim()) {
23059
23270
  continue;
23060
23271
  }
@@ -23120,11 +23331,16 @@ async function extractRolloutThreadMetadata(filePath) {
23120
23331
 
23121
23332
  async function buildRolloutThreadLabelIndex(knownFiles, sessionIndex) {
23122
23333
  const entries = new Map();
23123
- 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];
23124
23337
  const metadata = await extractRolloutThreadMetadata(filePath);
23125
23338
  if (metadata?.threadId) {
23126
23339
  entries.set(metadata.threadId, metadata);
23127
23340
  }
23341
+ if ((index + 1) % 8 === 0) {
23342
+ await yieldToEventLoop();
23343
+ }
23128
23344
  }
23129
23345
 
23130
23346
  const resolved = new Map();
@@ -23184,6 +23400,48 @@ async function buildRolloutThreadLabelIndex(knownFiles, sessionIndex) {
23184
23400
  return resolved;
23185
23401
  }
23186
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
+
23187
23445
  async function findLatestCodexLogsDbFile(codexHome) {
23188
23446
  let entries;
23189
23447
  try {
@@ -23807,6 +24065,10 @@ function sleep(ms) {
23807
24065
  return new Promise((resolve) => setTimeout(resolve, ms));
23808
24066
  }
23809
24067
 
24068
+ function yieldToEventLoop() {
24069
+ return new Promise((resolve) => setImmediate(resolve));
24070
+ }
24071
+
23810
24072
  function resolvePath(value) {
23811
24073
  if (!value) {
23812
24074
  return value;
@@ -23912,7 +24174,6 @@ async function main() {
23912
24174
  runtime.knownFiles = await listRolloutFiles(config.sessionsDir);
23913
24175
  runtime.logsDbFile = config.codexLogsDbFile || (await findLatestCodexLogsDbFile(config.codexHome)) || "";
23914
24176
  runtime.lastDirectoryScanAt = Date.now();
23915
- runtime.rolloutThreadLabels = await buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex);
23916
24177
 
23917
24178
  console.log(
23918
24179
  [
@@ -23985,12 +24246,9 @@ async function main() {
23985
24246
  normalizedTimelineStateChanged ||
23986
24247
  migratedPairedDevicesStateChanged ||
23987
24248
  restoredPendingPlanStateChanged ||
23988
- restoredTimelineImagePathsStateChanged ||
23989
24249
  backfilledAmbientSuggestionsStateChanged ||
23990
24250
  migratedRecentCodeEventsStateChanged ||
23991
24251
  restoredPendingUserInputStateChanged ||
23992
- backfilledMoltbookInboxChanged ||
23993
- recoveredMissingProviderStateChanged ||
23994
24252
  refreshResolvedThreadLabels({ config, runtime, state })
23995
24253
  ) {
23996
24254
  await saveState(config.stateFile, state);
@@ -24074,6 +24332,8 @@ async function main() {
24074
24332
  }
24075
24333
  }
24076
24334
 
24335
+ scheduleStartupMaintenance({ config, runtime, state });
24336
+
24077
24337
  timelineLiveHandle = startTimelineLiveSync({ config, runtime, state });
24078
24338
 
24079
24339
  // --- A2A Relay ---
@@ -24127,6 +24387,10 @@ async function main() {
24127
24387
  let lastA2aEnvCheckAt = 0;
24128
24388
  const A2A_ENV_CHECK_INTERVAL_MS = 30_000; // check every 30 seconds
24129
24389
 
24390
+ if (approvalServer) {
24391
+ await sleep(INITIAL_SCAN_WARMUP_MS);
24392
+ }
24393
+
24130
24394
  while (!runtime.stopping) {
24131
24395
  try {
24132
24396
  const dirty = await scanOnce({ config, runtime, state });
@@ -24144,6 +24408,8 @@ async function main() {
24144
24408
  }
24145
24409
  } catch (error) {
24146
24410
  console.error(`[scan-error] ${error.message}`);
24411
+ } finally {
24412
+ runtime.initialScanComplete = true;
24147
24413
  }
24148
24414
 
24149
24415
  // Hot-reload: detect a2a.env changes (new config or updated card)