viveworker 0.7.0-beta.0 → 0.7.0-beta.2
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 +38 -0
- package/package.json +2 -1
- package/scripts/moltbook-api.mjs +319 -72
- package/scripts/share-cli.mjs +116 -6
- package/scripts/viveworker-bridge.mjs +2637 -160
- package/scripts/viveworker.mjs +11 -0
- package/viveworker.env.example +4 -0
- package/web/app.css +787 -9
- package/web/app.js +1664 -61
- package/web/hazbase-passkey.js +107 -0
- package/web/i18n.js +254 -0
- package/web/sw.js +33 -15
|
@@ -12,18 +12,35 @@ import process from "node:process";
|
|
|
12
12
|
import { createInterface } from "node:readline";
|
|
13
13
|
import { inspect } from "node:util";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import zlib from "node:zlib";
|
|
15
16
|
import webPush from "web-push";
|
|
17
|
+
import * as hazbaseAuth from "@hazbase/auth";
|
|
16
18
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
17
19
|
import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "./lib/pairing.mjs";
|
|
18
20
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
19
21
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
20
22
|
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, listInboxItems } from "./moltbook-api.mjs";
|
|
23
|
+
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems } from "./moltbook-api.mjs";
|
|
22
24
|
|
|
23
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
26
|
const __dirname = path.dirname(__filename);
|
|
25
27
|
const workspaceRoot = path.resolve(__dirname, "..");
|
|
26
28
|
const webRoot = path.join(workspaceRoot, "web");
|
|
29
|
+
const {
|
|
30
|
+
bootstrapPasskeyAccount,
|
|
31
|
+
completePasskeyAssertion,
|
|
32
|
+
completePasskeyRegistration,
|
|
33
|
+
listSupportedChains,
|
|
34
|
+
requestEmailOtp: requestHazbaseEmailOtp,
|
|
35
|
+
requestPasskeyAssertionChallenge,
|
|
36
|
+
requestPasskeyRegistrationChallenge,
|
|
37
|
+
setApiEndpoint: setHazbaseApiEndpoint,
|
|
38
|
+
setClientKey: setHazbaseClientKey,
|
|
39
|
+
verifyEmailOtp: verifyHazbaseEmailOtp,
|
|
40
|
+
} = hazbaseAuth;
|
|
41
|
+
const listSupportedPayments = typeof hazbaseAuth.listSupportedPayments === "function"
|
|
42
|
+
? hazbaseAuth.listSupportedPayments.bind(hazbaseAuth)
|
|
43
|
+
: async () => ({ networks: [], defaultNetwork: "base-sepolia" });
|
|
27
44
|
const appPackageVersion = readPackageVersion();
|
|
28
45
|
const sessionCookieName = "viveworker_session";
|
|
29
46
|
const deviceCookieName = "viveworker_device";
|
|
@@ -38,6 +55,153 @@ const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
|
38
55
|
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
39
56
|
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
40
57
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
58
|
+
const HAZBASE_METADATA_TIMEOUT_MS = 1500;
|
|
59
|
+
|
|
60
|
+
// Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
|
|
61
|
+
// per tracked repo (`git diff --name-status`, `git status --porcelain`,
|
|
62
|
+
// `git diff`) plus a `git rev-parse` per unique dir — roughly 10+ subprocess
|
|
63
|
+
// spawns for a typical multi-repo state. Both /api/inbox/diff and
|
|
64
|
+
// /api/items/diff_thread/:token call this function; when the user taps a
|
|
65
|
+
// detail right after the inbox finished loading, the computation is
|
|
66
|
+
// duplicated unnecessarily. Short TTL + explicit invalidation when
|
|
67
|
+
// recentCodeEvents mutates keeps the result fresh without the recompute cost.
|
|
68
|
+
// Hoisted here so code-event migration at startup can call
|
|
69
|
+
// invalidateDiffThreadGroupsCache() before any of the use sites below
|
|
70
|
+
// execute, avoiding a temporal-dead-zone ReferenceError on `let`.
|
|
71
|
+
const DIFF_THREAD_GROUPS_CACHE_TTL_MS = 3000;
|
|
72
|
+
let diffThreadGroupsCache = null;
|
|
73
|
+
let diffThreadGroupsPending = null;
|
|
74
|
+
|
|
75
|
+
function invalidateDiffThreadGroupsCache() {
|
|
76
|
+
diffThreadGroupsCache = null;
|
|
77
|
+
// Don't touch diffThreadGroupsPending — an in-flight build reflects the
|
|
78
|
+
// state at the time it started, and callers racing the rebuild would
|
|
79
|
+
// otherwise pile up their own subprocess spawns.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getDiffThreadGroupsCached(runtime, state, config) {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (
|
|
85
|
+
diffThreadGroupsCache &&
|
|
86
|
+
now - diffThreadGroupsCache.computedAtMs < DIFF_THREAD_GROUPS_CACHE_TTL_MS
|
|
87
|
+
) {
|
|
88
|
+
return diffThreadGroupsCache.groups;
|
|
89
|
+
}
|
|
90
|
+
if (diffThreadGroupsPending) {
|
|
91
|
+
return diffThreadGroupsPending;
|
|
92
|
+
}
|
|
93
|
+
diffThreadGroupsPending = (async () => {
|
|
94
|
+
try {
|
|
95
|
+
const groups = await buildDiffThreadGroups(runtime, state, config);
|
|
96
|
+
diffThreadGroupsCache = { groups, computedAtMs: Date.now() };
|
|
97
|
+
return groups;
|
|
98
|
+
} finally {
|
|
99
|
+
diffThreadGroupsPending = null;
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
return diffThreadGroupsPending;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Coalesced state persistence. The full state JSON is ~8 MB on disk (dominated
|
|
106
|
+
// by `recentCodeEvents` and `recentTimelineEntries` caches); JSON.stringify
|
|
107
|
+
// blocks the event loop ~70 ms and fs.writeFile takes another ~80 ms. Before
|
|
108
|
+
// this helper, every tiny mutation (push subscribe/unsubscribe/test, locale
|
|
109
|
+
// change, claude-away toggle, A2A toggles, …) awaited a ~150 ms
|
|
110
|
+
// serialize+write before the HTTP response fired — and the event-loop block
|
|
111
|
+
// delayed every concurrent request on the same server too. That's what made
|
|
112
|
+
// the settings/notification buttons feel sluggish: the click → visible
|
|
113
|
+
// response round-trip paid the cost of re-serializing 5 MB of code-event
|
|
114
|
+
// history every time.
|
|
115
|
+
//
|
|
116
|
+
// `scheduleSaveState` queues a write within SAVE_STATE_DEBOUNCE_MS and returns
|
|
117
|
+
// synchronously, so HTTP handlers can respond immediately after mutating
|
|
118
|
+
// in-memory state. Multiple mutations within the window coalesce into one
|
|
119
|
+
// write. `flushPendingStateWrite` drains on graceful shutdown.
|
|
120
|
+
//
|
|
121
|
+
// Durability trade-off: at most ~SAVE_STATE_MAX_DELAY_MS of in-memory
|
|
122
|
+
// mutations can be lost on a hard crash. Callers that require stronger
|
|
123
|
+
// guarantees (pairing, revocation, the periodic scan loop) still `await
|
|
124
|
+
// saveState(...)` directly — only the hot settings/notification paths opt in
|
|
125
|
+
// to the coalescer.
|
|
126
|
+
const SAVE_STATE_DEBOUNCE_MS = 500;
|
|
127
|
+
const SAVE_STATE_MAX_DELAY_MS = 1500;
|
|
128
|
+
let saveStateTimer = null;
|
|
129
|
+
let saveStateFirstScheduleAtMs = 0;
|
|
130
|
+
let saveStateInFlight = null;
|
|
131
|
+
let saveStateDirty = false;
|
|
132
|
+
|
|
133
|
+
function scheduleSaveState(config, state) {
|
|
134
|
+
if (!config?.stateFile) return;
|
|
135
|
+
if (saveStateInFlight) {
|
|
136
|
+
// An in-flight write already reflects the current (or earlier) state.
|
|
137
|
+
// Mark dirty so the .finally handler starts another write when it
|
|
138
|
+
// finishes — overlapping writes would just stomp on each other.
|
|
139
|
+
saveStateDirty = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
if (!saveStateFirstScheduleAtMs) {
|
|
144
|
+
saveStateFirstScheduleAtMs = now;
|
|
145
|
+
}
|
|
146
|
+
if (saveStateTimer) {
|
|
147
|
+
clearTimeout(saveStateTimer);
|
|
148
|
+
}
|
|
149
|
+
const elapsed = now - saveStateFirstScheduleAtMs;
|
|
150
|
+
const remaining = SAVE_STATE_MAX_DELAY_MS - elapsed;
|
|
151
|
+
const delay = Math.max(0, Math.min(SAVE_STATE_DEBOUNCE_MS, remaining));
|
|
152
|
+
saveStateTimer = setTimeout(() => {
|
|
153
|
+
saveStateTimer = null;
|
|
154
|
+
saveStateFirstScheduleAtMs = 0;
|
|
155
|
+
startSaveStateFlush(config, state);
|
|
156
|
+
}, delay);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function startSaveStateFlush(config, state) {
|
|
160
|
+
saveStateDirty = false;
|
|
161
|
+
const promise = (async () => {
|
|
162
|
+
try {
|
|
163
|
+
await saveState(config.stateFile, state);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`[save-state-error] ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
saveStateInFlight = promise;
|
|
169
|
+
promise.finally(() => {
|
|
170
|
+
if (saveStateInFlight === promise) {
|
|
171
|
+
saveStateInFlight = null;
|
|
172
|
+
}
|
|
173
|
+
if (saveStateDirty) {
|
|
174
|
+
// A mutation arrived mid-flight. Start the follow-up write
|
|
175
|
+
// synchronously — we've already held the original debounce window,
|
|
176
|
+
// so tacking on another 500 ms would defeat coalescing. Going
|
|
177
|
+
// through scheduleSaveState() instead would also introduce a timer
|
|
178
|
+
// that flushPendingStateWrite() would have to know about.
|
|
179
|
+
saveStateDirty = false;
|
|
180
|
+
startSaveStateFlush(config, state);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function flushPendingStateWrite(config, state) {
|
|
186
|
+
// Promote any pending debounce timer to an immediate flush.
|
|
187
|
+
if (saveStateTimer) {
|
|
188
|
+
clearTimeout(saveStateTimer);
|
|
189
|
+
saveStateTimer = null;
|
|
190
|
+
saveStateFirstScheduleAtMs = 0;
|
|
191
|
+
startSaveStateFlush(config, state);
|
|
192
|
+
}
|
|
193
|
+
// Drain the in-flight write plus any cascades triggered by saveStateDirty.
|
|
194
|
+
// Bounded so a pathological re-entrancy bug can't hang shutdown.
|
|
195
|
+
for (let iter = 0; iter < 4 && (saveStateInFlight || saveStateDirty); iter += 1) {
|
|
196
|
+
if (saveStateInFlight) {
|
|
197
|
+
await saveStateInFlight.catch(() => {});
|
|
198
|
+
}
|
|
199
|
+
if (saveStateDirty && !saveStateInFlight) {
|
|
200
|
+
saveStateDirty = false;
|
|
201
|
+
startSaveStateFlush(config, state);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
41
205
|
|
|
42
206
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
43
207
|
const envFile = resolveEnvFile(cli.envFile);
|
|
@@ -47,6 +211,7 @@ await maybeRotateStartupPairingEnv(envFile);
|
|
|
47
211
|
|
|
48
212
|
const config = buildConfig(cli);
|
|
49
213
|
validateConfig(config);
|
|
214
|
+
configureHazbaseClient(config);
|
|
50
215
|
|
|
51
216
|
const runtime = {
|
|
52
217
|
fileStates: new Map(),
|
|
@@ -165,6 +330,7 @@ const backfilledAmbientSuggestionsStateChanged = backfillAmbientSuggestionsState
|
|
|
165
330
|
const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
|
|
166
331
|
const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
|
|
167
332
|
const backfilledMoltbookInboxChanged = await backfillMoltbookInboxHistory({ config, runtime, state });
|
|
333
|
+
const recoveredMissingProviderStateChanged = await recoverMissingProviderStateFromBackup({ config, runtime, state });
|
|
168
334
|
runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
|
|
169
335
|
runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
|
|
170
336
|
|
|
@@ -263,8 +429,23 @@ function clearDeviceLocaleOverride(state, deviceId) {
|
|
|
263
429
|
return true;
|
|
264
430
|
}
|
|
265
431
|
|
|
432
|
+
function autoPilotWriteLaneState(state) {
|
|
433
|
+
return {
|
|
434
|
+
content: state?.autoPilotWriteLaneContent === true,
|
|
435
|
+
uiTests: state?.autoPilotWriteLaneUiTests === true,
|
|
436
|
+
source: state?.autoPilotWriteLaneSource === true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function hasAnyAutoPilotWriteLaneEnabled(state) {
|
|
441
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
442
|
+
return lanes.content || lanes.uiTests || lanes.source;
|
|
443
|
+
}
|
|
444
|
+
|
|
266
445
|
function buildSessionLocalePayload(config, state, deviceId) {
|
|
267
446
|
const resolved = resolveDeviceLocaleInfo(config, state, deviceId);
|
|
447
|
+
const writeLanes = autoPilotWriteLaneState(state);
|
|
448
|
+
const trustedWritesEnabled = hasAnyAutoPilotWriteLaneEnabled(state);
|
|
268
449
|
return {
|
|
269
450
|
locale: resolved.locale,
|
|
270
451
|
localeSource: resolved.source,
|
|
@@ -273,6 +454,12 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
273
454
|
deviceDetectedLocale: resolved.detectedLocale || null,
|
|
274
455
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
275
456
|
claudeAwayMode: state?.claudeAwayMode === true,
|
|
457
|
+
autoPilotTrustedReads: state?.autoPilotTrustedReads === true,
|
|
458
|
+
autoPilotTrustedWrites: trustedWritesEnabled,
|
|
459
|
+
autoPilotTrustedWritesShadow: false,
|
|
460
|
+
autoPilotWriteLaneContent: writeLanes.content,
|
|
461
|
+
autoPilotWriteLaneUiTests: writeLanes.uiTests,
|
|
462
|
+
autoPilotWriteLaneSource: writeLanes.source,
|
|
276
463
|
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
277
464
|
a2aEnabled: Boolean(config.a2aApiKey),
|
|
278
465
|
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
@@ -770,6 +957,425 @@ function tokenizeShellWords(commandText) {
|
|
|
770
957
|
return tokens;
|
|
771
958
|
}
|
|
772
959
|
|
|
960
|
+
function isPathWithinRoot(rootPath, candidatePath) {
|
|
961
|
+
const normalizedRoot = cleanText(rootPath || "");
|
|
962
|
+
const normalizedCandidate = cleanText(candidatePath || "");
|
|
963
|
+
if (!normalizedRoot || !normalizedCandidate) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const relativePath = path.relative(normalizedRoot, normalizedCandidate);
|
|
967
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function isSensitiveCommandPath(candidatePath) {
|
|
971
|
+
const normalized = cleanText(candidatePath || "");
|
|
972
|
+
if (!normalized) {
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
const lower = normalized.toLowerCase();
|
|
976
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
977
|
+
const basename = segments[segments.length - 1] || "";
|
|
978
|
+
if (segments.some((segment) => [".ssh", ".aws", ".gnupg", ".azure", ".kube"].includes(segment))) {
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
if (basename === ".npmrc" || basename === ".netrc" || basename === ".env" || basename.startsWith(".env.")) {
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
if (basename.endsWith(".pem") || basename.endsWith(".key") || basename.endsWith(".p12") || basename.endsWith(".pfx")) {
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
if (/^id_[a-z0-9._-]+$/iu.test(basename) || basename.includes("secret") || basename.includes("credential")) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }) {
|
|
994
|
+
const rawToken = cleanText(token || "");
|
|
995
|
+
const normalizedCwd = cleanText(cwd || "");
|
|
996
|
+
const normalizedWorkspaceRoot = cleanText(workspaceRoot || normalizedCwd);
|
|
997
|
+
if (!rawToken || !normalizedCwd || !normalizedWorkspaceRoot) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
if (rawToken === "-" || rawToken.startsWith("~") || rawToken.startsWith("http://") || rawToken.startsWith("https://")) {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
const resolved = path.resolve(normalizedCwd, rawToken);
|
|
1004
|
+
if (!isPathWithinRoot(normalizedWorkspaceRoot, resolved)) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
if (isSensitiveCommandPath(resolved)) {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
return resolved;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function isGitObjectPathToken(token) {
|
|
1014
|
+
const normalized = cleanText(token || "");
|
|
1015
|
+
if (!normalized || normalized.startsWith("-")) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
return /^[^:\s][^:\s]*:.+/u.test(normalized);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function isLiteralGitPathspecToken(token) {
|
|
1022
|
+
const normalized = cleanText(token || "");
|
|
1023
|
+
if (!normalized || normalized === "--" || normalized.startsWith(":(") || normalized.startsWith(":/")) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
if (/[*?[\]{}]/u.test(normalized)) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
return !isGitObjectPathToken(normalized);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function isAllowedGitMetadataOnlyFlag(token) {
|
|
1033
|
+
const normalized = cleanText(token || "");
|
|
1034
|
+
if (!normalized) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
return [
|
|
1038
|
+
"--stat",
|
|
1039
|
+
"--shortstat",
|
|
1040
|
+
"--numstat",
|
|
1041
|
+
"--name-only",
|
|
1042
|
+
"--name-status",
|
|
1043
|
+
"--summary",
|
|
1044
|
+
"--compact-summary",
|
|
1045
|
+
"--oneline",
|
|
1046
|
+
"--no-patch",
|
|
1047
|
+
"-s",
|
|
1048
|
+
].includes(normalized);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function hasDisallowedGitPatchOutput(tokens) {
|
|
1052
|
+
return tokens.some((token) => {
|
|
1053
|
+
const normalized = cleanText(token || "");
|
|
1054
|
+
if (!normalized) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
return (
|
|
1058
|
+
normalized === "-p" ||
|
|
1059
|
+
normalized === "--patch" ||
|
|
1060
|
+
normalized === "--patch-with-stat" ||
|
|
1061
|
+
normalized === "--raw" ||
|
|
1062
|
+
normalized === "--cc" ||
|
|
1063
|
+
normalized === "--combined-all-paths" ||
|
|
1064
|
+
normalized === "--binary" ||
|
|
1065
|
+
normalized === "--word-diff" ||
|
|
1066
|
+
normalized.startsWith("--word-diff=") ||
|
|
1067
|
+
normalized === "-U" ||
|
|
1068
|
+
normalized.startsWith("-U") ||
|
|
1069
|
+
normalized === "--unified" ||
|
|
1070
|
+
normalized.startsWith("--unified=")
|
|
1071
|
+
);
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function extractGitPathspecTokens(tokens) {
|
|
1076
|
+
const separatorIndex = tokens.indexOf("--");
|
|
1077
|
+
if (separatorIndex < 0) {
|
|
1078
|
+
return [];
|
|
1079
|
+
}
|
|
1080
|
+
return tokens.slice(separatorIndex + 1).filter(Boolean);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function resolveTrustedDiffSectionPath({ token, cwd, workspaceRoot }) {
|
|
1084
|
+
const normalizedToken = cleanText(token || "");
|
|
1085
|
+
if (!normalizedToken) {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const preferredWorkspace = resolveTrustedReadTargetPath({
|
|
1089
|
+
token: normalizedToken,
|
|
1090
|
+
cwd: workspaceRoot,
|
|
1091
|
+
workspaceRoot,
|
|
1092
|
+
});
|
|
1093
|
+
if (preferredWorkspace) {
|
|
1094
|
+
return preferredWorkspace;
|
|
1095
|
+
}
|
|
1096
|
+
return resolveTrustedReadTargetPath({
|
|
1097
|
+
token: normalizedToken,
|
|
1098
|
+
cwd,
|
|
1099
|
+
workspaceRoot,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function extractTrustedReadFileRefs(command, tokens, cwd, workspaceRoot) {
|
|
1104
|
+
const normalizedCommand = cleanText(command || "");
|
|
1105
|
+
if (!normalizedCommand) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (normalizedCommand === "pwd" || (normalizedCommand === "git" && cleanText(tokens[1] || "") === "status")) {
|
|
1110
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1111
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (normalizedCommand === "git") {
|
|
1115
|
+
const gitSubcommand = cleanText(tokens[1] || "");
|
|
1116
|
+
if (!["diff", "show"].includes(gitSubcommand)) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
if (tokens.some((token) => token === "-C" || token.startsWith("-C") || token === "--output" || token.startsWith("--output="))) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const commandArgs = tokens.slice(2);
|
|
1123
|
+
if (commandArgs.some((token) => isGitObjectPathToken(token))) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
const explicitPathspecs = extractGitPathspecTokens(tokens);
|
|
1127
|
+
if (explicitPathspecs.length > 0) {
|
|
1128
|
+
if (explicitPathspecs.some((token) => !isLiteralGitPathspecToken(token))) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
const resolvedPaths = explicitPathspecs
|
|
1132
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1133
|
+
.filter(Boolean);
|
|
1134
|
+
return resolvedPaths.length === explicitPathspecs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1135
|
+
}
|
|
1136
|
+
if (hasDisallowedGitPatchOutput(commandArgs)) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
const metadataOnly = commandArgs.some((token) => isAllowedGitMetadataOnlyFlag(token));
|
|
1140
|
+
if (!metadataOnly) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1144
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (normalizedCommand === "ls") {
|
|
1148
|
+
const pathArgs = tokens.slice(1).filter((token) => !String(token || "").startsWith("-"));
|
|
1149
|
+
if (pathArgs.length === 0) {
|
|
1150
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1151
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1152
|
+
}
|
|
1153
|
+
const resolvedPaths = pathArgs
|
|
1154
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1155
|
+
.filter(Boolean);
|
|
1156
|
+
return resolvedPaths.length === pathArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (normalizedCommand === "find") {
|
|
1160
|
+
if (tokens.some((token) => ["-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprintf", "-fls"].includes(token))) {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
const pathArgs = [];
|
|
1164
|
+
for (const token of tokens.slice(1)) {
|
|
1165
|
+
if (!token || token === "--") continue;
|
|
1166
|
+
if (token === "!" || token === "(" || token === ")") break;
|
|
1167
|
+
if (token.startsWith("-")) break;
|
|
1168
|
+
pathArgs.push(token);
|
|
1169
|
+
}
|
|
1170
|
+
const effectivePaths = pathArgs.length ? pathArgs : ["."];
|
|
1171
|
+
const resolvedPaths = effectivePaths
|
|
1172
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1173
|
+
.filter(Boolean);
|
|
1174
|
+
return resolvedPaths.length === effectivePaths.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (normalizedCommand === "sed") {
|
|
1178
|
+
if (!tokens.includes("-n") || tokens.includes("-i") || tokens.includes("--in-place")) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
let script = "";
|
|
1182
|
+
const fileArgs = [];
|
|
1183
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1184
|
+
const token = tokens[index];
|
|
1185
|
+
if (!token) continue;
|
|
1186
|
+
if (!script) {
|
|
1187
|
+
if (token === "-e") {
|
|
1188
|
+
script = tokens[index + 1] || "";
|
|
1189
|
+
index += 1;
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (token.startsWith("-")) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
script = token;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (!token.startsWith("-")) {
|
|
1199
|
+
fileArgs.push(token);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (!script || !/^[0-9,$; p-]+$/u.test(script) || fileArgs.length === 0) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
const resolvedPaths = fileArgs
|
|
1206
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1207
|
+
.filter(Boolean);
|
|
1208
|
+
return resolvedPaths.length === fileArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (normalizedCommand === "rg") {
|
|
1212
|
+
let seenPattern = false;
|
|
1213
|
+
const fileArgs = [];
|
|
1214
|
+
for (const token of tokens.slice(1)) {
|
|
1215
|
+
if (!seenPattern) {
|
|
1216
|
+
if (token === "--") {
|
|
1217
|
+
seenPattern = true;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (String(token || "").startsWith("-")) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
seenPattern = true;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (!String(token || "").startsWith("-")) {
|
|
1227
|
+
fileArgs.push(token);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const effectivePaths = fileArgs.length ? fileArgs : ["."];
|
|
1231
|
+
const resolvedPaths = effectivePaths
|
|
1232
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1233
|
+
.filter(Boolean);
|
|
1234
|
+
return resolvedPaths.length === effectivePaths.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (["cat", "nl", "head", "tail", "wc"].includes(normalizedCommand)) {
|
|
1238
|
+
const fileArgs = tokens.slice(1).filter((token) => !String(token || "").startsWith("-"));
|
|
1239
|
+
if (fileArgs.length === 0) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
const resolvedPaths = fileArgs
|
|
1243
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1244
|
+
.filter(Boolean);
|
|
1245
|
+
return resolvedPaths.length === fileArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function stripAllowedTrustedReadRedirections(tokens) {
|
|
1252
|
+
const filtered = [];
|
|
1253
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1254
|
+
const token = String(tokens[index] || "");
|
|
1255
|
+
if (!token) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (token === "2>" || token === "2>>") {
|
|
1259
|
+
const target = String(tokens[index + 1] || "");
|
|
1260
|
+
if (target !== "/dev/null") {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
index += 1;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (/^2>>?\/dev\/null$/u.test(token)) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (token.includes(">") || token.includes("<")) {
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
filtered.push(token);
|
|
1273
|
+
}
|
|
1274
|
+
return filtered;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function isAllowedTrustedReadPostProcessStage(stageTokens) {
|
|
1278
|
+
if (!Array.isArray(stageTokens) || stageTokens.length === 0) {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
const [command, ...args] = stageTokens.map((token) => cleanText(token || ""));
|
|
1282
|
+
if (!command) {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
if (command === "head" || command === "tail") {
|
|
1286
|
+
if (args.length === 0) {
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
if (args.length === 1 && /^-\d+$/u.test(args[0])) {
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
if (args.length === 2 && args[0] === "-n" && /^\d+$/u.test(args[1])) {
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
if (command === "wc") {
|
|
1298
|
+
return args.length === 0 || (args.length === 1 && args[0] === "-l");
|
|
1299
|
+
}
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function normalizeTrustedReadTokens(tokens) {
|
|
1304
|
+
const strippedTokens = stripAllowedTrustedReadRedirections(tokens);
|
|
1305
|
+
if (!strippedTokens || strippedTokens.length === 0) {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
if (strippedTokens.some((token) => {
|
|
1309
|
+
const value = String(token || "");
|
|
1310
|
+
return (
|
|
1311
|
+
["||", "&&", ";", "&"].includes(value) ||
|
|
1312
|
+
value.includes("`") ||
|
|
1313
|
+
value.includes("$(")
|
|
1314
|
+
);
|
|
1315
|
+
})) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
const stages = [];
|
|
1319
|
+
let currentStage = [];
|
|
1320
|
+
for (const token of strippedTokens) {
|
|
1321
|
+
if (token === "|") {
|
|
1322
|
+
if (currentStage.length === 0) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
stages.push(currentStage);
|
|
1326
|
+
currentStage = [];
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
currentStage.push(token);
|
|
1330
|
+
}
|
|
1331
|
+
if (currentStage.length === 0) {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
stages.push(currentStage);
|
|
1335
|
+
if (stages.length === 0) {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
if (stages.slice(1).some((stageTokens) => !isAllowedTrustedReadPostProcessStage(stageTokens))) {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
return stages[0];
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
|
|
1345
|
+
const normalizedCwd = cleanText(cwd || "");
|
|
1346
|
+
const normalizedWorkspaceRoot = cleanText(workspaceRoot || normalizedCwd);
|
|
1347
|
+
const normalizedCommandText = unwrapShellCommand(commandText);
|
|
1348
|
+
const rawTokens = tokenizeShellWords(normalizedCommandText);
|
|
1349
|
+
if (!normalizedCommandText || !normalizedCwd || !normalizedWorkspaceRoot || rawTokens.length === 0) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (!isPathWithinRoot(normalizedWorkspaceRoot, path.resolve(normalizedCwd))) {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const tokens = normalizeTrustedReadTokens(rawTokens);
|
|
1358
|
+
if (!tokens || tokens.length === 0) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const command = cleanText(tokens[0]);
|
|
1363
|
+
if (!command || /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(command)) {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const fileRefs = extractTrustedReadFileRefs(command, tokens, normalizedCwd, normalizedWorkspaceRoot);
|
|
1368
|
+
if (!fileRefs) {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
command,
|
|
1374
|
+
commandText: normalizedCommandText,
|
|
1375
|
+
fileRefs,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
773
1379
|
function unwrapShellCommand(commandText) {
|
|
774
1380
|
const normalized = String(commandText || "").trim();
|
|
775
1381
|
if (!normalized) {
|
|
@@ -848,6 +1454,14 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
848
1454
|
return [];
|
|
849
1455
|
}
|
|
850
1456
|
|
|
1457
|
+
function autoPilotApprovalMessage(locale, commandText) {
|
|
1458
|
+
const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
|
|
1459
|
+
const commandBlock = cleanText(commandText || "")
|
|
1460
|
+
? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${cleanText(commandText || "")}\n\`\`\``
|
|
1461
|
+
: "";
|
|
1462
|
+
return [prefix, commandBlock].filter(Boolean).join("\n\n");
|
|
1463
|
+
}
|
|
1464
|
+
|
|
851
1465
|
function extractUpdatedFileRefsByType(outputText, patchText = "") {
|
|
852
1466
|
const parsedSections = parseApplyPatchSections(patchText);
|
|
853
1467
|
if (parsedSections.length > 0) {
|
|
@@ -2209,15 +2823,23 @@ function normalizeHistoryItems(rawItems, maxItems) {
|
|
|
2209
2823
|
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
|
|
2210
2824
|
const deduped = [];
|
|
2211
2825
|
const seen = new Set();
|
|
2826
|
+
const perProviderCount = {};
|
|
2827
|
+
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a", "viveworker"]);
|
|
2212
2828
|
for (const item of normalized) {
|
|
2213
2829
|
if (seen.has(item.stableId)) {
|
|
2214
2830
|
continue;
|
|
2215
2831
|
}
|
|
2832
|
+
const provider = normalizeProvider(item.provider);
|
|
2833
|
+
if (!knownProviders.has(provider)) {
|
|
2834
|
+
knownProviders.add(provider);
|
|
2835
|
+
}
|
|
2836
|
+
const providerCount = perProviderCount[provider] ?? 0;
|
|
2837
|
+
if (providerCount >= maxItems) {
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2216
2840
|
seen.add(item.stableId);
|
|
2217
2841
|
deduped.push(item);
|
|
2218
|
-
|
|
2219
|
-
break;
|
|
2220
|
-
}
|
|
2842
|
+
perProviderCount[provider] = providerCount + 1;
|
|
2221
2843
|
}
|
|
2222
2844
|
return deduped;
|
|
2223
2845
|
}
|
|
@@ -2226,6 +2848,9 @@ function normalizeHistoryItem(raw) {
|
|
|
2226
2848
|
if (!isPlainObject(raw)) {
|
|
2227
2849
|
return null;
|
|
2228
2850
|
}
|
|
2851
|
+
if (shouldHideClaudeInternalItem(raw)) {
|
|
2852
|
+
return null;
|
|
2853
|
+
}
|
|
2229
2854
|
|
|
2230
2855
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
2231
2856
|
const kind = cleanText(raw.kind ?? "");
|
|
@@ -2237,8 +2862,10 @@ function normalizeHistoryItem(raw) {
|
|
|
2237
2862
|
const threadLabel = skipHistoryThreadLabelRewrite
|
|
2238
2863
|
? rawThreadLabel
|
|
2239
2864
|
: preferTitleOnlyJsonThreadLabel(rawThreadLabel, threadId, raw.messageText, raw.summary, raw.detailText, raw.message);
|
|
2865
|
+
const rawTitle = cleanText(raw.title ?? "");
|
|
2240
2866
|
const title =
|
|
2241
|
-
|
|
2867
|
+
(!isFallbackTimelineTitle(rawTitle, kind, threadId) ? rawTitle : "") ||
|
|
2868
|
+
(threadLabel ? formatTitle(kindTitle(DEFAULT_LOCALE, kind), threadLabel) : kindTitle(DEFAULT_LOCALE, kind));
|
|
2242
2869
|
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
2243
2870
|
const summary = normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "";
|
|
2244
2871
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
@@ -2248,7 +2875,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2248
2875
|
|
|
2249
2876
|
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
2250
2877
|
|
|
2251
|
-
|
|
2878
|
+
const normalized = {
|
|
2252
2879
|
stableId,
|
|
2253
2880
|
token: cleanText(raw.token ?? "") || historyToken(stableId),
|
|
2254
2881
|
kind,
|
|
@@ -2276,6 +2903,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2276
2903
|
...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
|
|
2277
2904
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2278
2905
|
};
|
|
2906
|
+
return shouldHideClaudeInternalItem(normalized) ? null : normalized;
|
|
2279
2907
|
}
|
|
2280
2908
|
|
|
2281
2909
|
function historyToken(stableId) {
|
|
@@ -2305,10 +2933,41 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2305
2933
|
const perProviderCount = {};
|
|
2306
2934
|
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a", "viveworker"]);
|
|
2307
2935
|
const saturatedProviders = new Set();
|
|
2936
|
+
// Content-based dedup for user_message entries: the same logical user turn
|
|
2937
|
+
// can surface through two independent readers (rollout session files and
|
|
2938
|
+
// ~/.codex/history.jsonl) that compute different stableIds
|
|
2939
|
+
// (`user_message:<threadId>:<turn_id|ISO-ts>` vs
|
|
2940
|
+
// `user_message:<threadId>:<integer-id|unix-seconds>`), so the stableId-only
|
|
2941
|
+
// filter above doesn't catch them. Collapse entries with the same
|
|
2942
|
+
// (threadId, messageText) pair when their createdAtMs are within 60 s —
|
|
2943
|
+
// wide enough to absorb rollout-vs-history-file flush skew (we've seen
|
|
2944
|
+
// several seconds in the wild) but narrow enough not to collapse a genuine
|
|
2945
|
+
// repeat (e.g. user retries the same "run tests" command minutes apart).
|
|
2946
|
+
// Literal here rather than a named const because this function is first
|
|
2947
|
+
// called at top-level module init (line ~302) which runs before any const
|
|
2948
|
+
// declared below would leave the TDZ.
|
|
2949
|
+
const userMessageDedupWindowMs = 60_000;
|
|
2950
|
+
const userMessageIndex = new Map();
|
|
2308
2951
|
for (const item of normalized) {
|
|
2309
2952
|
if (seen.has(item.stableId)) {
|
|
2310
2953
|
continue;
|
|
2311
2954
|
}
|
|
2955
|
+
if (item.kind === "user_message") {
|
|
2956
|
+
const threadKey = cleanText(item.threadId || "");
|
|
2957
|
+
const textKey = cleanText(item.messageText || "");
|
|
2958
|
+
if (threadKey && textKey) {
|
|
2959
|
+
const key = `${threadKey}\u0001${textKey}`;
|
|
2960
|
+
const existingMs = userMessageIndex.get(key);
|
|
2961
|
+
const itemMs = Number(item.createdAtMs || 0);
|
|
2962
|
+
if (
|
|
2963
|
+
existingMs !== undefined &&
|
|
2964
|
+
Math.abs(existingMs - itemMs) <= userMessageDedupWindowMs
|
|
2965
|
+
) {
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
userMessageIndex.set(key, itemMs);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2312
2971
|
const prov = item.provider || "codex";
|
|
2313
2972
|
if (saturatedProviders.has(prov)) {
|
|
2314
2973
|
continue;
|
|
@@ -2352,6 +3011,7 @@ function migrateRecentCodeEventsState({ config, runtime, state }) {
|
|
|
2352
3011
|
const previousItems = Array.isArray(state.recentCodeEvents) ? normalizeCodeEvents(state.recentCodeEvents, config.maxCodeEvents) : [];
|
|
2353
3012
|
runtime.recentCodeEvents = nextItems;
|
|
2354
3013
|
state.recentCodeEvents = nextItems;
|
|
3014
|
+
invalidateDiffThreadGroupsCache();
|
|
2355
3015
|
return JSON.stringify(nextItems) !== JSON.stringify(previousItems);
|
|
2356
3016
|
}
|
|
2357
3017
|
|
|
@@ -2383,6 +3043,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
2383
3043
|
if (!isPlainObject(raw)) {
|
|
2384
3044
|
return null;
|
|
2385
3045
|
}
|
|
3046
|
+
if (shouldHideClaudeInternalItem(raw)) {
|
|
3047
|
+
return null;
|
|
3048
|
+
}
|
|
2386
3049
|
|
|
2387
3050
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
2388
3051
|
const kind = cleanText(raw.kind ?? "");
|
|
@@ -2420,14 +3083,15 @@ function normalizeTimelineEntry(raw) {
|
|
|
2420
3083
|
raw.detailText,
|
|
2421
3084
|
raw.message
|
|
2422
3085
|
);
|
|
3086
|
+
const rawTitle = cleanText(raw.title ?? "");
|
|
2423
3087
|
const title =
|
|
2424
|
-
|
|
3088
|
+
(!isFallbackTimelineTitle(rawTitle, kind, threadId) ? rawTitle : "") ||
|
|
2425
3089
|
(kind === "file_event" ? fileEventTitle(DEFAULT_LOCALE, fileEventType) : "") ||
|
|
2426
3090
|
threadLabel ||
|
|
2427
3091
|
kindTitle(DEFAULT_LOCALE, kind);
|
|
2428
3092
|
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
2429
3093
|
|
|
2430
|
-
|
|
3094
|
+
const normalized = {
|
|
2431
3095
|
stableId,
|
|
2432
3096
|
token: cleanText(raw.token ?? "") || historyToken(stableId),
|
|
2433
3097
|
kind,
|
|
@@ -2473,6 +3137,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
2473
3137
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2474
3138
|
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
2475
3139
|
};
|
|
3140
|
+
return shouldHideClaudeInternalItem(normalized) ? null : normalized;
|
|
2476
3141
|
}
|
|
2477
3142
|
|
|
2478
3143
|
function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
@@ -2563,6 +3228,9 @@ function recordCodeEvent({ config, runtime, state, entry }) {
|
|
|
2563
3228
|
);
|
|
2564
3229
|
runtime.recentCodeEvents = nextItems;
|
|
2565
3230
|
state.recentCodeEvents = nextItems;
|
|
3231
|
+
if (changed) {
|
|
3232
|
+
invalidateDiffThreadGroupsCache();
|
|
3233
|
+
}
|
|
2566
3234
|
return changed;
|
|
2567
3235
|
}
|
|
2568
3236
|
|
|
@@ -2614,6 +3282,9 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
|
|
|
2614
3282
|
);
|
|
2615
3283
|
runtime.recentCodeEvents = nextItems;
|
|
2616
3284
|
state.recentCodeEvents = nextItems;
|
|
3285
|
+
if (changed) {
|
|
3286
|
+
invalidateDiffThreadGroupsCache();
|
|
3287
|
+
}
|
|
2617
3288
|
return changed;
|
|
2618
3289
|
}
|
|
2619
3290
|
|
|
@@ -2840,13 +3511,21 @@ function pendingChoiceStableId(userInputRequest) {
|
|
|
2840
3511
|
return `choice:${userInputRequest.requestKey}`;
|
|
2841
3512
|
}
|
|
2842
3513
|
|
|
2843
|
-
function buildAppItemUrl(config, kind, token) {
|
|
3514
|
+
function buildAppItemUrl(config, kind, token, options = {}) {
|
|
2844
3515
|
const url = new URL(`${config.nativeApprovalPublicBaseUrl}/app`);
|
|
2845
3516
|
url.searchParams.set("item", `${kind}:${token}`);
|
|
3517
|
+
const tab = cleanText(options.tab || "");
|
|
3518
|
+
const subtab = cleanText(options.subtab || "");
|
|
3519
|
+
if (tab) {
|
|
3520
|
+
url.searchParams.set("tab", tab);
|
|
3521
|
+
}
|
|
3522
|
+
if (subtab) {
|
|
3523
|
+
url.searchParams.set("subtab", subtab);
|
|
3524
|
+
}
|
|
2846
3525
|
return url.toString();
|
|
2847
3526
|
}
|
|
2848
3527
|
|
|
2849
|
-
function buildPushPayload({ config, kind, token, stableId, title, body }) {
|
|
3528
|
+
function buildPushPayload({ config, kind, token, stableId, title, body, tab = "", subtab = "" }) {
|
|
2850
3529
|
return {
|
|
2851
3530
|
title: withNotificationIcon(kind, title),
|
|
2852
3531
|
body: formatNotificationBody(body, config.completionDetailThresholdChars) || body || title,
|
|
@@ -2855,7 +3534,7 @@ function buildPushPayload({ config, kind, token, stableId, title, body }) {
|
|
|
2855
3534
|
kind,
|
|
2856
3535
|
token,
|
|
2857
3536
|
stableId,
|
|
2858
|
-
url: buildAppItemUrl(config, kind, token),
|
|
3537
|
+
url: buildAppItemUrl(config, kind, token, { tab, subtab }),
|
|
2859
3538
|
},
|
|
2860
3539
|
};
|
|
2861
3540
|
}
|
|
@@ -2975,7 +3654,7 @@ function pushDeliveryKey(deviceId, stableId) {
|
|
|
2975
3654
|
return `${cleanText(deviceId || "")}:${cleanText(stableId || "")}`;
|
|
2976
3655
|
}
|
|
2977
3656
|
|
|
2978
|
-
async function deliverWebPushItem({ config, state, kind, token, stableId, title, body, buildLocalizedContent = null }) {
|
|
3657
|
+
async function deliverWebPushItem({ config, state, kind, token, stableId, title, body, tab = "", subtab = "", buildLocalizedContent = null }) {
|
|
2979
3658
|
if (!config.webPushEnabled || config.dryRun) {
|
|
2980
3659
|
return false;
|
|
2981
3660
|
}
|
|
@@ -3010,6 +3689,8 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3010
3689
|
stableId,
|
|
3011
3690
|
title: localizedContent?.title || title,
|
|
3012
3691
|
body: localizedContent?.body || body,
|
|
3692
|
+
tab,
|
|
3693
|
+
subtab,
|
|
3013
3694
|
})
|
|
3014
3695
|
);
|
|
3015
3696
|
await webPush.sendNotification(
|
|
@@ -3093,18 +3774,24 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3093
3774
|
}
|
|
3094
3775
|
|
|
3095
3776
|
if (config.webUiEnabled) {
|
|
3777
|
+
let claudeTranscriptChanged = false;
|
|
3096
3778
|
if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
|
|
3097
3779
|
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
|
|
3098
3780
|
runtime.lastClaudeScanAt = now;
|
|
3099
3781
|
}
|
|
3782
|
+
let claudeSessionTitlesChanged = false;
|
|
3100
3783
|
if (now - runtime.lastClaudeSessionTitleScanAt >= config.directoryScanIntervalMs) {
|
|
3101
|
-
await refreshClaudeSessionTitles(runtime);
|
|
3784
|
+
claudeSessionTitlesChanged = await refreshClaudeSessionTitles(runtime);
|
|
3102
3785
|
runtime.lastClaudeSessionTitleScanAt = now;
|
|
3103
3786
|
}
|
|
3104
3787
|
for (const filePath of runtime.claudeKnownFiles) {
|
|
3105
3788
|
const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
|
|
3789
|
+
claudeTranscriptChanged = claudeTranscriptChanged || changed;
|
|
3106
3790
|
dirty = dirty || changed;
|
|
3107
3791
|
}
|
|
3792
|
+
if (claudeTranscriptChanged || claudeSessionTitlesChanged) {
|
|
3793
|
+
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
3794
|
+
}
|
|
3108
3795
|
}
|
|
3109
3796
|
|
|
3110
3797
|
if (config.webUiEnabled) {
|
|
@@ -3330,6 +4017,7 @@ function fileEventCallIdFromStableId(stableId) {
|
|
|
3330
4017
|
}
|
|
3331
4018
|
|
|
3332
4019
|
async function refreshClaudeSessionTitles(runtime) {
|
|
4020
|
+
let changed = false;
|
|
3333
4021
|
// Read ~/Library/Application Support/Claude/claude-code-sessions/*/*/local_*.json
|
|
3334
4022
|
// Each file maps cliSessionId → title (auto-generated by Claude Desktop).
|
|
3335
4023
|
const baseDir = path.join(
|
|
@@ -3367,7 +4055,10 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
3367
4055
|
const cliSessionId = cleanText(data?.cliSessionId || "");
|
|
3368
4056
|
const title = cleanText(data?.title || "");
|
|
3369
4057
|
if (cliSessionId && title) {
|
|
3370
|
-
runtime.claudeSessionTitles.
|
|
4058
|
+
if (runtime.claudeSessionTitles.get(cliSessionId) !== title) {
|
|
4059
|
+
runtime.claudeSessionTitles.set(cliSessionId, title);
|
|
4060
|
+
changed = true;
|
|
4061
|
+
}
|
|
3371
4062
|
}
|
|
3372
4063
|
} catch {
|
|
3373
4064
|
// skip unreadable/invalid
|
|
@@ -3378,6 +4069,49 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
3378
4069
|
} catch {
|
|
3379
4070
|
// base dir missing — Claude Desktop not installed
|
|
3380
4071
|
}
|
|
4072
|
+
|
|
4073
|
+
for (const filePath of runtime.claudeKnownFiles ?? []) {
|
|
4074
|
+
let lines;
|
|
4075
|
+
try {
|
|
4076
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
4077
|
+
lines = raw.split("\n");
|
|
4078
|
+
} catch {
|
|
4079
|
+
continue;
|
|
4080
|
+
}
|
|
4081
|
+
for (const rawLine of lines) {
|
|
4082
|
+
const trimmed = rawLine.trim();
|
|
4083
|
+
if (!trimmed) continue;
|
|
4084
|
+
let record;
|
|
4085
|
+
try {
|
|
4086
|
+
record = JSON.parse(trimmed);
|
|
4087
|
+
} catch {
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
if (record?.entrypoint !== "sdk-cli" || record?.type !== "user") {
|
|
4091
|
+
continue;
|
|
4092
|
+
}
|
|
4093
|
+
const threadId = cleanText(record?.sessionId || "");
|
|
4094
|
+
const message = record?.message || {};
|
|
4095
|
+
let text = "";
|
|
4096
|
+
if (typeof message.content === "string") {
|
|
4097
|
+
text = message.content;
|
|
4098
|
+
} else if (Array.isArray(message.content)) {
|
|
4099
|
+
for (const block of message.content) {
|
|
4100
|
+
if (block?.type === "text" && block.text) {
|
|
4101
|
+
text += block.text;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
const derivedTitle = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
4106
|
+
if (threadId && derivedTitle && runtime.claudeSessionTitles.get(threadId) !== derivedTitle) {
|
|
4107
|
+
runtime.claudeSessionTitles.set(threadId, derivedTitle);
|
|
4108
|
+
changed = true;
|
|
4109
|
+
}
|
|
4110
|
+
break;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
return changed;
|
|
3381
4115
|
}
|
|
3382
4116
|
|
|
3383
4117
|
async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
@@ -3497,8 +4231,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3497
4231
|
fileState.threadLabel = path.basename(record.cwd);
|
|
3498
4232
|
}
|
|
3499
4233
|
|
|
3500
|
-
//
|
|
3501
|
-
|
|
4234
|
+
// Accept both Claude Desktop sessions and the current sdk-cli sessions
|
|
4235
|
+
// that Claude emits for agent-driven work. The latter regressed the
|
|
4236
|
+
// timeline/completed views because they were silently skipped here.
|
|
4237
|
+
if (record.entrypoint !== "claude-desktop" && record.entrypoint !== "sdk-cli") continue;
|
|
3502
4238
|
|
|
3503
4239
|
const type = record.type;
|
|
3504
4240
|
if (type !== "user" && type !== "assistant") continue;
|
|
@@ -3526,6 +4262,15 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3526
4262
|
}
|
|
3527
4263
|
text = cleanText(text);
|
|
3528
4264
|
if (!text) continue;
|
|
4265
|
+
if (type === "user" && record.entrypoint === "sdk-cli") {
|
|
4266
|
+
const derivedThreadLabel = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
4267
|
+
if (derivedThreadLabel) {
|
|
4268
|
+
fileState.threadLabel = derivedThreadLabel;
|
|
4269
|
+
if (threadId) {
|
|
4270
|
+
runtime.claudeSessionTitles.set(threadId, derivedThreadLabel);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
3529
4274
|
// Skip Claude Code internal system messages (task notifications, system
|
|
3530
4275
|
// reminders, etc.) — these are XML-like tags injected into the
|
|
3531
4276
|
// conversation that should not appear on the user-facing timeline.
|
|
@@ -3587,6 +4332,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3587
4332
|
config,
|
|
3588
4333
|
state,
|
|
3589
4334
|
kind: "assistant_final",
|
|
4335
|
+
tab: "timeline",
|
|
3590
4336
|
token: entry.token,
|
|
3591
4337
|
stableId: entry.stableId,
|
|
3592
4338
|
title: threadLabel || "Claude",
|
|
@@ -3608,6 +4354,129 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3608
4354
|
return dirty;
|
|
3609
4355
|
}
|
|
3610
4356
|
|
|
4357
|
+
async function loadRecoveryBackupState(stateFile) {
|
|
4358
|
+
const dir = path.dirname(stateFile);
|
|
4359
|
+
const base = path.basename(stateFile);
|
|
4360
|
+
let entries;
|
|
4361
|
+
try {
|
|
4362
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4363
|
+
} catch {
|
|
4364
|
+
return null;
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
const candidates = [];
|
|
4368
|
+
for (const entry of entries) {
|
|
4369
|
+
if (!entry.isFile()) continue;
|
|
4370
|
+
if (!entry.name.startsWith(`${base}.bak-`)) continue;
|
|
4371
|
+
const filePath = path.join(dir, entry.name);
|
|
4372
|
+
try {
|
|
4373
|
+
const stat = await fs.stat(filePath);
|
|
4374
|
+
candidates.push({ filePath, mtimeMs: Number(stat.mtimeMs) || 0 });
|
|
4375
|
+
} catch {
|
|
4376
|
+
// ignore unreadable backup
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
4381
|
+
for (const candidate of candidates) {
|
|
4382
|
+
try {
|
|
4383
|
+
const raw = await fs.readFile(candidate.filePath, "utf8");
|
|
4384
|
+
return JSON.parse(raw);
|
|
4385
|
+
} catch {
|
|
4386
|
+
// try next backup
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return null;
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4392
|
+
function providerSetForItems(items) {
|
|
4393
|
+
return new Set(
|
|
4394
|
+
(Array.isArray(items) ? items : [])
|
|
4395
|
+
.map((item) => normalizeProvider(item?.provider))
|
|
4396
|
+
.filter(Boolean)
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
function missingRecoveryProviders(items) {
|
|
4401
|
+
const present = providerSetForItems(items);
|
|
4402
|
+
return ["claude", "a2a"].filter((provider) => !present.has(provider));
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
function selectRecoveryItems(items, missingProviders) {
|
|
4406
|
+
const wanted = new Set(missingProviders);
|
|
4407
|
+
return (Array.isArray(items) ? items : []).filter((item) => wanted.has(normalizeProvider(item?.provider)));
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
async function recoverMissingProviderStateFromBackup({ config, runtime, state }) {
|
|
4411
|
+
const missingHistoryProviders = missingRecoveryProviders(state.recentHistoryItems ?? runtime.recentHistoryItems);
|
|
4412
|
+
const missingTimelineProviders = missingRecoveryProviders(state.recentTimelineEntries ?? runtime.recentTimelineEntries);
|
|
4413
|
+
const shouldRecoverCodeEvents = !Array.isArray(state.recentCodeEvents) || state.recentCodeEvents.length === 0;
|
|
4414
|
+
|
|
4415
|
+
if (missingHistoryProviders.length === 0 && missingTimelineProviders.length === 0 && !shouldRecoverCodeEvents) {
|
|
4416
|
+
return false;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
const backup = await loadRecoveryBackupState(config.stateFile);
|
|
4420
|
+
if (!backup) {
|
|
4421
|
+
return false;
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
let changed = false;
|
|
4425
|
+
|
|
4426
|
+
if (missingHistoryProviders.length > 0) {
|
|
4427
|
+
const mergedHistory = normalizeHistoryItems(
|
|
4428
|
+
[
|
|
4429
|
+
...(state.recentHistoryItems ?? runtime.recentHistoryItems ?? []),
|
|
4430
|
+
...selectRecoveryItems(backup.recentHistoryItems, missingHistoryProviders),
|
|
4431
|
+
],
|
|
4432
|
+
config.maxHistoryItems
|
|
4433
|
+
);
|
|
4434
|
+
if (JSON.stringify(mergedHistory) !== JSON.stringify(state.recentHistoryItems ?? runtime.recentHistoryItems ?? [])) {
|
|
4435
|
+
state.recentHistoryItems = mergedHistory;
|
|
4436
|
+
runtime.recentHistoryItems = mergedHistory;
|
|
4437
|
+
changed = true;
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
if (missingTimelineProviders.length > 0) {
|
|
4442
|
+
const mergedTimeline = normalizeTimelineEntries(
|
|
4443
|
+
[
|
|
4444
|
+
...(state.recentTimelineEntries ?? runtime.recentTimelineEntries ?? []),
|
|
4445
|
+
...selectRecoveryItems(backup.recentTimelineEntries, missingTimelineProviders),
|
|
4446
|
+
],
|
|
4447
|
+
config.maxTimelineEntries
|
|
4448
|
+
);
|
|
4449
|
+
if (JSON.stringify(mergedTimeline) !== JSON.stringify(state.recentTimelineEntries ?? runtime.recentTimelineEntries ?? [])) {
|
|
4450
|
+
state.recentTimelineEntries = mergedTimeline;
|
|
4451
|
+
runtime.recentTimelineEntries = mergedTimeline;
|
|
4452
|
+
changed = true;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
if (shouldRecoverCodeEvents) {
|
|
4457
|
+
const mergedCodeEvents = normalizeCodeEvents(
|
|
4458
|
+
[
|
|
4459
|
+
...(state.recentCodeEvents ?? runtime.recentCodeEvents ?? []),
|
|
4460
|
+
...(Array.isArray(backup.recentCodeEvents) ? backup.recentCodeEvents : []),
|
|
4461
|
+
],
|
|
4462
|
+
config.maxCodeEvents
|
|
4463
|
+
);
|
|
4464
|
+
if (JSON.stringify(mergedCodeEvents) !== JSON.stringify(state.recentCodeEvents ?? runtime.recentCodeEvents ?? [])) {
|
|
4465
|
+
state.recentCodeEvents = mergedCodeEvents;
|
|
4466
|
+
runtime.recentCodeEvents = mergedCodeEvents;
|
|
4467
|
+
invalidateDiffThreadGroupsCache();
|
|
4468
|
+
changed = true;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
if (changed) {
|
|
4473
|
+
console.log(
|
|
4474
|
+
`[state-recovery] recovered history=${missingHistoryProviders.join(",") || "none"} timeline=${missingTimelineProviders.join(",") || "none"} code=${shouldRecoverCodeEvents ? "yes" : "no"}`
|
|
4475
|
+
);
|
|
4476
|
+
}
|
|
4477
|
+
return changed;
|
|
4478
|
+
}
|
|
4479
|
+
|
|
3611
4480
|
async function processSqliteCompletionLog({ config, runtime, state, now }) {
|
|
3612
4481
|
const logsDbFile = cleanText(runtime.logsDbFile || config.codexLogsDbFile || "");
|
|
3613
4482
|
if (!logsDbFile) {
|
|
@@ -4826,6 +5695,8 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
4826
5695
|
config,
|
|
4827
5696
|
state,
|
|
4828
5697
|
kind: "completion",
|
|
5698
|
+
tab: "inbox",
|
|
5699
|
+
subtab: "completed",
|
|
4829
5700
|
token: historyToken(event.id),
|
|
4830
5701
|
stableId: event.id,
|
|
4831
5702
|
title: event.title,
|
|
@@ -5134,6 +6005,28 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5134
6005
|
request,
|
|
5135
6006
|
approval: existing,
|
|
5136
6007
|
});
|
|
6008
|
+
const existingAutoPilotWriteCandidate =
|
|
6009
|
+
existing.kind === "file"
|
|
6010
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval: existing })
|
|
6011
|
+
: null;
|
|
6012
|
+
if (existingAutoPilotWriteCandidate) {
|
|
6013
|
+
try {
|
|
6014
|
+
await autoApproveTrustedWrite({
|
|
6015
|
+
config,
|
|
6016
|
+
runtime,
|
|
6017
|
+
state,
|
|
6018
|
+
approval: existing,
|
|
6019
|
+
candidate: existingAutoPilotWriteCandidate,
|
|
6020
|
+
});
|
|
6021
|
+
continue;
|
|
6022
|
+
} catch (error) {
|
|
6023
|
+
existing.resolved = false;
|
|
6024
|
+
existing.resolving = false;
|
|
6025
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, existing);
|
|
6026
|
+
runtime.nativeApprovalsByToken.set(existing.token, existing);
|
|
6027
|
+
console.error(`[auto-pilot-write-error] ${requestKey} | ${error.message}`);
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
5137
6030
|
if (changed) {
|
|
5138
6031
|
const fileKeys = isPlainObject(existing.rawParams) ? Object.keys(existing.rawParams).join(",") : "";
|
|
5139
6032
|
console.log(
|
|
@@ -5156,6 +6049,57 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5156
6049
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
5157
6050
|
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
5158
6051
|
|
|
6052
|
+
const nativeAutoPilotMatch =
|
|
6053
|
+
approval.kind === "command"
|
|
6054
|
+
? maybeAutoApproveTrustedRead({
|
|
6055
|
+
state,
|
|
6056
|
+
commandText: approval.rawParams?.command ?? approval.rawParams?.cmd ?? "",
|
|
6057
|
+
cwd: approval.rawParams?.cwd ?? approval.rawParams?.grantRoot ?? "",
|
|
6058
|
+
workspaceRoot: approval.rawParams?.grantRoot ?? approval.rawParams?.cwd ?? "",
|
|
6059
|
+
})
|
|
6060
|
+
: null;
|
|
6061
|
+
if (nativeAutoPilotMatch) {
|
|
6062
|
+
try {
|
|
6063
|
+
await autoApproveTrustedRead({
|
|
6064
|
+
config,
|
|
6065
|
+
runtime,
|
|
6066
|
+
state,
|
|
6067
|
+
approval,
|
|
6068
|
+
policyMatch: nativeAutoPilotMatch,
|
|
6069
|
+
});
|
|
6070
|
+
continue;
|
|
6071
|
+
} catch (error) {
|
|
6072
|
+
approval.resolved = false;
|
|
6073
|
+
approval.resolving = false;
|
|
6074
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
6075
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
6076
|
+
console.error(`[auto-pilot-error] ${requestKey} | ${error.message}`);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
const nativeAutoPilotWriteCandidate =
|
|
6081
|
+
approval.kind === "file"
|
|
6082
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval })
|
|
6083
|
+
: null;
|
|
6084
|
+
if (nativeAutoPilotWriteCandidate) {
|
|
6085
|
+
try {
|
|
6086
|
+
await autoApproveTrustedWrite({
|
|
6087
|
+
config,
|
|
6088
|
+
runtime,
|
|
6089
|
+
state,
|
|
6090
|
+
approval,
|
|
6091
|
+
candidate: nativeAutoPilotWriteCandidate,
|
|
6092
|
+
});
|
|
6093
|
+
continue;
|
|
6094
|
+
} catch (error) {
|
|
6095
|
+
approval.resolved = false;
|
|
6096
|
+
approval.resolving = false;
|
|
6097
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
6098
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
6099
|
+
console.error(`[auto-pilot-write-error] ${requestKey} | ${error.message}`);
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
|
|
5159
6103
|
if (previousKeys.has(requestKey)) {
|
|
5160
6104
|
const fileKeys = isPlainObject(approval.rawParams) ? Object.keys(approval.rawParams).join(",") : "";
|
|
5161
6105
|
console.log(
|
|
@@ -5187,6 +6131,8 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5187
6131
|
config,
|
|
5188
6132
|
state,
|
|
5189
6133
|
kind: "approval",
|
|
6134
|
+
tab: "inbox",
|
|
6135
|
+
subtab: "pending",
|
|
5190
6136
|
token: approval.token,
|
|
5191
6137
|
stableId: pendingApprovalStableId(approval),
|
|
5192
6138
|
title: approval.title,
|
|
@@ -5310,6 +6256,8 @@ async function syncPlanImplementationRequests({
|
|
|
5310
6256
|
config,
|
|
5311
6257
|
state,
|
|
5312
6258
|
kind: "plan",
|
|
6259
|
+
tab: "inbox",
|
|
6260
|
+
subtab: "pending",
|
|
5313
6261
|
token: planRequest.token,
|
|
5314
6262
|
stableId: pendingPlanStableId(planRequest),
|
|
5315
6263
|
title: planRequest.title,
|
|
@@ -5515,6 +6463,8 @@ async function syncGenericUserInputRequests({
|
|
|
5515
6463
|
config,
|
|
5516
6464
|
state,
|
|
5517
6465
|
kind: "choice",
|
|
6466
|
+
tab: "inbox",
|
|
6467
|
+
subtab: "pending",
|
|
5518
6468
|
token: userInputRequest.token,
|
|
5519
6469
|
stableId: pendingChoiceStableId(userInputRequest),
|
|
5520
6470
|
title: userInputRequest.title,
|
|
@@ -5633,6 +6583,8 @@ async function buildNativeApprovalPayload({ config, runtime, conversationId, req
|
|
|
5633
6583
|
ownerClientId: runtime.threadOwnerClientIds.get(conversationId) ?? null,
|
|
5634
6584
|
approvalIds,
|
|
5635
6585
|
rawParams,
|
|
6586
|
+
cwd: cleanText(rawParams?.cwd ?? rawParams?.grantRoot ?? ""),
|
|
6587
|
+
workspaceRoot: cleanText(rawParams?.grantRoot ?? rawParams?.cwd ?? ""),
|
|
5636
6588
|
fileRefs: normalizeTimelineFileRefs(mergedDelta?.fileRefs ?? []),
|
|
5637
6589
|
diffText: normalizeTimelineDiffText(mergedDelta?.diffText ?? ""),
|
|
5638
6590
|
diffAvailable: Boolean(mergedDelta?.diffAvailable),
|
|
@@ -5669,6 +6621,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5669
6621
|
ownerClientId: approval.ownerClientId,
|
|
5670
6622
|
approvalIds: approval.approvalIds,
|
|
5671
6623
|
rawParams: approval.rawParams,
|
|
6624
|
+
cwd: approval.cwd,
|
|
6625
|
+
workspaceRoot: approval.workspaceRoot,
|
|
5672
6626
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
5673
6627
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
5674
6628
|
diffAvailable: Boolean(approval.diffAvailable),
|
|
@@ -5688,6 +6642,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5688
6642
|
approval.ownerClientId = payload.ownerClientId;
|
|
5689
6643
|
approval.approvalIds = payload.approvalIds;
|
|
5690
6644
|
approval.rawParams = payload.rawParams;
|
|
6645
|
+
approval.cwd = payload.cwd;
|
|
6646
|
+
approval.workspaceRoot = payload.workspaceRoot;
|
|
5691
6647
|
approval.fileRefs = payload.fileRefs;
|
|
5692
6648
|
approval.diffText = payload.diffText;
|
|
5693
6649
|
approval.diffAvailable = payload.diffAvailable;
|
|
@@ -5707,6 +6663,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5707
6663
|
ownerClientId: approval.ownerClientId,
|
|
5708
6664
|
approvalIds: approval.approvalIds,
|
|
5709
6665
|
rawParams: approval.rawParams,
|
|
6666
|
+
cwd: approval.cwd,
|
|
6667
|
+
workspaceRoot: approval.workspaceRoot,
|
|
5710
6668
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
5711
6669
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
5712
6670
|
diffAvailable: Boolean(approval.diffAvailable),
|
|
@@ -7767,6 +8725,67 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
|
|
|
7767
8725
|
return shortId(normalizedConversationId) || t(DEFAULT_LOCALE, "server.fallback.codexTask", { provider: providerDisplayName(DEFAULT_LOCALE, "codex") });
|
|
7768
8726
|
}
|
|
7769
8727
|
|
|
8728
|
+
function isFallbackConversationLabel(label, conversationId) {
|
|
8729
|
+
const normalizedLabel = cleanText(label || "");
|
|
8730
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
8731
|
+
if (!normalizedLabel) {
|
|
8732
|
+
return true;
|
|
8733
|
+
}
|
|
8734
|
+
if (normalizedConversationId && normalizedLabel === shortId(normalizedConversationId)) {
|
|
8735
|
+
return true;
|
|
8736
|
+
}
|
|
8737
|
+
return false;
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
function deriveClaudeSdkCliThreadLabel(messageText, conversationId = "") {
|
|
8741
|
+
const cleaned = stripNotificationMarkup(stripEnvironmentContextBlocks(messageText || ""));
|
|
8742
|
+
const single = cleanText(cleaned);
|
|
8743
|
+
if (!single || /^\d+$/u.test(single)) {
|
|
8744
|
+
return "";
|
|
8745
|
+
}
|
|
8746
|
+
|
|
8747
|
+
if (/^You are scoring Moltbook posts? for an AI agent\b/iu.test(single)) {
|
|
8748
|
+
return "Moltbook scoring";
|
|
8749
|
+
}
|
|
8750
|
+
if (/^Codex from another agent:/iu.test(single)) {
|
|
8751
|
+
return truncate(cleanText(single.replace(/^Codex from another agent:\s*/iu, "")), 90) || "Cross-agent task";
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
const firstSentence = cleanText(single.split(/(?<=[.!?。!?])\s+/u)[0] || "");
|
|
8755
|
+
const candidate = firstSentence || single;
|
|
8756
|
+
if (!candidate || candidate === shortId(conversationId)) {
|
|
8757
|
+
return "";
|
|
8758
|
+
}
|
|
8759
|
+
return truncate(candidate, 90);
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
function isHiddenClaudeInternalScoringText(text) {
|
|
8763
|
+
const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
|
|
8764
|
+
if (!single) {
|
|
8765
|
+
return false;
|
|
8766
|
+
}
|
|
8767
|
+
return /^You are scoring Moltbook posts? for an AI agent\b/iu.test(single);
|
|
8768
|
+
}
|
|
8769
|
+
|
|
8770
|
+
function shouldHideClaudeInternalItem(item) {
|
|
8771
|
+
if (!isPlainObject(item)) {
|
|
8772
|
+
return false;
|
|
8773
|
+
}
|
|
8774
|
+
if (normalizeProvider(item.provider) !== "claude") {
|
|
8775
|
+
return false;
|
|
8776
|
+
}
|
|
8777
|
+
const threadLabel = cleanText(item.threadLabel ?? "");
|
|
8778
|
+
if (threadLabel === "Moltbook scoring") {
|
|
8779
|
+
return true;
|
|
8780
|
+
}
|
|
8781
|
+
return (
|
|
8782
|
+
isHiddenClaudeInternalScoringText(item.messageText) ||
|
|
8783
|
+
isHiddenClaudeInternalScoringText(item.summary) ||
|
|
8784
|
+
isHiddenClaudeInternalScoringText(item.detailText) ||
|
|
8785
|
+
isHiddenClaudeInternalScoringText(item.message)
|
|
8786
|
+
);
|
|
8787
|
+
}
|
|
8788
|
+
|
|
7770
8789
|
function threadStateArchiveStatus(threadState) {
|
|
7771
8790
|
if (!isPlainObject(threadState)) {
|
|
7772
8791
|
return "";
|
|
@@ -8724,6 +9743,8 @@ function markDevicePaired(state, config, deviceId, metadata = {}, now = Date.now
|
|
|
8724
9743
|
return JSON.stringify(previous ?? null) !== JSON.stringify(next);
|
|
8725
9744
|
}
|
|
8726
9745
|
|
|
9746
|
+
const TOUCH_DEVICE_TRUST_DEBOUNCE_MS = 60_000;
|
|
9747
|
+
|
|
8727
9748
|
function touchDeviceTrust(state, config, deviceId, now = Date.now()) {
|
|
8728
9749
|
const normalizedDeviceId = cleanText(deviceId || "");
|
|
8729
9750
|
const current = getActiveDeviceTrustRecord(state, config, normalizedDeviceId, now);
|
|
@@ -8731,6 +9752,20 @@ function touchDeviceTrust(state, config, deviceId, now = Date.now()) {
|
|
|
8731
9752
|
return false;
|
|
8732
9753
|
}
|
|
8733
9754
|
|
|
9755
|
+
// Debounce: `lastAuthenticatedAtMs` is bumped to `now` on every
|
|
9756
|
+
// authenticated request. Without this guard, every `/api/session`
|
|
9757
|
+
// (and `/api/bootstrap`) call flags the record as "changed" and the
|
|
9758
|
+
// caller runs a full `saveState`, which synchronously stringifies an
|
|
9759
|
+
// 8+ MB state.json on the main thread and starves the event loop —
|
|
9760
|
+
// driving TLS handshake latency for any in-flight connection into the
|
|
9761
|
+
// seconds range. Skip the write when we've touched it recently; the
|
|
9762
|
+
// next real change (pairing, revocation, locale update, etc.) still
|
|
9763
|
+
// goes through unchanged.
|
|
9764
|
+
const previousTouch = Number(current.lastAuthenticatedAtMs) || 0;
|
|
9765
|
+
if (previousTouch > 0 && now - previousTouch < TOUCH_DEVICE_TRUST_DEBOUNCE_MS) {
|
|
9766
|
+
return false;
|
|
9767
|
+
}
|
|
9768
|
+
|
|
8734
9769
|
const next = {
|
|
8735
9770
|
...current,
|
|
8736
9771
|
lastAuthenticatedAtMs: now,
|
|
@@ -8853,6 +9888,22 @@ function buildDeviceSummary({ config, state, deviceId, record, currentDeviceId,
|
|
|
8853
9888
|
};
|
|
8854
9889
|
}
|
|
8855
9890
|
|
|
9891
|
+
// Shared between `/api/session` and `/api/bootstrap` so both return an
|
|
9892
|
+
// identical session shape.
|
|
9893
|
+
function buildSessionPayload({ config, state, session }) {
|
|
9894
|
+
return {
|
|
9895
|
+
authenticated: Boolean(session.authenticated),
|
|
9896
|
+
expiresAtMs: Number(session.expiresAtMs) || 0,
|
|
9897
|
+
pairingAvailable: isPairingAvailableForState(config, state),
|
|
9898
|
+
webPushEnabled: config.webPushEnabled,
|
|
9899
|
+
httpsEnabled: config.nativeApprovalPublicBaseUrl.startsWith("https://"),
|
|
9900
|
+
appVersion: appPackageVersion,
|
|
9901
|
+
deviceId: session.deviceId || null,
|
|
9902
|
+
temporaryPairing: session.temporaryPairing === true,
|
|
9903
|
+
...buildSessionLocalePayload(config, state, session.deviceId),
|
|
9904
|
+
};
|
|
9905
|
+
}
|
|
9906
|
+
|
|
8856
9907
|
function buildDevicesResponse({ config, state, session, locale }) {
|
|
8857
9908
|
return {
|
|
8858
9909
|
devices: activeTrustedDevices(state, config).map(({ deviceId, record }) =>
|
|
@@ -9608,7 +10659,7 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
9608
10659
|
}
|
|
9609
10660
|
|
|
9610
10661
|
async function buildDiffInboxItems(runtime, state, config, locale) {
|
|
9611
|
-
return (await
|
|
10662
|
+
return (await getDiffThreadGroupsCached(runtime, state, config)).map((group) => ({
|
|
9612
10663
|
kind: "diff_thread",
|
|
9613
10664
|
token: group.token,
|
|
9614
10665
|
threadId: group.threadId,
|
|
@@ -9767,14 +10818,24 @@ async function buildDiffThreadGroups(runtime, state, config) {
|
|
|
9767
10818
|
.sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
|
|
9768
10819
|
}
|
|
9769
10820
|
|
|
9770
|
-
|
|
10821
|
+
// Split into two halves so `/api/inbox` (pending + completed) returns
|
|
10822
|
+
// without waiting on the diff build, which spawns `git` subprocesses per
|
|
10823
|
+
// tracked repo inside `buildCurrentUnstagedChangesForRepo`. The diff half
|
|
10824
|
+
// is served from `/api/inbox/diff` and fetched separately by the PWA so
|
|
10825
|
+
// it doesn't block first paint of the inbox/completed lists.
|
|
10826
|
+
function buildInboxFastResponse(runtime, state, config, locale) {
|
|
9771
10827
|
return {
|
|
9772
10828
|
pending: buildPendingInboxItems(runtime, state, config, locale),
|
|
9773
|
-
diff: await buildDiffInboxItems(runtime, state, config, locale),
|
|
9774
10829
|
completed: buildCompletedInboxItems(runtime, state, config, locale),
|
|
9775
10830
|
};
|
|
9776
10831
|
}
|
|
9777
10832
|
|
|
10833
|
+
async function buildInboxDiffResponse(runtime, state, config, locale) {
|
|
10834
|
+
return {
|
|
10835
|
+
diff: await buildDiffInboxItems(runtime, state, config, locale),
|
|
10836
|
+
};
|
|
10837
|
+
}
|
|
10838
|
+
|
|
9778
10839
|
function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
9779
10840
|
const now = Date.now();
|
|
9780
10841
|
const items = [];
|
|
@@ -9988,9 +11049,10 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
9988
11049
|
};
|
|
9989
11050
|
}
|
|
9990
11051
|
|
|
9991
|
-
function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
11052
|
+
function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
9992
11053
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
9993
11054
|
const approvalKind = cleanText(approval.kind || "");
|
|
11055
|
+
const autoPilotReview = buildAutoPilotManualReview(runtime, state, approval, locale);
|
|
9994
11056
|
const detail = {
|
|
9995
11057
|
kind: "approval",
|
|
9996
11058
|
approvalKind,
|
|
@@ -10007,6 +11069,7 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
|
10007
11069
|
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
10008
11070
|
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
10009
11071
|
previousContext,
|
|
11072
|
+
autoPilotReview,
|
|
10010
11073
|
readOnly: approval.readOnly === true,
|
|
10011
11074
|
actions: approval.readOnly === true
|
|
10012
11075
|
? []
|
|
@@ -10031,78 +11094,265 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
|
10031
11094
|
return detail;
|
|
10032
11095
|
}
|
|
10033
11096
|
|
|
10034
|
-
function
|
|
10035
|
-
|
|
10036
|
-
const approvalCreatedAtMs = Number(approval?.createdAtMs) || 0;
|
|
10037
|
-
if (!threadId || !approvalCreatedAtMs) {
|
|
10038
|
-
return null;
|
|
10039
|
-
}
|
|
10040
|
-
|
|
10041
|
-
const previousEntry = runtime.recentTimelineEntries
|
|
10042
|
-
.filter((entry) => {
|
|
10043
|
-
if (!timelineMessageKinds.has(entry.kind)) {
|
|
10044
|
-
return false;
|
|
10045
|
-
}
|
|
10046
|
-
if (cleanText(entry.threadId || "") !== threadId) {
|
|
10047
|
-
return false;
|
|
10048
|
-
}
|
|
10049
|
-
return Number(entry.createdAtMs) > 0 && Number(entry.createdAtMs) < approvalCreatedAtMs;
|
|
10050
|
-
})
|
|
10051
|
-
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0))[0];
|
|
10052
|
-
|
|
10053
|
-
if (!previousEntry) {
|
|
11097
|
+
function buildAutoPilotManualReview(runtime, state, approval, locale) {
|
|
11098
|
+
if (!isPlainObject(approval) || approval.readOnly === true) {
|
|
10054
11099
|
return null;
|
|
10055
11100
|
}
|
|
10056
|
-
|
|
10057
|
-
const sourceText = normalizeLongText(previousEntry.messageText || previousEntry.summary || "");
|
|
10058
|
-
if (!sourceText) {
|
|
11101
|
+
if (cleanText(approval.kind || "") !== "file") {
|
|
10059
11102
|
return null;
|
|
10060
11103
|
}
|
|
10061
|
-
|
|
10062
|
-
return {
|
|
10063
|
-
kind: previousEntry.kind,
|
|
10064
|
-
createdAtMs: Number(previousEntry.createdAtMs) || 0,
|
|
10065
|
-
messageHtml: renderMessageHtml(sourceText, "<p></p>"),
|
|
10066
|
-
};
|
|
11104
|
+
return buildAutoPilotWriteManualReview(runtime, state, approval, locale);
|
|
10067
11105
|
}
|
|
10068
11106
|
|
|
10069
|
-
function
|
|
10070
|
-
|
|
10071
|
-
|
|
11107
|
+
function buildAutoPilotWriteManualReview(runtime, state, approval, locale) {
|
|
11108
|
+
const writesEnabled = isAutoPilotTrustedWritesEnabled(state);
|
|
11109
|
+
if (!writesEnabled) {
|
|
11110
|
+
return {
|
|
11111
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11112
|
+
body: t(locale, "detail.autoPilot.writeDisabled"),
|
|
11113
|
+
};
|
|
10072
11114
|
}
|
|
10073
11115
|
|
|
10074
|
-
const
|
|
10075
|
-
const
|
|
10076
|
-
if (!
|
|
10077
|
-
return
|
|
11116
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
11117
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
11118
|
+
if (!cwd || !workspaceRoot) {
|
|
11119
|
+
return {
|
|
11120
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11121
|
+
body: t(locale, "detail.autoPilot.writeMissingWorkspace"),
|
|
11122
|
+
};
|
|
10078
11123
|
}
|
|
10079
11124
|
|
|
10080
|
-
const
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
}
|
|
10088
|
-
if (Number(candidate?.createdAtMs) <= 0 || Number(candidate?.createdAtMs) >= interruptedCreatedAtMs) {
|
|
10089
|
-
return false;
|
|
10090
|
-
}
|
|
10091
|
-
return !isTurnAbortedDisplayMessage(candidate?.messageText);
|
|
10092
|
-
})
|
|
10093
|
-
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0];
|
|
11125
|
+
const diffText = normalizeTimelineDiffText(approval?.diffText ?? "");
|
|
11126
|
+
if (!diffText) {
|
|
11127
|
+
return {
|
|
11128
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11129
|
+
body: t(locale, "detail.autoPilot.writeMissingDiff"),
|
|
11130
|
+
};
|
|
11131
|
+
}
|
|
10094
11132
|
|
|
10095
|
-
|
|
10096
|
-
|
|
11133
|
+
const rawFileRefs = normalizeTimelineFileRefs(approval?.fileRefs ?? []);
|
|
11134
|
+
if (rawFileRefs.length === 0) {
|
|
11135
|
+
return {
|
|
11136
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11137
|
+
body: t(locale, "detail.autoPilot.writeMissingFiles"),
|
|
11138
|
+
};
|
|
10097
11139
|
}
|
|
10098
11140
|
|
|
10099
|
-
const
|
|
10100
|
-
|
|
10101
|
-
|
|
11141
|
+
const resolvedFileRefs = resolveTrustedWriteFileRefs({
|
|
11142
|
+
fileRefs: approval?.fileRefs ?? [],
|
|
11143
|
+
cwd,
|
|
11144
|
+
workspaceRoot,
|
|
11145
|
+
});
|
|
11146
|
+
if (!resolvedFileRefs) {
|
|
11147
|
+
return {
|
|
11148
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11149
|
+
body: t(locale, "detail.autoPilot.writeInvalidFiles"),
|
|
11150
|
+
};
|
|
10102
11151
|
}
|
|
10103
11152
|
|
|
10104
|
-
|
|
10105
|
-
|
|
11153
|
+
const counts = diffLineCounts(diffText);
|
|
11154
|
+
const totalChangedLines =
|
|
11155
|
+
Math.max(0, Number(counts.addedLines) || 0) +
|
|
11156
|
+
Math.max(0, Number(counts.removedLines) || 0);
|
|
11157
|
+
if (totalChangedLines === 0) {
|
|
11158
|
+
return {
|
|
11159
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11160
|
+
body: t(locale, "detail.autoPilot.writeMissingDiff"),
|
|
11161
|
+
};
|
|
11162
|
+
}
|
|
11163
|
+
if (totalChangedLines > 120) {
|
|
11164
|
+
return {
|
|
11165
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11166
|
+
body: t(locale, "detail.autoPilot.writeDiffTooLarge"),
|
|
11167
|
+
};
|
|
11168
|
+
}
|
|
11169
|
+
|
|
11170
|
+
const diffSections = splitUnifiedDiffTextByFile(diffText);
|
|
11171
|
+
if (diffSections.length === 0) {
|
|
11172
|
+
return {
|
|
11173
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11174
|
+
body: t(locale, "detail.autoPilot.writeDiffUnreadable"),
|
|
11175
|
+
};
|
|
11176
|
+
}
|
|
11177
|
+
if (diffSections.length > 3) {
|
|
11178
|
+
return {
|
|
11179
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11180
|
+
body: t(locale, "detail.autoPilot.writeDiffTooLarge"),
|
|
11181
|
+
};
|
|
11182
|
+
}
|
|
11183
|
+
|
|
11184
|
+
if (
|
|
11185
|
+
/^(?:new file mode|deleted file mode|rename from|rename to|old mode|new mode|similarity index|dissimilarity index|GIT binary patch|Binary files )/mu.test(diffText)
|
|
11186
|
+
) {
|
|
11187
|
+
return {
|
|
11188
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11189
|
+
body: t(locale, "detail.autoPilot.writeDangerousDiff"),
|
|
11190
|
+
};
|
|
11191
|
+
}
|
|
11192
|
+
|
|
11193
|
+
const diffSectionRefs = resolveTrustedWriteDiffSectionFileRefs({
|
|
11194
|
+
diffSections,
|
|
11195
|
+
cwd,
|
|
11196
|
+
workspaceRoot,
|
|
11197
|
+
});
|
|
11198
|
+
if (!diffSectionRefs || !sameNormalizedFileRefSet(diffSectionRefs, resolvedFileRefs)) {
|
|
11199
|
+
return {
|
|
11200
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11201
|
+
body: t(locale, "detail.autoPilot.writeDiffMismatch"),
|
|
11202
|
+
};
|
|
11203
|
+
}
|
|
11204
|
+
|
|
11205
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
11206
|
+
const isContentOnly = resolvedFileRefs.every((fileRef) => isAutoPilotContentWritePath(fileRef));
|
|
11207
|
+
if (isContentOnly) {
|
|
11208
|
+
if (!lanes.content) {
|
|
11209
|
+
return {
|
|
11210
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11211
|
+
body: t(locale, "detail.autoPilot.writeContentDisabled"),
|
|
11212
|
+
};
|
|
11213
|
+
}
|
|
11214
|
+
return {
|
|
11215
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11216
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11217
|
+
};
|
|
11218
|
+
}
|
|
11219
|
+
|
|
11220
|
+
const isUiTestsOnly = resolvedFileRefs.every((fileRef) => isAutoPilotUiTestWritePath(fileRef));
|
|
11221
|
+
if (isUiTestsOnly) {
|
|
11222
|
+
if (!lanes.uiTests) {
|
|
11223
|
+
return {
|
|
11224
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11225
|
+
body: t(locale, "detail.autoPilot.writeUiDisabled"),
|
|
11226
|
+
};
|
|
11227
|
+
}
|
|
11228
|
+
if (totalChangedLines > 80) {
|
|
11229
|
+
return {
|
|
11230
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11231
|
+
body: t(locale, "detail.autoPilot.writeUiTooLarge"),
|
|
11232
|
+
};
|
|
11233
|
+
}
|
|
11234
|
+
if (hasUnsafeUiOrTestWriteDiff(diffText)) {
|
|
11235
|
+
return {
|
|
11236
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11237
|
+
body: t(locale, "detail.autoPilot.writeUiUnsafe"),
|
|
11238
|
+
};
|
|
11239
|
+
}
|
|
11240
|
+
return {
|
|
11241
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11242
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11243
|
+
};
|
|
11244
|
+
}
|
|
11245
|
+
|
|
11246
|
+
const isSourceOnly = resolvedFileRefs.every((fileRef) => isAutoPilotSourceWritePath(fileRef));
|
|
11247
|
+
if (isSourceOnly) {
|
|
11248
|
+
if (!lanes.source) {
|
|
11249
|
+
return {
|
|
11250
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11251
|
+
body: t(locale, "detail.autoPilot.writeSourceDisabled"),
|
|
11252
|
+
};
|
|
11253
|
+
}
|
|
11254
|
+
if (totalChangedLines > 40) {
|
|
11255
|
+
return {
|
|
11256
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11257
|
+
body: t(locale, "detail.autoPilot.writeSourceTooLarge"),
|
|
11258
|
+
};
|
|
11259
|
+
}
|
|
11260
|
+
if (hasUnsafeSourceWriteDiff(diffText)) {
|
|
11261
|
+
return {
|
|
11262
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11263
|
+
body: t(locale, "detail.autoPilot.writeSourceUnsafe"),
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
if (!hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs })) {
|
|
11267
|
+
return {
|
|
11268
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11269
|
+
body: t(locale, "detail.autoPilot.writeSourceContinuity"),
|
|
11270
|
+
};
|
|
11271
|
+
}
|
|
11272
|
+
return {
|
|
11273
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11274
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11275
|
+
};
|
|
11276
|
+
}
|
|
11277
|
+
|
|
11278
|
+
return {
|
|
11279
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11280
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11281
|
+
};
|
|
11282
|
+
}
|
|
11283
|
+
|
|
11284
|
+
function buildPreviousApprovalContext(runtime, approval) {
|
|
11285
|
+
const threadId = cleanText(approval?.conversationId || "");
|
|
11286
|
+
const approvalCreatedAtMs = Number(approval?.createdAtMs) || 0;
|
|
11287
|
+
if (!threadId || !approvalCreatedAtMs) {
|
|
11288
|
+
return null;
|
|
11289
|
+
}
|
|
11290
|
+
|
|
11291
|
+
const previousEntry = runtime.recentTimelineEntries
|
|
11292
|
+
.filter((entry) => {
|
|
11293
|
+
if (!timelineMessageKinds.has(entry.kind)) {
|
|
11294
|
+
return false;
|
|
11295
|
+
}
|
|
11296
|
+
if (cleanText(entry.threadId || "") !== threadId) {
|
|
11297
|
+
return false;
|
|
11298
|
+
}
|
|
11299
|
+
return Number(entry.createdAtMs) > 0 && Number(entry.createdAtMs) < approvalCreatedAtMs;
|
|
11300
|
+
})
|
|
11301
|
+
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0))[0];
|
|
11302
|
+
|
|
11303
|
+
if (!previousEntry) {
|
|
11304
|
+
return null;
|
|
11305
|
+
}
|
|
11306
|
+
|
|
11307
|
+
const sourceText = normalizeLongText(previousEntry.messageText || previousEntry.summary || "");
|
|
11308
|
+
if (!sourceText) {
|
|
11309
|
+
return null;
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
return {
|
|
11313
|
+
kind: previousEntry.kind,
|
|
11314
|
+
createdAtMs: Number(previousEntry.createdAtMs) || 0,
|
|
11315
|
+
messageHtml: renderMessageHtml(sourceText, "<p></p>"),
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
|
|
11319
|
+
function buildInterruptedTimelineContext(runtime, entry, locale) {
|
|
11320
|
+
if (!runtime || !isTurnAbortedDisplayMessage(entry?.messageText)) {
|
|
11321
|
+
return null;
|
|
11322
|
+
}
|
|
11323
|
+
|
|
11324
|
+
const threadId = cleanText(entry?.threadId || "");
|
|
11325
|
+
const interruptedCreatedAtMs = Number(entry?.createdAtMs) || 0;
|
|
11326
|
+
if (!threadId || !interruptedCreatedAtMs) {
|
|
11327
|
+
return null;
|
|
11328
|
+
}
|
|
11329
|
+
|
|
11330
|
+
const previousEntry = runtime.recentTimelineEntries
|
|
11331
|
+
.filter((candidate) => {
|
|
11332
|
+
if (!timelineMessageKinds.has(cleanText(candidate?.kind || ""))) {
|
|
11333
|
+
return false;
|
|
11334
|
+
}
|
|
11335
|
+
if (cleanText(candidate?.threadId || "") !== threadId) {
|
|
11336
|
+
return false;
|
|
11337
|
+
}
|
|
11338
|
+
if (Number(candidate?.createdAtMs) <= 0 || Number(candidate?.createdAtMs) >= interruptedCreatedAtMs) {
|
|
11339
|
+
return false;
|
|
11340
|
+
}
|
|
11341
|
+
return !isTurnAbortedDisplayMessage(candidate?.messageText);
|
|
11342
|
+
})
|
|
11343
|
+
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0];
|
|
11344
|
+
|
|
11345
|
+
if (!previousEntry) {
|
|
11346
|
+
return null;
|
|
11347
|
+
}
|
|
11348
|
+
|
|
11349
|
+
const sourceText = normalizeLongText(previousEntry.messageText || previousEntry.summary || "");
|
|
11350
|
+
if (!sourceText) {
|
|
11351
|
+
return null;
|
|
11352
|
+
}
|
|
11353
|
+
|
|
11354
|
+
return {
|
|
11355
|
+
kind: previousEntry.kind,
|
|
10106
11356
|
label: t(locale, "detail.interruptedTask"),
|
|
10107
11357
|
createdAtMs: Number(previousEntry.createdAtMs) || 0,
|
|
10108
11358
|
messageHtml: renderMessageHtml(sourceText, "<p></p>"),
|
|
@@ -10951,46 +12201,572 @@ function claudeToolFingerprint(toolName, toolInput) {
|
|
|
10951
12201
|
}
|
|
10952
12202
|
}
|
|
10953
12203
|
|
|
10954
|
-
function formatClaudeQuestionAnswers(questions, answers) {
|
|
10955
|
-
if (!Array.isArray(questions) || !Array.isArray(answers)) return "";
|
|
10956
|
-
const lines = [];
|
|
10957
|
-
for (let i = 0; i < questions.length; i++) {
|
|
10958
|
-
const q = questions[i] || {};
|
|
10959
|
-
const ans = answers[i] || {};
|
|
10960
|
-
const questionText = String(q.question || q.header || "").trim();
|
|
10961
|
-
if (!questionText) continue;
|
|
10962
|
-
const options = Array.isArray(q.options) ? q.options : [];
|
|
10963
|
-
const optionIndices = Array.isArray(ans.optionIndices) ? ans.optionIndices : [];
|
|
10964
|
-
const labels = optionIndices
|
|
10965
|
-
.map((idx) => options[idx]?.label)
|
|
10966
|
-
.filter((label) => typeof label === "string" && label.length > 0);
|
|
10967
|
-
const note = typeof ans.note === "string" ? ans.note.trim() : "";
|
|
10968
|
-
let answerLine = "";
|
|
10969
|
-
if (labels.length > 0) {
|
|
10970
|
-
answerLine = labels.join(", ");
|
|
10971
|
-
}
|
|
10972
|
-
if (note) {
|
|
10973
|
-
answerLine = answerLine ? `${answerLine} (note: ${note})` : note;
|
|
10974
|
-
}
|
|
10975
|
-
if (!answerLine) {
|
|
10976
|
-
answerLine = "(no answer)";
|
|
10977
|
-
}
|
|
10978
|
-
lines.push(`Q${i + 1}: ${questionText}`);
|
|
10979
|
-
lines.push(`A: ${answerLine}`);
|
|
12204
|
+
function formatClaudeQuestionAnswers(questions, answers) {
|
|
12205
|
+
if (!Array.isArray(questions) || !Array.isArray(answers)) return "";
|
|
12206
|
+
const lines = [];
|
|
12207
|
+
for (let i = 0; i < questions.length; i++) {
|
|
12208
|
+
const q = questions[i] || {};
|
|
12209
|
+
const ans = answers[i] || {};
|
|
12210
|
+
const questionText = String(q.question || q.header || "").trim();
|
|
12211
|
+
if (!questionText) continue;
|
|
12212
|
+
const options = Array.isArray(q.options) ? q.options : [];
|
|
12213
|
+
const optionIndices = Array.isArray(ans.optionIndices) ? ans.optionIndices : [];
|
|
12214
|
+
const labels = optionIndices
|
|
12215
|
+
.map((idx) => options[idx]?.label)
|
|
12216
|
+
.filter((label) => typeof label === "string" && label.length > 0);
|
|
12217
|
+
const note = typeof ans.note === "string" ? ans.note.trim() : "";
|
|
12218
|
+
let answerLine = "";
|
|
12219
|
+
if (labels.length > 0) {
|
|
12220
|
+
answerLine = labels.join(", ");
|
|
12221
|
+
}
|
|
12222
|
+
if (note) {
|
|
12223
|
+
answerLine = answerLine ? `${answerLine} (note: ${note})` : note;
|
|
12224
|
+
}
|
|
12225
|
+
if (!answerLine) {
|
|
12226
|
+
answerLine = "(no answer)";
|
|
12227
|
+
}
|
|
12228
|
+
lines.push(`Q${i + 1}: ${questionText}`);
|
|
12229
|
+
lines.push(`A: ${answerLine}`);
|
|
12230
|
+
}
|
|
12231
|
+
return lines.join("\n");
|
|
12232
|
+
}
|
|
12233
|
+
|
|
12234
|
+
function findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint) {
|
|
12235
|
+
let match = null;
|
|
12236
|
+
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
12237
|
+
if (approval.resolved) continue;
|
|
12238
|
+
if (approval.conversationId !== threadId) continue;
|
|
12239
|
+
if (approval.claudeToolName && toolName && approval.claudeToolName !== toolName) continue;
|
|
12240
|
+
if (approval.claudeToolFingerprint && fingerprint && approval.claudeToolFingerprint !== fingerprint) continue;
|
|
12241
|
+
if (!match || approval.createdAtMs > match.createdAtMs) match = approval;
|
|
12242
|
+
}
|
|
12243
|
+
return match;
|
|
12244
|
+
}
|
|
12245
|
+
|
|
12246
|
+
function maybeAutoApproveTrustedRead({ state, commandText, cwd, workspaceRoot }) {
|
|
12247
|
+
if (state?.autoPilotTrustedReads !== true) {
|
|
12248
|
+
return null;
|
|
12249
|
+
}
|
|
12250
|
+
return evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot });
|
|
12251
|
+
}
|
|
12252
|
+
|
|
12253
|
+
function isAutoPilotTrustedWritesEnabled(state) {
|
|
12254
|
+
return hasAnyAutoPilotWriteLaneEnabled(state);
|
|
12255
|
+
}
|
|
12256
|
+
|
|
12257
|
+
function isDeniedTrustedWritePath(candidatePath) {
|
|
12258
|
+
const normalized = cleanText(candidatePath || "");
|
|
12259
|
+
if (!normalized) {
|
|
12260
|
+
return true;
|
|
12261
|
+
}
|
|
12262
|
+
const lower = normalized.toLowerCase();
|
|
12263
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12264
|
+
const basename = segments[segments.length - 1] || "";
|
|
12265
|
+
if (isSensitiveCommandPath(normalized)) {
|
|
12266
|
+
return true;
|
|
12267
|
+
}
|
|
12268
|
+
if (segments.some((segment) => [".github", ".gitlab", ".terraform", ".claude", ".husky", ".vscode"].includes(segment))) {
|
|
12269
|
+
return true;
|
|
12270
|
+
}
|
|
12271
|
+
if (
|
|
12272
|
+
[
|
|
12273
|
+
"package.json",
|
|
12274
|
+
"package-lock.json",
|
|
12275
|
+
"pnpm-lock.yaml",
|
|
12276
|
+
"yarn.lock",
|
|
12277
|
+
"bun.lockb",
|
|
12278
|
+
"cargo.toml",
|
|
12279
|
+
"cargo.lock",
|
|
12280
|
+
"gemfile",
|
|
12281
|
+
"gemfile.lock",
|
|
12282
|
+
"podfile",
|
|
12283
|
+
"podfile.lock",
|
|
12284
|
+
"composer.json",
|
|
12285
|
+
"composer.lock",
|
|
12286
|
+
"pipfile",
|
|
12287
|
+
"pipfile.lock",
|
|
12288
|
+
"poetry.lock",
|
|
12289
|
+
"requirements.txt",
|
|
12290
|
+
"dockerfile",
|
|
12291
|
+
"wrangler.toml",
|
|
12292
|
+
"tsconfig.json",
|
|
12293
|
+
"tsconfig.tsbuildinfo",
|
|
12294
|
+
].includes(basename)
|
|
12295
|
+
) {
|
|
12296
|
+
return true;
|
|
12297
|
+
}
|
|
12298
|
+
return false;
|
|
12299
|
+
}
|
|
12300
|
+
|
|
12301
|
+
function isAutoPilotContentWritePath(candidatePath) {
|
|
12302
|
+
const normalized = cleanText(candidatePath || "");
|
|
12303
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12304
|
+
return false;
|
|
12305
|
+
}
|
|
12306
|
+
const lower = normalized.toLowerCase();
|
|
12307
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12308
|
+
const basename = segments[segments.length - 1] || "";
|
|
12309
|
+
const extension = path.extname(basename);
|
|
12310
|
+
if ([".md", ".mdx", ".txt", ".rst", ".adoc"].includes(extension)) {
|
|
12311
|
+
return true;
|
|
12312
|
+
}
|
|
12313
|
+
if (["license", "notice", "copying", "readme", "changelog", "contributing"].includes(path.basename(basename, extension))) {
|
|
12314
|
+
return true;
|
|
12315
|
+
}
|
|
12316
|
+
if (segments.includes("i18n") && [".js", ".ts", ".json", ".yaml", ".yml"].includes(extension)) {
|
|
12317
|
+
return true;
|
|
12318
|
+
}
|
|
12319
|
+
if (segments.includes("messages") && extension === ".json") {
|
|
12320
|
+
return true;
|
|
12321
|
+
}
|
|
12322
|
+
return false;
|
|
12323
|
+
}
|
|
12324
|
+
|
|
12325
|
+
function isAutoPilotUiTestWritePath(candidatePath) {
|
|
12326
|
+
const normalized = cleanText(candidatePath || "");
|
|
12327
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12328
|
+
return false;
|
|
12329
|
+
}
|
|
12330
|
+
const lower = normalized.toLowerCase();
|
|
12331
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12332
|
+
const basename = segments[segments.length - 1] || "";
|
|
12333
|
+
const extension = path.extname(basename);
|
|
12334
|
+
if ([".css", ".scss", ".sass", ".less", ".styl", ".pcss"].includes(extension)) {
|
|
12335
|
+
return true;
|
|
12336
|
+
}
|
|
12337
|
+
if (segments.includes("__tests__") || /\.(test|spec)\.[cm]?[jt]sx?$/u.test(basename)) {
|
|
12338
|
+
return true;
|
|
12339
|
+
}
|
|
12340
|
+
if ((segments.includes("web") || segments.includes("components")) && [".js", ".jsx", ".ts", ".tsx", ".html"].includes(extension)) {
|
|
12341
|
+
return true;
|
|
12342
|
+
}
|
|
12343
|
+
return false;
|
|
12344
|
+
}
|
|
12345
|
+
|
|
12346
|
+
function isAutoPilotSourceWritePath(candidatePath) {
|
|
12347
|
+
const normalized = cleanText(candidatePath || "");
|
|
12348
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12349
|
+
return false;
|
|
12350
|
+
}
|
|
12351
|
+
if (isAutoPilotContentWritePath(normalized) || isAutoPilotUiTestWritePath(normalized)) {
|
|
12352
|
+
return false;
|
|
12353
|
+
}
|
|
12354
|
+
const extension = path.extname(normalized.toLowerCase());
|
|
12355
|
+
return [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(extension);
|
|
12356
|
+
}
|
|
12357
|
+
|
|
12358
|
+
function extractAddedDiffLines(diffText) {
|
|
12359
|
+
return normalizeTimelineDiffText(diffText)
|
|
12360
|
+
.split("\n")
|
|
12361
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"));
|
|
12362
|
+
}
|
|
12363
|
+
|
|
12364
|
+
function addedDiffLinesContain(diffText, pattern) {
|
|
12365
|
+
return extractAddedDiffLines(diffText).some((line) => pattern.test(line.slice(1)));
|
|
12366
|
+
}
|
|
12367
|
+
|
|
12368
|
+
function hasUnsafeUiOrTestWriteDiff(diffText) {
|
|
12369
|
+
const patterns = [
|
|
12370
|
+
/\bprocess\.env\b/u,
|
|
12371
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
12372
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
12373
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
12374
|
+
/\bcrypto\b/u,
|
|
12375
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
12376
|
+
];
|
|
12377
|
+
return (
|
|
12378
|
+
addedDiffLinesContain(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
12379
|
+
addedDiffLinesContain(diffText, /\brequire\s*\(/u) ||
|
|
12380
|
+
patterns.some((pattern) => addedDiffLinesContain(diffText, pattern))
|
|
12381
|
+
);
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
function hasUnsafeSourceWriteDiff(diffText) {
|
|
12385
|
+
const patterns = [
|
|
12386
|
+
/\bprocess\.env\b/u,
|
|
12387
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
12388
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
12389
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
12390
|
+
/\b(?:net|tls|dgram|http2?)\b/u,
|
|
12391
|
+
/\bcrypto\b/u,
|
|
12392
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
12393
|
+
];
|
|
12394
|
+
return (
|
|
12395
|
+
addedDiffLinesContain(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
12396
|
+
addedDiffLinesContain(diffText, /\brequire\s*\(/u) ||
|
|
12397
|
+
patterns.some((pattern) => addedDiffLinesContain(diffText, pattern))
|
|
12398
|
+
);
|
|
12399
|
+
}
|
|
12400
|
+
|
|
12401
|
+
const AUTO_PILOT_SOURCE_CONTINUITY_WINDOW_MS = 15 * 60 * 1000;
|
|
12402
|
+
|
|
12403
|
+
function isReadContinuityTimelineEntry(entry) {
|
|
12404
|
+
if (!isPlainObject(entry)) {
|
|
12405
|
+
return false;
|
|
12406
|
+
}
|
|
12407
|
+
if (cleanText(entry.kind || "") === "file_event" && cleanText(entry.fileEventType || "") === "read") {
|
|
12408
|
+
return true;
|
|
12409
|
+
}
|
|
12410
|
+
return (
|
|
12411
|
+
cleanText(entry.kind || "") === "approval" &&
|
|
12412
|
+
cleanText(entry.outcome || "") === "approved" &&
|
|
12413
|
+
!normalizeTimelineDiffText(entry.diffText ?? "") &&
|
|
12414
|
+
normalizeTimelineFileRefs(entry.fileRefs ?? []).length > 0
|
|
12415
|
+
);
|
|
12416
|
+
}
|
|
12417
|
+
|
|
12418
|
+
function resolveRecentReadContinuityFileRefs({ entry, cwd, workspaceRoot }) {
|
|
12419
|
+
const normalizedRefs = normalizeTimelineFileRefs(entry?.fileRefs ?? []);
|
|
12420
|
+
if (normalizedRefs.length === 0) {
|
|
12421
|
+
return [];
|
|
12422
|
+
}
|
|
12423
|
+
const resolvedRefs = [];
|
|
12424
|
+
for (const fileRef of normalizedRefs) {
|
|
12425
|
+
const resolved = resolveTrustedReadTargetPath({
|
|
12426
|
+
token: fileRef,
|
|
12427
|
+
cwd,
|
|
12428
|
+
workspaceRoot,
|
|
12429
|
+
});
|
|
12430
|
+
if (resolved) {
|
|
12431
|
+
resolvedRefs.push(resolved);
|
|
12432
|
+
}
|
|
12433
|
+
}
|
|
12434
|
+
return normalizeTimelineFileRefs(resolvedRefs);
|
|
12435
|
+
}
|
|
12436
|
+
|
|
12437
|
+
function hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs }) {
|
|
12438
|
+
const normalizedTargets = normalizeTimelineFileRefs(resolvedFileRefs ?? []);
|
|
12439
|
+
if (normalizedTargets.length !== 1) {
|
|
12440
|
+
return false;
|
|
12441
|
+
}
|
|
12442
|
+
const targetFileRef = normalizedTargets[0];
|
|
12443
|
+
const conversationId = cleanText(approval?.conversationId || "");
|
|
12444
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
12445
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
12446
|
+
if (!cwd || !workspaceRoot) {
|
|
12447
|
+
return false;
|
|
12448
|
+
}
|
|
12449
|
+
const timelineEntries =
|
|
12450
|
+
Array.isArray(runtime?.recentTimelineEntries) && runtime.recentTimelineEntries.length > 0
|
|
12451
|
+
? runtime.recentTimelineEntries
|
|
12452
|
+
: normalizeTimelineEntries(state?.recentTimelineEntries ?? [], 40);
|
|
12453
|
+
const now = Date.now();
|
|
12454
|
+
for (const entry of timelineEntries) {
|
|
12455
|
+
if (!isReadContinuityTimelineEntry(entry)) {
|
|
12456
|
+
continue;
|
|
12457
|
+
}
|
|
12458
|
+
const createdAtMs = Number(entry?.createdAtMs) || 0;
|
|
12459
|
+
if (!createdAtMs || now - createdAtMs > AUTO_PILOT_SOURCE_CONTINUITY_WINDOW_MS) {
|
|
12460
|
+
continue;
|
|
12461
|
+
}
|
|
12462
|
+
const entryThreadId = cleanText(entry?.threadId || extractConversationIdFromStableId(entry?.stableId) || "");
|
|
12463
|
+
if (conversationId && entryThreadId && entryThreadId !== conversationId) {
|
|
12464
|
+
continue;
|
|
12465
|
+
}
|
|
12466
|
+
const entryFileRefs = resolveRecentReadContinuityFileRefs({
|
|
12467
|
+
entry,
|
|
12468
|
+
cwd,
|
|
12469
|
+
workspaceRoot,
|
|
12470
|
+
});
|
|
12471
|
+
if (entryFileRefs.includes(targetFileRef)) {
|
|
12472
|
+
return true;
|
|
12473
|
+
}
|
|
12474
|
+
}
|
|
12475
|
+
return false;
|
|
12476
|
+
}
|
|
12477
|
+
|
|
12478
|
+
function classifyTrustedWriteLane({ runtime, state, approval, resolvedFileRefs, diffText, totalChangedLines }) {
|
|
12479
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
12480
|
+
if (
|
|
12481
|
+
lanes.content &&
|
|
12482
|
+
resolvedFileRefs.length >= 1 &&
|
|
12483
|
+
resolvedFileRefs.length <= 3 &&
|
|
12484
|
+
totalChangedLines <= 120 &&
|
|
12485
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotContentWritePath(fileRef))
|
|
12486
|
+
) {
|
|
12487
|
+
return "content";
|
|
12488
|
+
}
|
|
12489
|
+
if (
|
|
12490
|
+
lanes.uiTests &&
|
|
12491
|
+
resolvedFileRefs.length >= 1 &&
|
|
12492
|
+
resolvedFileRefs.length <= 2 &&
|
|
12493
|
+
totalChangedLines <= 80 &&
|
|
12494
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotUiTestWritePath(fileRef)) &&
|
|
12495
|
+
!hasUnsafeUiOrTestWriteDiff(diffText)
|
|
12496
|
+
) {
|
|
12497
|
+
return "ui_tests";
|
|
12498
|
+
}
|
|
12499
|
+
if (
|
|
12500
|
+
lanes.source &&
|
|
12501
|
+
resolvedFileRefs.length === 1 &&
|
|
12502
|
+
totalChangedLines <= 40 &&
|
|
12503
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotSourceWritePath(fileRef)) &&
|
|
12504
|
+
!hasUnsafeSourceWriteDiff(diffText) &&
|
|
12505
|
+
hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs })
|
|
12506
|
+
) {
|
|
12507
|
+
return "source";
|
|
12508
|
+
}
|
|
12509
|
+
return "";
|
|
12510
|
+
}
|
|
12511
|
+
|
|
12512
|
+
function resolveTrustedWriteFileRefs({ fileRefs, cwd, workspaceRoot }) {
|
|
12513
|
+
const normalizedRefs = normalizeTimelineFileRefs(fileRefs ?? []);
|
|
12514
|
+
if (normalizedRefs.length === 0 || normalizedRefs.length > 3) {
|
|
12515
|
+
return null;
|
|
12516
|
+
}
|
|
12517
|
+
const resolvedRefs = [];
|
|
12518
|
+
for (const fileRef of normalizedRefs) {
|
|
12519
|
+
const resolved = resolveTrustedReadTargetPath({
|
|
12520
|
+
token: fileRef,
|
|
12521
|
+
cwd,
|
|
12522
|
+
workspaceRoot,
|
|
12523
|
+
});
|
|
12524
|
+
if (!resolved || isDeniedTrustedWritePath(resolved)) {
|
|
12525
|
+
return null;
|
|
12526
|
+
}
|
|
12527
|
+
resolvedRefs.push(resolved);
|
|
12528
|
+
}
|
|
12529
|
+
return normalizeTimelineFileRefs(resolvedRefs);
|
|
12530
|
+
}
|
|
12531
|
+
|
|
12532
|
+
function sameNormalizedFileRefSet(leftRefs, rightRefs) {
|
|
12533
|
+
const left = normalizeTimelineFileRefs(leftRefs ?? []);
|
|
12534
|
+
const right = normalizeTimelineFileRefs(rightRefs ?? []);
|
|
12535
|
+
if (left.length !== right.length) {
|
|
12536
|
+
return false;
|
|
12537
|
+
}
|
|
12538
|
+
const rightSet = new Set(right);
|
|
12539
|
+
return left.every((value) => rightSet.has(value));
|
|
12540
|
+
}
|
|
12541
|
+
|
|
12542
|
+
function resolveTrustedWriteDiffSectionFileRefs({ diffSections, cwd, workspaceRoot }) {
|
|
12543
|
+
const resolvedSections = [];
|
|
12544
|
+
for (const section of diffSections) {
|
|
12545
|
+
const paths = extractUnifiedDiffSectionPaths(section);
|
|
12546
|
+
const sectionFileRef = cleanText(paths.newFileRef || paths.oldFileRef || "");
|
|
12547
|
+
if (!sectionFileRef) {
|
|
12548
|
+
return null;
|
|
12549
|
+
}
|
|
12550
|
+
if (paths.oldFileRef && paths.newFileRef && cleanText(paths.oldFileRef) !== cleanText(paths.newFileRef)) {
|
|
12551
|
+
return null;
|
|
12552
|
+
}
|
|
12553
|
+
const resolved = resolveTrustedDiffSectionPath({
|
|
12554
|
+
token: sectionFileRef,
|
|
12555
|
+
cwd,
|
|
12556
|
+
workspaceRoot,
|
|
12557
|
+
});
|
|
12558
|
+
if (!resolved || isDeniedTrustedWritePath(resolved)) {
|
|
12559
|
+
return null;
|
|
12560
|
+
}
|
|
12561
|
+
resolvedSections.push(resolved);
|
|
12562
|
+
}
|
|
12563
|
+
return normalizeTimelineFileRefs(resolvedSections);
|
|
12564
|
+
}
|
|
12565
|
+
|
|
12566
|
+
function evaluateTrustedWriteApprovalCandidate({ runtime, state, approval }) {
|
|
12567
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
12568
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
12569
|
+
const resolvedFileRefs = resolveTrustedWriteFileRefs({
|
|
12570
|
+
fileRefs: approval?.fileRefs ?? [],
|
|
12571
|
+
cwd,
|
|
12572
|
+
workspaceRoot,
|
|
12573
|
+
});
|
|
12574
|
+
const diffText = normalizeTimelineDiffText(approval?.diffText ?? "");
|
|
12575
|
+
if (!cwd || !workspaceRoot || !resolvedFileRefs || !diffText) {
|
|
12576
|
+
return null;
|
|
12577
|
+
}
|
|
12578
|
+
const counts = diffLineCounts(diffText);
|
|
12579
|
+
const totalChangedLines =
|
|
12580
|
+
Math.max(0, Number(counts.addedLines) || 0) +
|
|
12581
|
+
Math.max(0, Number(counts.removedLines) || 0);
|
|
12582
|
+
if (totalChangedLines === 0 || totalChangedLines > 120) {
|
|
12583
|
+
return null;
|
|
12584
|
+
}
|
|
12585
|
+
const diffSections = splitUnifiedDiffTextByFile(diffText);
|
|
12586
|
+
if (diffSections.length === 0 || diffSections.length > 3) {
|
|
12587
|
+
return null;
|
|
12588
|
+
}
|
|
12589
|
+
if (
|
|
12590
|
+
/^(?:new file mode|deleted file mode|rename from|rename to|old mode|new mode|similarity index|dissimilarity index|GIT binary patch|Binary files )/mu.test(diffText)
|
|
12591
|
+
) {
|
|
12592
|
+
return null;
|
|
12593
|
+
}
|
|
12594
|
+
const diffSectionRefs = resolveTrustedWriteDiffSectionFileRefs({
|
|
12595
|
+
diffSections,
|
|
12596
|
+
cwd,
|
|
12597
|
+
workspaceRoot,
|
|
12598
|
+
});
|
|
12599
|
+
if (!diffSectionRefs || !sameNormalizedFileRefSet(diffSectionRefs, resolvedFileRefs)) {
|
|
12600
|
+
return null;
|
|
12601
|
+
}
|
|
12602
|
+
const lane = classifyTrustedWriteLane({
|
|
12603
|
+
runtime,
|
|
12604
|
+
state: state ?? approval?.stateSnapshot ?? null,
|
|
12605
|
+
approval,
|
|
12606
|
+
resolvedFileRefs: diffSectionRefs,
|
|
12607
|
+
diffText,
|
|
12608
|
+
totalChangedLines,
|
|
12609
|
+
});
|
|
12610
|
+
if (!lane) {
|
|
12611
|
+
return null;
|
|
12612
|
+
}
|
|
12613
|
+
return {
|
|
12614
|
+
fileRefs: diffSectionRefs,
|
|
12615
|
+
diffText,
|
|
12616
|
+
diffAddedLines: Math.max(0, Number(counts.addedLines) || 0),
|
|
12617
|
+
diffRemovedLines: Math.max(0, Number(counts.removedLines) || 0),
|
|
12618
|
+
totalChangedLines,
|
|
12619
|
+
lane,
|
|
12620
|
+
};
|
|
12621
|
+
}
|
|
12622
|
+
|
|
12623
|
+
function maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval }) {
|
|
12624
|
+
if (!isAutoPilotTrustedWritesEnabled(state)) {
|
|
12625
|
+
return null;
|
|
12626
|
+
}
|
|
12627
|
+
if (cleanText(approval?.kind || "") !== "file") {
|
|
12628
|
+
return null;
|
|
12629
|
+
}
|
|
12630
|
+
return evaluateTrustedWriteApprovalCandidate({
|
|
12631
|
+
runtime,
|
|
12632
|
+
state,
|
|
12633
|
+
approval: { ...approval, stateSnapshot: state },
|
|
12634
|
+
});
|
|
12635
|
+
}
|
|
12636
|
+
|
|
12637
|
+
function autoPilotTrustedWriteMessage(locale, approval, candidate) {
|
|
12638
|
+
const prefix = t(locale, "server.message.autoPilotTrustedWriteApproved");
|
|
12639
|
+
const fileBlock = candidate?.fileRefs?.length
|
|
12640
|
+
? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${candidate.fileRefs.join("\n")}\n\`\`\``
|
|
12641
|
+
: "";
|
|
12642
|
+
const sizeLine =
|
|
12643
|
+
candidate && Number.isFinite(candidate.diffAddedLines) && Number.isFinite(candidate.diffRemovedLines)
|
|
12644
|
+
? `${t(locale, "server.message.diffSummaryLabel")} +${candidate.diffAddedLines} / -${candidate.diffRemovedLines}`
|
|
12645
|
+
: "";
|
|
12646
|
+
const originalMessage = cleanText(approval?.messageText || "");
|
|
12647
|
+
return [prefix, sizeLine, fileBlock, originalMessage].filter(Boolean).join("\n\n");
|
|
12648
|
+
}
|
|
12649
|
+
|
|
12650
|
+
async function recordAutoApprovedTrustedRead({
|
|
12651
|
+
config,
|
|
12652
|
+
runtime,
|
|
12653
|
+
state,
|
|
12654
|
+
approval,
|
|
12655
|
+
policyMatch,
|
|
12656
|
+
}) {
|
|
12657
|
+
const stateChanged = recordActionHistoryItem({
|
|
12658
|
+
config,
|
|
12659
|
+
runtime,
|
|
12660
|
+
state,
|
|
12661
|
+
kind: "approval",
|
|
12662
|
+
stableId: `approval:${approval.requestKey}:autopilot`,
|
|
12663
|
+
token: approval.token,
|
|
12664
|
+
title: approval.title,
|
|
12665
|
+
threadLabel: approval.threadLabel || "",
|
|
12666
|
+
messageText: autoPilotApprovalMessage(config.defaultLocale, policyMatch.commandText),
|
|
12667
|
+
summary: t(config.defaultLocale, "server.message.autoPilotTrustedReadSummary"),
|
|
12668
|
+
fileRefs: normalizeTimelineFileRefs(policyMatch.fileRefs ?? approval.fileRefs ?? []),
|
|
12669
|
+
diffText: "",
|
|
12670
|
+
diffSource: "",
|
|
12671
|
+
diffAvailable: false,
|
|
12672
|
+
diffAddedLines: 0,
|
|
12673
|
+
diffRemovedLines: 0,
|
|
12674
|
+
outcome: "approved",
|
|
12675
|
+
provider: approval.provider || "codex",
|
|
12676
|
+
});
|
|
12677
|
+
if (stateChanged) {
|
|
12678
|
+
await saveState(config.stateFile, state);
|
|
12679
|
+
}
|
|
12680
|
+
}
|
|
12681
|
+
|
|
12682
|
+
async function recordAutoApprovedTrustedWrite({
|
|
12683
|
+
config,
|
|
12684
|
+
runtime,
|
|
12685
|
+
state,
|
|
12686
|
+
approval,
|
|
12687
|
+
candidate,
|
|
12688
|
+
}) {
|
|
12689
|
+
const stateChanged = recordActionHistoryItem({
|
|
12690
|
+
config,
|
|
12691
|
+
runtime,
|
|
12692
|
+
state,
|
|
12693
|
+
kind: "approval",
|
|
12694
|
+
stableId: `approval:${approval.requestKey}:autopilot-write:${candidate.lane || "content"}`,
|
|
12695
|
+
token: approval.token,
|
|
12696
|
+
title: approval.title,
|
|
12697
|
+
threadLabel: approval.threadLabel || "",
|
|
12698
|
+
messageText: autoPilotTrustedWriteMessage(config.defaultLocale, approval, candidate),
|
|
12699
|
+
summary: t(config.defaultLocale, "server.message.autoPilotTrustedWriteSummary"),
|
|
12700
|
+
fileRefs: candidate.fileRefs,
|
|
12701
|
+
diffText: candidate.diffText,
|
|
12702
|
+
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
12703
|
+
diffAvailable: true,
|
|
12704
|
+
diffAddedLines: candidate.diffAddedLines,
|
|
12705
|
+
diffRemovedLines: candidate.diffRemovedLines,
|
|
12706
|
+
outcome: "approved",
|
|
12707
|
+
provider: approval.provider || "codex",
|
|
12708
|
+
});
|
|
12709
|
+
if (stateChanged) {
|
|
12710
|
+
await saveState(config.stateFile, state);
|
|
12711
|
+
}
|
|
12712
|
+
}
|
|
12713
|
+
|
|
12714
|
+
async function autoApproveTrustedRead({
|
|
12715
|
+
config,
|
|
12716
|
+
runtime,
|
|
12717
|
+
state,
|
|
12718
|
+
approval,
|
|
12719
|
+
policyMatch,
|
|
12720
|
+
}) {
|
|
12721
|
+
if (approval.provider === "claude" && approval.resolveClaudeWaiter) {
|
|
12722
|
+
approval.resolveClaudeWaiter({
|
|
12723
|
+
permissionDecision: "allow",
|
|
12724
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedReadReason"),
|
|
12725
|
+
});
|
|
12726
|
+
} else if (approval.provider !== "claude") {
|
|
12727
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, "accept");
|
|
10980
12728
|
}
|
|
10981
|
-
|
|
12729
|
+
approval.resolved = true;
|
|
12730
|
+
approval.resolving = false;
|
|
12731
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
12732
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
12733
|
+
await recordAutoApprovedTrustedRead({
|
|
12734
|
+
config,
|
|
12735
|
+
runtime,
|
|
12736
|
+
state,
|
|
12737
|
+
approval,
|
|
12738
|
+
policyMatch,
|
|
12739
|
+
});
|
|
12740
|
+
console.log(`[auto-pilot-approved] ${approval.requestKey} | ${policyMatch.command}`);
|
|
10982
12741
|
}
|
|
10983
12742
|
|
|
10984
|
-
function
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
12743
|
+
async function autoApproveTrustedWrite({
|
|
12744
|
+
config,
|
|
12745
|
+
runtime,
|
|
12746
|
+
state,
|
|
12747
|
+
approval,
|
|
12748
|
+
candidate,
|
|
12749
|
+
}) {
|
|
12750
|
+
if (approval.provider === "claude" && approval.resolveClaudeWaiter) {
|
|
12751
|
+
approval.resolveClaudeWaiter({
|
|
12752
|
+
permissionDecision: "allow",
|
|
12753
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedWriteReason"),
|
|
12754
|
+
});
|
|
12755
|
+
} else if (approval.provider !== "claude") {
|
|
12756
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, "accept");
|
|
10992
12757
|
}
|
|
10993
|
-
|
|
12758
|
+
approval.resolved = true;
|
|
12759
|
+
approval.resolving = false;
|
|
12760
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
12761
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
12762
|
+
await recordAutoApprovedTrustedWrite({
|
|
12763
|
+
config,
|
|
12764
|
+
runtime,
|
|
12765
|
+
state,
|
|
12766
|
+
approval,
|
|
12767
|
+
candidate,
|
|
12768
|
+
});
|
|
12769
|
+
console.log(`[auto-pilot-approved-write] ${approval.requestKey} | files=${candidate.fileRefs.length} | lines=${candidate.totalChangedLines}`);
|
|
10994
12770
|
}
|
|
10995
12771
|
|
|
10996
12772
|
async function handleNativeApprovalDecision({ config, runtime, state, approval, decision }) {
|
|
@@ -11047,7 +12823,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
11047
12823
|
|
|
11048
12824
|
async function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
11049
12825
|
if (kind === "diff_thread") {
|
|
11050
|
-
const group = (await
|
|
12826
|
+
const group = (await getDiffThreadGroupsCached(runtime, state, config)).find((entry) => entry.token === token);
|
|
11051
12827
|
return group ? buildDiffThreadDetail(group, locale) : null;
|
|
11052
12828
|
}
|
|
11053
12829
|
if (kind === "file_event") {
|
|
@@ -11113,7 +12889,7 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
|
|
|
11113
12889
|
if (kind === "approval") {
|
|
11114
12890
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
11115
12891
|
if (approval) {
|
|
11116
|
-
return buildPendingApprovalDetail(runtime, approval, locale);
|
|
12892
|
+
return buildPendingApprovalDetail(runtime, state, approval, locale);
|
|
11117
12893
|
}
|
|
11118
12894
|
const historicalApproval = historyItemByToken(runtime, kind, token);
|
|
11119
12895
|
return historicalApproval ? buildHistoryDetail(historicalApproval, locale, runtime) : null;
|
|
@@ -11343,21 +13119,81 @@ function contentTypeForFile(filePath) {
|
|
|
11343
13119
|
}
|
|
11344
13120
|
}
|
|
11345
13121
|
|
|
11346
|
-
|
|
13122
|
+
// Per-file cache of {raw, gzip, mtimeMs}. Gzip of the ~464KB app shell
|
|
13123
|
+
// (app.js 278KB + app.css 76KB + i18n.js 110KB) takes a few ms to compute,
|
|
13124
|
+
// and without this the bridge would recompress on every request. Keyed by
|
|
13125
|
+
// resolved filePath so /app and /index.html share a cache slot.
|
|
13126
|
+
const WEB_ASSET_CACHE = new Map();
|
|
13127
|
+
const GZIPPABLE_EXTENSIONS = new Set([
|
|
13128
|
+
".js",
|
|
13129
|
+
".mjs",
|
|
13130
|
+
".css",
|
|
13131
|
+
".html",
|
|
13132
|
+
".htm",
|
|
13133
|
+
".json",
|
|
13134
|
+
".webmanifest",
|
|
13135
|
+
".svg",
|
|
13136
|
+
".txt",
|
|
13137
|
+
".map",
|
|
13138
|
+
]);
|
|
13139
|
+
const MIN_GZIP_BYTES = 512;
|
|
13140
|
+
|
|
13141
|
+
async function loadWebAssetEntry(filePath) {
|
|
13142
|
+
const stat = await fs.stat(filePath);
|
|
13143
|
+
const cached = WEB_ASSET_CACHE.get(filePath);
|
|
13144
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
13145
|
+
return cached;
|
|
13146
|
+
}
|
|
13147
|
+
const raw = await fs.readFile(filePath);
|
|
13148
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
13149
|
+
let gzip = null;
|
|
13150
|
+
if (GZIPPABLE_EXTENSIONS.has(extension) && raw.length >= MIN_GZIP_BYTES) {
|
|
13151
|
+
try {
|
|
13152
|
+
gzip = zlib.gzipSync(raw, { level: 6 });
|
|
13153
|
+
} catch {
|
|
13154
|
+
gzip = null;
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
const entry = {
|
|
13158
|
+
raw,
|
|
13159
|
+
gzip,
|
|
13160
|
+
mtimeMs: stat.mtimeMs,
|
|
13161
|
+
size: stat.size,
|
|
13162
|
+
contentType: contentTypeForFile(filePath),
|
|
13163
|
+
};
|
|
13164
|
+
WEB_ASSET_CACHE.set(filePath, entry);
|
|
13165
|
+
return entry;
|
|
13166
|
+
}
|
|
13167
|
+
|
|
13168
|
+
function clientAcceptsGzip(req) {
|
|
13169
|
+
const header = req?.headers?.["accept-encoding"];
|
|
13170
|
+
if (!header) return false;
|
|
13171
|
+
const value = Array.isArray(header) ? header.join(",") : String(header);
|
|
13172
|
+
return /\bgzip\b/i.test(value);
|
|
13173
|
+
}
|
|
13174
|
+
|
|
13175
|
+
async function serveWebAsset(res, urlPath, req) {
|
|
11347
13176
|
const filePath = resolveWebAsset(urlPath);
|
|
11348
13177
|
if (!filePath) {
|
|
11349
13178
|
return false;
|
|
11350
13179
|
}
|
|
11351
13180
|
|
|
11352
13181
|
try {
|
|
11353
|
-
const
|
|
13182
|
+
const entry = await loadWebAssetEntry(filePath);
|
|
13183
|
+
const wantsGzip = entry.gzip && clientAcceptsGzip(req);
|
|
11354
13184
|
res.statusCode = 200;
|
|
11355
|
-
res.setHeader("Content-Type",
|
|
13185
|
+
res.setHeader("Content-Type", entry.contentType);
|
|
11356
13186
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
11357
13187
|
if (urlPath === "/sw.js") {
|
|
11358
13188
|
res.setHeader("Service-Worker-Allowed", "/");
|
|
11359
13189
|
}
|
|
11360
|
-
|
|
13190
|
+
if (wantsGzip) {
|
|
13191
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
13192
|
+
res.setHeader("Vary", "Accept-Encoding");
|
|
13193
|
+
res.end(entry.gzip);
|
|
13194
|
+
} else {
|
|
13195
|
+
res.end(entry.raw);
|
|
13196
|
+
}
|
|
11361
13197
|
return true;
|
|
11362
13198
|
} catch {
|
|
11363
13199
|
return false;
|
|
@@ -11489,10 +13325,11 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11489
13325
|
url.pathname === "/app.js" ||
|
|
11490
13326
|
url.pathname === "/app.css" ||
|
|
11491
13327
|
url.pathname === "/i18n.js" ||
|
|
13328
|
+
url.pathname === "/hazbase-passkey.js" ||
|
|
11492
13329
|
url.pathname === "/sw.js" ||
|
|
11493
13330
|
url.pathname.startsWith("/icons/")
|
|
11494
13331
|
) {
|
|
11495
|
-
const served = await serveWebAsset(res, url.pathname);
|
|
13332
|
+
const served = await serveWebAsset(res, url.pathname, req);
|
|
11496
13333
|
if (served) {
|
|
11497
13334
|
return;
|
|
11498
13335
|
}
|
|
@@ -11632,16 +13469,41 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11632
13469
|
if (session.authenticated && session.restoredFromDevice) {
|
|
11633
13470
|
setSessionCookie(res, config);
|
|
11634
13471
|
}
|
|
13472
|
+
return writeJson(res, 200, buildSessionPayload({ config, state, session }));
|
|
13473
|
+
}
|
|
13474
|
+
|
|
13475
|
+
// Collapses the PWA's boot-time fan-out (session + inbox + timeline
|
|
13476
|
+
// + devices) into a single HTTPS round-trip. iOS Safari tears down
|
|
13477
|
+
// connections aggressively, so each parallel fetch pays its own
|
|
13478
|
+
// TLS handshake — and `JSON.stringify(state)` hiccups on other
|
|
13479
|
+
// requests make those handshakes bursty. One endpoint = one
|
|
13480
|
+
// handshake + one response, which drops the "Completed list is
|
|
13481
|
+
// blank for several seconds after launch" latency floor.
|
|
13482
|
+
if (url.pathname === "/api/bootstrap" && req.method === "GET") {
|
|
13483
|
+
const session = readSession(req, config, state);
|
|
13484
|
+
const localeInfo = resolveDeviceLocaleInfo(config, state, session.deviceId);
|
|
13485
|
+
if (session.authenticated && session.deviceId) {
|
|
13486
|
+
const trustChanged = touchDeviceTrust(state, config, session.deviceId);
|
|
13487
|
+
const metadataChanged = updateCurrentDeviceSnapshot(state, config, session.deviceId, {
|
|
13488
|
+
userAgent: requestUserAgent(req),
|
|
13489
|
+
lastLocale: localeInfo.locale,
|
|
13490
|
+
});
|
|
13491
|
+
if (trustChanged || metadataChanged) {
|
|
13492
|
+
await saveState(config.stateFile, state);
|
|
13493
|
+
}
|
|
13494
|
+
}
|
|
13495
|
+
if (session.authenticated && session.restoredFromDevice) {
|
|
13496
|
+
setSessionCookie(res, config);
|
|
13497
|
+
}
|
|
13498
|
+
const sessionPayload = buildSessionPayload({ config, state, session });
|
|
13499
|
+
if (!session.authenticated) {
|
|
13500
|
+
return writeJson(res, 200, { session: sessionPayload });
|
|
13501
|
+
}
|
|
11635
13502
|
return writeJson(res, 200, {
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
httpsEnabled: config.nativeApprovalPublicBaseUrl.startsWith("https://"),
|
|
11641
|
-
appVersion: appPackageVersion,
|
|
11642
|
-
deviceId: session.deviceId || null,
|
|
11643
|
-
temporaryPairing: session.temporaryPairing === true,
|
|
11644
|
-
...buildSessionLocalePayload(config, state, session.deviceId),
|
|
13503
|
+
session: sessionPayload,
|
|
13504
|
+
inbox: buildInboxFastResponse(runtime, state, config, localeInfo.locale),
|
|
13505
|
+
timeline: buildTimelineResponse(runtime, state, config, localeInfo.locale),
|
|
13506
|
+
devices: buildDevicesResponse({ config, state, session, locale: localeInfo.locale }),
|
|
11645
13507
|
});
|
|
11646
13508
|
}
|
|
11647
13509
|
|
|
@@ -11756,7 +13618,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11756
13618
|
lastLocale: resolveDeviceLocaleInfo(config, state, session.deviceId).locale,
|
|
11757
13619
|
}) || changed;
|
|
11758
13620
|
if (changed) {
|
|
11759
|
-
|
|
13621
|
+
scheduleSaveState(config, state);
|
|
11760
13622
|
}
|
|
11761
13623
|
return writeJson(res, 200, {
|
|
11762
13624
|
ok: true,
|
|
@@ -11778,12 +13640,80 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11778
13640
|
const enabled = payload?.enabled === true;
|
|
11779
13641
|
if (state.claudeAwayMode !== enabled) {
|
|
11780
13642
|
state.claudeAwayMode = enabled;
|
|
11781
|
-
|
|
13643
|
+
scheduleSaveState(config, state);
|
|
11782
13644
|
}
|
|
11783
13645
|
await syncClaudeAwayModeSentinel(config, enabled);
|
|
11784
13646
|
return writeJson(res, 200, { ok: true, enabled });
|
|
11785
13647
|
}
|
|
11786
13648
|
|
|
13649
|
+
if (url.pathname === "/api/settings/auto-pilot" && req.method === "POST") {
|
|
13650
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
13651
|
+
if (!session) {
|
|
13652
|
+
return;
|
|
13653
|
+
}
|
|
13654
|
+
let payload;
|
|
13655
|
+
try {
|
|
13656
|
+
payload = await parseJsonBody(req);
|
|
13657
|
+
} catch {
|
|
13658
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
13659
|
+
}
|
|
13660
|
+
const trustedReadsEnabled =
|
|
13661
|
+
typeof payload?.trustedReadsEnabled === "boolean"
|
|
13662
|
+
? payload.trustedReadsEnabled === true
|
|
13663
|
+
: state.autoPilotTrustedReads === true;
|
|
13664
|
+
const hasLaneUpdate =
|
|
13665
|
+
typeof payload?.writeLaneContentEnabled === "boolean" ||
|
|
13666
|
+
typeof payload?.writeLaneUiTestsEnabled === "boolean" ||
|
|
13667
|
+
typeof payload?.writeLaneSourceEnabled === "boolean";
|
|
13668
|
+
const legacyTrustedWritesEnabled =
|
|
13669
|
+
typeof payload?.trustedWritesEnabled === "boolean"
|
|
13670
|
+
? payload.trustedWritesEnabled === true
|
|
13671
|
+
: typeof payload?.trustedWritesShadowEnabled === "boolean"
|
|
13672
|
+
? payload.trustedWritesShadowEnabled === true
|
|
13673
|
+
: null;
|
|
13674
|
+
const currentLanes = autoPilotWriteLaneState(state);
|
|
13675
|
+
const writeLaneContentEnabled =
|
|
13676
|
+
typeof payload?.writeLaneContentEnabled === "boolean"
|
|
13677
|
+
? payload.writeLaneContentEnabled === true
|
|
13678
|
+
: !hasLaneUpdate && legacyTrustedWritesEnabled != null
|
|
13679
|
+
? legacyTrustedWritesEnabled
|
|
13680
|
+
: currentLanes.content;
|
|
13681
|
+
const writeLaneUiTestsEnabled =
|
|
13682
|
+
typeof payload?.writeLaneUiTestsEnabled === "boolean"
|
|
13683
|
+
? payload.writeLaneUiTestsEnabled === true
|
|
13684
|
+
: currentLanes.uiTests;
|
|
13685
|
+
const writeLaneSourceEnabled =
|
|
13686
|
+
typeof payload?.writeLaneSourceEnabled === "boolean"
|
|
13687
|
+
? payload.writeLaneSourceEnabled === true
|
|
13688
|
+
: currentLanes.source;
|
|
13689
|
+
const trustedWritesEnabled =
|
|
13690
|
+
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled;
|
|
13691
|
+
if (
|
|
13692
|
+
state.autoPilotTrustedReads !== trustedReadsEnabled ||
|
|
13693
|
+
state.autoPilotWriteLaneContent !== writeLaneContentEnabled ||
|
|
13694
|
+
state.autoPilotWriteLaneUiTests !== writeLaneUiTestsEnabled ||
|
|
13695
|
+
state.autoPilotWriteLaneSource !== writeLaneSourceEnabled ||
|
|
13696
|
+
isAutoPilotTrustedWritesEnabled(state) !== trustedWritesEnabled
|
|
13697
|
+
) {
|
|
13698
|
+
state.autoPilotTrustedReads = trustedReadsEnabled;
|
|
13699
|
+
state.autoPilotTrustedWrites = trustedWritesEnabled;
|
|
13700
|
+
state.autoPilotTrustedWritesShadow = false;
|
|
13701
|
+
state.autoPilotWriteLaneContent = writeLaneContentEnabled;
|
|
13702
|
+
state.autoPilotWriteLaneUiTests = writeLaneUiTestsEnabled;
|
|
13703
|
+
state.autoPilotWriteLaneSource = writeLaneSourceEnabled;
|
|
13704
|
+
await saveState(config.stateFile, state);
|
|
13705
|
+
}
|
|
13706
|
+
return writeJson(res, 200, {
|
|
13707
|
+
ok: true,
|
|
13708
|
+
trustedReadsEnabled,
|
|
13709
|
+
trustedWritesEnabled,
|
|
13710
|
+
trustedWritesShadowEnabled: false,
|
|
13711
|
+
writeLaneContentEnabled,
|
|
13712
|
+
writeLaneUiTestsEnabled,
|
|
13713
|
+
writeLaneSourceEnabled,
|
|
13714
|
+
});
|
|
13715
|
+
}
|
|
13716
|
+
|
|
11787
13717
|
if (url.pathname === "/api/settings/a2a-executor" && req.method === "POST") {
|
|
11788
13718
|
const session = requireMutatingApiSession(req, res, config, state);
|
|
11789
13719
|
if (!session) return;
|
|
@@ -11797,7 +13727,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11797
13727
|
const preference = valid.has(payload?.preference) ? payload.preference : "auto";
|
|
11798
13728
|
if (state.a2aExecutorPreference !== preference) {
|
|
11799
13729
|
state.a2aExecutorPreference = preference;
|
|
11800
|
-
|
|
13730
|
+
scheduleSaveState(config, state);
|
|
11801
13731
|
}
|
|
11802
13732
|
return writeJson(res, 200, { ok: true, preference });
|
|
11803
13733
|
}
|
|
@@ -11967,6 +13897,251 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11967
13897
|
});
|
|
11968
13898
|
}
|
|
11969
13899
|
|
|
13900
|
+
|
|
13901
|
+
if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
|
|
13902
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
13903
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
13904
|
+
if (!hookAuth) {
|
|
13905
|
+
const session = requireApiSession(req, res, config, state);
|
|
13906
|
+
if (!session) return;
|
|
13907
|
+
}
|
|
13908
|
+
if (!config.hazbaseApiUrl) {
|
|
13909
|
+
return writeJson(res, 200, { enabled: false });
|
|
13910
|
+
}
|
|
13911
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
13912
|
+
const [paymentsResult, chainsResult] = await Promise.allSettled([
|
|
13913
|
+
fetchHazbaseMetadata(config, "/api/meta/payments"),
|
|
13914
|
+
fetchHazbaseMetadata(config, "/api/meta/chains"),
|
|
13915
|
+
]);
|
|
13916
|
+
const payments = paymentsResult.status === "fulfilled" ? paymentsResult.value : null;
|
|
13917
|
+
const chains = chainsResult.status === "fulfilled" ? chainsResult.value : null;
|
|
13918
|
+
const errors = [];
|
|
13919
|
+
if (paymentsResult.status === "rejected") {
|
|
13920
|
+
errors.push(paymentsResult.reason?.message || String(paymentsResult.reason || "payments metadata unavailable"));
|
|
13921
|
+
}
|
|
13922
|
+
if (chainsResult.status === "rejected") {
|
|
13923
|
+
errors.push(chainsResult.reason?.message || String(chainsResult.reason || "chains metadata unavailable"));
|
|
13924
|
+
}
|
|
13925
|
+
const supportedChains = Array.isArray(chains?.chains)
|
|
13926
|
+
? chains.chains.filter((entry) => Number(entry.chainId) === 8453 || Number(entry.chainId) === 84532)
|
|
13927
|
+
: [];
|
|
13928
|
+
const payoutAddresses = {
|
|
13929
|
+
8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
|
|
13930
|
+
84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
|
|
13931
|
+
};
|
|
13932
|
+
return writeJson(res, 200, {
|
|
13933
|
+
enabled: true,
|
|
13934
|
+
apiUrl: config.hazbaseApiUrl,
|
|
13935
|
+
signedIn: Boolean(hazbase.accessToken),
|
|
13936
|
+
email: hazbase.email,
|
|
13937
|
+
userId: hazbase.userId,
|
|
13938
|
+
sessionId: hazbase.sessionId,
|
|
13939
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
13940
|
+
credentialId: hazbase.credentialId,
|
|
13941
|
+
highTrustExpiresAt: hazbase.highTrustExpiresAt,
|
|
13942
|
+
accounts: hazbase.accounts,
|
|
13943
|
+
supportedPayments: payments?.networks || [],
|
|
13944
|
+
supportedChains,
|
|
13945
|
+
defaultPaymentNetwork: payments?.defaultNetwork || "base-sepolia",
|
|
13946
|
+
payoutAddresses,
|
|
13947
|
+
error: errors.join(" | "),
|
|
13948
|
+
});
|
|
13949
|
+
}
|
|
13950
|
+
|
|
13951
|
+
if (url.pathname === "/api/hazbase/payout-address" && req.method === "GET") {
|
|
13952
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
13953
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
13954
|
+
if (!hookAuth) {
|
|
13955
|
+
const session = requireApiSession(req, res, config, state);
|
|
13956
|
+
if (!session) return;
|
|
13957
|
+
}
|
|
13958
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
13959
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
13960
|
+
const chainId = Number(url.searchParams.get("chainId") || 0);
|
|
13961
|
+
if (chainId !== 8453 && chainId !== 84532) {
|
|
13962
|
+
return writeJson(res, 400, { error: "unsupported-chain" });
|
|
13963
|
+
}
|
|
13964
|
+
const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
|
|
13965
|
+
if (!account?.smartAccountAddress) {
|
|
13966
|
+
return writeJson(res, 409, { error: "hazbase-wallet-account-missing" });
|
|
13967
|
+
}
|
|
13968
|
+
return writeJson(res, 200, {
|
|
13969
|
+
chainId,
|
|
13970
|
+
payoutMethod: "hazbase_wallet",
|
|
13971
|
+
payoutAddress: account.smartAccountAddress,
|
|
13972
|
+
smartAccountAddress: account.smartAccountAddress,
|
|
13973
|
+
accountVariant: account.accountVariant || "",
|
|
13974
|
+
relayMode: account.relayMode || "",
|
|
13975
|
+
});
|
|
13976
|
+
}
|
|
13977
|
+
|
|
13978
|
+
if (url.pathname === "/api/hazbase/request-otp" && req.method === "POST") {
|
|
13979
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
13980
|
+
if (!session) return;
|
|
13981
|
+
const body = await parseJsonBody(req);
|
|
13982
|
+
const email = cleanText(body?.email || "");
|
|
13983
|
+
if (!email) return writeJson(res, 400, { error: "email-required" });
|
|
13984
|
+
const result = await requestHazbaseEmailOtp({ email });
|
|
13985
|
+
state.hazbase = { ...normalizeHazbaseState(state.hazbase), email };
|
|
13986
|
+
await saveState(config.stateFile, state);
|
|
13987
|
+
return writeJson(res, 200, result);
|
|
13988
|
+
}
|
|
13989
|
+
|
|
13990
|
+
if (url.pathname === "/api/hazbase/verify-otp" && req.method === "POST") {
|
|
13991
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
13992
|
+
if (!session) return;
|
|
13993
|
+
const body = await parseJsonBody(req);
|
|
13994
|
+
const email = cleanText(body?.email || state.hazbase?.email || "");
|
|
13995
|
+
const code = cleanText(body?.code || "");
|
|
13996
|
+
if (!email || !code) return writeJson(res, 400, { error: "otp-required" });
|
|
13997
|
+
const result = await verifyHazbaseEmailOtp({ email, code });
|
|
13998
|
+
state.hazbase = {
|
|
13999
|
+
...normalizeHazbaseState(state.hazbase),
|
|
14000
|
+
email: result.email || email,
|
|
14001
|
+
accessToken: cleanText(result.accessToken || ""),
|
|
14002
|
+
sessionId: cleanText(result.sessionId || ""),
|
|
14003
|
+
userId: cleanText(result.userId || ""),
|
|
14004
|
+
accounts: Array.isArray(result.accounts) ? result.accounts : [],
|
|
14005
|
+
highTrustToken: "",
|
|
14006
|
+
highTrustExpiresAt: "",
|
|
14007
|
+
};
|
|
14008
|
+
await saveState(config.stateFile, state);
|
|
14009
|
+
return writeJson(res, 200, result);
|
|
14010
|
+
}
|
|
14011
|
+
|
|
14012
|
+
if (url.pathname === "/api/hazbase/passkey/register/challenge" && req.method === "POST") {
|
|
14013
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14014
|
+
if (!session) return;
|
|
14015
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14016
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14017
|
+
const body = await parseJsonBody(req);
|
|
14018
|
+
let partnerOrigin;
|
|
14019
|
+
try {
|
|
14020
|
+
partnerOrigin = deriveHazbasePartnerPasskeyOrigin(req, config);
|
|
14021
|
+
} catch (error) {
|
|
14022
|
+
return writeJson(res, 400, {
|
|
14023
|
+
error: error?.code || "hazbase-passkey-local-host-required",
|
|
14024
|
+
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14025
|
+
});
|
|
14026
|
+
}
|
|
14027
|
+
const result = await requestPasskeyRegistrationChallenge({
|
|
14028
|
+
emailSession: hazbase.accessToken,
|
|
14029
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14030
|
+
partnerOrigin,
|
|
14031
|
+
});
|
|
14032
|
+
return writeJson(res, 200, result);
|
|
14033
|
+
}
|
|
14034
|
+
|
|
14035
|
+
if (url.pathname === "/api/hazbase/passkey/register/complete" && req.method === "POST") {
|
|
14036
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14037
|
+
if (!session) return;
|
|
14038
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14039
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14040
|
+
const body = await parseJsonBody(req);
|
|
14041
|
+
const result = await completePasskeyRegistration({
|
|
14042
|
+
emailSession: hazbase.accessToken,
|
|
14043
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14044
|
+
credential: body?.credential,
|
|
14045
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14046
|
+
});
|
|
14047
|
+
state.hazbase = {
|
|
14048
|
+
...hazbase,
|
|
14049
|
+
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14050
|
+
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14051
|
+
};
|
|
14052
|
+
await saveState(config.stateFile, state);
|
|
14053
|
+
return writeJson(res, 200, result);
|
|
14054
|
+
}
|
|
14055
|
+
|
|
14056
|
+
if (url.pathname === "/api/hazbase/passkey/assert/challenge" && req.method === "POST") {
|
|
14057
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14058
|
+
if (!session) return;
|
|
14059
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14060
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14061
|
+
const body = await parseJsonBody(req);
|
|
14062
|
+
let partnerOrigin;
|
|
14063
|
+
try {
|
|
14064
|
+
partnerOrigin = deriveHazbasePartnerPasskeyOrigin(req, config);
|
|
14065
|
+
} catch (error) {
|
|
14066
|
+
return writeJson(res, 400, {
|
|
14067
|
+
error: error?.code || "hazbase-passkey-local-host-required",
|
|
14068
|
+
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14069
|
+
});
|
|
14070
|
+
}
|
|
14071
|
+
const result = await requestPasskeyAssertionChallenge({
|
|
14072
|
+
emailSession: hazbase.accessToken,
|
|
14073
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14074
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14075
|
+
partnerOrigin,
|
|
14076
|
+
});
|
|
14077
|
+
return writeJson(res, 200, result);
|
|
14078
|
+
}
|
|
14079
|
+
|
|
14080
|
+
if (url.pathname === "/api/hazbase/passkey/assert/complete" && req.method === "POST") {
|
|
14081
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14082
|
+
if (!session) return;
|
|
14083
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14084
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14085
|
+
const body = await parseJsonBody(req);
|
|
14086
|
+
const result = await completePasskeyAssertion({
|
|
14087
|
+
emailSession: hazbase.accessToken,
|
|
14088
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14089
|
+
credential: body?.credential,
|
|
14090
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14091
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14092
|
+
});
|
|
14093
|
+
state.hazbase = {
|
|
14094
|
+
...hazbase,
|
|
14095
|
+
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14096
|
+
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14097
|
+
highTrustToken: cleanText(result.highTrustToken || ""),
|
|
14098
|
+
highTrustExpiresAt: cleanText(result.highTrustExpiresAt || ""),
|
|
14099
|
+
};
|
|
14100
|
+
await saveState(config.stateFile, state);
|
|
14101
|
+
return writeJson(res, 200, result);
|
|
14102
|
+
}
|
|
14103
|
+
|
|
14104
|
+
if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST") {
|
|
14105
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14106
|
+
if (!session) return;
|
|
14107
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14108
|
+
if (!hazbase.accessToken || !hazbase.deviceBindingId || !hazbase.highTrustToken) {
|
|
14109
|
+
return writeJson(res, 409, { error: "hazbase-bootstrap-prerequisites-missing" });
|
|
14110
|
+
}
|
|
14111
|
+
const body = await parseJsonBody(req);
|
|
14112
|
+
const chainId = Number(body?.chainId || 0);
|
|
14113
|
+
if (chainId !== 8453 && chainId !== 84532) {
|
|
14114
|
+
return writeJson(res, 400, { error: "unsupported-chain" });
|
|
14115
|
+
}
|
|
14116
|
+
const result = await bootstrapPasskeyAccount({
|
|
14117
|
+
emailSession: hazbase.accessToken,
|
|
14118
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
14119
|
+
highTrustToken: hazbase.highTrustToken,
|
|
14120
|
+
chainId,
|
|
14121
|
+
});
|
|
14122
|
+
const nextAccounts = mergeHazbaseAccountSummaries(hazbase.accounts, [{
|
|
14123
|
+
smartAccountAddress: result.smartAccountAddress,
|
|
14124
|
+
chainId: result.chainId,
|
|
14125
|
+
accountVariant: result.accountVariant,
|
|
14126
|
+
relayMode: result.relayMode,
|
|
14127
|
+
primaryDeviceBindingId: result.deviceBindingId,
|
|
14128
|
+
}]);
|
|
14129
|
+
state.hazbase = {
|
|
14130
|
+
...hazbase,
|
|
14131
|
+
accounts: nextAccounts,
|
|
14132
|
+
};
|
|
14133
|
+
await saveState(config.stateFile, state);
|
|
14134
|
+
return writeJson(res, 200, result);
|
|
14135
|
+
}
|
|
14136
|
+
|
|
14137
|
+
if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
14138
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14139
|
+
if (!session) return;
|
|
14140
|
+
state.hazbase = normalizeHazbaseState(null);
|
|
14141
|
+
await saveState(config.stateFile, state);
|
|
14142
|
+
return writeJson(res, 200, { ok: true });
|
|
14143
|
+
}
|
|
14144
|
+
|
|
11970
14145
|
// Toggle acceptPublicTasks on the A2A relay.
|
|
11971
14146
|
if (url.pathname === "/api/a2a/public-tasks" && req.method === "POST") {
|
|
11972
14147
|
const session = requireApiSession(req, res, config, state);
|
|
@@ -11975,11 +14150,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11975
14150
|
const accept = body?.accept === true;
|
|
11976
14151
|
config.a2aAcceptPublicTasks = accept;
|
|
11977
14152
|
state.a2aAcceptPublicTasks = accept;
|
|
11978
|
-
|
|
11979
|
-
await saveState(config.stateFile, state);
|
|
11980
|
-
} catch (error) {
|
|
11981
|
-
console.error(`[a2a-public-tasks-save] ${error.message}`);
|
|
11982
|
-
}
|
|
14153
|
+
scheduleSaveState(config, state);
|
|
11983
14154
|
// Re-register with relay to propagate the flag.
|
|
11984
14155
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
11985
14156
|
try {
|
|
@@ -12154,6 +14325,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12154
14325
|
await deliverWebPushItem({
|
|
12155
14326
|
config, state,
|
|
12156
14327
|
kind: "thread_share",
|
|
14328
|
+
tab: "inbox",
|
|
14329
|
+
subtab: "pending",
|
|
12157
14330
|
token,
|
|
12158
14331
|
stableId: `thread_share:${shareId}`,
|
|
12159
14332
|
title: `Thread Share: ${sourceLabel || sourceTool || "agent"} → ${targetLabel || targetTool}`,
|
|
@@ -12251,6 +14424,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12251
14424
|
await deliverWebPushItem({
|
|
12252
14425
|
config, state,
|
|
12253
14426
|
kind: "thread_share",
|
|
14427
|
+
tab: "inbox",
|
|
14428
|
+
subtab: "completed",
|
|
12254
14429
|
token: share.token,
|
|
12255
14430
|
stableId: `thread_share_fallback:${share.shareId}`,
|
|
12256
14431
|
title: "Thread Share: target unreachable",
|
|
@@ -12393,7 +14568,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12393
14568
|
lastLocale: resolveDeviceLocaleInfo(config, state, session.deviceId).locale,
|
|
12394
14569
|
});
|
|
12395
14570
|
if (changed || metadataChanged) {
|
|
12396
|
-
|
|
14571
|
+
scheduleSaveState(config, state);
|
|
12397
14572
|
}
|
|
12398
14573
|
return writeJson(res, 200, {
|
|
12399
14574
|
ok: true,
|
|
@@ -12416,7 +14591,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12416
14591
|
changed = deletePushSubscriptionsForDevice(state, session.deviceId) || changed;
|
|
12417
14592
|
}
|
|
12418
14593
|
if (changed) {
|
|
12419
|
-
|
|
14594
|
+
scheduleSaveState(config, state);
|
|
12420
14595
|
}
|
|
12421
14596
|
return writeJson(res, 200, { ok: true, subscribed: false });
|
|
12422
14597
|
}
|
|
@@ -12428,13 +14603,13 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12428
14603
|
}
|
|
12429
14604
|
try {
|
|
12430
14605
|
await sendPushTestToDevice({ config, state, session });
|
|
12431
|
-
|
|
14606
|
+
scheduleSaveState(config, state);
|
|
12432
14607
|
return writeJson(res, 200, { ok: true });
|
|
12433
14608
|
} catch (error) {
|
|
12434
14609
|
const statusCode = Number(error?.statusCode) || 0;
|
|
12435
14610
|
if (statusCode === 404 || statusCode === 410) {
|
|
12436
14611
|
deletePushSubscriptionsForDevice(state, session.deviceId);
|
|
12437
|
-
|
|
14612
|
+
scheduleSaveState(config, state);
|
|
12438
14613
|
return writeJson(res, 410, { error: "push-subscription-expired" });
|
|
12439
14614
|
}
|
|
12440
14615
|
return writeJson(res, 500, {
|
|
@@ -12640,6 +14815,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12640
14815
|
diffSource: String(body.diffSource || "claude_permission_request"),
|
|
12641
14816
|
diffAddedLines: Math.max(0, Number(body.diffAddedLines) || 0),
|
|
12642
14817
|
diffRemovedLines: Math.max(0, Number(body.diffRemovedLines) || 0),
|
|
14818
|
+
cwd: body.cwd ? String(body.cwd) : "",
|
|
14819
|
+
workspaceRoot: body.cwd ? String(body.cwd) : "",
|
|
12643
14820
|
createdAtMs: Number(body.createdAtMs) || Date.now(),
|
|
12644
14821
|
resolved: false,
|
|
12645
14822
|
resolving: false,
|
|
@@ -12653,6 +14830,59 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12653
14830
|
readOnly: body.notifyOnly === true,
|
|
12654
14831
|
};
|
|
12655
14832
|
|
|
14833
|
+
const claudeAutoPilotMatch =
|
|
14834
|
+
approval.kind === "command"
|
|
14835
|
+
? maybeAutoApproveTrustedRead({
|
|
14836
|
+
state,
|
|
14837
|
+
commandText: body?.toolInput?.command ?? "",
|
|
14838
|
+
cwd: body.cwd ? String(body.cwd) : "",
|
|
14839
|
+
workspaceRoot: body.cwd ? String(body.cwd) : "",
|
|
14840
|
+
})
|
|
14841
|
+
: null;
|
|
14842
|
+
if (claudeAutoPilotMatch) {
|
|
14843
|
+
try {
|
|
14844
|
+
await recordAutoApprovedTrustedRead({
|
|
14845
|
+
config,
|
|
14846
|
+
runtime,
|
|
14847
|
+
state,
|
|
14848
|
+
approval,
|
|
14849
|
+
policyMatch: claudeAutoPilotMatch,
|
|
14850
|
+
});
|
|
14851
|
+
console.log(`[auto-pilot-approved] ${approval.requestKey} | ${claudeAutoPilotMatch.command}`);
|
|
14852
|
+
return writeJson(res, 200, {
|
|
14853
|
+
permissionDecision: "allow",
|
|
14854
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedReadReason"),
|
|
14855
|
+
});
|
|
14856
|
+
} catch (error) {
|
|
14857
|
+
console.error(`[auto-pilot-error] ${approval.requestKey} | ${error.message}`);
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
|
|
14861
|
+
const claudeAutoPilotWriteCandidate =
|
|
14862
|
+
approval.kind === "file"
|
|
14863
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval })
|
|
14864
|
+
: null;
|
|
14865
|
+
if (claudeAutoPilotWriteCandidate) {
|
|
14866
|
+
try {
|
|
14867
|
+
await recordAutoApprovedTrustedWrite({
|
|
14868
|
+
config,
|
|
14869
|
+
runtime,
|
|
14870
|
+
state,
|
|
14871
|
+
approval,
|
|
14872
|
+
candidate: claudeAutoPilotWriteCandidate,
|
|
14873
|
+
});
|
|
14874
|
+
console.log(
|
|
14875
|
+
`[auto-pilot-approved-write] ${approval.requestKey} | files=${claudeAutoPilotWriteCandidate.fileRefs.length} | lines=${claudeAutoPilotWriteCandidate.totalChangedLines}`
|
|
14876
|
+
);
|
|
14877
|
+
return writeJson(res, 200, {
|
|
14878
|
+
permissionDecision: "allow",
|
|
14879
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedWriteReason"),
|
|
14880
|
+
});
|
|
14881
|
+
} catch (error) {
|
|
14882
|
+
console.error(`[auto-pilot-write-error] ${approval.requestKey} | ${error.message}`);
|
|
14883
|
+
}
|
|
14884
|
+
}
|
|
14885
|
+
|
|
12656
14886
|
runtime.nativeApprovalsByToken.set(token, approval);
|
|
12657
14887
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
12658
14888
|
|
|
@@ -12660,6 +14890,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12660
14890
|
config,
|
|
12661
14891
|
state,
|
|
12662
14892
|
kind: "approval",
|
|
14893
|
+
tab: "inbox",
|
|
14894
|
+
subtab: "pending",
|
|
12663
14895
|
token: approval.token,
|
|
12664
14896
|
stableId: pendingApprovalStableId(approval),
|
|
12665
14897
|
title: approval.title,
|
|
@@ -12789,6 +15021,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12789
15021
|
config,
|
|
12790
15022
|
state,
|
|
12791
15023
|
kind: "moltbook_reply",
|
|
15024
|
+
tab: "inbox",
|
|
15025
|
+
subtab: "pending",
|
|
12792
15026
|
token,
|
|
12793
15027
|
stableId: `moltbook_reply:${item.sourceId}`,
|
|
12794
15028
|
title: item.title,
|
|
@@ -12970,6 +15204,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12970
15204
|
config,
|
|
12971
15205
|
state,
|
|
12972
15206
|
kind: "moltbook_draft",
|
|
15207
|
+
tab: "inbox",
|
|
15208
|
+
subtab: "pending",
|
|
12973
15209
|
token,
|
|
12974
15210
|
stableId: `moltbook_draft:${sourceId}`,
|
|
12975
15211
|
title: pushTitle,
|
|
@@ -13066,6 +15302,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13066
15302
|
try {
|
|
13067
15303
|
await deliverWebPushItem({
|
|
13068
15304
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
15305
|
+
tab: "inbox",
|
|
15306
|
+
subtab: "completed",
|
|
13069
15307
|
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
13070
15308
|
title: "Draft post failed",
|
|
13071
15309
|
body: String(postError.message || "").slice(0, 160),
|
|
@@ -13089,7 +15327,16 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13089
15327
|
return;
|
|
13090
15328
|
}
|
|
13091
15329
|
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
13092
|
-
return writeJson(res, 200,
|
|
15330
|
+
return writeJson(res, 200, buildInboxFastResponse(runtime, state, config, locale));
|
|
15331
|
+
}
|
|
15332
|
+
|
|
15333
|
+
if (url.pathname === "/api/inbox/diff" && req.method === "GET") {
|
|
15334
|
+
const session = requireApiSession(req, res, config, state);
|
|
15335
|
+
if (!session) {
|
|
15336
|
+
return;
|
|
15337
|
+
}
|
|
15338
|
+
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
15339
|
+
return writeJson(res, 200, await buildInboxDiffResponse(runtime, state, config, locale));
|
|
13093
15340
|
}
|
|
13094
15341
|
|
|
13095
15342
|
if (url.pathname === "/api/timeline" && req.method === "GET") {
|
|
@@ -15038,6 +17285,12 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15038
17285
|
const finalTitle = draft.decision.title || draft.postTitle;
|
|
15039
17286
|
|
|
15040
17287
|
let verification;
|
|
17288
|
+
// Captured so the post-verify history record knows which post/comment the
|
|
17289
|
+
// challenge_text was attached to. `draft.postId` only exists for replies;
|
|
17290
|
+
// for `original_post` drafts the id only shows up in the POST /posts
|
|
17291
|
+
// response.
|
|
17292
|
+
let createdPostId = null;
|
|
17293
|
+
let createdCommentId = null;
|
|
15041
17294
|
|
|
15042
17295
|
if (draft.draftType === "original_post") {
|
|
15043
17296
|
const result = await mb("/posts", {
|
|
@@ -15051,6 +17304,7 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15051
17304
|
});
|
|
15052
17305
|
const post = result?.post || null;
|
|
15053
17306
|
verification = post?.verification || null;
|
|
17307
|
+
createdPostId = post?.id || null;
|
|
15054
17308
|
console.log(`[moltbook-draft-post] Posted original post (id=${post?.id})`);
|
|
15055
17309
|
|
|
15056
17310
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
@@ -15066,6 +17320,8 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15066
17320
|
});
|
|
15067
17321
|
const comment = result?.comment || null;
|
|
15068
17322
|
verification = comment?.verification || null;
|
|
17323
|
+
createdPostId = draft.postId || null;
|
|
17324
|
+
createdCommentId = comment?.id || null;
|
|
15069
17325
|
console.log(`[moltbook-draft-post] Posted reply (commentId=${comment?.id}) to post ${draft.postId}`);
|
|
15070
17326
|
|
|
15071
17327
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
@@ -15080,41 +17336,79 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15080
17336
|
}
|
|
15081
17337
|
|
|
15082
17338
|
// Solve verification puzzle if present.
|
|
17339
|
+
//
|
|
17340
|
+
// Every attempt — success, server rejection, or complete failure — gets
|
|
17341
|
+
// recorded via `recordPuzzleAttempt` so we can build a corpus of
|
|
17342
|
+
// challenge_text at `~/.viveworker/moltbook-verify-history.jsonl`. This is
|
|
17343
|
+
// the only way to reconstruct failing puzzles; Moltbook only returns
|
|
17344
|
+
// `challenge_text` in the initial POST response.
|
|
15083
17345
|
if (verification) {
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
17346
|
+
const solverAnswer = solveVerificationPuzzle(verification.challenge_text);
|
|
17347
|
+
let submittedAnswer = solverAnswer;
|
|
17348
|
+
let llmAnswer = null;
|
|
17349
|
+
let outcome = "unknown";
|
|
17350
|
+
let verifyError = null;
|
|
17351
|
+
|
|
17352
|
+
if (submittedAnswer == null) {
|
|
17353
|
+
llmAnswer = await solvePuzzleWithLLM(verification.challenge_text);
|
|
17354
|
+
submittedAnswer = llmAnswer;
|
|
15088
17355
|
}
|
|
15089
|
-
|
|
17356
|
+
|
|
17357
|
+
if (submittedAnswer != null) {
|
|
15090
17358
|
try {
|
|
15091
17359
|
await mb("/verify", {
|
|
15092
17360
|
method: "POST",
|
|
15093
|
-
body: JSON.stringify({ verification_code: verification.verification_code, answer }),
|
|
17361
|
+
body: JSON.stringify({ verification_code: verification.verification_code, answer: submittedAnswer }),
|
|
15094
17362
|
});
|
|
15095
|
-
console.log(`[moltbook-draft-verify] Verified with answer ${
|
|
15096
|
-
|
|
17363
|
+
console.log(`[moltbook-draft-verify] Verified with answer ${submittedAnswer}`);
|
|
17364
|
+
outcome = solverAnswer != null && llmAnswer == null ? "solver-verified" : "llm-verified";
|
|
17365
|
+
} catch (err) {
|
|
17366
|
+
verifyError = err.message;
|
|
15097
17367
|
// Wrong answer from solver — retry with LLM.
|
|
15098
|
-
if (/incorrect/i.test(
|
|
15099
|
-
|
|
15100
|
-
if (llmAnswer && llmAnswer !==
|
|
17368
|
+
if (/incorrect/i.test(err.message) && solverAnswer != null && llmAnswer == null) {
|
|
17369
|
+
llmAnswer = await solvePuzzleWithLLM(verification.challenge_text);
|
|
17370
|
+
if (llmAnswer && llmAnswer !== solverAnswer) {
|
|
15101
17371
|
try {
|
|
15102
17372
|
await mb("/verify", {
|
|
15103
17373
|
method: "POST",
|
|
15104
17374
|
body: JSON.stringify({ verification_code: verification.verification_code, answer: llmAnswer }),
|
|
15105
17375
|
});
|
|
15106
17376
|
console.log(`[moltbook-draft-verify] Verified with LLM retry answer ${llmAnswer}`);
|
|
17377
|
+
submittedAnswer = llmAnswer;
|
|
17378
|
+
verifyError = null;
|
|
17379
|
+
outcome = "solver-incorrect-llm-verified";
|
|
15107
17380
|
} catch (retryError) {
|
|
15108
17381
|
console.error(`[moltbook-draft-verify] LLM retry failed: ${retryError.message}`);
|
|
17382
|
+
verifyError = retryError.message;
|
|
17383
|
+
outcome = "both-incorrect";
|
|
15109
17384
|
}
|
|
17385
|
+
} else {
|
|
17386
|
+
outcome = "solver-incorrect-llm-agreed-or-null";
|
|
15110
17387
|
}
|
|
15111
17388
|
} else {
|
|
15112
|
-
console.error(`[moltbook-draft-verify] Failed: ${
|
|
17389
|
+
console.error(`[moltbook-draft-verify] Failed: ${err.message}`);
|
|
17390
|
+
outcome = solverAnswer != null ? "solver-rejected" : "llm-rejected";
|
|
15113
17391
|
}
|
|
15114
17392
|
}
|
|
15115
17393
|
} else {
|
|
15116
17394
|
console.error(`[moltbook-draft-verify] Could not solve puzzle for draft ${draft.token}`);
|
|
15117
|
-
|
|
17395
|
+
outcome = "both-failed-to-solve";
|
|
17396
|
+
}
|
|
17397
|
+
|
|
17398
|
+
// Best-effort corpus write. Never throws.
|
|
17399
|
+
await recordPuzzleAttempt({
|
|
17400
|
+
draftToken: draft.token,
|
|
17401
|
+
draftType: draft.draftType,
|
|
17402
|
+
postId: createdPostId,
|
|
17403
|
+
commentId: createdCommentId,
|
|
17404
|
+
challenge_text: verification.challenge_text,
|
|
17405
|
+
verification_code: verification.verification_code,
|
|
17406
|
+
solverAnswer,
|
|
17407
|
+
llmAnswer,
|
|
17408
|
+
submittedAnswer,
|
|
17409
|
+
outcome,
|
|
17410
|
+
verifyError,
|
|
17411
|
+
});
|
|
15118
17412
|
}
|
|
15119
17413
|
|
|
15120
17414
|
// Push notification for successful post.
|
|
@@ -15122,6 +17416,8 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15122
17416
|
const pushTitle = draft.draftType === "original_post" ? finalTitle : `Reply → ${draft.postTitle || "Moltbook"}`;
|
|
15123
17417
|
await deliverWebPushItem({
|
|
15124
17418
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
17419
|
+
tab: "inbox",
|
|
17420
|
+
subtab: "completed",
|
|
15125
17421
|
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
15126
17422
|
title: "Moltbook posted",
|
|
15127
17423
|
body: truncate(singleLine(pushTitle), 160),
|
|
@@ -15154,6 +17450,9 @@ function buildConfig(cli) {
|
|
|
15154
17450
|
a2aShareUrl: stripTrailingSlash(
|
|
15155
17451
|
process.env.VIVEWORKER_SHARE_URL || "https://share.viveworker.com"
|
|
15156
17452
|
),
|
|
17453
|
+
hazbaseApiUrl: stripTrailingSlash(process.env.HAZBASE_API_URL || "https://api.hazbase.com"),
|
|
17454
|
+
hazbaseClientKey: cleanText(process.env.HAZBASE_CLIENT_KEY || "viveworker-internal"),
|
|
17455
|
+
hazbaseDeviceLabel: cleanText(process.env.HAZBASE_DEVICE_LABEL || "viveworker"),
|
|
15157
17456
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
15158
17457
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
15159
17458
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -15241,6 +17540,110 @@ function buildConfig(cli) {
|
|
|
15241
17540
|
};
|
|
15242
17541
|
}
|
|
15243
17542
|
|
|
17543
|
+
|
|
17544
|
+
function normalizeHazbaseState(raw) {
|
|
17545
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
17546
|
+
return {
|
|
17547
|
+
email: cleanText(value.email ?? ""),
|
|
17548
|
+
accessToken: cleanText(value.accessToken ?? ""),
|
|
17549
|
+
sessionId: cleanText(value.sessionId ?? ""),
|
|
17550
|
+
userId: cleanText(value.userId ?? ""),
|
|
17551
|
+
deviceBindingId: cleanText(value.deviceBindingId ?? ""),
|
|
17552
|
+
credentialId: cleanText(value.credentialId ?? ""),
|
|
17553
|
+
highTrustToken: cleanText(value.highTrustToken ?? ""),
|
|
17554
|
+
highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
|
|
17555
|
+
accounts: Array.isArray(value.accounts) ? value.accounts : [],
|
|
17556
|
+
};
|
|
17557
|
+
}
|
|
17558
|
+
|
|
17559
|
+
function hazbasePasskeyLocalHostError() {
|
|
17560
|
+
const error = new Error("Open viveworker on its .local hostname to use hazBase passkeys.");
|
|
17561
|
+
error.code = "hazbase-passkey-local-host-required";
|
|
17562
|
+
return error;
|
|
17563
|
+
}
|
|
17564
|
+
|
|
17565
|
+
function deriveHazbasePartnerPasskeyOrigin(req, config) {
|
|
17566
|
+
const rawOrigin = cleanText(req.headers.origin || "");
|
|
17567
|
+
const rawHost = cleanText(req.headers.host || "");
|
|
17568
|
+
const candidateOrigin = rawOrigin || (rawHost ? `https://${rawHost}` : "");
|
|
17569
|
+
if (!candidateOrigin) {
|
|
17570
|
+
throw hazbasePasskeyLocalHostError();
|
|
17571
|
+
}
|
|
17572
|
+
|
|
17573
|
+
let parsed;
|
|
17574
|
+
try {
|
|
17575
|
+
parsed = new URL(candidateOrigin);
|
|
17576
|
+
} catch {
|
|
17577
|
+
throw hazbasePasskeyLocalHostError();
|
|
17578
|
+
}
|
|
17579
|
+
|
|
17580
|
+
const hostname = cleanText(parsed.hostname || "").toLowerCase();
|
|
17581
|
+
if (parsed.protocol !== "https:" || !hostname || net.isIP(hostname) || !hostname.endsWith(".local")) {
|
|
17582
|
+
throw hazbasePasskeyLocalHostError();
|
|
17583
|
+
}
|
|
17584
|
+
|
|
17585
|
+
return {
|
|
17586
|
+
origin: `${parsed.protocol}//${parsed.host}`.toLowerCase(),
|
|
17587
|
+
rpId: hostname,
|
|
17588
|
+
clientKey: config.hazbaseClientKey || "viveworker-internal",
|
|
17589
|
+
};
|
|
17590
|
+
}
|
|
17591
|
+
|
|
17592
|
+
function configureHazbaseClient(config) {
|
|
17593
|
+
try {
|
|
17594
|
+
setHazbaseClientKey(config.hazbaseClientKey || "viveworker-internal");
|
|
17595
|
+
setHazbaseApiEndpoint(config.hazbaseApiUrl || "https://api.hazbase.com");
|
|
17596
|
+
} catch (error) {
|
|
17597
|
+
console.error(`[hazbase-config] ${error.message}`);
|
|
17598
|
+
}
|
|
17599
|
+
}
|
|
17600
|
+
|
|
17601
|
+
async function fetchHazbaseMetadata(config, endpoint, timeoutMs = HAZBASE_METADATA_TIMEOUT_MS) {
|
|
17602
|
+
const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
|
|
17603
|
+
if (!baseUrl) return null;
|
|
17604
|
+
const timeout = Math.max(250, Number(timeoutMs) || HAZBASE_METADATA_TIMEOUT_MS);
|
|
17605
|
+
const controller = new AbortController();
|
|
17606
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
17607
|
+
timer.unref?.();
|
|
17608
|
+
try {
|
|
17609
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
17610
|
+
method: "GET",
|
|
17611
|
+
headers: { Accept: "application/json" },
|
|
17612
|
+
signal: controller.signal,
|
|
17613
|
+
});
|
|
17614
|
+
if (!response.ok) {
|
|
17615
|
+
const errorText = await response.text().catch(() => "");
|
|
17616
|
+
throw new Error(`${endpoint} failed: ${cleanText(errorText || response.statusText || "upstream error")}`);
|
|
17617
|
+
}
|
|
17618
|
+
const payload = await response.json().catch(() => null);
|
|
17619
|
+
return payload?.data ?? payload ?? null;
|
|
17620
|
+
} catch (error) {
|
|
17621
|
+
if (error?.name === "AbortError") {
|
|
17622
|
+
throw new Error(`${endpoint} timed out after ${timeout}ms`);
|
|
17623
|
+
}
|
|
17624
|
+
throw error;
|
|
17625
|
+
} finally {
|
|
17626
|
+
clearTimeout(timer);
|
|
17627
|
+
}
|
|
17628
|
+
}
|
|
17629
|
+
|
|
17630
|
+
function mergeHazbaseAccountSummaries(existing, incoming) {
|
|
17631
|
+
const merged = new Map();
|
|
17632
|
+
for (const entry of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(incoming) ? incoming : [])]) {
|
|
17633
|
+
if (!entry || !entry.smartAccountAddress) continue;
|
|
17634
|
+
const key = `${Number(entry.chainId) || 0}:${String(entry.smartAccountAddress).toLowerCase()}`;
|
|
17635
|
+
merged.set(key, entry);
|
|
17636
|
+
}
|
|
17637
|
+
return [...merged.values()];
|
|
17638
|
+
}
|
|
17639
|
+
|
|
17640
|
+
function resolveHazbaseAccountForChain(accounts, chainId) {
|
|
17641
|
+
const normalizedChainId = Number(chainId || 0);
|
|
17642
|
+
if (!normalizedChainId) return null;
|
|
17643
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
17644
|
+
return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
|
|
17645
|
+
}
|
|
17646
|
+
|
|
15244
17647
|
function validateConfig(config) {
|
|
15245
17648
|
let publicBaseUrl = null;
|
|
15246
17649
|
try {
|
|
@@ -15396,6 +17799,23 @@ async function loadState(stateFile) {
|
|
|
15396
17799
|
try {
|
|
15397
17800
|
const raw = await fs.readFile(stateFile, "utf8");
|
|
15398
17801
|
const parsed = JSON.parse(raw);
|
|
17802
|
+
const hasExplicitWriteLaneState =
|
|
17803
|
+
typeof parsed.autoPilotWriteLaneContent === "boolean" ||
|
|
17804
|
+
typeof parsed.autoPilotWriteLaneUiTests === "boolean" ||
|
|
17805
|
+
typeof parsed.autoPilotWriteLaneSource === "boolean";
|
|
17806
|
+
const legacyTrustedWritesEnabled = parsed.autoPilotTrustedWrites === true || parsed.autoPilotTrustedWritesShadow === true;
|
|
17807
|
+
const autoPilotWriteLaneContent =
|
|
17808
|
+
typeof parsed.autoPilotWriteLaneContent === "boolean"
|
|
17809
|
+
? parsed.autoPilotWriteLaneContent === true
|
|
17810
|
+
: !hasExplicitWriteLaneState && legacyTrustedWritesEnabled;
|
|
17811
|
+
const autoPilotWriteLaneUiTests =
|
|
17812
|
+
typeof parsed.autoPilotWriteLaneUiTests === "boolean"
|
|
17813
|
+
? parsed.autoPilotWriteLaneUiTests === true
|
|
17814
|
+
: false;
|
|
17815
|
+
const autoPilotWriteLaneSource =
|
|
17816
|
+
typeof parsed.autoPilotWriteLaneSource === "boolean"
|
|
17817
|
+
? parsed.autoPilotWriteLaneSource === true
|
|
17818
|
+
: false;
|
|
15399
17819
|
return {
|
|
15400
17820
|
fileOffsets: parsed.fileOffsets ?? {},
|
|
15401
17821
|
seenEvents: parsed.seenEvents ?? {},
|
|
@@ -15421,7 +17841,14 @@ async function loadState(stateFile) {
|
|
|
15421
17841
|
pairingConsumedAt: Number(parsed.pairingConsumedAt) || 0,
|
|
15422
17842
|
pairingConsumedCredential: cleanText(parsed.pairingConsumedCredential ?? ""),
|
|
15423
17843
|
claudeAwayMode: parsed.claudeAwayMode === true,
|
|
17844
|
+
autoPilotTrustedReads: parsed.autoPilotTrustedReads === true,
|
|
17845
|
+
autoPilotTrustedWrites: autoPilotWriteLaneContent || autoPilotWriteLaneUiTests || autoPilotWriteLaneSource,
|
|
17846
|
+
autoPilotTrustedWritesShadow: false,
|
|
17847
|
+
autoPilotWriteLaneContent,
|
|
17848
|
+
autoPilotWriteLaneUiTests,
|
|
17849
|
+
autoPilotWriteLaneSource,
|
|
15424
17850
|
a2aAcceptPublicTasks: parsed.a2aAcceptPublicTasks === true,
|
|
17851
|
+
hazbase: normalizeHazbaseState(parsed.hazbase),
|
|
15425
17852
|
};
|
|
15426
17853
|
} catch {
|
|
15427
17854
|
return {
|
|
@@ -15449,6 +17876,13 @@ async function loadState(stateFile) {
|
|
|
15449
17876
|
pairingConsumedAt: 0,
|
|
15450
17877
|
pairingConsumedCredential: "",
|
|
15451
17878
|
claudeAwayMode: false,
|
|
17879
|
+
autoPilotTrustedReads: false,
|
|
17880
|
+
autoPilotTrustedWrites: false,
|
|
17881
|
+
autoPilotTrustedWritesShadow: false,
|
|
17882
|
+
autoPilotWriteLaneContent: false,
|
|
17883
|
+
autoPilotWriteLaneUiTests: false,
|
|
17884
|
+
autoPilotWriteLaneSource: false,
|
|
17885
|
+
hazbase: normalizeHazbaseState(null),
|
|
15452
17886
|
};
|
|
15453
17887
|
}
|
|
15454
17888
|
}
|
|
@@ -15473,7 +17907,11 @@ async function syncClaudeAwayModeSentinel(config, enabled) {
|
|
|
15473
17907
|
}
|
|
15474
17908
|
|
|
15475
17909
|
async function saveState(stateFile, state) {
|
|
15476
|
-
|
|
17910
|
+
// No indent. The state file is ~8 MB and humans don't read it; pretty-print
|
|
17911
|
+
// just costs a few extra ms of stringify and ~10% more bytes on disk. The
|
|
17912
|
+
// single newline at the end keeps diffs/hexdumps working on the off chance
|
|
17913
|
+
// someone cats the file.
|
|
17914
|
+
const output = JSON.stringify(state);
|
|
15477
17915
|
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
15478
17916
|
await fs.writeFile(stateFile, `${output}\n`, "utf8");
|
|
15479
17917
|
}
|
|
@@ -16081,6 +18519,7 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
16081
18519
|
) {
|
|
16082
18520
|
runtime.recentCodeEvents = nextCodeEvents;
|
|
16083
18521
|
state.recentCodeEvents = nextCodeEvents;
|
|
18522
|
+
invalidateDiffThreadGroupsCache();
|
|
16084
18523
|
changed = true;
|
|
16085
18524
|
}
|
|
16086
18525
|
|
|
@@ -16271,8 +18710,8 @@ function extractTitleOnlyJsonTitle(value) {
|
|
|
16271
18710
|
}
|
|
16272
18711
|
|
|
16273
18712
|
function preferTitleOnlyJsonThreadLabel(rawThreadLabel, conversationId, ...candidates) {
|
|
16274
|
-
|
|
16275
|
-
if (preferredThreadLabel) {
|
|
18713
|
+
const preferredThreadLabel = sanitizeResolvedThreadLabel(rawThreadLabel, conversationId);
|
|
18714
|
+
if (preferredThreadLabel && !isFallbackConversationLabel(preferredThreadLabel, conversationId)) {
|
|
16276
18715
|
return preferredThreadLabel;
|
|
16277
18716
|
}
|
|
16278
18717
|
for (const candidate of candidates) {
|
|
@@ -16280,8 +18719,12 @@ function preferTitleOnlyJsonThreadLabel(rawThreadLabel, conversationId, ...candi
|
|
|
16280
18719
|
if (titleOnlyJsonTitle) {
|
|
16281
18720
|
return titleOnlyJsonTitle;
|
|
16282
18721
|
}
|
|
18722
|
+
const derivedSdkCliLabel = deriveClaudeSdkCliThreadLabel(candidate, conversationId);
|
|
18723
|
+
if (derivedSdkCliLabel) {
|
|
18724
|
+
return derivedSdkCliLabel;
|
|
18725
|
+
}
|
|
16283
18726
|
}
|
|
16284
|
-
return cleanText(rawThreadLabel || "");
|
|
18727
|
+
return cleanText(preferredThreadLabel || rawThreadLabel || "");
|
|
16285
18728
|
}
|
|
16286
18729
|
|
|
16287
18730
|
function stripMarkdownLinks(value) {
|
|
@@ -16353,6 +18796,25 @@ function shortId(value) {
|
|
|
16353
18796
|
return text.length > 8 ? text.slice(0, 8) : text;
|
|
16354
18797
|
}
|
|
16355
18798
|
|
|
18799
|
+
function isFallbackTimelineTitle(rawTitle, kind, threadId) {
|
|
18800
|
+
const normalizedTitle = cleanText(rawTitle || "");
|
|
18801
|
+
const normalizedKind = cleanText(kind || "");
|
|
18802
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
18803
|
+
if (!normalizedTitle) {
|
|
18804
|
+
return true;
|
|
18805
|
+
}
|
|
18806
|
+
if (normalizedThreadId && normalizedTitle === shortId(normalizedThreadId)) {
|
|
18807
|
+
return true;
|
|
18808
|
+
}
|
|
18809
|
+
if (normalizedTitle === kindTitle(DEFAULT_LOCALE, normalizedKind)) {
|
|
18810
|
+
return true;
|
|
18811
|
+
}
|
|
18812
|
+
if (normalizedThreadId && normalizedTitle === formatTitle(kindTitle(DEFAULT_LOCALE, normalizedKind), shortId(normalizedThreadId))) {
|
|
18813
|
+
return true;
|
|
18814
|
+
}
|
|
18815
|
+
return false;
|
|
18816
|
+
}
|
|
18817
|
+
|
|
16356
18818
|
function sleep(ms) {
|
|
16357
18819
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16358
18820
|
}
|
|
@@ -16507,6 +18969,7 @@ async function main() {
|
|
|
16507
18969
|
migratedRecentCodeEventsStateChanged ||
|
|
16508
18970
|
restoredPendingUserInputStateChanged ||
|
|
16509
18971
|
backfilledMoltbookInboxChanged ||
|
|
18972
|
+
recoveredMissingProviderStateChanged ||
|
|
16510
18973
|
refreshResolvedThreadLabels({ config, runtime, state })
|
|
16511
18974
|
) {
|
|
16512
18975
|
await saveState(config.stateFile, state);
|
|
@@ -16613,7 +19076,16 @@ async function main() {
|
|
|
16613
19076
|
try {
|
|
16614
19077
|
const dirty = await scanOnce({ config, runtime, state });
|
|
16615
19078
|
if (dirty) {
|
|
16616
|
-
|
|
19079
|
+
// Fire-and-forget via the debouncer rather than awaiting — the
|
|
19080
|
+
// ~8 MB state file used to block the scan loop for ~50-100 ms on
|
|
19081
|
+
// every dirty iteration, which directly delays the next rollout /
|
|
19082
|
+
// history.jsonl read and therefore how quickly new user_message
|
|
19083
|
+
// entries reach the phone. runtime/state.recentTimelineEntries are
|
|
19084
|
+
// already updated synchronously inside recordTimelineEntry, so the
|
|
19085
|
+
// /api/timeline response sees fresh data the moment the client
|
|
19086
|
+
// next polls, regardless of when the disk write lands. Pending
|
|
19087
|
+
// writes are drained in the `finally` block below on SIGINT/SIGTERM.
|
|
19088
|
+
scheduleSaveState(config, state);
|
|
16617
19089
|
}
|
|
16618
19090
|
} catch (error) {
|
|
16619
19091
|
console.error(`[scan-error] ${error.message}`);
|
|
@@ -16690,5 +19162,10 @@ async function main() {
|
|
|
16690
19162
|
if (approvalServer) {
|
|
16691
19163
|
await stopHttpServer(approvalServer);
|
|
16692
19164
|
}
|
|
19165
|
+
|
|
19166
|
+
// Drain any state mutations that were queued via scheduleSaveState but
|
|
19167
|
+
// not yet written to disk — otherwise SIGINT/SIGTERM mid-debounce would
|
|
19168
|
+
// silently drop the last ~500 ms of settings/notification changes.
|
|
19169
|
+
await flushPendingStateWrite(config, state);
|
|
16693
19170
|
}
|
|
16694
19171
|
}
|