viveworker 0.8.10 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/scripts/lib/immutable-json-patch.mjs +142 -0
- package/scripts/lib/ipc-frame-worker.mjs +30 -0
- package/scripts/lib/ipc-message-projector.mjs +221 -0
- package/scripts/lib/length-prefixed-frame-decoder.mjs +128 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +24 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +10 -0
- package/scripts/lib/work-items.mjs +179 -0
- package/scripts/moltbook-api.mjs +27 -0
- package/scripts/moltbook-cli.mjs +12 -4
- package/scripts/moltbook-scout-auto.sh +105 -37
- package/scripts/viveworker-bridge.mjs +472 -206
- package/scripts/viveworker.mjs +5 -0
- package/web/app.css +106 -0
- package/web/app.js +183 -28
- package/web/build-id.js +1 -1
- package/web/i18n.js +44 -10
- package/web/remote-pairing/api-router.js +356 -11
- package/web/remote-pairing/transport.js +7 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const WORK_ITEM_SCHEMA_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
4
|
+
const STATE_PRIORITY = new Map([
|
|
5
|
+
["in_progress", 1],
|
|
6
|
+
["needs_action", 2],
|
|
7
|
+
["completed", 3],
|
|
8
|
+
["failed", 3],
|
|
9
|
+
["canceled", 3],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function cleanText(value) {
|
|
13
|
+
return String(value ?? "").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeProvider(value) {
|
|
17
|
+
const provider = cleanText(value).toLowerCase();
|
|
18
|
+
if (["claude", "moltbook", "a2a", "viveworker", "mcp"].includes(provider)) {
|
|
19
|
+
return provider;
|
|
20
|
+
}
|
|
21
|
+
return "codex";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function workItemCategory(item) {
|
|
25
|
+
switch (cleanText(item?.kind)) {
|
|
26
|
+
case "approval":
|
|
27
|
+
return cleanText(item?.approvalKind).includes("payment") ? "payment" : "approval";
|
|
28
|
+
case "plan":
|
|
29
|
+
case "plan_ready":
|
|
30
|
+
return "plan";
|
|
31
|
+
case "choice":
|
|
32
|
+
return "question";
|
|
33
|
+
case "moltbook_draft":
|
|
34
|
+
return "draft";
|
|
35
|
+
case "moltbook_reply":
|
|
36
|
+
return "reply";
|
|
37
|
+
case "thread_share":
|
|
38
|
+
return "handoff";
|
|
39
|
+
case "a2a_task":
|
|
40
|
+
return "delegation";
|
|
41
|
+
case "a2a_task_result":
|
|
42
|
+
return "delegation_result";
|
|
43
|
+
case "activity_status":
|
|
44
|
+
return "agent_run";
|
|
45
|
+
case "completion":
|
|
46
|
+
case "assistant_final":
|
|
47
|
+
return "result";
|
|
48
|
+
default:
|
|
49
|
+
return "work";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function terminalState(item) {
|
|
54
|
+
const taskStatus = cleanText(item?.taskStatus).toLowerCase();
|
|
55
|
+
if (["failed", "error"].includes(taskStatus)) {
|
|
56
|
+
return "failed";
|
|
57
|
+
}
|
|
58
|
+
if (["canceled", "cancelled", "rejected"].includes(taskStatus)) {
|
|
59
|
+
return "canceled";
|
|
60
|
+
}
|
|
61
|
+
return "completed";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function workItemPriority(item, state) {
|
|
65
|
+
if (workItemCategory(item) === "payment") {
|
|
66
|
+
return "urgent";
|
|
67
|
+
}
|
|
68
|
+
if (state === "needs_action") {
|
|
69
|
+
return cleanText(item?.kind) === "thread_share" ? "normal" : "high";
|
|
70
|
+
}
|
|
71
|
+
return state === "in_progress" ? "normal" : "low";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function workItemId(item) {
|
|
75
|
+
const provider = normalizeProvider(item?.provider);
|
|
76
|
+
const kind = cleanText(item?.kind) || "item";
|
|
77
|
+
const token = cleanText(item?.token);
|
|
78
|
+
const fallback = [item?.threadId, item?.createdAtMs, item?.title]
|
|
79
|
+
.map(cleanText)
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(":");
|
|
82
|
+
return `${provider}:${kind}:${token || fallback || "unknown"}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function workItemThreadKey(item) {
|
|
86
|
+
const threadId = cleanText(item?.threadId);
|
|
87
|
+
return threadId ? `${normalizeProvider(item?.provider)}:${threadId}` : "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function enrichWorkItem(item, state) {
|
|
91
|
+
const normalizedState = TERMINAL_STATES.has(state) ? state : cleanText(state) || "needs_action";
|
|
92
|
+
const requiresAction = normalizedState === "needs_action" && item?.supported !== false;
|
|
93
|
+
return {
|
|
94
|
+
...item,
|
|
95
|
+
provider: normalizeProvider(item?.provider),
|
|
96
|
+
work: {
|
|
97
|
+
schemaVersion: WORK_ITEM_SCHEMA_VERSION,
|
|
98
|
+
id: workItemId(item),
|
|
99
|
+
state: normalizedState,
|
|
100
|
+
category: workItemCategory(item),
|
|
101
|
+
priority: workItemPriority(item, normalizedState),
|
|
102
|
+
requiresAction,
|
|
103
|
+
sourceRef: {
|
|
104
|
+
provider: normalizeProvider(item?.provider),
|
|
105
|
+
kind: cleanText(item?.kind),
|
|
106
|
+
token: cleanText(item?.token),
|
|
107
|
+
threadId: cleanText(item?.threadId),
|
|
108
|
+
},
|
|
109
|
+
occurredAtMs: Math.max(0, Number(item?.createdAtMs) || 0),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compareWorkItems(left, right) {
|
|
115
|
+
const priorityRank = { urgent: 3, high: 2, normal: 1, low: 0 };
|
|
116
|
+
const priorityDelta = (priorityRank[right?.work?.priority] ?? 0) - (priorityRank[left?.work?.priority] ?? 0);
|
|
117
|
+
if (priorityDelta !== 0) {
|
|
118
|
+
return priorityDelta;
|
|
119
|
+
}
|
|
120
|
+
return Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeBucket(items, state) {
|
|
124
|
+
return (Array.isArray(items) ? items : [])
|
|
125
|
+
.filter((item) => item && typeof item === "object")
|
|
126
|
+
.map((item) => enrichWorkItem(item, state));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dedupeAcrossStates(items) {
|
|
130
|
+
const byId = new Map();
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
const id = cleanText(item?.work?.id);
|
|
133
|
+
if (!id) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const previous = byId.get(id);
|
|
137
|
+
if (!previous) {
|
|
138
|
+
byId.set(id, item);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const previousRank = STATE_PRIORITY.get(previous.work.state) ?? 0;
|
|
142
|
+
const nextRank = STATE_PRIORITY.get(item.work.state) ?? 0;
|
|
143
|
+
if (nextRank > previousRank || (nextRank === previousRank && Number(item.createdAtMs) > Number(previous.createdAtMs))) {
|
|
144
|
+
byId.set(id, item);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...byId.values()];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildWorkItemResponse({ pending = [], inProgress = [], completed = [], generatedAtMs = Date.now() } = {}) {
|
|
151
|
+
const pendingItems = normalizeBucket(pending, "needs_action");
|
|
152
|
+
const blockedThreadKeys = new Set(pendingItems.map(workItemThreadKey).filter(Boolean));
|
|
153
|
+
const activeItems = normalizeBucket(inProgress, "in_progress")
|
|
154
|
+
.filter((item) => !blockedThreadKeys.has(workItemThreadKey(item)));
|
|
155
|
+
const completedItems = (Array.isArray(completed) ? completed : [])
|
|
156
|
+
.filter((item) => item && typeof item === "object")
|
|
157
|
+
.map((item) => enrichWorkItem(item, terminalState(item)));
|
|
158
|
+
|
|
159
|
+
const deduped = dedupeAcrossStates([...activeItems, ...pendingItems, ...completedItems]);
|
|
160
|
+
const needsAction = deduped.filter((item) => item.work.state === "needs_action").sort(compareWorkItems);
|
|
161
|
+
const active = deduped.filter((item) => item.work.state === "in_progress").sort(compareWorkItems);
|
|
162
|
+
const terminal = deduped.filter((item) => TERMINAL_STATES.has(item.work.state)).sort(compareWorkItems);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
schemaVersion: WORK_ITEM_SCHEMA_VERSION,
|
|
166
|
+
generatedAtMs: Math.max(0, Number(generatedAtMs) || Date.now()),
|
|
167
|
+
counts: {
|
|
168
|
+
needsAction: needsAction.length,
|
|
169
|
+
inProgress: active.length,
|
|
170
|
+
completed: terminal.filter((item) => item.work.state === "completed").length,
|
|
171
|
+
failed: terminal.filter((item) => item.work.state === "failed").length,
|
|
172
|
+
},
|
|
173
|
+
pending: needsAction,
|
|
174
|
+
inProgress: active,
|
|
175
|
+
completed: terminal,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { WORK_ITEM_SCHEMA_VERSION };
|
package/scripts/moltbook-api.mjs
CHANGED
|
@@ -62,6 +62,33 @@ export const DEFAULT_INBOX_DIR = path.join(os.homedir(), ".viveworker", "moltboo
|
|
|
62
62
|
export const DEFAULT_SCOUT_STATE_FILE = path.join(os.homedir(), ".viveworker", "moltbook-scout-state.json");
|
|
63
63
|
export const DEFAULT_DRAFTS_DIR = path.join(os.homedir(), ".viveworker", "moltbook-drafts");
|
|
64
64
|
|
|
65
|
+
const PROVIDER_ERROR_TEXT_PATTERNS = [
|
|
66
|
+
/\bfailed to authenticate\b/iu,
|
|
67
|
+
/\binvalid authentication credentials\b/iu,
|
|
68
|
+
/\bauthentication_error\b/iu,
|
|
69
|
+
/\bapi error:\s*(?:401|403)\b/iu,
|
|
70
|
+
/\bincorrect api key\b/iu,
|
|
71
|
+
/\binvalid api key\b/iu,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export function looksLikeProviderErrorText(value) {
|
|
75
|
+
const text = String(value || "").trim();
|
|
76
|
+
if (!text) return false;
|
|
77
|
+
const compact = text.replace(/\s+/gu, " ");
|
|
78
|
+
if (PROVIDER_ERROR_TEXT_PATTERNS.some((pattern) => pattern.test(compact))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (!compact.startsWith("{")) return false;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(compact);
|
|
84
|
+
const type = String(parsed?.error?.type || parsed?.type || "").toLowerCase();
|
|
85
|
+
const message = String(parsed?.error?.message || parsed?.message || "").toLowerCase();
|
|
86
|
+
return type === "authentication_error" || message.includes("invalid authentication credentials");
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
export async function loadMoltbookEnv(envFile = DEFAULT_ENV_FILE) {
|
|
66
93
|
const env = {};
|
|
67
94
|
try {
|
package/scripts/moltbook-cli.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
pickInboxReplyCandidate,
|
|
37
37
|
reconcileInboxDraftMarkers,
|
|
38
38
|
getMoltbookReplyQuotaState,
|
|
39
|
+
looksLikeProviderErrorText,
|
|
39
40
|
loopbackFetch,
|
|
40
41
|
} from "./moltbook-api.mjs";
|
|
41
42
|
|
|
@@ -572,6 +573,9 @@ async function cmdPropose(postId, flags) {
|
|
|
572
573
|
if (!postId) fail("usage: viveworker moltbook propose <postId> --text \"...\"");
|
|
573
574
|
const text = typeof flags.text === "string" ? flags.text : "";
|
|
574
575
|
if (!text.trim()) fail("--text is required and must be non-empty");
|
|
576
|
+
if (looksLikeProviderErrorText(text)) {
|
|
577
|
+
fail("--text looks like an LLM/provider authentication error; refusing to create a Moltbook draft");
|
|
578
|
+
}
|
|
575
579
|
const ttlSec = Number(flags.ttl) || Number(flags.timeout) || 86400;
|
|
576
580
|
const parentCommentId = typeof flags["parent-id"] === "string" ? flags["parent-id"] : "";
|
|
577
581
|
const postTitle = typeof flags.title === "string" ? flags.title : "";
|
|
@@ -948,10 +952,6 @@ async function cmdCompose(flags) {
|
|
|
948
952
|
return;
|
|
949
953
|
}
|
|
950
954
|
|
|
951
|
-
// Mark slot as attempted (even before drafting, to avoid re-proposal on deny).
|
|
952
|
-
state.composeSlotsAttempted = [...attempted, slot];
|
|
953
|
-
await writeScoutState(state);
|
|
954
|
-
|
|
955
955
|
// Load persona.
|
|
956
956
|
let persona = "";
|
|
957
957
|
try {
|
|
@@ -1020,6 +1020,14 @@ async function cmdComposePropose(flags) {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
const token = submitRes?.token;
|
|
1022
1022
|
if (!token) fail(`bridge did not return a token: ${JSON.stringify(submitRes)}`);
|
|
1023
|
+
if (slot) {
|
|
1024
|
+
const state = rollScoutDayIfNeeded(await readScoutState());
|
|
1025
|
+
const attempted = Array.isArray(state.composeSlotsAttempted) ? state.composeSlotsAttempted : [];
|
|
1026
|
+
if (!attempted.includes(slot)) {
|
|
1027
|
+
state.composeSlotsAttempted = [...attempted, slot];
|
|
1028
|
+
await writeScoutState(state);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1023
1031
|
|
|
1024
1032
|
console.log(JSON.stringify({ ok: true, token, ttlSec, fireAndForget: true }));
|
|
1025
1033
|
}
|
|
@@ -39,19 +39,88 @@ WINDOW_MS=$((WINDOW_SEC * 1000))
|
|
|
39
39
|
MAX_SKIP=5
|
|
40
40
|
|
|
41
41
|
# ── Detect harness ────────────────────────────────────────────
|
|
42
|
+
resolve_harness_bin() {
|
|
43
|
+
local harness="$1"
|
|
44
|
+
local configured="${2:-}"
|
|
45
|
+
local resolved=""
|
|
46
|
+
|
|
47
|
+
if [ -n "$configured" ]; then
|
|
48
|
+
if [ -x "$configured" ]; then
|
|
49
|
+
printf '%s\n' "$configured"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
resolved=$(command -v "$configured" 2>/dev/null || true)
|
|
53
|
+
if [ -n "$resolved" ] && [ -x "$resolved" ]; then
|
|
54
|
+
printf '%s\n' "$resolved"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
resolved=$(command -v "$harness" 2>/dev/null || true)
|
|
60
|
+
if [ -n "$resolved" ] && [ -x "$resolved" ]; then
|
|
61
|
+
printf '%s\n' "$resolved"
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Desktop app updates can move the bundled Codex binary without rewriting
|
|
66
|
+
# an already-installed LaunchAgent. Resolve the current app bundle at every
|
|
67
|
+
# run so a stale absolute path does not disable scoring and drafting.
|
|
68
|
+
if [ "$harness" = "codex" ]; then
|
|
69
|
+
local candidate=""
|
|
70
|
+
for candidate in \
|
|
71
|
+
"/Applications/ChatGPT.app/Contents/Resources/codex" \
|
|
72
|
+
"/Applications/Codex.app/Contents/Resources/codex" \
|
|
73
|
+
"$HOME/Applications/ChatGPT.app/Contents/Resources/codex" \
|
|
74
|
+
"$HOME/Applications/Codex.app/Contents/Resources/codex"; do
|
|
75
|
+
if [ -x "$candidate" ]; then
|
|
76
|
+
printf '%s\n' "$candidate"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
42
85
|
HARNESS="${SCOUT_HARNESS:-}"
|
|
86
|
+
CONFIGURED_HARNESS_BIN="${SCOUT_HARNESS_BIN:-}"
|
|
43
87
|
HARNESS_BIN=""
|
|
44
|
-
if [ -
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
if [ -n "$HARNESS" ]; then
|
|
89
|
+
HARNESS_BIN=$(resolve_harness_bin "$HARNESS" "$CONFIGURED_HARNESS_BIN" || true)
|
|
90
|
+
else
|
|
91
|
+
HARNESS_BIN=$(resolve_harness_bin "codex" "" || true)
|
|
92
|
+
if [ -n "$HARNESS_BIN" ]; then
|
|
48
93
|
HARNESS="codex"
|
|
94
|
+
else
|
|
95
|
+
HARNESS_BIN=$(resolve_harness_bin "claude" "" || true)
|
|
96
|
+
if [ -n "$HARNESS_BIN" ]; then
|
|
97
|
+
HARNESS="claude"
|
|
98
|
+
fi
|
|
49
99
|
fi
|
|
50
100
|
fi
|
|
51
|
-
if [ -
|
|
52
|
-
|
|
101
|
+
if [ -z "${SCOUT_DRAFT_TIMEOUT:-}" ] && [ "$HARNESS" = "codex" ]; then
|
|
102
|
+
DRAFT_TIMEOUT_SEC=300
|
|
53
103
|
fi
|
|
54
104
|
|
|
105
|
+
run_harness_prompt() {
|
|
106
|
+
local timeout_sec="$1"
|
|
107
|
+
local prompt="$2"
|
|
108
|
+
|
|
109
|
+
if [ "$HARNESS" = "claude" ]; then
|
|
110
|
+
portable_timeout "$timeout_sec" "$HARNESS_BIN" -p "$prompt" --output-format text
|
|
111
|
+
elif [ "$HARNESS" = "codex" ]; then
|
|
112
|
+
printf '%s' "$prompt" | portable_timeout "$timeout_sec" "$HARNESS_BIN" exec \
|
|
113
|
+
-C "$SCRIPT_DIR/.." \
|
|
114
|
+
--sandbox read-only \
|
|
115
|
+
--ephemeral \
|
|
116
|
+
--ignore-user-config \
|
|
117
|
+
--ignore-rules \
|
|
118
|
+
--color never
|
|
119
|
+
else
|
|
120
|
+
return 127
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
55
124
|
# ── Load persona & avoid keywords ─────────────────────────────
|
|
56
125
|
PERSONA=""
|
|
57
126
|
AVOID_PATTERN=""
|
|
@@ -86,6 +155,19 @@ candidate_should_skip() {
|
|
|
86
155
|
printf '%s\n%s' "$1" "$2" | grep -iqE "$AVOID_PATTERN"
|
|
87
156
|
}
|
|
88
157
|
|
|
158
|
+
looks_like_harness_error() {
|
|
159
|
+
printf '%s' "$1" | grep -Eiq 'failed to authenticate|invalid authentication credentials|authentication_error|api error:[[:space:]]*(401|403)|incorrect api key|invalid api key'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
abort_if_harness_error() {
|
|
163
|
+
local stage="$1"
|
|
164
|
+
local text="$2"
|
|
165
|
+
if looks_like_harness_error "$text"; then
|
|
166
|
+
echo "[scout-auto] $stage: harness returned a provider authentication error; not proposing a Moltbook draft"
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
170
|
+
|
|
89
171
|
# Temp file for JSON interchange
|
|
90
172
|
SCOUT_TMP=$(mktemp /tmp/viveworker-scout-XXXXXX.json)
|
|
91
173
|
LOCK_DIR="${TMPDIR:-/tmp}/viveworker-moltbook-scout.lock"
|
|
@@ -160,16 +242,12 @@ INTENT: (1-2 sentences in Japanese: why this reply angle fits the incoming comme
|
|
|
160
242
|
INBOX_PROMPT_EOF
|
|
161
243
|
)"
|
|
162
244
|
|
|
163
|
-
INBOX_DRAFT_TEXT
|
|
164
|
-
if [ "$HARNESS" = "claude" ]; then
|
|
165
|
-
INBOX_DRAFT_TEXT=$(portable_timeout "$DRAFT_TIMEOUT_SEC" "$HARNESS_BIN" -p "$INBOX_PROMPT" --output-format text 2>/dev/null) || true
|
|
166
|
-
elif [ "$HARNESS" = "codex" ]; then
|
|
167
|
-
INBOX_DRAFT_TEXT=$(portable_timeout "$DRAFT_TIMEOUT_SEC" "$HARNESS_BIN" exec "$INBOX_PROMPT" 2>/dev/null) || true
|
|
168
|
-
fi
|
|
245
|
+
INBOX_DRAFT_TEXT=$(run_harness_prompt "$DRAFT_TIMEOUT_SEC" "$INBOX_PROMPT" 2>/dev/null) || true
|
|
169
246
|
|
|
170
247
|
if [ -z "$INBOX_DRAFT_TEXT" ]; then
|
|
171
248
|
echo "[scout-auto] inbox: harness returned empty draft or timed out after ${DRAFT_TIMEOUT_SEC}s"
|
|
172
249
|
else
|
|
250
|
+
abort_if_harness_error "inbox" "$INBOX_DRAFT_TEXT"
|
|
173
251
|
INBOX_INTENT=""
|
|
174
252
|
INBOX_REPLY_BODY=""
|
|
175
253
|
if echo "$INBOX_DRAFT_TEXT" | grep -q "^---$"; then
|
|
@@ -210,16 +288,17 @@ fi
|
|
|
210
288
|
# Max 3 approved posts/day. Each slot proposed at most once.
|
|
211
289
|
# ═══════════════════════════════════════════════════════════════
|
|
212
290
|
COMPOSE_MAX="${COMPOSE_MAX:-3}"
|
|
291
|
+
COMPOSE_ACTIVITY_LIMIT="${SCOUT_COMPOSE_ACTIVITY_LIMIT:-30}"
|
|
213
292
|
COMPOSE_JSON=$("$NODE" "$VIVEWORKER" moltbook compose --max-daily "$COMPOSE_MAX" 2>/dev/null) || true
|
|
214
293
|
COMPOSE_STATUS=$(json_field "$COMPOSE_JSON" "status")
|
|
215
294
|
|
|
216
295
|
if [ "$COMPOSE_STATUS" = "material" ] && [ -n "$HARNESS_BIN" ]; then
|
|
217
296
|
COMPOSE_SLOT=$(json_field "$COMPOSE_JSON" "slot")
|
|
218
297
|
ACTIVITY_COUNT=$(json_field "$COMPOSE_JSON" "activityCount")
|
|
219
|
-
ACTIVITY_SUMMARY=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);const e=o.activitySummary||[];console.log(e.map(x=>'- ['+x.kind+'] '+x.title+(x.summary?' — '+x.summary:'')).join('\n'))}catch{}})")
|
|
220
|
-
RECENT_TITLES=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);
|
|
298
|
+
ACTIVITY_SUMMARY=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);const limit=Number('$COMPOSE_ACTIVITY_LIMIT')||30;const e=(o.activitySummary||[]).slice(0,limit);console.log(e.map(x=>'- ['+x.kind+'] '+x.title+(x.summary?' — '+x.summary:'')).join('\n'))}catch{}})")
|
|
299
|
+
RECENT_TITLES=$(echo "$COMPOSE_JSON" | "$NODE" -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);const titles=(o.recentTitles||[]).map(x=>typeof x==='string'?x:(x&&x.title)||'').filter(Boolean).slice(0,10);console.log(titles.join(', '))}catch{}})")
|
|
221
300
|
|
|
222
|
-
echo "[scout-auto] compose ($COMPOSE_SLOT): ${ACTIVITY_COUNT} activities found"
|
|
301
|
+
echo "[scout-auto] compose ($COMPOSE_SLOT): ${ACTIVITY_COUNT} activities found (using ${COMPOSE_ACTIVITY_LIMIT})"
|
|
223
302
|
|
|
224
303
|
# Slot-specific tone instructions.
|
|
225
304
|
case "$COMPOSE_SLOT" in
|
|
@@ -275,14 +354,10 @@ INTENT: (1-2 sentences in Japanese: why this is worth posting from this persona
|
|
|
275
354
|
COMPOSE_EOF
|
|
276
355
|
)"
|
|
277
356
|
|
|
278
|
-
COMPOSE_DRAFT
|
|
279
|
-
if [ "$HARNESS" = "claude" ]; then
|
|
280
|
-
COMPOSE_DRAFT=$(portable_timeout 60 "$HARNESS_BIN" -p "$COMPOSE_PROMPT" --output-format text 2>/dev/null) || true
|
|
281
|
-
elif [ "$HARNESS" = "codex" ]; then
|
|
282
|
-
COMPOSE_DRAFT=$(portable_timeout 60 "$HARNESS_BIN" exec "$COMPOSE_PROMPT" 2>/dev/null) || true
|
|
283
|
-
fi
|
|
357
|
+
COMPOSE_DRAFT=$(run_harness_prompt "$DRAFT_TIMEOUT_SEC" "$COMPOSE_PROMPT" 2>/dev/null) || true
|
|
284
358
|
|
|
285
359
|
if [ -n "$COMPOSE_DRAFT" ]; then
|
|
360
|
+
abort_if_harness_error "compose ($COMPOSE_SLOT)" "$COMPOSE_DRAFT"
|
|
286
361
|
# Check for NO_MATCH signal (persona doesn't align with today's work).
|
|
287
362
|
if echo "$COMPOSE_DRAFT" | grep -q "NO_MATCH"; then
|
|
288
363
|
echo "[scout-auto] compose ($COMPOSE_SLOT): persona has no match with current activities — skipping"
|
|
@@ -393,17 +468,13 @@ PROMPT_EOF
|
|
|
393
468
|
)"
|
|
394
469
|
|
|
395
470
|
echo "[scout-auto] drafting via $HARNESS for '$BEST_TITLE'"
|
|
396
|
-
DRAFT_TEXT
|
|
397
|
-
if [ "$HARNESS" = "claude" ]; then
|
|
398
|
-
DRAFT_TEXT=$(portable_timeout "$DRAFT_TIMEOUT_SEC" "$HARNESS_BIN" -p "$DRAFT_PROMPT" --output-format text 2>/dev/null) || true
|
|
399
|
-
elif [ "$HARNESS" = "codex" ]; then
|
|
400
|
-
DRAFT_TEXT=$(portable_timeout "$DRAFT_TIMEOUT_SEC" "$HARNESS_BIN" exec "$DRAFT_PROMPT" 2>/dev/null) || true
|
|
401
|
-
fi
|
|
471
|
+
DRAFT_TEXT=$(run_harness_prompt "$DRAFT_TIMEOUT_SEC" "$DRAFT_PROMPT" 2>/dev/null) || true
|
|
402
472
|
|
|
403
473
|
if [ -z "$DRAFT_TEXT" ]; then
|
|
404
474
|
echo "[scout-auto] error: harness returned empty draft or timed out after ${DRAFT_TIMEOUT_SEC}s"
|
|
405
475
|
exit 1
|
|
406
476
|
fi
|
|
477
|
+
abort_if_harness_error "draft" "$DRAFT_TEXT"
|
|
407
478
|
|
|
408
479
|
# Parse INTENT and reply body
|
|
409
480
|
INTENT=""
|
|
@@ -498,11 +569,13 @@ Post body (first 500 chars): ${POST_CONTENT:0:500}
|
|
|
498
569
|
|
|
499
570
|
Reply with ONLY a number from 0 to 100. Nothing else."
|
|
500
571
|
|
|
501
|
-
SCORE_RAW
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
572
|
+
SCORE_RAW=$(run_harness_prompt 30 "$SCORE_PROMPT" 2>/dev/null) || true
|
|
573
|
+
abort_if_harness_error "score" "$SCORE_RAW"
|
|
574
|
+
|
|
575
|
+
if [ -z "$SCORE_RAW" ]; then
|
|
576
|
+
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
577
|
+
echo "[scout-auto] skipping (score failed): $POST_TITLE (by @$POST_AUTHOR) [$SKIP_COUNT/$MAX_SKIP]"
|
|
578
|
+
continue
|
|
506
579
|
fi
|
|
507
580
|
|
|
508
581
|
SCORE=$(echo "$SCORE_RAW" | tr -dc '0-9\n' | head -1)
|
|
@@ -515,11 +588,6 @@ Reply with ONLY a number from 0 to 100. Nothing else."
|
|
|
515
588
|
continue
|
|
516
589
|
fi
|
|
517
590
|
|
|
518
|
-
if [ -z "$SCORE_RAW" ]; then
|
|
519
|
-
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
520
|
-
echo "[scout-auto] skipping (score failed): $POST_TITLE (by @$POST_AUTHOR) [$SKIP_COUNT/$MAX_SKIP]"
|
|
521
|
-
continue
|
|
522
|
-
fi
|
|
523
591
|
else
|
|
524
592
|
SCORE=50
|
|
525
593
|
fi
|