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.
- package/README.md +3 -2
- package/package.json +1 -1
- package/scripts/lib/immutable-json-patch.mjs +142 -0
- package/scripts/lib/ipc-frame-worker.mjs +30 -0
- package/scripts/lib/ipc-message-projector.mjs +221 -0
- package/scripts/lib/length-prefixed-frame-decoder.mjs +128 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +24 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +10 -0
- package/scripts/lib/work-items.mjs +179 -0
- package/scripts/moltbook-api.mjs +27 -0
- package/scripts/moltbook-cli.mjs +12 -4
- package/scripts/moltbook-scout-auto.sh +105 -37
- package/scripts/viveworker-bridge.mjs +472 -206
- package/scripts/viveworker.mjs +5 -0
- package/web/app.css +106 -0
- package/web/app.js +183 -28
- package/web/build-id.js +1 -1
- package/web/i18n.js +44 -10
- package/web/remote-pairing/api-router.js +356 -11
- package/web/remote-pairing/transport.js +7 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
-
|
|
4796
|
-
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
4882
|
+
refreshRolloutThreadLabelIndexInBackground({ config, runtime, state });
|
|
4797
4883
|
}
|
|
4798
4884
|
|
|
4799
|
-
for (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
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
|
-
|
|
10395
|
+
const worker = this.ensureFrameWorker();
|
|
10396
|
+
for (const frame of frames) {
|
|
10177
10397
|
try {
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|