viveworker 0.5.2 → 0.5.5
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 +2 -2
- package/package.json +1 -1
- package/scripts/a2a-cli.mjs +1 -1
- package/scripts/moltbook-api.mjs +56 -16
- package/scripts/moltbook-watcher.mjs +30 -11
- package/scripts/viveworker-bridge.mjs +585 -72
- package/web/app.css +175 -1
- package/web/app.js +408 -17
- package/web/i18n.js +88 -2
|
@@ -18,7 +18,7 @@ import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "
|
|
|
18
18
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
19
19
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
20
20
|
import { registerWithRelay, startRelayPolling, stopRelayPolling, postRelayResult, getRelayStatus, updatePublicTasksFlag } from "./a2a-relay-client.mjs";
|
|
21
|
-
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM } from "./moltbook-api.mjs";
|
|
21
|
+
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, listInboxItems } from "./moltbook-api.mjs";
|
|
22
22
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
24
|
const __dirname = path.dirname(__filename);
|
|
@@ -29,7 +29,7 @@ const sessionCookieName = "viveworker_session";
|
|
|
29
29
|
const deviceCookieName = "viveworker_device";
|
|
30
30
|
const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "approval", "plan", "choice", "info", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
31
31
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
32
|
-
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
32
|
+
const timelineKinds = new Set([...timelineMessageKinds, "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
33
33
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
34
34
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
35
35
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -95,6 +95,11 @@ const runtime = {
|
|
|
95
95
|
pairingAttemptsByRemoteAddress: new Map(),
|
|
96
96
|
ipcClient: null,
|
|
97
97
|
stopping: false,
|
|
98
|
+
// In-memory cache for the `/api/share/status` endpoint. A 30s TTL avoids
|
|
99
|
+
// hammering the share worker on every settings-page render. Purely runtime —
|
|
100
|
+
// deliberately NOT persisted via `state` since the cache is worthless after
|
|
101
|
+
// a restart and would just bloat the state file.
|
|
102
|
+
a2aShareStatusCache: null,
|
|
98
103
|
};
|
|
99
104
|
const state = await loadState(config.stateFile);
|
|
100
105
|
runtime.threadRegistry = await loadThreadRegistry(config.threadRegistryFile);
|
|
@@ -156,8 +161,10 @@ runtime.recentHistoryItems = initialHistoryItems;
|
|
|
156
161
|
runtime.recentTimelineEntries = initialTimelineEntries;
|
|
157
162
|
state.recentHistoryItems = initialHistoryItems;
|
|
158
163
|
state.recentTimelineEntries = initialTimelineEntries;
|
|
164
|
+
const backfilledAmbientSuggestionsStateChanged = backfillAmbientSuggestionsState({ config, runtime, state });
|
|
159
165
|
const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
|
|
160
166
|
const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
|
|
167
|
+
const backfilledMoltbookInboxChanged = await backfillMoltbookInboxHistory({ config, runtime, state });
|
|
161
168
|
runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
|
|
162
169
|
runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
|
|
163
170
|
|
|
@@ -269,6 +276,9 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
269
276
|
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
270
277
|
a2aEnabled: Boolean(config.a2aApiKey),
|
|
271
278
|
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
279
|
+
// Share piggy-backs on A2A credentials — it's "enabled" as soon as both
|
|
280
|
+
// halves (user id + API key) are provisioned.
|
|
281
|
+
a2aShareEnabled: Boolean(config.a2aRelayUserId && config.a2aApiKey),
|
|
272
282
|
a2aExecutors: runtime.a2aAvailableExecutors || { codex: false, claude: false },
|
|
273
283
|
a2aExecutorPreference: state.a2aExecutorPreference || "ask",
|
|
274
284
|
};
|
|
@@ -282,6 +292,8 @@ function kindTitle(locale, kind) {
|
|
|
282
292
|
return t(locale, "server.title.assistantCommentary");
|
|
283
293
|
case "assistant_final":
|
|
284
294
|
return t(locale, "server.title.assistantFinal");
|
|
295
|
+
case "ambient_suggestions":
|
|
296
|
+
return t(locale, "server.title.ambientSuggestions");
|
|
285
297
|
case "approval":
|
|
286
298
|
return t(locale, "server.title.approval");
|
|
287
299
|
case "plan":
|
|
@@ -310,6 +322,117 @@ function kindTitle(locale, kind) {
|
|
|
310
322
|
}
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
function normalizeAmbientSuggestion(raw) {
|
|
326
|
+
if (!isPlainObject(raw)) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const title = cleanText(raw.title ?? "");
|
|
331
|
+
const prompt = normalizeLongText(raw.prompt ?? "");
|
|
332
|
+
if (!title || !prompt) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const threadAction = isPlainObject(raw.threadAction)
|
|
337
|
+
? {
|
|
338
|
+
...(cleanText(raw.threadAction.type ?? "") ? { type: cleanText(raw.threadAction.type) } : {}),
|
|
339
|
+
}
|
|
340
|
+
: null;
|
|
341
|
+
const appIds = Array.isArray(raw.appIds)
|
|
342
|
+
? raw.appIds.map((value) => cleanText(value ?? "")).filter(Boolean)
|
|
343
|
+
: [];
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
...(cleanText(raw.id ?? "") ? { id: cleanText(raw.id) } : {}),
|
|
347
|
+
title,
|
|
348
|
+
prompt,
|
|
349
|
+
...(cleanText(raw.description ?? "") ? { description: cleanText(raw.description) } : {}),
|
|
350
|
+
...(threadAction && threadAction.type ? { threadAction } : {}),
|
|
351
|
+
...(appIds.length > 0 ? { appIds } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeAmbientSuggestions(raw) {
|
|
356
|
+
if (!Array.isArray(raw)) {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
return raw.map((entry) => normalizeAmbientSuggestion(entry)).filter(Boolean);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parseAmbientSuggestionsEnvelope(value) {
|
|
363
|
+
const normalized = cleanText(value ?? "");
|
|
364
|
+
if (!normalized) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let payload;
|
|
369
|
+
try {
|
|
370
|
+
payload = JSON.parse(normalized);
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!isPlainObject(payload)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const hasSuggestionsField = Array.isArray(payload.suggestions);
|
|
380
|
+
const hasExcludeField = Array.isArray(payload.exclude);
|
|
381
|
+
if (!hasSuggestionsField && !hasExcludeField) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const suggestions = hasSuggestionsField ? normalizeAmbientSuggestions(payload.suggestions) : [];
|
|
386
|
+
if (suggestions.length > 0) {
|
|
387
|
+
return { type: "suggestions", suggestions };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
type: "metadata",
|
|
392
|
+
hasSuggestionsField,
|
|
393
|
+
hasExcludeField,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseAmbientSuggestionsPayload(value) {
|
|
398
|
+
const envelope = parseAmbientSuggestionsEnvelope(value);
|
|
399
|
+
return envelope?.type === "suggestions" ? envelope.suggestions : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isAmbientSuggestionsMetadataPayload(value) {
|
|
403
|
+
return parseAmbientSuggestionsEnvelope(value) !== null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function ambientSuggestionsSummary(locale, suggestions) {
|
|
407
|
+
const count = Math.max(0, Array.isArray(suggestions) ? suggestions.length : 0);
|
|
408
|
+
if (count <= 0) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
const firstTitle = cleanText(suggestions[0]?.title || "");
|
|
412
|
+
return t(locale, "summary.ambientSuggestions", {
|
|
413
|
+
count,
|
|
414
|
+
firstTitle,
|
|
415
|
+
more: Math.max(0, count - 1),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function ambientSuggestionsMessageText(locale, suggestions) {
|
|
420
|
+
if (!Array.isArray(suggestions) || suggestions.length === 0) {
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
return [
|
|
424
|
+
kindTitle(locale, "ambient_suggestions"),
|
|
425
|
+
"",
|
|
426
|
+
...suggestions.flatMap((suggestion, index) => {
|
|
427
|
+
const description = cleanText(suggestion.description || "");
|
|
428
|
+
return [
|
|
429
|
+
`${index + 1}. ${cleanText(suggestion.title || "")}`,
|
|
430
|
+
...(description ? [description] : []),
|
|
431
|
+
];
|
|
432
|
+
}),
|
|
433
|
+
].join("\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
313
436
|
function looksLikeGeneratedThreadTitle(value) {
|
|
314
437
|
const normalized = cleanText(value || "");
|
|
315
438
|
if (!normalized.includes("|")) {
|
|
@@ -2241,6 +2364,8 @@ function timelineKindSortPriority(kind) {
|
|
|
2241
2364
|
case "plan_ready":
|
|
2242
2365
|
case "choice":
|
|
2243
2366
|
return 60;
|
|
2367
|
+
case "ambient_suggestions":
|
|
2368
|
+
return 52;
|
|
2244
2369
|
case "file_event":
|
|
2245
2370
|
return 45;
|
|
2246
2371
|
case "assistant_final":
|
|
@@ -2272,6 +2397,10 @@ function normalizeTimelineEntry(raw) {
|
|
|
2272
2397
|
const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
|
|
2273
2398
|
const diffSource = normalizeTimelineDiffSource(raw.diffSource ?? "");
|
|
2274
2399
|
const diffCounts = diffLineCounts(diffText);
|
|
2400
|
+
const suggestions = normalizeAmbientSuggestions(raw.suggestions ?? []);
|
|
2401
|
+
if (kind === "ambient_suggestions" && suggestions.length === 0) {
|
|
2402
|
+
return null;
|
|
2403
|
+
}
|
|
2275
2404
|
const summary =
|
|
2276
2405
|
normalizeNotificationText(raw.summary ?? "") ||
|
|
2277
2406
|
formatNotificationBody(messageText, 180) ||
|
|
@@ -2342,6 +2471,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
2342
2471
|
// A2A task-specific fields
|
|
2343
2472
|
...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
|
|
2344
2473
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2474
|
+
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
2345
2475
|
};
|
|
2346
2476
|
}
|
|
2347
2477
|
|
|
@@ -2516,6 +2646,9 @@ function historyItemFromEvent(event) {
|
|
|
2516
2646
|
|
|
2517
2647
|
const kind = event.kind === "task_complete" ? "completion" : "plan_ready";
|
|
2518
2648
|
const messageText = normalizeLongText(event.detailText || event.message || "");
|
|
2649
|
+
if (kind === "completion" && isAmbientSuggestionsMetadataPayload(messageText)) {
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2519
2652
|
return normalizeHistoryItem({
|
|
2520
2653
|
stableId: event.id,
|
|
2521
2654
|
kind,
|
|
@@ -2549,6 +2682,94 @@ function recordHistoryItem({ config, runtime, state, item }) {
|
|
|
2549
2682
|
return changed;
|
|
2550
2683
|
}
|
|
2551
2684
|
|
|
2685
|
+
function backfillAmbientSuggestionsState({ config, runtime, state }) {
|
|
2686
|
+
const nextTimelineEntries = normalizeTimelineEntries(
|
|
2687
|
+
(runtime.recentTimelineEntries ?? []).map((entry) => {
|
|
2688
|
+
const entryKind = cleanText(entry?.kind || "");
|
|
2689
|
+
if (entryKind === "ambient_suggestions") {
|
|
2690
|
+
const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
|
|
2691
|
+
if (suggestions.length === 0) {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
return {
|
|
2695
|
+
...entry,
|
|
2696
|
+
title: kindTitle(DEFAULT_LOCALE, "ambient_suggestions"),
|
|
2697
|
+
summary: ambientSuggestionsSummary(DEFAULT_LOCALE, suggestions),
|
|
2698
|
+
messageText: ambientSuggestionsMessageText(DEFAULT_LOCALE, suggestions),
|
|
2699
|
+
suggestions,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
if (entryKind !== "assistant_final") {
|
|
2703
|
+
return entry;
|
|
2704
|
+
}
|
|
2705
|
+
const envelope = parseAmbientSuggestionsEnvelope(entry?.messageText ?? "");
|
|
2706
|
+
if (!envelope) {
|
|
2707
|
+
return entry;
|
|
2708
|
+
}
|
|
2709
|
+
if (envelope.type !== "suggestions") {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
const suggestions = envelope.suggestions;
|
|
2713
|
+
return {
|
|
2714
|
+
...entry,
|
|
2715
|
+
kind: "ambient_suggestions",
|
|
2716
|
+
title: kindTitle(DEFAULT_LOCALE, "ambient_suggestions"),
|
|
2717
|
+
summary: ambientSuggestionsSummary(DEFAULT_LOCALE, suggestions),
|
|
2718
|
+
messageText: ambientSuggestionsMessageText(DEFAULT_LOCALE, suggestions),
|
|
2719
|
+
suggestions,
|
|
2720
|
+
};
|
|
2721
|
+
}),
|
|
2722
|
+
config.maxTimelineEntries
|
|
2723
|
+
);
|
|
2724
|
+
|
|
2725
|
+
const nextHistoryItems = normalizeHistoryItems(
|
|
2726
|
+
(runtime.recentHistoryItems ?? []).filter((item) => {
|
|
2727
|
+
const kind = cleanText(item?.kind || "");
|
|
2728
|
+
if (kind !== "assistant_final" && kind !== "completion") {
|
|
2729
|
+
return true;
|
|
2730
|
+
}
|
|
2731
|
+
return !isAmbientSuggestionsMetadataPayload(item?.messageText ?? "");
|
|
2732
|
+
}),
|
|
2733
|
+
config.maxHistoryItems
|
|
2734
|
+
);
|
|
2735
|
+
|
|
2736
|
+
const timelineChanged =
|
|
2737
|
+
JSON.stringify(nextTimelineEntries) !== JSON.stringify(runtime.recentTimelineEntries ?? []);
|
|
2738
|
+
const historyChanged =
|
|
2739
|
+
JSON.stringify(nextHistoryItems) !== JSON.stringify(runtime.recentHistoryItems ?? []);
|
|
2740
|
+
|
|
2741
|
+
if (timelineChanged) {
|
|
2742
|
+
runtime.recentTimelineEntries = nextTimelineEntries;
|
|
2743
|
+
state.recentTimelineEntries = nextTimelineEntries;
|
|
2744
|
+
}
|
|
2745
|
+
if (historyChanged) {
|
|
2746
|
+
runtime.recentHistoryItems = nextHistoryItems;
|
|
2747
|
+
state.recentHistoryItems = nextHistoryItems;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
return timelineChanged || historyChanged;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// Flip a previously-recorded history item's readOnly flag without disturbing
|
|
2754
|
+
// its ordering or other fields. Used by moltbook_reply / moltbook_draft flows
|
|
2755
|
+
// to transition pending → resolved so the entry starts matching the completed
|
|
2756
|
+
// tab's readOnly filter. Returns true if a mutation happened (caller should
|
|
2757
|
+
// saveState in that case).
|
|
2758
|
+
function flipHistoryItemReadOnly({ runtime, state, stableId, readOnly = true }) {
|
|
2759
|
+
const normalizedStableId = cleanText(stableId || "");
|
|
2760
|
+
if (!normalizedStableId) return false;
|
|
2761
|
+
const idx = runtime.recentHistoryItems.findIndex((entry) => entry.stableId === normalizedStableId);
|
|
2762
|
+
if (idx === -1) return false;
|
|
2763
|
+
const current = runtime.recentHistoryItems[idx];
|
|
2764
|
+
if (current.readOnly === readOnly) return false;
|
|
2765
|
+
const next = { ...current, readOnly };
|
|
2766
|
+
const nextItems = runtime.recentHistoryItems.slice();
|
|
2767
|
+
nextItems[idx] = next;
|
|
2768
|
+
runtime.recentHistoryItems = nextItems;
|
|
2769
|
+
state.recentHistoryItems = nextItems;
|
|
2770
|
+
return true;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2552
2773
|
function recordActionHistoryItem({
|
|
2553
2774
|
config,
|
|
2554
2775
|
runtime,
|
|
@@ -3760,6 +3981,66 @@ async function backfillPersistedTimelineImagePaths({ config, runtime, state }) {
|
|
|
3760
3981
|
return true;
|
|
3761
3982
|
}
|
|
3762
3983
|
|
|
3984
|
+
// Walk ~/.viveworker/moltbook-inbox/ on startup and synthesize readOnly
|
|
3985
|
+
// history entries for items the CLI / reconcile marked as replied or skipped.
|
|
3986
|
+
// Without this, an inbox item that was handled before the history hooks
|
|
3987
|
+
// existed — or that was resolved purely via the CLI path, which doesn't touch
|
|
3988
|
+
// the bridge's in-memory maps — never makes it into `recentHistoryItems` and
|
|
3989
|
+
// so never appears in the completed tab. We match the same stableId / token
|
|
3990
|
+
// scheme as the live push (sourceId = `comment:<commentId>`) so a future
|
|
3991
|
+
// watcher push for the same commentId replaces this entry cleanly rather than
|
|
3992
|
+
// creating a duplicate.
|
|
3993
|
+
async function backfillMoltbookInboxHistory({ config, runtime, state }) {
|
|
3994
|
+
let changed = false;
|
|
3995
|
+
try {
|
|
3996
|
+
const inboxItems = await listInboxItems();
|
|
3997
|
+
for (const inbox of inboxItems) {
|
|
3998
|
+
const status = String(inbox?.status || "").toLowerCase();
|
|
3999
|
+
if (status !== "replied" && status !== "skipped") continue;
|
|
4000
|
+
const commentId = String(inbox?.commentId || "");
|
|
4001
|
+
if (!commentId) continue;
|
|
4002
|
+
const sourceId = `comment:${commentId}`;
|
|
4003
|
+
const stableId = `moltbook_reply:${sourceId}`;
|
|
4004
|
+
if (runtime.recentHistoryItems.some((entry) => entry.stableId === stableId)) continue;
|
|
4005
|
+
const token = historyToken(`moltbook:${sourceId}`);
|
|
4006
|
+
const createdAtMs = Number(Date.parse(inbox.updatedAt || inbox.createdAt || "")) || Date.now();
|
|
4007
|
+
const postTitle = cleanText(inbox.postTitle || "");
|
|
4008
|
+
const contextText = cleanText(inbox.contextText || "");
|
|
4009
|
+
const replyText = cleanText(inbox.replyText || "");
|
|
4010
|
+
const messageText = status === "replied" ? (replyText || contextText) : contextText;
|
|
4011
|
+
const summary = (status === "replied" ? (replyText || contextText) : contextText).slice(0, 160);
|
|
4012
|
+
const titlePrefix = status === "replied" ? "replied" : "skipped";
|
|
4013
|
+
const title = postTitle ? `${titlePrefix} → ${postTitle}` : (status === "replied" ? "Moltbook reply" : "Moltbook skipped");
|
|
4014
|
+
const recorded = recordHistoryItem({
|
|
4015
|
+
config,
|
|
4016
|
+
runtime,
|
|
4017
|
+
state,
|
|
4018
|
+
item: {
|
|
4019
|
+
stableId,
|
|
4020
|
+
token,
|
|
4021
|
+
kind: "moltbook_reply",
|
|
4022
|
+
threadId: "moltbook",
|
|
4023
|
+
threadLabel: postTitle || "Moltbook",
|
|
4024
|
+
title,
|
|
4025
|
+
summary,
|
|
4026
|
+
messageText,
|
|
4027
|
+
createdAtMs,
|
|
4028
|
+
readOnly: true,
|
|
4029
|
+
provider: "moltbook",
|
|
4030
|
+
},
|
|
4031
|
+
});
|
|
4032
|
+
if (recorded) changed = true;
|
|
4033
|
+
}
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
console.error(`[moltbook-inbox-backfill] ${error.message}`);
|
|
4036
|
+
return false;
|
|
4037
|
+
}
|
|
4038
|
+
if (changed) {
|
|
4039
|
+
console.log(`[moltbook-inbox-backfill] Synthesized history entries for resolved inbox items`);
|
|
4040
|
+
}
|
|
4041
|
+
return changed;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3763
4044
|
async function backfillRecentTimelineEntryDiffs({ config, runtime, state }) {
|
|
3764
4045
|
const nextEntries = runtime.recentTimelineEntries.map((entry) => ({ ...entry }));
|
|
3765
4046
|
let changed = false;
|
|
@@ -4051,6 +4332,20 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
|
|
|
4051
4332
|
if (!messageText) {
|
|
4052
4333
|
return null;
|
|
4053
4334
|
}
|
|
4335
|
+
const ambientSuggestionsEnvelope = kind === "assistant_final" ? parseAmbientSuggestionsEnvelope(messageText) : null;
|
|
4336
|
+
if (ambientSuggestionsEnvelope?.type === "metadata") {
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
const ambientSuggestions = ambientSuggestionsEnvelope?.type === "suggestions"
|
|
4340
|
+
? ambientSuggestionsEnvelope.suggestions
|
|
4341
|
+
: null;
|
|
4342
|
+
const entryKind = ambientSuggestions ? "ambient_suggestions" : kind;
|
|
4343
|
+
const entryMessageText = ambientSuggestions
|
|
4344
|
+
? ambientSuggestionsMessageText(config.defaultLocale, ambientSuggestions)
|
|
4345
|
+
: messageText;
|
|
4346
|
+
const entrySummary = ambientSuggestions
|
|
4347
|
+
? ambientSuggestionsSummary(config.defaultLocale, ambientSuggestions)
|
|
4348
|
+
: formatNotificationBody(messageText, 180) || messageText;
|
|
4054
4349
|
|
|
4055
4350
|
const createdAtMs = Math.max(0, Number(row.ts) || 0) * 1000 || Date.now();
|
|
4056
4351
|
const threadLabel = getNativeThreadLabel({
|
|
@@ -4058,17 +4353,18 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
|
|
|
4058
4353
|
conversationId: threadId,
|
|
4059
4354
|
cwd: "",
|
|
4060
4355
|
});
|
|
4061
|
-
const stableId = messageTimelineStableId(
|
|
4356
|
+
const stableId = messageTimelineStableId(entryKind, threadId, item.id || row.id, entryMessageText, createdAtMs);
|
|
4062
4357
|
|
|
4063
4358
|
return normalizeTimelineEntry({
|
|
4064
4359
|
stableId,
|
|
4065
4360
|
token: historyToken(stableId),
|
|
4066
|
-
kind,
|
|
4361
|
+
kind: entryKind,
|
|
4067
4362
|
threadId,
|
|
4068
4363
|
threadLabel,
|
|
4069
|
-
title: threadLabel || kindTitle(config.defaultLocale,
|
|
4070
|
-
summary:
|
|
4071
|
-
messageText,
|
|
4364
|
+
title: ambientSuggestions ? kindTitle(config.defaultLocale, entryKind) : threadLabel || kindTitle(config.defaultLocale, entryKind),
|
|
4365
|
+
summary: entrySummary,
|
|
4366
|
+
messageText: entryMessageText,
|
|
4367
|
+
...(ambientSuggestions ? { suggestions: ambientSuggestions } : {}),
|
|
4072
4368
|
createdAtMs,
|
|
4073
4369
|
readOnly: true,
|
|
4074
4370
|
});
|
|
@@ -4520,7 +4816,11 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
4520
4816
|
item: historyItemFromEvent(event),
|
|
4521
4817
|
}) || dirty;
|
|
4522
4818
|
|
|
4523
|
-
|
|
4819
|
+
const isAmbientSuggestionsCompletion =
|
|
4820
|
+
event.kind === "task_complete" &&
|
|
4821
|
+
isAmbientSuggestionsMetadataPayload(normalizeLongText(event.detailText || event.message || ""));
|
|
4822
|
+
|
|
4823
|
+
if (event.kind === "task_complete" && !isAmbientSuggestionsCompletion) {
|
|
4524
4824
|
dirty =
|
|
4525
4825
|
(await deliverWebPushItem({
|
|
4526
4826
|
config,
|
|
@@ -4537,7 +4837,7 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
4537
4837
|
})) || dirty;
|
|
4538
4838
|
}
|
|
4539
4839
|
|
|
4540
|
-
if (config.enableNtfy) {
|
|
4840
|
+
if (config.enableNtfy && !isAmbientSuggestionsCompletion) {
|
|
4541
4841
|
try {
|
|
4542
4842
|
await publishNtfy(config, event);
|
|
4543
4843
|
} catch (error) {
|
|
@@ -9262,12 +9562,26 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
9262
9562
|
const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
|
|
9263
9563
|
runtime.recentHistoryItems = items;
|
|
9264
9564
|
const completedKinds = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task_result"]);
|
|
9265
|
-
|
|
9565
|
+
|
|
9566
|
+
// Infrequent kinds (A2A, thread_share) get evicted from the fixed-size
|
|
9567
|
+
// history FIFO by the high volume of approval/assistant_final entries.
|
|
9568
|
+
// Fall back to recentTimelineEntries so completed A2A tasks are never lost
|
|
9569
|
+
// while they're still within the timeline window.
|
|
9570
|
+
const infrequentKinds = new Set(["a2a_task_result", "thread_share"]);
|
|
9571
|
+
const historyStableIds = new Set(items.map((i) => i.stableId).filter(Boolean));
|
|
9572
|
+
const timelineEntries = (runtime.recentTimelineEntries ?? [])
|
|
9573
|
+
.filter((e) => infrequentKinds.has(cleanText(e?.kind || "")) && !historyStableIds.has(e.stableId));
|
|
9574
|
+
const merged = [...items, ...timelineEntries];
|
|
9575
|
+
|
|
9576
|
+
return merged
|
|
9266
9577
|
.filter((item) => {
|
|
9267
9578
|
const k = cleanText(item?.kind || "");
|
|
9268
9579
|
if (!completedKinds.has(k)) return false;
|
|
9269
|
-
// Only resolved approvals (readOnly = true)
|
|
9270
|
-
|
|
9580
|
+
// Only resolved approvals (readOnly = true). Moltbook reply/draft items
|
|
9581
|
+
// are gated the same way so pending entries don't double-show as
|
|
9582
|
+
// completed while they're also surfaced via moltbookItemsByToken /
|
|
9583
|
+
// moltbookDraftsByToken on the pending list.
|
|
9584
|
+
if ((k === "approval" || k === "moltbook_reply" || k === "moltbook_draft") && !item.readOnly) return false;
|
|
9271
9585
|
return true;
|
|
9272
9586
|
})
|
|
9273
9587
|
.slice()
|
|
@@ -9539,22 +9853,44 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
9539
9853
|
}
|
|
9540
9854
|
|
|
9541
9855
|
for (const historyItem of normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems)) {
|
|
9542
|
-
|
|
9856
|
+
let historyKind = historyItem.kind;
|
|
9857
|
+
let historySummary = historyItem.summary;
|
|
9858
|
+
let historyMessageText = historyItem.messageText;
|
|
9859
|
+
let historyTitle = historyItem.threadLabel ? formatTitle(kindTitle(locale, historyItem.kind), historyItem.threadLabel) : historyItem.title;
|
|
9860
|
+
let historySuggestions = [];
|
|
9861
|
+
|
|
9862
|
+
if (historyItem.kind === "assistant_final" || historyItem.kind === "completion") {
|
|
9863
|
+
const ambientSuggestionsEnvelope = parseAmbientSuggestionsEnvelope(historyItem.messageText ?? "");
|
|
9864
|
+
if (ambientSuggestionsEnvelope?.type === "metadata") {
|
|
9865
|
+
continue;
|
|
9866
|
+
}
|
|
9867
|
+
if (ambientSuggestionsEnvelope?.type === "suggestions") {
|
|
9868
|
+
const ambientSuggestions = ambientSuggestionsEnvelope.suggestions;
|
|
9869
|
+
historyKind = "ambient_suggestions";
|
|
9870
|
+
historyTitle = kindTitle(locale, historyKind);
|
|
9871
|
+
historySummary = ambientSuggestionsSummary(locale, ambientSuggestions);
|
|
9872
|
+
historyMessageText = ambientSuggestionsMessageText(locale, ambientSuggestions);
|
|
9873
|
+
historySuggestions = ambientSuggestions;
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9876
|
+
|
|
9877
|
+
if (!timelineKinds.has(historyKind)) {
|
|
9543
9878
|
continue;
|
|
9544
9879
|
}
|
|
9545
9880
|
items.push(
|
|
9546
9881
|
normalizeTimelineEntry({
|
|
9547
9882
|
stableId: historyItem.stableId,
|
|
9548
9883
|
token: historyItem.token,
|
|
9549
|
-
kind:
|
|
9884
|
+
kind: historyKind,
|
|
9550
9885
|
threadId: cleanText(extractConversationIdFromStableId(historyItem.stableId) || ""),
|
|
9551
9886
|
threadLabel: historyItem.threadLabel,
|
|
9552
|
-
title:
|
|
9553
|
-
summary:
|
|
9554
|
-
messageText:
|
|
9887
|
+
title: historyTitle,
|
|
9888
|
+
summary: historySummary,
|
|
9889
|
+
messageText: historyMessageText,
|
|
9555
9890
|
outcome: historyItem.outcome,
|
|
9556
9891
|
createdAtMs: historyItem.createdAtMs,
|
|
9557
9892
|
provider: normalizeProvider(historyItem.provider),
|
|
9893
|
+
...(historySuggestions.length > 0 && historyKind === "ambient_suggestions" ? { suggestions: historySuggestions } : {}),
|
|
9558
9894
|
})
|
|
9559
9895
|
);
|
|
9560
9896
|
}
|
|
@@ -9978,6 +10314,25 @@ function buildTimelineMessageDetail(entry, locale, runtime = null) {
|
|
|
9978
10314
|
};
|
|
9979
10315
|
}
|
|
9980
10316
|
|
|
10317
|
+
function buildAmbientSuggestionsDetail(entry, locale) {
|
|
10318
|
+
const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
|
|
10319
|
+
return {
|
|
10320
|
+
kind: "ambient_suggestions",
|
|
10321
|
+
token: cleanText(entry?.token || ""),
|
|
10322
|
+
threadId: cleanText(entry?.threadId || ""),
|
|
10323
|
+
title: cleanText(entry?.title || "") || kindTitle(locale, "ambient_suggestions"),
|
|
10324
|
+
threadLabel: cleanText(entry?.threadLabel || ""),
|
|
10325
|
+
createdAtMs: Number(entry?.createdAtMs) || 0,
|
|
10326
|
+
messageHtml: renderMessageHtml(
|
|
10327
|
+
t(locale, "detail.ambientSuggestions.copy"),
|
|
10328
|
+
`<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`
|
|
10329
|
+
),
|
|
10330
|
+
suggestions,
|
|
10331
|
+
readOnly: true,
|
|
10332
|
+
actions: [],
|
|
10333
|
+
};
|
|
10334
|
+
}
|
|
10335
|
+
|
|
9981
10336
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
9982
10337
|
const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
|
|
9983
10338
|
return {
|
|
@@ -10699,6 +11054,33 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10699
11054
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
10700
11055
|
return entry ? buildTimelineFileEventDetail(entry, locale) : null;
|
|
10701
11056
|
}
|
|
11057
|
+
if (kind === "ambient_suggestions") {
|
|
11058
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
11059
|
+
if (entry) {
|
|
11060
|
+
return buildAmbientSuggestionsDetail(entry, locale);
|
|
11061
|
+
}
|
|
11062
|
+
const historicalSource = (runtime.recentHistoryItems ?? []).find((item) => {
|
|
11063
|
+
if (cleanText(item?.token || "") !== cleanText(token || "")) {
|
|
11064
|
+
return false;
|
|
11065
|
+
}
|
|
11066
|
+
const itemKind = cleanText(item?.kind || "");
|
|
11067
|
+
if (itemKind !== "assistant_final" && itemKind !== "completion") {
|
|
11068
|
+
return false;
|
|
11069
|
+
}
|
|
11070
|
+
return parseAmbientSuggestionsPayload(item?.messageText ?? "") !== null;
|
|
11071
|
+
});
|
|
11072
|
+
if (!historicalSource) {
|
|
11073
|
+
return null;
|
|
11074
|
+
}
|
|
11075
|
+
return buildAmbientSuggestionsDetail({
|
|
11076
|
+
token: historicalSource.token,
|
|
11077
|
+
threadId: historyItemThreadId(historicalSource),
|
|
11078
|
+
threadLabel: historicalSource.threadLabel,
|
|
11079
|
+
title: kindTitle(locale, "ambient_suggestions"),
|
|
11080
|
+
createdAtMs: historicalSource.createdAtMs,
|
|
11081
|
+
suggestions: parseAmbientSuggestionsPayload(historicalSource.messageText ?? "") ?? [],
|
|
11082
|
+
}, locale);
|
|
11083
|
+
}
|
|
10702
11084
|
if (timelineMessageKinds.has(kind)) {
|
|
10703
11085
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
10704
11086
|
if (!entry) return null;
|
|
@@ -11429,7 +11811,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11429
11811
|
return writeJson(res, 200, { enabled: false });
|
|
11430
11812
|
}
|
|
11431
11813
|
try {
|
|
11432
|
-
const { readScoutState, rollScoutDayIfNeeded } = await import("./moltbook-api.mjs");
|
|
11814
|
+
const { readScoutState, rollScoutDayIfNeeded, createMoltbookClient } = await import("./moltbook-api.mjs");
|
|
11433
11815
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
11434
11816
|
const batch = scoutState.batch;
|
|
11435
11817
|
const batchInfo = batch && batch.candidates?.length ? {
|
|
@@ -11438,8 +11820,39 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11438
11820
|
topScore: Math.max(...batch.candidates.map((c) => c.score || 0)),
|
|
11439
11821
|
remainingSeconds: Math.max(0, Math.round(((batch.startedAt || 0) + (batch.windowMs || 1800000) - Date.now()) / 1000)),
|
|
11440
11822
|
} : null;
|
|
11823
|
+
|
|
11824
|
+
// Agent profile — lazy-fetched, cached 1h. Stale cache survives
|
|
11825
|
+
// fetch failures so the UI never goes blank mid-outage.
|
|
11826
|
+
let account = null;
|
|
11827
|
+
try {
|
|
11828
|
+
const PROFILE_TTL_MS = 60 * 60 * 1000;
|
|
11829
|
+
const cached = runtime.moltbookAgentProfile;
|
|
11830
|
+
const nowMs = Date.now();
|
|
11831
|
+
if (!cached || nowMs - (cached.fetchedAtMs || 0) > PROFILE_TTL_MS) {
|
|
11832
|
+
const mb = createMoltbookClient(config.moltbookApiKey);
|
|
11833
|
+
const meRes = await mb("/agents/me");
|
|
11834
|
+
const name = cleanText(meRes?.agent?.name || meRes?.agent?.display_name || "");
|
|
11835
|
+
if (name) {
|
|
11836
|
+
runtime.moltbookAgentProfile = {
|
|
11837
|
+
name,
|
|
11838
|
+
profileUrl: `https://www.moltbook.com/u/${encodeURIComponent(name)}`,
|
|
11839
|
+
fetchedAtMs: nowMs,
|
|
11840
|
+
};
|
|
11841
|
+
}
|
|
11842
|
+
}
|
|
11843
|
+
} catch {
|
|
11844
|
+
// Fall through — if fetch fails, use whatever's cached (possibly null).
|
|
11845
|
+
}
|
|
11846
|
+
if (runtime.moltbookAgentProfile) {
|
|
11847
|
+
account = {
|
|
11848
|
+
name: runtime.moltbookAgentProfile.name,
|
|
11849
|
+
profileUrl: runtime.moltbookAgentProfile.profileUrl,
|
|
11850
|
+
};
|
|
11851
|
+
}
|
|
11852
|
+
|
|
11441
11853
|
return writeJson(res, 200, {
|
|
11442
11854
|
enabled: true,
|
|
11855
|
+
account,
|
|
11443
11856
|
day: scoutState.day,
|
|
11444
11857
|
sentToday: scoutState.sentToday,
|
|
11445
11858
|
maxDaily: 5,
|
|
@@ -11478,6 +11891,82 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11478
11891
|
});
|
|
11479
11892
|
}
|
|
11480
11893
|
|
|
11894
|
+
// A2A Share status (HTML hosting) for the settings UI. Reuses A2A
|
|
11895
|
+
// credentials; enabled iff both user id + API key are provisioned.
|
|
11896
|
+
// Results from the upstream `/api/list` call are cached for 30s so a
|
|
11897
|
+
// settings-page render doesn't hammer the worker on every refresh tick.
|
|
11898
|
+
if (url.pathname === "/api/share/status" && req.method === "GET") {
|
|
11899
|
+
const session = requireApiSession(req, res, config, state);
|
|
11900
|
+
if (!session) return;
|
|
11901
|
+
const enabled = Boolean(config.a2aRelayUserId && config.a2aApiKey);
|
|
11902
|
+
if (!enabled) {
|
|
11903
|
+
return writeJson(res, 200, { enabled: false });
|
|
11904
|
+
}
|
|
11905
|
+
const shareHost = (() => {
|
|
11906
|
+
try { return new URL(config.a2aShareUrl).host; } catch { return config.a2aShareUrl || ""; }
|
|
11907
|
+
})();
|
|
11908
|
+
// Mirror the constants baked into share-worker/worker.js. If those
|
|
11909
|
+
// change, bump here too — there's no shared module between the two.
|
|
11910
|
+
const limits = {
|
|
11911
|
+
maxFileBytes: 5 * 1024 * 1024,
|
|
11912
|
+
maxTotalBytes: 5 * 1024 * 1024,
|
|
11913
|
+
maxFiles: 10,
|
|
11914
|
+
defaultExpiresDays: 30,
|
|
11915
|
+
maxExpiresDays: 30,
|
|
11916
|
+
uploadRatePerHour: 10,
|
|
11917
|
+
};
|
|
11918
|
+
const cacheKey = `${config.a2aRelayUserId}|${config.a2aShareUrl}`;
|
|
11919
|
+
const nowMs = Date.now();
|
|
11920
|
+
const cached = runtime.a2aShareStatusCache;
|
|
11921
|
+
if (cached && cached.key === cacheKey && nowMs - cached.fetchedAtMs < 30_000) {
|
|
11922
|
+
return writeJson(res, 200, {
|
|
11923
|
+
enabled: true,
|
|
11924
|
+
userId: config.a2aRelayUserId,
|
|
11925
|
+
shareUrl: config.a2aShareUrl,
|
|
11926
|
+
shareHost,
|
|
11927
|
+
limits,
|
|
11928
|
+
...cached.payload,
|
|
11929
|
+
fetchedAtMs: cached.fetchedAtMs,
|
|
11930
|
+
});
|
|
11931
|
+
}
|
|
11932
|
+
let upstream = { items: [], quota: null, error: null };
|
|
11933
|
+
const controller = new AbortController();
|
|
11934
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
11935
|
+
try {
|
|
11936
|
+
const resUpstream = await fetch(`${config.a2aShareUrl}/api/list`, {
|
|
11937
|
+
method: "GET",
|
|
11938
|
+
headers: {
|
|
11939
|
+
"x-a2a-user": config.a2aRelayUserId,
|
|
11940
|
+
"x-a2a-key": config.a2aApiKey,
|
|
11941
|
+
},
|
|
11942
|
+
signal: controller.signal,
|
|
11943
|
+
});
|
|
11944
|
+
if (resUpstream.ok) {
|
|
11945
|
+
const body = await resUpstream.json().catch(() => ({}));
|
|
11946
|
+
upstream.items = Array.isArray(body.items) ? body.items : [];
|
|
11947
|
+
upstream.quota = body.quota || null;
|
|
11948
|
+
} else {
|
|
11949
|
+
upstream.error = `HTTP ${resUpstream.status}`;
|
|
11950
|
+
}
|
|
11951
|
+
} catch (error) {
|
|
11952
|
+
upstream.error = error?.name === "AbortError"
|
|
11953
|
+
? "upstream timeout"
|
|
11954
|
+
: (error?.message || String(error));
|
|
11955
|
+
} finally {
|
|
11956
|
+
clearTimeout(timer);
|
|
11957
|
+
}
|
|
11958
|
+
runtime.a2aShareStatusCache = { key: cacheKey, fetchedAtMs: nowMs, payload: upstream };
|
|
11959
|
+
return writeJson(res, 200, {
|
|
11960
|
+
enabled: true,
|
|
11961
|
+
userId: config.a2aRelayUserId,
|
|
11962
|
+
shareUrl: config.a2aShareUrl,
|
|
11963
|
+
shareHost,
|
|
11964
|
+
limits,
|
|
11965
|
+
...upstream,
|
|
11966
|
+
fetchedAtMs: nowMs,
|
|
11967
|
+
});
|
|
11968
|
+
}
|
|
11969
|
+
|
|
11481
11970
|
// Toggle acceptPublicTasks on the A2A relay.
|
|
11482
11971
|
if (url.pathname === "/api/a2a/public-tasks" && req.method === "POST") {
|
|
11483
11972
|
const session = requireApiSession(req, res, config, state);
|
|
@@ -12247,6 +12736,14 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12247
12736
|
if (eventType === "resolve") {
|
|
12248
12737
|
const token = historyToken(`moltbook:${sourceId}`);
|
|
12249
12738
|
runtime.moltbookItemsByToken.delete(token);
|
|
12739
|
+
// Flip the history item to readOnly so it moves from pending → completed.
|
|
12740
|
+
try {
|
|
12741
|
+
if (flipHistoryItemReadOnly({ runtime, state, stableId: `moltbook_reply:${sourceId}` })) {
|
|
12742
|
+
await saveState(config.stateFile, state);
|
|
12743
|
+
}
|
|
12744
|
+
} catch (error) {
|
|
12745
|
+
console.error(`[moltbook-reply-resolve] ${error.message}`);
|
|
12746
|
+
}
|
|
12250
12747
|
return writeJson(res, 200, { ok: true });
|
|
12251
12748
|
}
|
|
12252
12749
|
const token = historyToken(`moltbook:${sourceId}`);
|
|
@@ -12268,24 +12765,21 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12268
12765
|
};
|
|
12269
12766
|
runtime.moltbookItemsByToken.set(token, item);
|
|
12270
12767
|
try {
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
provider: "moltbook",
|
|
12287
|
-
},
|
|
12288
|
-
});
|
|
12768
|
+
const historyEntry = {
|
|
12769
|
+
stableId: `moltbook_reply:${sourceId}`,
|
|
12770
|
+
token,
|
|
12771
|
+
kind: "moltbook_reply",
|
|
12772
|
+
threadId: item.threadId,
|
|
12773
|
+
threadLabel: item.threadLabel,
|
|
12774
|
+
title: item.title,
|
|
12775
|
+
summary: item.summary,
|
|
12776
|
+
messageText: item.contextText,
|
|
12777
|
+
createdAtMs: item.createdAtMs,
|
|
12778
|
+
readOnly: false,
|
|
12779
|
+
provider: "moltbook",
|
|
12780
|
+
};
|
|
12781
|
+
recordTimelineEntry({ config, runtime, state, entry: historyEntry });
|
|
12782
|
+
recordHistoryItem({ config, runtime, state, item: historyEntry });
|
|
12289
12783
|
await saveState(config.stateFile, state);
|
|
12290
12784
|
} catch (error) {
|
|
12291
12785
|
console.error(`[moltbook-timeline-save] ${error.message}`);
|
|
@@ -12351,6 +12845,13 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12351
12845
|
}
|
|
12352
12846
|
item.resolved = true;
|
|
12353
12847
|
runtime.moltbookItemsByToken.delete(token);
|
|
12848
|
+
try {
|
|
12849
|
+
if (flipHistoryItemReadOnly({ runtime, state, stableId: `moltbook_reply:${item.sourceId}` })) {
|
|
12850
|
+
await saveState(config.stateFile, state);
|
|
12851
|
+
}
|
|
12852
|
+
} catch (error) {
|
|
12853
|
+
console.error(`[moltbook-reply-phone] ${error.message}`);
|
|
12854
|
+
}
|
|
12354
12855
|
return writeJson(res, 200, { ok: true, action });
|
|
12355
12856
|
}
|
|
12356
12857
|
|
|
@@ -12419,36 +12920,33 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12419
12920
|
runtime.moltbookDraftsByToken.set(token, draft);
|
|
12420
12921
|
await writeDraft(draft).catch((e) => console.error(`[moltbook-draft-persist] ${e.message}`));
|
|
12421
12922
|
try {
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
provider: "moltbook",
|
|
12450
|
-
},
|
|
12451
|
-
});
|
|
12923
|
+
const draftEntry = {
|
|
12924
|
+
stableId: `moltbook_draft:${sourceId}`,
|
|
12925
|
+
token,
|
|
12926
|
+
kind: "moltbook_draft",
|
|
12927
|
+
threadId: "moltbook",
|
|
12928
|
+
threadLabel: postTitle || "Moltbook",
|
|
12929
|
+
title: draftType === "original_post"
|
|
12930
|
+
? (postTitle || "Moltbook new post")
|
|
12931
|
+
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
12932
|
+
summary: contextSummary || String(draftText).slice(0, 160),
|
|
12933
|
+
messageText: contextSummary || draftText,
|
|
12934
|
+
draftText,
|
|
12935
|
+
draftType,
|
|
12936
|
+
submoltName,
|
|
12937
|
+
intent,
|
|
12938
|
+
slot,
|
|
12939
|
+
postId,
|
|
12940
|
+
postUrl,
|
|
12941
|
+
postTitle,
|
|
12942
|
+
postAuthor,
|
|
12943
|
+
postBody,
|
|
12944
|
+
createdAtMs: draft.createdAtMs,
|
|
12945
|
+
readOnly: false,
|
|
12946
|
+
provider: "moltbook",
|
|
12947
|
+
};
|
|
12948
|
+
recordTimelineEntry({ config, runtime, state, entry: draftEntry });
|
|
12949
|
+
recordHistoryItem({ config, runtime, state, item: draftEntry });
|
|
12452
12950
|
await saveState(config.stateFile, state);
|
|
12453
12951
|
} catch (error) {
|
|
12454
12952
|
console.error(`[moltbook-draft-timeline-save] ${error.message}`);
|
|
@@ -12549,6 +13047,15 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12549
13047
|
// Persist decision to disk.
|
|
12550
13048
|
await writeDraft(draft).catch((e) => console.error(`[moltbook-draft-persist-decision] ${e.message}`));
|
|
12551
13049
|
|
|
13050
|
+
// Flip the history entry to readOnly so it moves from pending → completed.
|
|
13051
|
+
try {
|
|
13052
|
+
if (flipHistoryItemReadOnly({ runtime, state, stableId: `moltbook_draft:${draft.sourceId}` })) {
|
|
13053
|
+
await saveState(config.stateFile, state);
|
|
13054
|
+
}
|
|
13055
|
+
} catch (error) {
|
|
13056
|
+
console.error(`[moltbook-draft-decision-flip] ${error.message}`);
|
|
13057
|
+
}
|
|
13058
|
+
|
|
12552
13059
|
if (action === "approve") {
|
|
12553
13060
|
// Execute posting asynchronously — fire-and-forget from the HTTP handler.
|
|
12554
13061
|
(async () => {
|
|
@@ -14564,12 +15071,11 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
14564
15071
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
14565
15072
|
scoutState.sentToday += 1;
|
|
14566
15073
|
markPostSeen(scoutState, draft.postId, "published");
|
|
14567
|
-
//
|
|
14568
|
-
//
|
|
14569
|
-
// `composedToday`
|
|
14570
|
-
//
|
|
14571
|
-
|
|
14572
|
-
// when the user only published one actual new post for the day.
|
|
15074
|
+
// Append the reply to `recentComposeTitles` so it shows up in the
|
|
15075
|
+
// "最近の投稿" list with its reply badge. `recordComposeAttempt` knows
|
|
15076
|
+
// not to bump `composedToday` when type === "reply", so the
|
|
15077
|
+
// "本日の新規投稿数" counter only reflects original posts.
|
|
15078
|
+
recordComposeAttempt(scoutState, draft.postTitle || draft.postId, draft.postId, "reply");
|
|
14573
15079
|
await writeScoutState(scoutState);
|
|
14574
15080
|
}
|
|
14575
15081
|
|
|
@@ -14643,6 +15149,11 @@ function buildConfig(cli) {
|
|
|
14643
15149
|
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
14644
15150
|
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
14645
15151
|
a2aRelayRegisterSecret: cleanText(process.env.A2A_RELAY_REGISTER_SECRET || ""),
|
|
15152
|
+
// share.viveworker.com — HTML hosting backed by the same A2A credentials.
|
|
15153
|
+
// Override for staging / self-hosted deployments via VIVEWORKER_SHARE_URL.
|
|
15154
|
+
a2aShareUrl: stripTrailingSlash(
|
|
15155
|
+
process.env.VIVEWORKER_SHARE_URL || "https://share.viveworker.com"
|
|
15156
|
+
),
|
|
14646
15157
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
14647
15158
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
14648
15159
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -15992,8 +16503,10 @@ async function main() {
|
|
|
15992
16503
|
migratedPairedDevicesStateChanged ||
|
|
15993
16504
|
restoredPendingPlanStateChanged ||
|
|
15994
16505
|
restoredTimelineImagePathsStateChanged ||
|
|
16506
|
+
backfilledAmbientSuggestionsStateChanged ||
|
|
15995
16507
|
migratedRecentCodeEventsStateChanged ||
|
|
15996
16508
|
restoredPendingUserInputStateChanged ||
|
|
16509
|
+
backfilledMoltbookInboxChanged ||
|
|
15997
16510
|
refreshResolvedThreadLabels({ config, runtime, state })
|
|
15998
16511
|
) {
|
|
15999
16512
|
await saveState(config.stateFile, state);
|