viveworker 0.4.7 → 0.4.9
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/package.json +1 -1
- package/scripts/a2a-executor.mjs +267 -41
- package/scripts/a2a-handler.mjs +6 -1
- package/scripts/a2a-relay-client.mjs +13 -0
- package/scripts/moltbook-api.mjs +214 -5
- package/scripts/moltbook-cli.mjs +12 -397
- package/scripts/viveworker-bridge.mjs +471 -47
- package/web/app.css +249 -1
- package/web/app.js +307 -55
- package/web/i18n.js +69 -5
|
@@ -17,7 +17,8 @@ import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale,
|
|
|
17
17
|
import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
|
|
18
18
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
19
19
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
20
|
-
import { registerWithRelay, startRelayPolling, stopRelayPolling, postRelayResult, getRelayStatus } from "./a2a-relay-client.mjs";
|
|
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
22
|
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = path.dirname(__filename);
|
|
@@ -26,9 +27,9 @@ const webRoot = path.join(workspaceRoot, "web");
|
|
|
26
27
|
const appPackageVersion = readPackageVersion();
|
|
27
28
|
const sessionCookieName = "viveworker_session";
|
|
28
29
|
const deviceCookieName = "viveworker_device";
|
|
29
|
-
const historyKinds = new Set(["completion", "plan_ready", "approval", "plan", "choice", "info"]);
|
|
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"]);
|
|
30
31
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
31
|
-
const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "
|
|
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
33
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
33
34
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
34
35
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -101,6 +102,50 @@ await syncClaudeAwayModeSentinel(config, state.claudeAwayMode === true);
|
|
|
101
102
|
const migratedPairedDevicesStateChanged = migratePairedDevicesState({ config, state });
|
|
102
103
|
const restoredPendingPlanStateChanged = restorePendingPlanRequests({ config, runtime, state });
|
|
103
104
|
const restoredPendingUserInputStateChanged = restorePendingUserInputRequests({ config, runtime, state });
|
|
105
|
+
|
|
106
|
+
// Detect available A2A executors (codex / claude CLIs).
|
|
107
|
+
try {
|
|
108
|
+
const { detectAvailableExecutors } = await import("./a2a-executor.mjs");
|
|
109
|
+
runtime.a2aAvailableExecutors = detectAvailableExecutors();
|
|
110
|
+
console.log(`[a2a-executors] codex=${runtime.a2aAvailableExecutors.codex}, claude=${runtime.a2aAvailableExecutors.claude}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
runtime.a2aAvailableExecutors = { codex: false, claude: false };
|
|
113
|
+
console.error(`[a2a-executors] Detection failed: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Restore persisted moltbook drafts that haven't expired.
|
|
117
|
+
try {
|
|
118
|
+
const pendingDrafts = await listPendingDrafts();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
for (const draft of pendingDrafts) {
|
|
121
|
+
const age = now - (draft.createdAtMs || 0);
|
|
122
|
+
const ttl = draft.ttlMs || 86400000;
|
|
123
|
+
if (age > ttl) {
|
|
124
|
+
await deleteDraft(draft.token).catch(() => {});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
runtime.moltbookDraftsByToken.set(draft.token, { ...draft, decisionWaiters: [] });
|
|
128
|
+
}
|
|
129
|
+
if (runtime.moltbookDraftsByToken.size) {
|
|
130
|
+
console.log(`[moltbook-drafts] Restored ${runtime.moltbookDraftsByToken.size} pending draft(s) from disk`);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`[moltbook-drafts-restore] ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Periodic TTL sweeper for expired drafts (every 60s).
|
|
137
|
+
setInterval(async () => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const [token, draft] of runtime.moltbookDraftsByToken) {
|
|
140
|
+
if (draft.decision) continue;
|
|
141
|
+
const age = now - (draft.createdAtMs || 0);
|
|
142
|
+
if (age > (draft.ttlMs || 86400000)) {
|
|
143
|
+
runtime.moltbookDraftsByToken.delete(token);
|
|
144
|
+
await deleteDraft(token).catch(() => {});
|
|
145
|
+
console.log(`[moltbook-draft-expired] ${token} (age=${Math.round(age / 1000)}s)`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, 60_000).unref?.();
|
|
104
149
|
const initialHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
105
150
|
const initialTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
106
151
|
const normalizedHistoryStateChanged =
|
|
@@ -224,6 +269,8 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
224
269
|
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
225
270
|
a2aEnabled: Boolean(config.a2aApiKey),
|
|
226
271
|
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
272
|
+
a2aExecutors: runtime.a2aAvailableExecutors || { codex: false, claude: false },
|
|
273
|
+
a2aExecutorPreference: state.a2aExecutorPreference || "ask",
|
|
227
274
|
};
|
|
228
275
|
}
|
|
229
276
|
|
|
@@ -249,6 +296,15 @@ function kindTitle(locale, kind) {
|
|
|
249
296
|
return t(locale, "common.fileEvent");
|
|
250
297
|
case "diff_thread":
|
|
251
298
|
return t(locale, "common.diff");
|
|
299
|
+
case "a2a_task":
|
|
300
|
+
return "A2A";
|
|
301
|
+
case "a2a_task_result":
|
|
302
|
+
return "A2A";
|
|
303
|
+
case "moltbook_reply":
|
|
304
|
+
case "moltbook_draft":
|
|
305
|
+
return "Moltbook";
|
|
306
|
+
case "thread_share":
|
|
307
|
+
return t(locale, "server.title.threadShare") || "Thread Share";
|
|
252
308
|
default:
|
|
253
309
|
return t(locale, "common.item");
|
|
254
310
|
}
|
|
@@ -293,6 +349,7 @@ function notificationIconPrefix(kind) {
|
|
|
293
349
|
case "completion":
|
|
294
350
|
return "✅";
|
|
295
351
|
case "a2a_task":
|
|
352
|
+
case "a2a_task_result":
|
|
296
353
|
return "🤝";
|
|
297
354
|
default:
|
|
298
355
|
return "";
|
|
@@ -2006,6 +2063,7 @@ function normalizeProvider(value) {
|
|
|
2006
2063
|
if (normalized === "claude") return "claude";
|
|
2007
2064
|
if (normalized === "moltbook") return "moltbook";
|
|
2008
2065
|
if (normalized === "a2a") return "a2a";
|
|
2066
|
+
if (normalized === "viveworker") return "viveworker";
|
|
2009
2067
|
return "codex";
|
|
2010
2068
|
}
|
|
2011
2069
|
|
|
@@ -2050,7 +2108,12 @@ function normalizeHistoryItem(raw) {
|
|
|
2050
2108
|
const kind = cleanText(raw.kind ?? "");
|
|
2051
2109
|
const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
|
|
2052
2110
|
const rawThreadLabel = cleanText(raw.threadLabel ?? "");
|
|
2053
|
-
const
|
|
2111
|
+
const historyProvider = normalizeProvider(raw.provider);
|
|
2112
|
+
const skipHistoryThreadLabelRewrite = historyProvider === "moltbook" || historyProvider === "a2a" || historyProvider === "viveworker"
|
|
2113
|
+
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
2114
|
+
const threadLabel = skipHistoryThreadLabelRewrite
|
|
2115
|
+
? rawThreadLabel
|
|
2116
|
+
: preferTitleOnlyJsonThreadLabel(rawThreadLabel, threadId, raw.messageText, raw.summary, raw.detailText, raw.message);
|
|
2054
2117
|
const title =
|
|
2055
2118
|
cleanText(raw.title ?? "") || (threadLabel ? formatTitle(kindTitle(DEFAULT_LOCALE, kind), threadLabel) : kindTitle(DEFAULT_LOCALE, kind));
|
|
2056
2119
|
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
@@ -2084,6 +2147,11 @@ function normalizeHistoryItem(raw) {
|
|
|
2084
2147
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
2085
2148
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
2086
2149
|
provider: normalizeProvider(raw.provider),
|
|
2150
|
+
// Moltbook draft-specific
|
|
2151
|
+
...(raw.draftType != null ? { draftType: cleanText(raw.draftType) } : {}),
|
|
2152
|
+
// A2A task-specific
|
|
2153
|
+
...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
|
|
2154
|
+
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2087
2155
|
};
|
|
2088
2156
|
}
|
|
2089
2157
|
|
|
@@ -2112,7 +2180,7 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2112
2180
|
const deduped = [];
|
|
2113
2181
|
const seen = new Set();
|
|
2114
2182
|
const perProviderCount = {};
|
|
2115
|
-
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a"]);
|
|
2183
|
+
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a", "viveworker"]);
|
|
2116
2184
|
const saturatedProviders = new Set();
|
|
2117
2185
|
for (const item of normalized) {
|
|
2118
2186
|
if (seen.has(item.stableId)) {
|
|
@@ -2210,8 +2278,10 @@ function normalizeTimelineEntry(raw) {
|
|
|
2210
2278
|
(kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
|
|
2211
2279
|
"";
|
|
2212
2280
|
const rawProvider = normalizeProvider(raw.provider);
|
|
2281
|
+
const skipThreadLabelRewrite = rawProvider === "moltbook" || rawProvider === "a2a" || rawProvider === "viveworker"
|
|
2282
|
+
|| kind === "moltbook_reply" || kind === "moltbook_draft" || kind === "a2a_task" || kind === "a2a_task_result" || kind === "thread_share";
|
|
2213
2283
|
const threadLabel =
|
|
2214
|
-
|
|
2284
|
+
skipThreadLabelRewrite
|
|
2215
2285
|
? cleanText(raw.threadLabel ?? "")
|
|
2216
2286
|
: preferTitleOnlyJsonThreadLabel(
|
|
2217
2287
|
cleanText(raw.threadLabel ?? ""),
|
|
@@ -2269,6 +2339,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
2269
2339
|
...(raw.draftType != null ? { draftType: cleanText(raw.draftType) } : {}),
|
|
2270
2340
|
...(raw.submoltName != null ? { submoltName: cleanText(raw.submoltName) } : {}),
|
|
2271
2341
|
...(raw.slot != null ? { slot: cleanText(raw.slot) } : {}),
|
|
2342
|
+
// A2A task-specific fields
|
|
2343
|
+
...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
|
|
2344
|
+
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2272
2345
|
};
|
|
2273
2346
|
}
|
|
2274
2347
|
|
|
@@ -3237,8 +3310,19 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3237
3310
|
// conversation that should not appear on the user-facing timeline.
|
|
3238
3311
|
if (/^<(?:task-notification|system-reminder)\b/i.test(text.trim())) continue;
|
|
3239
3312
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3313
|
+
// Classify assistant messages using Claude's stop_reason:
|
|
3314
|
+
// "end_turn" → final answer (like Codex phase "final_answer")
|
|
3315
|
+
// "tool_use" → intermediate, about to call tools (commentary)
|
|
3316
|
+
// null/other → intermediate thinking (commentary)
|
|
3317
|
+
const stopReason = msg.stop_reason || "";
|
|
3318
|
+
const kind = type === "user"
|
|
3319
|
+
? "user_message"
|
|
3320
|
+
: stopReason === "end_turn"
|
|
3321
|
+
? "assistant_final"
|
|
3322
|
+
: "assistant_commentary";
|
|
3323
|
+
// Use kind-independent stableId so re-scans with corrected classification
|
|
3324
|
+
// replace old entries rather than creating duplicates.
|
|
3325
|
+
const stableId = `claude_msg:${threadId}:${uuid}`;
|
|
3242
3326
|
const entry = normalizeTimelineEntry({
|
|
3243
3327
|
stableId,
|
|
3244
3328
|
token: historyToken(stableId),
|
|
@@ -3255,6 +3339,46 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3255
3339
|
});
|
|
3256
3340
|
if (entry) {
|
|
3257
3341
|
dirty = recordTimelineEntry({ config, runtime, state, entry }) || dirty;
|
|
3342
|
+
|
|
3343
|
+
// Also record Claude assistant_final as a history item for the completed list
|
|
3344
|
+
if (kind === "assistant_final") {
|
|
3345
|
+
dirty = recordHistoryItem({
|
|
3346
|
+
config, runtime, state,
|
|
3347
|
+
item: {
|
|
3348
|
+
stableId: entry.stableId,
|
|
3349
|
+
kind: entry.kind,
|
|
3350
|
+
threadId: entry.threadId,
|
|
3351
|
+
title: entry.title,
|
|
3352
|
+
threadLabel: entry.threadLabel,
|
|
3353
|
+
summary: entry.summary,
|
|
3354
|
+
messageText: entry.messageText,
|
|
3355
|
+
createdAtMs: entry.createdAtMs,
|
|
3356
|
+
readOnly: true,
|
|
3357
|
+
provider: "claude",
|
|
3358
|
+
},
|
|
3359
|
+
}) || dirty;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// Push notification for Claude final answers (skip during startup replay)
|
|
3363
|
+
if (kind === "assistant_final" && !fileState.startupCutoffMs) {
|
|
3364
|
+
try {
|
|
3365
|
+
await deliverWebPushItem({
|
|
3366
|
+
config,
|
|
3367
|
+
state,
|
|
3368
|
+
kind: "assistant_final",
|
|
3369
|
+
token: entry.token,
|
|
3370
|
+
stableId: entry.stableId,
|
|
3371
|
+
title: threadLabel || "Claude",
|
|
3372
|
+
body: truncate(singleLine(text), 160),
|
|
3373
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
3374
|
+
title: formatLocalizedTitle(locale, "server.title.assistantFinal", threadLabel),
|
|
3375
|
+
body: truncate(singleLine(text), 160),
|
|
3376
|
+
}),
|
|
3377
|
+
});
|
|
3378
|
+
} catch (pushError) {
|
|
3379
|
+
console.error(`[claude-push-error] ${pushError.message}`);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3258
3382
|
}
|
|
3259
3383
|
}
|
|
3260
3384
|
|
|
@@ -3400,6 +3524,29 @@ async function processSqliteTimelineLog({ config, runtime, state, now }) {
|
|
|
3400
3524
|
state,
|
|
3401
3525
|
entry,
|
|
3402
3526
|
}) || dirty;
|
|
3527
|
+
|
|
3528
|
+
// Also record Codex assistant_final as a history item so it appears
|
|
3529
|
+
// in the completed inbox and supports reply (replaces "completion").
|
|
3530
|
+
if (entry.kind === "assistant_final") {
|
|
3531
|
+
dirty =
|
|
3532
|
+
recordHistoryItem({
|
|
3533
|
+
config,
|
|
3534
|
+
runtime,
|
|
3535
|
+
state,
|
|
3536
|
+
item: {
|
|
3537
|
+
stableId: entry.stableId,
|
|
3538
|
+
kind: entry.kind,
|
|
3539
|
+
threadId: entry.threadId,
|
|
3540
|
+
title: entry.title,
|
|
3541
|
+
threadLabel: entry.threadLabel,
|
|
3542
|
+
summary: entry.summary,
|
|
3543
|
+
messageText: entry.messageText,
|
|
3544
|
+
createdAtMs: entry.createdAtMs,
|
|
3545
|
+
readOnly: true,
|
|
3546
|
+
provider: "codex",
|
|
3547
|
+
},
|
|
3548
|
+
}) || dirty;
|
|
3549
|
+
}
|
|
3403
3550
|
}
|
|
3404
3551
|
|
|
3405
3552
|
state.sqliteMessageCursorId = cursorId;
|
|
@@ -9056,16 +9203,18 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
9056
9203
|
for (const draft of runtime.moltbookDraftsByToken.values()) {
|
|
9057
9204
|
if (draft.decision) continue;
|
|
9058
9205
|
const title = draft.postTitle ? `draft → ${draft.postTitle}` : "Moltbook draft";
|
|
9206
|
+
const draftThreadLabel = draft.postTitle || "Moltbook";
|
|
9059
9207
|
items.push({
|
|
9060
9208
|
kind: "moltbook_draft",
|
|
9061
9209
|
token: draft.token,
|
|
9062
9210
|
threadId: "moltbook",
|
|
9063
|
-
threadLabel:
|
|
9211
|
+
threadLabel: draftThreadLabel,
|
|
9064
9212
|
title,
|
|
9065
9213
|
summary: draft.contextSummary || String(draft.draftText || "").slice(0, 160),
|
|
9066
9214
|
primaryLabel: t(locale, "server.action.review"),
|
|
9067
9215
|
createdAtMs: Number(draft.createdAtMs) || now,
|
|
9068
9216
|
provider: "moltbook",
|
|
9217
|
+
draftType: draft.draftType || "reply",
|
|
9069
9218
|
});
|
|
9070
9219
|
}
|
|
9071
9220
|
|
|
@@ -9075,7 +9224,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
9075
9224
|
kind: "a2a_task",
|
|
9076
9225
|
token: task.token,
|
|
9077
9226
|
threadId: "a2a",
|
|
9078
|
-
threadLabel: "
|
|
9227
|
+
threadLabel: cleanText(task.instruction || "").slice(0, 80),
|
|
9079
9228
|
title: `A2A: ${cleanText(task.instruction || "").slice(0, 80)}`,
|
|
9080
9229
|
summary: cleanText(task.instruction || "").slice(0, 160),
|
|
9081
9230
|
primaryLabel: t(locale, "server.action.review"),
|
|
@@ -9112,8 +9261,15 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
9112
9261
|
function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
9113
9262
|
const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
|
|
9114
9263
|
runtime.recentHistoryItems = items;
|
|
9264
|
+
const completedKinds = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task_result"]);
|
|
9115
9265
|
return items
|
|
9116
|
-
.filter((item) =>
|
|
9266
|
+
.filter((item) => {
|
|
9267
|
+
const k = cleanText(item?.kind || "");
|
|
9268
|
+
if (!completedKinds.has(k)) return false;
|
|
9269
|
+
// Only resolved approvals (readOnly = true)
|
|
9270
|
+
if (k === "approval" && !item.readOnly) return false;
|
|
9271
|
+
return true;
|
|
9272
|
+
})
|
|
9117
9273
|
.slice()
|
|
9118
9274
|
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0))
|
|
9119
9275
|
.map((item) => ({
|
|
@@ -9121,12 +9277,19 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
9121
9277
|
token: item.token,
|
|
9122
9278
|
threadId: cleanText(item.threadId || extractConversationIdFromStableId(item.stableId) || ""),
|
|
9123
9279
|
threadLabel: item.threadLabel || "",
|
|
9124
|
-
title: item.
|
|
9280
|
+
title: item.kind === "assistant_final"
|
|
9281
|
+
? (item.threadLabel ? formatTitle(kindTitle(locale, item.kind), item.threadLabel) : item.title)
|
|
9282
|
+
: (item.kind === "a2a_task_result" && item.instruction)
|
|
9283
|
+
? cleanText(item.instruction).slice(0, 80)
|
|
9284
|
+
: (item.kind === "moltbook_reply" || item.kind === "moltbook_draft")
|
|
9285
|
+
? cleanText(item.summary || item.messageText || "").slice(0, 80) || item.title
|
|
9286
|
+
: item.title || kindTitle(locale, item.kind),
|
|
9125
9287
|
summary: item.summary,
|
|
9126
9288
|
fileRefs: normalizeTimelineFileRefs(item.fileRefs ?? []),
|
|
9127
9289
|
primaryLabel: t(locale, "server.action.detail"),
|
|
9128
9290
|
createdAtMs: item.createdAtMs,
|
|
9129
9291
|
provider: normalizeProvider(item.provider),
|
|
9292
|
+
...(item.draftType != null ? { draftType: item.draftType } : {}),
|
|
9130
9293
|
}));
|
|
9131
9294
|
}
|
|
9132
9295
|
|
|
@@ -9480,6 +9643,7 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
9480
9643
|
outcome: entry.outcome || "",
|
|
9481
9644
|
createdAtMs: entry.createdAtMs,
|
|
9482
9645
|
provider: normalizeProvider(entry.provider),
|
|
9646
|
+
...(entry.draftType != null ? { draftType: entry.draftType } : {}),
|
|
9483
9647
|
}));
|
|
9484
9648
|
|
|
9485
9649
|
return {
|
|
@@ -9696,8 +9860,11 @@ function historyItemThreadId(item) {
|
|
|
9696
9860
|
return cleanText(item?.threadId || extractConversationIdFromStableId(item?.stableId) || "");
|
|
9697
9861
|
}
|
|
9698
9862
|
|
|
9699
|
-
|
|
9700
|
-
|
|
9863
|
+
const REPLYABLE_HISTORY_KINDS = new Set(["assistant_final"]);
|
|
9864
|
+
|
|
9865
|
+
function isLatestReplyableHistoryItem(runtime, item) {
|
|
9866
|
+
const itemKind = cleanText(item?.kind || "");
|
|
9867
|
+
if (!runtime || !REPLYABLE_HISTORY_KINDS.has(itemKind)) {
|
|
9701
9868
|
return false;
|
|
9702
9869
|
}
|
|
9703
9870
|
|
|
@@ -9707,14 +9874,24 @@ function isLatestCompletionHistoryItem(runtime, item) {
|
|
|
9707
9874
|
return false;
|
|
9708
9875
|
}
|
|
9709
9876
|
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9877
|
+
// Find the latest replyable item (completion or assistant_final) for this thread
|
|
9878
|
+
// Sort by createdAtMs descending since array order is not guaranteed
|
|
9879
|
+
let latestForThread = null;
|
|
9880
|
+
let latestTs = 0;
|
|
9881
|
+
for (const entry of runtime.recentHistoryItems) {
|
|
9882
|
+
if (!REPLYABLE_HISTORY_KINDS.has(cleanText(entry?.kind || "")) || historyItemThreadId(entry) !== threadId) continue;
|
|
9883
|
+
const ts = Number(entry.createdAtMs) || 0;
|
|
9884
|
+
if (!latestForThread || ts > latestTs) {
|
|
9885
|
+
latestForThread = entry;
|
|
9886
|
+
latestTs = ts;
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9713
9889
|
return cleanText(latestForThread?.token || "") === token;
|
|
9714
9890
|
}
|
|
9715
9891
|
|
|
9716
9892
|
function findNewerThreadMessageAfterCompletion(runtime, completionItem) {
|
|
9717
|
-
|
|
9893
|
+
const itemKind = cleanText(completionItem?.kind || "");
|
|
9894
|
+
if (!runtime || !REPLYABLE_HISTORY_KINDS.has(itemKind)) {
|
|
9718
9895
|
return null;
|
|
9719
9896
|
}
|
|
9720
9897
|
|
|
@@ -9740,10 +9917,10 @@ function findNewerThreadMessageAfterCompletion(runtime, completionItem) {
|
|
|
9740
9917
|
function buildHistoryDetail(item, locale, runtime = null) {
|
|
9741
9918
|
const threadId = historyItemThreadId(item);
|
|
9742
9919
|
const replyEnabled =
|
|
9743
|
-
item.kind
|
|
9920
|
+
REPLYABLE_HISTORY_KINDS.has(item.kind) &&
|
|
9744
9921
|
Boolean(threadId) &&
|
|
9745
9922
|
Boolean(runtime?.ipcClient) &&
|
|
9746
|
-
|
|
9923
|
+
isLatestReplyableHistoryItem(runtime, item);
|
|
9747
9924
|
return {
|
|
9748
9925
|
kind: item.kind,
|
|
9749
9926
|
token: item.token,
|
|
@@ -10197,7 +10374,20 @@ async function handleCompletionReply({
|
|
|
10197
10374
|
if (!conversationId) {
|
|
10198
10375
|
throw new Error("completion-reply-unavailable");
|
|
10199
10376
|
}
|
|
10200
|
-
|
|
10377
|
+
// For assistant_final, check against timeline entries (avoids maxHistoryItems eviction).
|
|
10378
|
+
// For legacy completion, check against history items.
|
|
10379
|
+
const itemKind = cleanText(completionItem?.kind || "");
|
|
10380
|
+
if (itemKind === "assistant_final") {
|
|
10381
|
+
const itemTs = Number(completionItem.createdAtMs) || 0;
|
|
10382
|
+
const hasNewer = runtime.recentTimelineEntries.some(
|
|
10383
|
+
(e) => e.kind === "assistant_final" &&
|
|
10384
|
+
cleanText(e.threadId || "") === conversationId &&
|
|
10385
|
+
(Number(e.createdAtMs) || 0) > itemTs
|
|
10386
|
+
);
|
|
10387
|
+
if (hasNewer) {
|
|
10388
|
+
throw new Error("completion-reply-unavailable");
|
|
10389
|
+
}
|
|
10390
|
+
} else if (!isLatestReplyableHistoryItem(runtime, completionItem)) {
|
|
10201
10391
|
throw new Error("completion-reply-unavailable");
|
|
10202
10392
|
}
|
|
10203
10393
|
const newerThreadMessage = findNewerThreadMessageAfterCompletion(runtime, completionItem);
|
|
@@ -10484,7 +10674,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
10484
10674
|
title: approval.title,
|
|
10485
10675
|
threadLabel: approval.threadLabel || "",
|
|
10486
10676
|
messageText: `${approvalDecisionMessage(decision, config.defaultLocale, approval.provider)}\n\n${approval.messageText}`,
|
|
10487
|
-
summary: approvalDecisionMessage(decision, config.defaultLocale, approval.provider),
|
|
10677
|
+
summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage(decision, config.defaultLocale, approval.provider),
|
|
10488
10678
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
10489
10679
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
10490
10680
|
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
@@ -10511,7 +10701,32 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10511
10701
|
}
|
|
10512
10702
|
if (timelineMessageKinds.has(kind)) {
|
|
10513
10703
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
10514
|
-
|
|
10704
|
+
if (!entry) return null;
|
|
10705
|
+
const detail = buildTimelineMessageDetail(entry, locale, runtime);
|
|
10706
|
+
// Add reply support for Codex assistant_final entries only (replaces completion reply).
|
|
10707
|
+
// Check directly against timeline entries (not history items) to avoid
|
|
10708
|
+
// maxHistoryItems eviction issues.
|
|
10709
|
+
if (kind === "assistant_final") {
|
|
10710
|
+
const entryProvider = normalizeProvider(entry.provider);
|
|
10711
|
+
if (entryProvider === "codex" && runtime?.ipcClient) {
|
|
10712
|
+
const entryThreadId = cleanText(entry.threadId || "");
|
|
10713
|
+
const entryTs = Number(entry.createdAtMs) || 0;
|
|
10714
|
+
let isLatestForThread = true;
|
|
10715
|
+
for (const other of runtime.recentTimelineEntries) {
|
|
10716
|
+
if (other.kind === "assistant_final" &&
|
|
10717
|
+
cleanText(other.threadId || "") === entryThreadId &&
|
|
10718
|
+
(Number(other.createdAtMs) || 0) > entryTs) {
|
|
10719
|
+
isLatestForThread = false;
|
|
10720
|
+
break;
|
|
10721
|
+
}
|
|
10722
|
+
}
|
|
10723
|
+
if (isLatestForThread) {
|
|
10724
|
+
detail.reply = { enabled: true, supportsPlanMode: true, supportsImages: true };
|
|
10725
|
+
detail.provider = entryProvider;
|
|
10726
|
+
}
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
return detail;
|
|
10515
10730
|
}
|
|
10516
10731
|
if (kind === "approval") {
|
|
10517
10732
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
@@ -10614,20 +10829,21 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10614
10829
|
};
|
|
10615
10830
|
}
|
|
10616
10831
|
|
|
10617
|
-
if (kind === "a2a_task") {
|
|
10618
|
-
const task = runtime.a2aTasksByToken.get(token);
|
|
10832
|
+
if (kind === "a2a_task" || kind === "a2a_task_result") {
|
|
10833
|
+
const task = kind === "a2a_task" ? runtime.a2aTasksByToken.get(token) : null;
|
|
10619
10834
|
const entry = task
|
|
10620
10835
|
? null
|
|
10621
|
-
: runtime.recentTimelineEntries.find((e) => e.kind ===
|
|
10836
|
+
: runtime.recentTimelineEntries.find((e) => e.kind === kind && e.token === token);
|
|
10622
10837
|
const source = task || entry;
|
|
10623
10838
|
if (!source) return null;
|
|
10624
|
-
const instruction = task ? task.instruction : entry.
|
|
10839
|
+
const instruction = task ? task.instruction : entry.instruction || entry.summary || "";
|
|
10840
|
+
const responseText = kind === "a2a_task_result" && !task ? entry.messageText || "" : "";
|
|
10625
10841
|
const messageHtml = escapeHtml(instruction)
|
|
10626
10842
|
.split("\n")
|
|
10627
10843
|
.map((line) => `<p>${line}</p>`)
|
|
10628
10844
|
.join("");
|
|
10629
10845
|
return {
|
|
10630
|
-
kind
|
|
10846
|
+
kind,
|
|
10631
10847
|
token,
|
|
10632
10848
|
threadId: "a2a",
|
|
10633
10849
|
threadLabel: "A2A",
|
|
@@ -10636,8 +10852,9 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
10636
10852
|
messageHtml,
|
|
10637
10853
|
provider: "a2a",
|
|
10638
10854
|
instruction,
|
|
10855
|
+
messageText: responseText,
|
|
10639
10856
|
taskId: task?.id || "",
|
|
10640
|
-
taskStatus: task?.status || entry?.taskStatus || "
|
|
10857
|
+
taskStatus: task?.status || entry?.taskStatus || (kind === "a2a_task_result" ? "completed" : "submitted"),
|
|
10641
10858
|
callerInfo: task?.callerInfo || {},
|
|
10642
10859
|
createdAtMs: source.createdAtMs || Date.now(),
|
|
10643
10860
|
readOnly: !task || task.status !== "submitted",
|
|
@@ -10942,14 +11159,34 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10942
11159
|
}
|
|
10943
11160
|
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
10944
11161
|
const instruction = cleanText(body.instruction || "");
|
|
11162
|
+
const requestedExecutor = cleanText(body.executor || "");
|
|
10945
11163
|
resolveA2ATaskDecision(task, { action, instruction });
|
|
10946
11164
|
|
|
11165
|
+
// Update task stats.
|
|
11166
|
+
if (!state.a2aTaskStats) state.a2aTaskStats = { received: 0, completed: 0, denied: 0 };
|
|
10947
11167
|
if (action === "approve") {
|
|
10948
|
-
|
|
11168
|
+
state.a2aTaskStats.completed += 1;
|
|
11169
|
+
} else {
|
|
11170
|
+
state.a2aTaskStats.denied += 1;
|
|
11171
|
+
}
|
|
11172
|
+
saveState(config.stateFile, state).catch(() => {});
|
|
11173
|
+
|
|
11174
|
+
if (action === "approve") {
|
|
11175
|
+
// Resolve which executor to use.
|
|
11176
|
+
const available = runtime.a2aAvailableExecutors || { codex: false, claude: false };
|
|
11177
|
+
const preference = requestedExecutor || state.a2aExecutorPreference || "ask";
|
|
11178
|
+
let executor;
|
|
11179
|
+
if (preference === "codex" && available.codex) executor = "codex";
|
|
11180
|
+
else if (preference === "claude" && available.claude) executor = "claude";
|
|
11181
|
+
else if (available.codex) executor = "codex";
|
|
11182
|
+
else if (available.claude) executor = "claude";
|
|
11183
|
+
else executor = "codex"; // fallback
|
|
11184
|
+
|
|
11185
|
+
// Execute task asynchronously
|
|
10949
11186
|
(async () => {
|
|
10950
11187
|
try {
|
|
10951
11188
|
const { executeA2ATask } = await import("./a2a-executor.mjs");
|
|
10952
|
-
await executeA2ATask(task, config, runtime, state, { recordTimelineEntry, saveState });
|
|
11189
|
+
await executeA2ATask(task, config, runtime, state, { recordTimelineEntry, recordHistoryItem, saveState, deliverWebPushItem }, executor);
|
|
10953
11190
|
} catch (error) {
|
|
10954
11191
|
console.error(`[a2a-execute-error] ${error.message}`);
|
|
10955
11192
|
failA2ATask(task, error.message);
|
|
@@ -11165,6 +11402,24 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11165
11402
|
return writeJson(res, 200, { ok: true, enabled });
|
|
11166
11403
|
}
|
|
11167
11404
|
|
|
11405
|
+
if (url.pathname === "/api/settings/a2a-executor" && req.method === "POST") {
|
|
11406
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11407
|
+
if (!session) return;
|
|
11408
|
+
let payload;
|
|
11409
|
+
try {
|
|
11410
|
+
payload = await parseJsonBody(req);
|
|
11411
|
+
} catch {
|
|
11412
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11413
|
+
}
|
|
11414
|
+
const valid = new Set(["auto", "codex", "claude", "ask"]);
|
|
11415
|
+
const preference = valid.has(payload?.preference) ? payload.preference : "auto";
|
|
11416
|
+
if (state.a2aExecutorPreference !== preference) {
|
|
11417
|
+
state.a2aExecutorPreference = preference;
|
|
11418
|
+
await saveState(config.stateFile, state);
|
|
11419
|
+
}
|
|
11420
|
+
return writeJson(res, 200, { ok: true, preference });
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11168
11423
|
if (url.pathname === "/api/moltbook/scout-status" && req.method === "GET") {
|
|
11169
11424
|
const session = requireApiSession(req, res, config, state);
|
|
11170
11425
|
if (!session) {
|
|
@@ -11208,6 +11463,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11208
11463
|
return writeJson(res, 200, { enabled: false });
|
|
11209
11464
|
}
|
|
11210
11465
|
const relay = getRelayStatus();
|
|
11466
|
+
const stats = state.a2aTaskStats || { received: 0, completed: 0, denied: 0 };
|
|
11211
11467
|
return writeJson(res, 200, {
|
|
11212
11468
|
enabled: true,
|
|
11213
11469
|
connected: relay.polling && relay.lastPollOk,
|
|
@@ -11217,9 +11473,36 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11217
11473
|
userId: config.a2aRelayUserId,
|
|
11218
11474
|
relayUrl: config.a2aRelayUrl,
|
|
11219
11475
|
apiKeyConfigured: Boolean(config.a2aApiKey),
|
|
11476
|
+
acceptPublicTasks: config.a2aAcceptPublicTasks === true,
|
|
11477
|
+
taskStats: stats,
|
|
11220
11478
|
});
|
|
11221
11479
|
}
|
|
11222
11480
|
|
|
11481
|
+
// Toggle acceptPublicTasks on the A2A relay.
|
|
11482
|
+
if (url.pathname === "/api/a2a/public-tasks" && req.method === "POST") {
|
|
11483
|
+
const session = requireApiSession(req, res, config, state);
|
|
11484
|
+
if (!session) return;
|
|
11485
|
+
const body = await parseJsonBody(req);
|
|
11486
|
+
const accept = body?.accept === true;
|
|
11487
|
+
config.a2aAcceptPublicTasks = accept;
|
|
11488
|
+
state.a2aAcceptPublicTasks = accept;
|
|
11489
|
+
try {
|
|
11490
|
+
await saveState(config.stateFile, state);
|
|
11491
|
+
} catch (error) {
|
|
11492
|
+
console.error(`[a2a-public-tasks-save] ${error.message}`);
|
|
11493
|
+
}
|
|
11494
|
+
// Re-register with relay to propagate the flag.
|
|
11495
|
+
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
11496
|
+
try {
|
|
11497
|
+
await updatePublicTasksFlag({ config, buildAgentCard, acceptPublicTasks: accept });
|
|
11498
|
+
console.log(`[a2a-relay] acceptPublicTasks updated to ${accept}`);
|
|
11499
|
+
} catch (error) {
|
|
11500
|
+
console.error(`[a2a-relay-public-update] ${error.message}`);
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
return writeJson(res, 200, { ok: true, acceptPublicTasks: accept });
|
|
11504
|
+
}
|
|
11505
|
+
|
|
11223
11506
|
// ─── Thread sharing ──────────────────────────────────────────────
|
|
11224
11507
|
|
|
11225
11508
|
function buildThreadShareContent(shareType, body) {
|
|
@@ -12074,10 +12357,12 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12074
12357
|
// ---------------------------------------------------------------
|
|
12075
12358
|
// Moltbook scout draft channel
|
|
12076
12359
|
//
|
|
12077
|
-
// The
|
|
12078
|
-
//
|
|
12079
|
-
//
|
|
12080
|
-
// phone
|
|
12360
|
+
// The CLI submits a draft via POST /api/providers/moltbook/draft
|
|
12361
|
+
// and exits immediately (fire-and-forget). The draft is persisted
|
|
12362
|
+
// to ~/.viveworker/moltbook-drafts/ and survives bridge restarts.
|
|
12363
|
+
// When the phone approves via POST /api/items/moltbook-draft/:token/decision,
|
|
12364
|
+
// the bridge posts to the Moltbook API and solves the verification
|
|
12365
|
+
// puzzle inline.
|
|
12081
12366
|
// ---------------------------------------------------------------
|
|
12082
12367
|
|
|
12083
12368
|
if (url.pathname === "/api/providers/moltbook/draft" && req.method === "POST") {
|
|
@@ -12110,6 +12395,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12110
12395
|
const postBody = typeof body.postBody === "string" ? body.postBody : "";
|
|
12111
12396
|
const intent = typeof body.intent === "string" ? body.intent : "";
|
|
12112
12397
|
const slot = cleanText(body.slot || "");
|
|
12398
|
+
const ttlMs = Math.max(60000, Math.min(Number(body.ttlMs) || 86400000, 86400000));
|
|
12113
12399
|
const draft = {
|
|
12114
12400
|
token,
|
|
12115
12401
|
sourceId,
|
|
@@ -12125,11 +12411,13 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12125
12411
|
intent,
|
|
12126
12412
|
slot,
|
|
12127
12413
|
contextSummary,
|
|
12414
|
+
ttlMs,
|
|
12128
12415
|
createdAtMs: Date.now(),
|
|
12129
12416
|
decisionWaiters: [],
|
|
12130
12417
|
decision: null,
|
|
12131
12418
|
};
|
|
12132
12419
|
runtime.moltbookDraftsByToken.set(token, draft);
|
|
12420
|
+
await writeDraft(draft).catch((e) => console.error(`[moltbook-draft-persist] ${e.message}`));
|
|
12133
12421
|
try {
|
|
12134
12422
|
recordTimelineEntry({
|
|
12135
12423
|
config,
|
|
@@ -12140,7 +12428,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12140
12428
|
token,
|
|
12141
12429
|
kind: "moltbook_draft",
|
|
12142
12430
|
threadId: "moltbook",
|
|
12143
|
-
threadLabel: "Moltbook",
|
|
12431
|
+
threadLabel: postTitle || "Moltbook",
|
|
12144
12432
|
title: draftType === "original_post"
|
|
12145
12433
|
? (postTitle || "Moltbook new post")
|
|
12146
12434
|
: (postTitle ? `draft → ${postTitle}` : "Moltbook draft"),
|
|
@@ -12258,9 +12546,33 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12258
12546
|
// ignore
|
|
12259
12547
|
}
|
|
12260
12548
|
}
|
|
12261
|
-
//
|
|
12262
|
-
|
|
12263
|
-
|
|
12549
|
+
// Persist decision to disk.
|
|
12550
|
+
await writeDraft(draft).catch((e) => console.error(`[moltbook-draft-persist-decision] ${e.message}`));
|
|
12551
|
+
|
|
12552
|
+
if (action === "approve") {
|
|
12553
|
+
// Execute posting asynchronously — fire-and-forget from the HTTP handler.
|
|
12554
|
+
(async () => {
|
|
12555
|
+
try {
|
|
12556
|
+
await executeMoltbookDraftPost(draft, config, runtime, state);
|
|
12557
|
+
} catch (postError) {
|
|
12558
|
+
console.error(`[moltbook-draft-post-error] ${postError.message}`);
|
|
12559
|
+
try {
|
|
12560
|
+
await deliverWebPushItem({
|
|
12561
|
+
config, state, kind: "moltbook_draft", token: draft.token,
|
|
12562
|
+
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
12563
|
+
title: "Draft post failed",
|
|
12564
|
+
body: String(postError.message || "").slice(0, 160),
|
|
12565
|
+
});
|
|
12566
|
+
} catch { /* ignore push error */ }
|
|
12567
|
+
}
|
|
12568
|
+
await deleteDraft(token).catch(() => {});
|
|
12569
|
+
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
12570
|
+
})();
|
|
12571
|
+
} else {
|
|
12572
|
+
// Deny — clean up.
|
|
12573
|
+
await deleteDraft(token).catch(() => {});
|
|
12574
|
+
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
12575
|
+
}
|
|
12264
12576
|
return writeJson(res, 200, { ok: true, action });
|
|
12265
12577
|
}
|
|
12266
12578
|
|
|
@@ -12326,15 +12638,19 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12326
12638
|
return writeJson(res, 200, detail);
|
|
12327
12639
|
}
|
|
12328
12640
|
|
|
12329
|
-
const apiCompletionReplyMatch = url.pathname.match(/^\/api\/items\/completion\/([^/]+)\/reply$/u);
|
|
12641
|
+
const apiCompletionReplyMatch = url.pathname.match(/^\/api\/items\/(completion|assistant_final)\/([^/]+)\/reply$/u);
|
|
12330
12642
|
if (apiCompletionReplyMatch && req.method === "POST") {
|
|
12331
12643
|
const session = requireMutatingApiSession(req, res, config, state);
|
|
12332
12644
|
if (!session) {
|
|
12333
12645
|
return;
|
|
12334
12646
|
}
|
|
12335
12647
|
|
|
12336
|
-
const
|
|
12337
|
-
const
|
|
12648
|
+
const replyKind = apiCompletionReplyMatch[1];
|
|
12649
|
+
const token = decodeURIComponent(apiCompletionReplyMatch[2]);
|
|
12650
|
+
// Try history items first, fall back to timeline entries for assistant_final
|
|
12651
|
+
// (history items may be evicted by maxHistoryItems limit)
|
|
12652
|
+
const completionItem = historyItemByToken(runtime, replyKind, token)
|
|
12653
|
+
|| (replyKind === "assistant_final" ? timelineEntryByToken(runtime, token, replyKind) : null);
|
|
12338
12654
|
if (!completionItem) {
|
|
12339
12655
|
return writeJson(res, 404, { error: "item-not-found" });
|
|
12340
12656
|
}
|
|
@@ -14204,6 +14520,106 @@ function readMoltbookEnvKey() {
|
|
|
14204
14520
|
return "";
|
|
14205
14521
|
}
|
|
14206
14522
|
|
|
14523
|
+
// Post an approved Moltbook draft (reply or original post), solve the
|
|
14524
|
+
// verification puzzle, and update scout state. Called asynchronously from
|
|
14525
|
+
// the decision handler.
|
|
14526
|
+
async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
14527
|
+
if (!config.moltbookApiKey) throw new Error("MOLTBOOK_API_KEY not configured");
|
|
14528
|
+
const mb = createMoltbookClient(config.moltbookApiKey);
|
|
14529
|
+
|
|
14530
|
+
const finalText = draft.decision.text || draft.draftText;
|
|
14531
|
+
const finalTitle = draft.decision.title || draft.postTitle;
|
|
14532
|
+
|
|
14533
|
+
let verification;
|
|
14534
|
+
|
|
14535
|
+
if (draft.draftType === "original_post") {
|
|
14536
|
+
const result = await mb("/posts", {
|
|
14537
|
+
method: "POST",
|
|
14538
|
+
body: JSON.stringify({
|
|
14539
|
+
submolt_name: draft.submoltName,
|
|
14540
|
+
submolt: draft.submoltName,
|
|
14541
|
+
title: finalTitle,
|
|
14542
|
+
content: finalText,
|
|
14543
|
+
}),
|
|
14544
|
+
});
|
|
14545
|
+
const post = result?.post || null;
|
|
14546
|
+
verification = post?.verification || null;
|
|
14547
|
+
console.log(`[moltbook-draft-post] Posted original post (id=${post?.id})`);
|
|
14548
|
+
|
|
14549
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
14550
|
+
recordComposeAttempt(scoutState, finalTitle, post?.id, "post");
|
|
14551
|
+
await writeScoutState(scoutState);
|
|
14552
|
+
} else {
|
|
14553
|
+
const result = await mb(`/posts/${draft.postId}/comments`, {
|
|
14554
|
+
method: "POST",
|
|
14555
|
+
body: JSON.stringify({
|
|
14556
|
+
content: finalText,
|
|
14557
|
+
...(draft.parentCommentId ? { parent_id: draft.parentCommentId } : {}),
|
|
14558
|
+
}),
|
|
14559
|
+
});
|
|
14560
|
+
const comment = result?.comment || null;
|
|
14561
|
+
verification = comment?.verification || null;
|
|
14562
|
+
console.log(`[moltbook-draft-post] Posted reply (commentId=${comment?.id}) to post ${draft.postId}`);
|
|
14563
|
+
|
|
14564
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
14565
|
+
scoutState.sentToday += 1;
|
|
14566
|
+
markPostSeen(scoutState, draft.postId, "published");
|
|
14567
|
+
recordComposeAttempt(scoutState, draft.postTitle || draft.postId, draft.postId, "reply");
|
|
14568
|
+
await writeScoutState(scoutState);
|
|
14569
|
+
}
|
|
14570
|
+
|
|
14571
|
+
// Solve verification puzzle if present.
|
|
14572
|
+
if (verification) {
|
|
14573
|
+
let answer = solveVerificationPuzzle(verification.challenge_text);
|
|
14574
|
+
const source = answer != null ? "solver" : null;
|
|
14575
|
+
if (answer == null) {
|
|
14576
|
+
answer = await solvePuzzleWithLLM(verification.challenge_text);
|
|
14577
|
+
}
|
|
14578
|
+
if (answer != null) {
|
|
14579
|
+
try {
|
|
14580
|
+
await mb("/verify", {
|
|
14581
|
+
method: "POST",
|
|
14582
|
+
body: JSON.stringify({ verification_code: verification.verification_code, answer }),
|
|
14583
|
+
});
|
|
14584
|
+
console.log(`[moltbook-draft-verify] Verified with answer ${answer}`);
|
|
14585
|
+
} catch (verifyError) {
|
|
14586
|
+
// Wrong answer from solver — retry with LLM.
|
|
14587
|
+
if (/incorrect/i.test(verifyError.message) && source === "solver") {
|
|
14588
|
+
const llmAnswer = await solvePuzzleWithLLM(verification.challenge_text);
|
|
14589
|
+
if (llmAnswer && llmAnswer !== answer) {
|
|
14590
|
+
try {
|
|
14591
|
+
await mb("/verify", {
|
|
14592
|
+
method: "POST",
|
|
14593
|
+
body: JSON.stringify({ verification_code: verification.verification_code, answer: llmAnswer }),
|
|
14594
|
+
});
|
|
14595
|
+
console.log(`[moltbook-draft-verify] Verified with LLM retry answer ${llmAnswer}`);
|
|
14596
|
+
} catch (retryError) {
|
|
14597
|
+
console.error(`[moltbook-draft-verify] LLM retry failed: ${retryError.message}`);
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
} else {
|
|
14601
|
+
console.error(`[moltbook-draft-verify] Failed: ${verifyError.message}`);
|
|
14602
|
+
}
|
|
14603
|
+
}
|
|
14604
|
+
} else {
|
|
14605
|
+
console.error(`[moltbook-draft-verify] Could not solve puzzle for draft ${draft.token}`);
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
|
|
14609
|
+
// Push notification for successful post.
|
|
14610
|
+
try {
|
|
14611
|
+
const pushTitle = draft.draftType === "original_post" ? finalTitle : `Reply → ${draft.postTitle || "Moltbook"}`;
|
|
14612
|
+
await deliverWebPushItem({
|
|
14613
|
+
config, state, kind: "moltbook_draft", token: draft.token,
|
|
14614
|
+
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
14615
|
+
title: "Moltbook posted",
|
|
14616
|
+
body: truncate(singleLine(pushTitle), 160),
|
|
14617
|
+
});
|
|
14618
|
+
} catch { /* ignore push error */ }
|
|
14619
|
+
|
|
14620
|
+
console.log(`[moltbook-draft] Successfully posted draft ${draft.token} (type=${draft.draftType})`);
|
|
14621
|
+
}
|
|
14622
|
+
|
|
14207
14623
|
function buildConfig(cli) {
|
|
14208
14624
|
const codexHome = resolvePath(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
14209
14625
|
const stateFile = resolvePath(process.env.STATE_FILE || path.join(workspaceRoot, ".viveworker-state.json"));
|
|
@@ -14489,6 +14905,7 @@ async function loadState(stateFile) {
|
|
|
14489
14905
|
pairingConsumedAt: Number(parsed.pairingConsumedAt) || 0,
|
|
14490
14906
|
pairingConsumedCredential: cleanText(parsed.pairingConsumedCredential ?? ""),
|
|
14491
14907
|
claudeAwayMode: parsed.claudeAwayMode === true,
|
|
14908
|
+
a2aAcceptPublicTasks: parsed.a2aAcceptPublicTasks === true,
|
|
14492
14909
|
};
|
|
14493
14910
|
} catch {
|
|
14494
14911
|
return {
|
|
@@ -15034,11 +15451,17 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
15034
15451
|
|
|
15035
15452
|
const nextHistoryItems = normalizeHistoryItems(
|
|
15036
15453
|
runtime.recentHistoryItems.map((item) => {
|
|
15454
|
+
// Moltbook / A2A / thread-share entries carry their own titles — skip relabel.
|
|
15455
|
+
const itemKind = item.kind || "";
|
|
15456
|
+
if (itemKind === "moltbook_reply" || itemKind === "moltbook_draft" || itemKind === "a2a_task" || itemKind === "a2a_task_result" || itemKind === "thread_share") {
|
|
15457
|
+
return item;
|
|
15458
|
+
}
|
|
15037
15459
|
const conversationId = extractConversationIdFromStableId(item.stableId);
|
|
15038
15460
|
if (!conversationId) {
|
|
15039
15461
|
return item;
|
|
15040
15462
|
}
|
|
15041
|
-
const
|
|
15463
|
+
const claudeTitle = runtime.claudeSessionTitles.get(conversationId) || "";
|
|
15464
|
+
const nativeThreadLabel = claudeTitle || getNativeThreadLabel({
|
|
15042
15465
|
runtime,
|
|
15043
15466
|
conversationId,
|
|
15044
15467
|
cwd: "",
|
|
@@ -15069,10 +15492,10 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
15069
15492
|
|
|
15070
15493
|
const nextTimelineEntries = normalizeTimelineEntries(
|
|
15071
15494
|
runtime.recentTimelineEntries.map((entry) => {
|
|
15072
|
-
// Moltbook entries carry their own
|
|
15073
|
-
//
|
|
15074
|
-
|
|
15075
|
-
if (
|
|
15495
|
+
// Moltbook / A2A / thread-share entries carry their own titles.
|
|
15496
|
+
// Skip the native-thread relabel pass so we don't overwrite them.
|
|
15497
|
+
const entryKind = entry.kind || "";
|
|
15498
|
+
if (entryKind === "moltbook_reply" || entryKind === "moltbook_draft" || entryKind === "a2a_task" || entryKind === "a2a_task_result" || entryKind === "thread_share") {
|
|
15076
15499
|
return entry;
|
|
15077
15500
|
}
|
|
15078
15501
|
const threadId = cleanText(entry.threadId || "");
|
|
@@ -15651,6 +16074,7 @@ async function main() {
|
|
|
15651
16074
|
|
|
15652
16075
|
// --- A2A Relay ---
|
|
15653
16076
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
16077
|
+
config.a2aAcceptPublicTasks = state.a2aAcceptPublicTasks === true;
|
|
15654
16078
|
const regResult = await registerWithRelay({ config, buildAgentCard });
|
|
15655
16079
|
if (regResult.ok) {
|
|
15656
16080
|
startRelayPolling({
|