viveworker 0.7.0-beta.1 → 0.7.0-beta.3
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 +39 -0
- package/package.json +3 -1
- package/scripts/share-cli.mjs +554 -3
- package/scripts/viveworker-bridge.mjs +3395 -218
- package/viveworker.env.example +8 -0
- package/web/app.css +916 -109
- package/web/app.js +1762 -43
- package/web/hazbase-passkey.js +107 -0
- package/web/i18n.js +276 -0
- package/web/sw.js +33 -15
|
@@ -12,7 +12,9 @@ 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";
|
|
@@ -24,6 +26,21 @@ 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,155 @@ 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
|
+
const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
|
|
60
|
+
const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
// Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
|
|
63
|
+
// per tracked repo (`git diff --name-status`, `git status --porcelain`,
|
|
64
|
+
// `git diff`) plus a `git rev-parse` per unique dir — roughly 10+ subprocess
|
|
65
|
+
// spawns for a typical multi-repo state. Both /api/inbox/diff and
|
|
66
|
+
// /api/items/diff_thread/:token call this function; when the user taps a
|
|
67
|
+
// detail right after the inbox finished loading, the computation is
|
|
68
|
+
// duplicated unnecessarily. Short TTL + explicit invalidation when
|
|
69
|
+
// recentCodeEvents mutates keeps the result fresh without the recompute cost.
|
|
70
|
+
// Hoisted here so code-event migration at startup can call
|
|
71
|
+
// invalidateDiffThreadGroupsCache() before any of the use sites below
|
|
72
|
+
// execute, avoiding a temporal-dead-zone ReferenceError on `let`.
|
|
73
|
+
const DIFF_THREAD_GROUPS_CACHE_TTL_MS = 3000;
|
|
74
|
+
let diffThreadGroupsCache = null;
|
|
75
|
+
let diffThreadGroupsPending = null;
|
|
76
|
+
|
|
77
|
+
function invalidateDiffThreadGroupsCache() {
|
|
78
|
+
diffThreadGroupsCache = null;
|
|
79
|
+
// Don't touch diffThreadGroupsPending — an in-flight build reflects the
|
|
80
|
+
// state at the time it started, and callers racing the rebuild would
|
|
81
|
+
// otherwise pile up their own subprocess spawns.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getDiffThreadGroupsCached(runtime, state, config) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
if (
|
|
87
|
+
diffThreadGroupsCache &&
|
|
88
|
+
now - diffThreadGroupsCache.computedAtMs < DIFF_THREAD_GROUPS_CACHE_TTL_MS
|
|
89
|
+
) {
|
|
90
|
+
return diffThreadGroupsCache.groups;
|
|
91
|
+
}
|
|
92
|
+
if (diffThreadGroupsPending) {
|
|
93
|
+
return diffThreadGroupsPending;
|
|
94
|
+
}
|
|
95
|
+
diffThreadGroupsPending = (async () => {
|
|
96
|
+
try {
|
|
97
|
+
const groups = await buildDiffThreadGroups(runtime, state, config);
|
|
98
|
+
diffThreadGroupsCache = { groups, computedAtMs: Date.now() };
|
|
99
|
+
return groups;
|
|
100
|
+
} finally {
|
|
101
|
+
diffThreadGroupsPending = null;
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
return diffThreadGroupsPending;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Coalesced state persistence. The full state JSON is ~8 MB on disk (dominated
|
|
108
|
+
// by `recentCodeEvents` and `recentTimelineEntries` caches); JSON.stringify
|
|
109
|
+
// blocks the event loop ~70 ms and fs.writeFile takes another ~80 ms. Before
|
|
110
|
+
// this helper, every tiny mutation (push subscribe/unsubscribe/test, locale
|
|
111
|
+
// change, claude-away toggle, A2A toggles, …) awaited a ~150 ms
|
|
112
|
+
// serialize+write before the HTTP response fired — and the event-loop block
|
|
113
|
+
// delayed every concurrent request on the same server too. That's what made
|
|
114
|
+
// the settings/notification buttons feel sluggish: the click → visible
|
|
115
|
+
// response round-trip paid the cost of re-serializing 5 MB of code-event
|
|
116
|
+
// history every time.
|
|
117
|
+
//
|
|
118
|
+
// `scheduleSaveState` queues a write within SAVE_STATE_DEBOUNCE_MS and returns
|
|
119
|
+
// synchronously, so HTTP handlers can respond immediately after mutating
|
|
120
|
+
// in-memory state. Multiple mutations within the window coalesce into one
|
|
121
|
+
// write. `flushPendingStateWrite` drains on graceful shutdown.
|
|
122
|
+
//
|
|
123
|
+
// Durability trade-off: at most ~SAVE_STATE_MAX_DELAY_MS of in-memory
|
|
124
|
+
// mutations can be lost on a hard crash. Callers that require stronger
|
|
125
|
+
// guarantees (pairing, revocation, the periodic scan loop) still `await
|
|
126
|
+
// saveState(...)` directly — only the hot settings/notification paths opt in
|
|
127
|
+
// to the coalescer.
|
|
128
|
+
const SAVE_STATE_DEBOUNCE_MS = 500;
|
|
129
|
+
const SAVE_STATE_MAX_DELAY_MS = 1500;
|
|
130
|
+
let saveStateTimer = null;
|
|
131
|
+
let saveStateFirstScheduleAtMs = 0;
|
|
132
|
+
let saveStateInFlight = null;
|
|
133
|
+
let saveStateDirty = false;
|
|
134
|
+
|
|
135
|
+
function scheduleSaveState(config, state) {
|
|
136
|
+
if (!config?.stateFile) return;
|
|
137
|
+
if (saveStateInFlight) {
|
|
138
|
+
// An in-flight write already reflects the current (or earlier) state.
|
|
139
|
+
// Mark dirty so the .finally handler starts another write when it
|
|
140
|
+
// finishes — overlapping writes would just stomp on each other.
|
|
141
|
+
saveStateDirty = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
if (!saveStateFirstScheduleAtMs) {
|
|
146
|
+
saveStateFirstScheduleAtMs = now;
|
|
147
|
+
}
|
|
148
|
+
if (saveStateTimer) {
|
|
149
|
+
clearTimeout(saveStateTimer);
|
|
150
|
+
}
|
|
151
|
+
const elapsed = now - saveStateFirstScheduleAtMs;
|
|
152
|
+
const remaining = SAVE_STATE_MAX_DELAY_MS - elapsed;
|
|
153
|
+
const delay = Math.max(0, Math.min(SAVE_STATE_DEBOUNCE_MS, remaining));
|
|
154
|
+
saveStateTimer = setTimeout(() => {
|
|
155
|
+
saveStateTimer = null;
|
|
156
|
+
saveStateFirstScheduleAtMs = 0;
|
|
157
|
+
startSaveStateFlush(config, state);
|
|
158
|
+
}, delay);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function startSaveStateFlush(config, state) {
|
|
162
|
+
saveStateDirty = false;
|
|
163
|
+
const promise = (async () => {
|
|
164
|
+
try {
|
|
165
|
+
await saveState(config.stateFile, state);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`[save-state-error] ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
saveStateInFlight = promise;
|
|
171
|
+
promise.finally(() => {
|
|
172
|
+
if (saveStateInFlight === promise) {
|
|
173
|
+
saveStateInFlight = null;
|
|
174
|
+
}
|
|
175
|
+
if (saveStateDirty) {
|
|
176
|
+
// A mutation arrived mid-flight. Start the follow-up write
|
|
177
|
+
// synchronously — we've already held the original debounce window,
|
|
178
|
+
// so tacking on another 500 ms would defeat coalescing. Going
|
|
179
|
+
// through scheduleSaveState() instead would also introduce a timer
|
|
180
|
+
// that flushPendingStateWrite() would have to know about.
|
|
181
|
+
saveStateDirty = false;
|
|
182
|
+
startSaveStateFlush(config, state);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function flushPendingStateWrite(config, state) {
|
|
188
|
+
// Promote any pending debounce timer to an immediate flush.
|
|
189
|
+
if (saveStateTimer) {
|
|
190
|
+
clearTimeout(saveStateTimer);
|
|
191
|
+
saveStateTimer = null;
|
|
192
|
+
saveStateFirstScheduleAtMs = 0;
|
|
193
|
+
startSaveStateFlush(config, state);
|
|
194
|
+
}
|
|
195
|
+
// Drain the in-flight write plus any cascades triggered by saveStateDirty.
|
|
196
|
+
// Bounded so a pathological re-entrancy bug can't hang shutdown.
|
|
197
|
+
for (let iter = 0; iter < 4 && (saveStateInFlight || saveStateDirty); iter += 1) {
|
|
198
|
+
if (saveStateInFlight) {
|
|
199
|
+
await saveStateInFlight.catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
if (saveStateDirty && !saveStateInFlight) {
|
|
202
|
+
saveStateDirty = false;
|
|
203
|
+
startSaveStateFlush(config, state);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
41
207
|
|
|
42
208
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
43
209
|
const envFile = resolveEnvFile(cli.envFile);
|
|
@@ -47,6 +213,7 @@ await maybeRotateStartupPairingEnv(envFile);
|
|
|
47
213
|
|
|
48
214
|
const config = buildConfig(cli);
|
|
49
215
|
validateConfig(config);
|
|
216
|
+
configureHazbaseClient(config);
|
|
50
217
|
|
|
51
218
|
const runtime = {
|
|
52
219
|
fileStates: new Map(),
|
|
@@ -100,6 +267,11 @@ const runtime = {
|
|
|
100
267
|
// deliberately NOT persisted via `state` since the cache is worthless after
|
|
101
268
|
// a restart and would just bloat the state file.
|
|
102
269
|
a2aShareStatusCache: null,
|
|
270
|
+
// Latest npm version cache. Checked only from the technical settings page
|
|
271
|
+
// and cached here so a weak network or repeated settings renders never
|
|
272
|
+
// hammer registry.npmjs.org.
|
|
273
|
+
npmVersionStatusCache: null,
|
|
274
|
+
npmVersionStatusPending: null,
|
|
103
275
|
};
|
|
104
276
|
const state = await loadState(config.stateFile);
|
|
105
277
|
runtime.threadRegistry = await loadThreadRegistry(config.threadRegistryFile);
|
|
@@ -165,6 +337,7 @@ const backfilledAmbientSuggestionsStateChanged = backfillAmbientSuggestionsState
|
|
|
165
337
|
const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
|
|
166
338
|
const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
|
|
167
339
|
const backfilledMoltbookInboxChanged = await backfillMoltbookInboxHistory({ config, runtime, state });
|
|
340
|
+
const recoveredMissingProviderStateChanged = await recoverMissingProviderStateFromBackup({ config, runtime, state });
|
|
168
341
|
runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
|
|
169
342
|
runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
|
|
170
343
|
|
|
@@ -263,8 +436,23 @@ function clearDeviceLocaleOverride(state, deviceId) {
|
|
|
263
436
|
return true;
|
|
264
437
|
}
|
|
265
438
|
|
|
439
|
+
function autoPilotWriteLaneState(state) {
|
|
440
|
+
return {
|
|
441
|
+
content: state?.autoPilotWriteLaneContent === true,
|
|
442
|
+
uiTests: state?.autoPilotWriteLaneUiTests === true,
|
|
443
|
+
source: state?.autoPilotWriteLaneSource === true,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function hasAnyAutoPilotWriteLaneEnabled(state) {
|
|
448
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
449
|
+
return lanes.content || lanes.uiTests || lanes.source;
|
|
450
|
+
}
|
|
451
|
+
|
|
266
452
|
function buildSessionLocalePayload(config, state, deviceId) {
|
|
267
453
|
const resolved = resolveDeviceLocaleInfo(config, state, deviceId);
|
|
454
|
+
const writeLanes = autoPilotWriteLaneState(state);
|
|
455
|
+
const trustedWritesEnabled = hasAnyAutoPilotWriteLaneEnabled(state);
|
|
268
456
|
return {
|
|
269
457
|
locale: resolved.locale,
|
|
270
458
|
localeSource: resolved.source,
|
|
@@ -273,6 +461,12 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
273
461
|
deviceDetectedLocale: resolved.detectedLocale || null,
|
|
274
462
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
275
463
|
claudeAwayMode: state?.claudeAwayMode === true,
|
|
464
|
+
autoPilotTrustedReads: state?.autoPilotTrustedReads === true,
|
|
465
|
+
autoPilotTrustedWrites: trustedWritesEnabled,
|
|
466
|
+
autoPilotTrustedWritesShadow: false,
|
|
467
|
+
autoPilotWriteLaneContent: writeLanes.content,
|
|
468
|
+
autoPilotWriteLaneUiTests: writeLanes.uiTests,
|
|
469
|
+
autoPilotWriteLaneSource: writeLanes.source,
|
|
276
470
|
moltbookEnabled: Boolean(config.moltbookApiKey),
|
|
277
471
|
a2aEnabled: Boolean(config.a2aApiKey),
|
|
278
472
|
a2aRelayEnabled: Boolean(config.a2aRelayUrl && config.a2aRelayUserId),
|
|
@@ -770,6 +964,425 @@ function tokenizeShellWords(commandText) {
|
|
|
770
964
|
return tokens;
|
|
771
965
|
}
|
|
772
966
|
|
|
967
|
+
function isPathWithinRoot(rootPath, candidatePath) {
|
|
968
|
+
const normalizedRoot = cleanText(rootPath || "");
|
|
969
|
+
const normalizedCandidate = cleanText(candidatePath || "");
|
|
970
|
+
if (!normalizedRoot || !normalizedCandidate) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
const relativePath = path.relative(normalizedRoot, normalizedCandidate);
|
|
974
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function isSensitiveCommandPath(candidatePath) {
|
|
978
|
+
const normalized = cleanText(candidatePath || "");
|
|
979
|
+
if (!normalized) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
const lower = normalized.toLowerCase();
|
|
983
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
984
|
+
const basename = segments[segments.length - 1] || "";
|
|
985
|
+
if (segments.some((segment) => [".ssh", ".aws", ".gnupg", ".azure", ".kube"].includes(segment))) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
if (basename === ".npmrc" || basename === ".netrc" || basename === ".env" || basename.startsWith(".env.")) {
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
if (basename.endsWith(".pem") || basename.endsWith(".key") || basename.endsWith(".p12") || basename.endsWith(".pfx")) {
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
if (/^id_[a-z0-9._-]+$/iu.test(basename) || basename.includes("secret") || basename.includes("credential")) {
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }) {
|
|
1001
|
+
const rawToken = cleanText(token || "");
|
|
1002
|
+
const normalizedCwd = cleanText(cwd || "");
|
|
1003
|
+
const normalizedWorkspaceRoot = cleanText(workspaceRoot || normalizedCwd);
|
|
1004
|
+
if (!rawToken || !normalizedCwd || !normalizedWorkspaceRoot) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
if (rawToken === "-" || rawToken.startsWith("~") || rawToken.startsWith("http://") || rawToken.startsWith("https://")) {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
const resolved = path.resolve(normalizedCwd, rawToken);
|
|
1011
|
+
if (!isPathWithinRoot(normalizedWorkspaceRoot, resolved)) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
if (isSensitiveCommandPath(resolved)) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
return resolved;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function isGitObjectPathToken(token) {
|
|
1021
|
+
const normalized = cleanText(token || "");
|
|
1022
|
+
if (!normalized || normalized.startsWith("-")) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
return /^[^:\s][^:\s]*:.+/u.test(normalized);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function isLiteralGitPathspecToken(token) {
|
|
1029
|
+
const normalized = cleanText(token || "");
|
|
1030
|
+
if (!normalized || normalized === "--" || normalized.startsWith(":(") || normalized.startsWith(":/")) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
if (/[*?[\]{}]/u.test(normalized)) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
return !isGitObjectPathToken(normalized);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function isAllowedGitMetadataOnlyFlag(token) {
|
|
1040
|
+
const normalized = cleanText(token || "");
|
|
1041
|
+
if (!normalized) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
return [
|
|
1045
|
+
"--stat",
|
|
1046
|
+
"--shortstat",
|
|
1047
|
+
"--numstat",
|
|
1048
|
+
"--name-only",
|
|
1049
|
+
"--name-status",
|
|
1050
|
+
"--summary",
|
|
1051
|
+
"--compact-summary",
|
|
1052
|
+
"--oneline",
|
|
1053
|
+
"--no-patch",
|
|
1054
|
+
"-s",
|
|
1055
|
+
].includes(normalized);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function hasDisallowedGitPatchOutput(tokens) {
|
|
1059
|
+
return tokens.some((token) => {
|
|
1060
|
+
const normalized = cleanText(token || "");
|
|
1061
|
+
if (!normalized) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
return (
|
|
1065
|
+
normalized === "-p" ||
|
|
1066
|
+
normalized === "--patch" ||
|
|
1067
|
+
normalized === "--patch-with-stat" ||
|
|
1068
|
+
normalized === "--raw" ||
|
|
1069
|
+
normalized === "--cc" ||
|
|
1070
|
+
normalized === "--combined-all-paths" ||
|
|
1071
|
+
normalized === "--binary" ||
|
|
1072
|
+
normalized === "--word-diff" ||
|
|
1073
|
+
normalized.startsWith("--word-diff=") ||
|
|
1074
|
+
normalized === "-U" ||
|
|
1075
|
+
normalized.startsWith("-U") ||
|
|
1076
|
+
normalized === "--unified" ||
|
|
1077
|
+
normalized.startsWith("--unified=")
|
|
1078
|
+
);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function extractGitPathspecTokens(tokens) {
|
|
1083
|
+
const separatorIndex = tokens.indexOf("--");
|
|
1084
|
+
if (separatorIndex < 0) {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
return tokens.slice(separatorIndex + 1).filter(Boolean);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function resolveTrustedDiffSectionPath({ token, cwd, workspaceRoot }) {
|
|
1091
|
+
const normalizedToken = cleanText(token || "");
|
|
1092
|
+
if (!normalizedToken) {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
const preferredWorkspace = resolveTrustedReadTargetPath({
|
|
1096
|
+
token: normalizedToken,
|
|
1097
|
+
cwd: workspaceRoot,
|
|
1098
|
+
workspaceRoot,
|
|
1099
|
+
});
|
|
1100
|
+
if (preferredWorkspace) {
|
|
1101
|
+
return preferredWorkspace;
|
|
1102
|
+
}
|
|
1103
|
+
return resolveTrustedReadTargetPath({
|
|
1104
|
+
token: normalizedToken,
|
|
1105
|
+
cwd,
|
|
1106
|
+
workspaceRoot,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function extractTrustedReadFileRefs(command, tokens, cwd, workspaceRoot) {
|
|
1111
|
+
const normalizedCommand = cleanText(command || "");
|
|
1112
|
+
if (!normalizedCommand) {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (normalizedCommand === "pwd" || (normalizedCommand === "git" && cleanText(tokens[1] || "") === "status")) {
|
|
1117
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1118
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (normalizedCommand === "git") {
|
|
1122
|
+
const gitSubcommand = cleanText(tokens[1] || "");
|
|
1123
|
+
if (!["diff", "show"].includes(gitSubcommand)) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
if (tokens.some((token) => token === "-C" || token.startsWith("-C") || token === "--output" || token.startsWith("--output="))) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
const commandArgs = tokens.slice(2);
|
|
1130
|
+
if (commandArgs.some((token) => isGitObjectPathToken(token))) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
const explicitPathspecs = extractGitPathspecTokens(tokens);
|
|
1134
|
+
if (explicitPathspecs.length > 0) {
|
|
1135
|
+
if (explicitPathspecs.some((token) => !isLiteralGitPathspecToken(token))) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
const resolvedPaths = explicitPathspecs
|
|
1139
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1140
|
+
.filter(Boolean);
|
|
1141
|
+
return resolvedPaths.length === explicitPathspecs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1142
|
+
}
|
|
1143
|
+
if (hasDisallowedGitPatchOutput(commandArgs)) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
const metadataOnly = commandArgs.some((token) => isAllowedGitMetadataOnlyFlag(token));
|
|
1147
|
+
if (!metadataOnly) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1151
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (normalizedCommand === "ls") {
|
|
1155
|
+
const pathArgs = tokens.slice(1).filter((token) => !String(token || "").startsWith("-"));
|
|
1156
|
+
if (pathArgs.length === 0) {
|
|
1157
|
+
const resolved = resolveTrustedReadTargetPath({ token: ".", cwd, workspaceRoot });
|
|
1158
|
+
return resolved ? normalizeTimelineFileRefs([resolved]) : null;
|
|
1159
|
+
}
|
|
1160
|
+
const resolvedPaths = pathArgs
|
|
1161
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1162
|
+
.filter(Boolean);
|
|
1163
|
+
return resolvedPaths.length === pathArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (normalizedCommand === "find") {
|
|
1167
|
+
if (tokens.some((token) => ["-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprintf", "-fls"].includes(token))) {
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
const pathArgs = [];
|
|
1171
|
+
for (const token of tokens.slice(1)) {
|
|
1172
|
+
if (!token || token === "--") continue;
|
|
1173
|
+
if (token === "!" || token === "(" || token === ")") break;
|
|
1174
|
+
if (token.startsWith("-")) break;
|
|
1175
|
+
pathArgs.push(token);
|
|
1176
|
+
}
|
|
1177
|
+
const effectivePaths = pathArgs.length ? pathArgs : ["."];
|
|
1178
|
+
const resolvedPaths = effectivePaths
|
|
1179
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1180
|
+
.filter(Boolean);
|
|
1181
|
+
return resolvedPaths.length === effectivePaths.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (normalizedCommand === "sed") {
|
|
1185
|
+
if (!tokens.includes("-n") || tokens.includes("-i") || tokens.includes("--in-place")) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
let script = "";
|
|
1189
|
+
const fileArgs = [];
|
|
1190
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1191
|
+
const token = tokens[index];
|
|
1192
|
+
if (!token) continue;
|
|
1193
|
+
if (!script) {
|
|
1194
|
+
if (token === "-e") {
|
|
1195
|
+
script = tokens[index + 1] || "";
|
|
1196
|
+
index += 1;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (token.startsWith("-")) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
script = token;
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (!token.startsWith("-")) {
|
|
1206
|
+
fileArgs.push(token);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (!script || !/^[0-9,$; p-]+$/u.test(script) || fileArgs.length === 0) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
const resolvedPaths = fileArgs
|
|
1213
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1214
|
+
.filter(Boolean);
|
|
1215
|
+
return resolvedPaths.length === fileArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (normalizedCommand === "rg") {
|
|
1219
|
+
let seenPattern = false;
|
|
1220
|
+
const fileArgs = [];
|
|
1221
|
+
for (const token of tokens.slice(1)) {
|
|
1222
|
+
if (!seenPattern) {
|
|
1223
|
+
if (token === "--") {
|
|
1224
|
+
seenPattern = true;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (String(token || "").startsWith("-")) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
seenPattern = true;
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (!String(token || "").startsWith("-")) {
|
|
1234
|
+
fileArgs.push(token);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const effectivePaths = fileArgs.length ? fileArgs : ["."];
|
|
1238
|
+
const resolvedPaths = effectivePaths
|
|
1239
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1240
|
+
.filter(Boolean);
|
|
1241
|
+
return resolvedPaths.length === effectivePaths.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (["cat", "nl", "head", "tail", "wc"].includes(normalizedCommand)) {
|
|
1245
|
+
const fileArgs = tokens.slice(1).filter((token) => !String(token || "").startsWith("-"));
|
|
1246
|
+
if (fileArgs.length === 0) {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const resolvedPaths = fileArgs
|
|
1250
|
+
.map((token) => resolveTrustedReadTargetPath({ token, cwd, workspaceRoot }))
|
|
1251
|
+
.filter(Boolean);
|
|
1252
|
+
return resolvedPaths.length === fileArgs.length ? normalizeTimelineFileRefs(resolvedPaths) : null;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function stripAllowedTrustedReadRedirections(tokens) {
|
|
1259
|
+
const filtered = [];
|
|
1260
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1261
|
+
const token = String(tokens[index] || "");
|
|
1262
|
+
if (!token) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (token === "2>" || token === "2>>") {
|
|
1266
|
+
const target = String(tokens[index + 1] || "");
|
|
1267
|
+
if (target !== "/dev/null") {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
index += 1;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (/^2>>?\/dev\/null$/u.test(token)) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (token.includes(">") || token.includes("<")) {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
filtered.push(token);
|
|
1280
|
+
}
|
|
1281
|
+
return filtered;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function isAllowedTrustedReadPostProcessStage(stageTokens) {
|
|
1285
|
+
if (!Array.isArray(stageTokens) || stageTokens.length === 0) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
const [command, ...args] = stageTokens.map((token) => cleanText(token || ""));
|
|
1289
|
+
if (!command) {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
if (command === "head" || command === "tail") {
|
|
1293
|
+
if (args.length === 0) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
if (args.length === 1 && /^-\d+$/u.test(args[0])) {
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
if (args.length === 2 && args[0] === "-n" && /^\d+$/u.test(args[1])) {
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
if (command === "wc") {
|
|
1305
|
+
return args.length === 0 || (args.length === 1 && args[0] === "-l");
|
|
1306
|
+
}
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function normalizeTrustedReadTokens(tokens) {
|
|
1311
|
+
const strippedTokens = stripAllowedTrustedReadRedirections(tokens);
|
|
1312
|
+
if (!strippedTokens || strippedTokens.length === 0) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
if (strippedTokens.some((token) => {
|
|
1316
|
+
const value = String(token || "");
|
|
1317
|
+
return (
|
|
1318
|
+
["||", "&&", ";", "&"].includes(value) ||
|
|
1319
|
+
value.includes("`") ||
|
|
1320
|
+
value.includes("$(")
|
|
1321
|
+
);
|
|
1322
|
+
})) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
const stages = [];
|
|
1326
|
+
let currentStage = [];
|
|
1327
|
+
for (const token of strippedTokens) {
|
|
1328
|
+
if (token === "|") {
|
|
1329
|
+
if (currentStage.length === 0) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
stages.push(currentStage);
|
|
1333
|
+
currentStage = [];
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
currentStage.push(token);
|
|
1337
|
+
}
|
|
1338
|
+
if (currentStage.length === 0) {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
stages.push(currentStage);
|
|
1342
|
+
if (stages.length === 0) {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
if (stages.slice(1).some((stageTokens) => !isAllowedTrustedReadPostProcessStage(stageTokens))) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
return stages[0];
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
|
|
1352
|
+
const normalizedCwd = cleanText(cwd || "");
|
|
1353
|
+
const normalizedWorkspaceRoot = cleanText(workspaceRoot || normalizedCwd);
|
|
1354
|
+
const normalizedCommandText = unwrapShellCommand(commandText);
|
|
1355
|
+
const rawTokens = tokenizeShellWords(normalizedCommandText);
|
|
1356
|
+
if (!normalizedCommandText || !normalizedCwd || !normalizedWorkspaceRoot || rawTokens.length === 0) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (!isPathWithinRoot(normalizedWorkspaceRoot, path.resolve(normalizedCwd))) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const tokens = normalizeTrustedReadTokens(rawTokens);
|
|
1365
|
+
if (!tokens || tokens.length === 0) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const command = cleanText(tokens[0]);
|
|
1370
|
+
if (!command || /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(command)) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const fileRefs = extractTrustedReadFileRefs(command, tokens, normalizedCwd, normalizedWorkspaceRoot);
|
|
1375
|
+
if (!fileRefs) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return {
|
|
1380
|
+
command,
|
|
1381
|
+
commandText: normalizedCommandText,
|
|
1382
|
+
fileRefs,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
773
1386
|
function unwrapShellCommand(commandText) {
|
|
774
1387
|
const normalized = String(commandText || "").trim();
|
|
775
1388
|
if (!normalized) {
|
|
@@ -848,6 +1461,14 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
848
1461
|
return [];
|
|
849
1462
|
}
|
|
850
1463
|
|
|
1464
|
+
function autoPilotApprovalMessage(locale, commandText) {
|
|
1465
|
+
const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
|
|
1466
|
+
const commandBlock = cleanText(commandText || "")
|
|
1467
|
+
? `${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${cleanText(commandText || "")}\n\`\`\``
|
|
1468
|
+
: "";
|
|
1469
|
+
return [prefix, commandBlock].filter(Boolean).join("\n\n");
|
|
1470
|
+
}
|
|
1471
|
+
|
|
851
1472
|
function extractUpdatedFileRefsByType(outputText, patchText = "") {
|
|
852
1473
|
const parsedSections = parseApplyPatchSections(patchText);
|
|
853
1474
|
if (parsedSections.length > 0) {
|
|
@@ -2195,6 +2816,7 @@ function providerDisplayName(locale, provider) {
|
|
|
2195
2816
|
if (p === "claude") return t(locale, "common.claude");
|
|
2196
2817
|
if (p === "moltbook") return "Moltbook";
|
|
2197
2818
|
if (p === "a2a") return "A2A";
|
|
2819
|
+
if (p === "viveworker") return t(locale, "common.appName");
|
|
2198
2820
|
return t(locale, "common.codex");
|
|
2199
2821
|
}
|
|
2200
2822
|
|
|
@@ -2209,15 +2831,23 @@ function normalizeHistoryItems(rawItems, maxItems) {
|
|
|
2209
2831
|
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
|
|
2210
2832
|
const deduped = [];
|
|
2211
2833
|
const seen = new Set();
|
|
2834
|
+
const perProviderCount = {};
|
|
2835
|
+
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a", "viveworker"]);
|
|
2212
2836
|
for (const item of normalized) {
|
|
2213
2837
|
if (seen.has(item.stableId)) {
|
|
2214
2838
|
continue;
|
|
2215
2839
|
}
|
|
2840
|
+
const provider = normalizeProvider(item.provider);
|
|
2841
|
+
if (!knownProviders.has(provider)) {
|
|
2842
|
+
knownProviders.add(provider);
|
|
2843
|
+
}
|
|
2844
|
+
const providerCount = perProviderCount[provider] ?? 0;
|
|
2845
|
+
if (providerCount >= maxItems) {
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2216
2848
|
seen.add(item.stableId);
|
|
2217
2849
|
deduped.push(item);
|
|
2218
|
-
|
|
2219
|
-
break;
|
|
2220
|
-
}
|
|
2850
|
+
perProviderCount[provider] = providerCount + 1;
|
|
2221
2851
|
}
|
|
2222
2852
|
return deduped;
|
|
2223
2853
|
}
|
|
@@ -2226,6 +2856,9 @@ function normalizeHistoryItem(raw) {
|
|
|
2226
2856
|
if (!isPlainObject(raw)) {
|
|
2227
2857
|
return null;
|
|
2228
2858
|
}
|
|
2859
|
+
if (shouldHideInternalTimelineItem(raw)) {
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2229
2862
|
|
|
2230
2863
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
2231
2864
|
const kind = cleanText(raw.kind ?? "");
|
|
@@ -2237,8 +2870,10 @@ function normalizeHistoryItem(raw) {
|
|
|
2237
2870
|
const threadLabel = skipHistoryThreadLabelRewrite
|
|
2238
2871
|
? rawThreadLabel
|
|
2239
2872
|
: preferTitleOnlyJsonThreadLabel(rawThreadLabel, threadId, raw.messageText, raw.summary, raw.detailText, raw.message);
|
|
2873
|
+
const rawTitle = cleanText(raw.title ?? "");
|
|
2240
2874
|
const title =
|
|
2241
|
-
|
|
2875
|
+
(!isFallbackTimelineTitle(rawTitle, kind, threadId) ? rawTitle : "") ||
|
|
2876
|
+
(threadLabel ? formatTitle(kindTitle(DEFAULT_LOCALE, kind), threadLabel) : kindTitle(DEFAULT_LOCALE, kind));
|
|
2242
2877
|
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
2243
2878
|
const summary = normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "";
|
|
2244
2879
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
@@ -2248,7 +2883,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2248
2883
|
|
|
2249
2884
|
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
2250
2885
|
|
|
2251
|
-
|
|
2886
|
+
const normalized = {
|
|
2252
2887
|
stableId,
|
|
2253
2888
|
token: cleanText(raw.token ?? "") || historyToken(stableId),
|
|
2254
2889
|
kind,
|
|
@@ -2276,6 +2911,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2276
2911
|
...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
|
|
2277
2912
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2278
2913
|
};
|
|
2914
|
+
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
2279
2915
|
}
|
|
2280
2916
|
|
|
2281
2917
|
function historyToken(stableId) {
|
|
@@ -2305,10 +2941,41 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2305
2941
|
const perProviderCount = {};
|
|
2306
2942
|
const knownProviders = new Set(["codex", "claude", "moltbook", "a2a", "viveworker"]);
|
|
2307
2943
|
const saturatedProviders = new Set();
|
|
2944
|
+
// Content-based dedup for user_message entries: the same logical user turn
|
|
2945
|
+
// can surface through two independent readers (rollout session files and
|
|
2946
|
+
// ~/.codex/history.jsonl) that compute different stableIds
|
|
2947
|
+
// (`user_message:<threadId>:<turn_id|ISO-ts>` vs
|
|
2948
|
+
// `user_message:<threadId>:<integer-id|unix-seconds>`), so the stableId-only
|
|
2949
|
+
// filter above doesn't catch them. Collapse entries with the same
|
|
2950
|
+
// (threadId, messageText) pair when their createdAtMs are within 60 s —
|
|
2951
|
+
// wide enough to absorb rollout-vs-history-file flush skew (we've seen
|
|
2952
|
+
// several seconds in the wild) but narrow enough not to collapse a genuine
|
|
2953
|
+
// repeat (e.g. user retries the same "run tests" command minutes apart).
|
|
2954
|
+
// Literal here rather than a named const because this function is first
|
|
2955
|
+
// called at top-level module init (line ~302) which runs before any const
|
|
2956
|
+
// declared below would leave the TDZ.
|
|
2957
|
+
const userMessageDedupWindowMs = 60_000;
|
|
2958
|
+
const userMessageIndex = new Map();
|
|
2308
2959
|
for (const item of normalized) {
|
|
2309
2960
|
if (seen.has(item.stableId)) {
|
|
2310
2961
|
continue;
|
|
2311
2962
|
}
|
|
2963
|
+
if (item.kind === "user_message") {
|
|
2964
|
+
const threadKey = cleanText(item.threadId || "");
|
|
2965
|
+
const textKey = cleanText(item.messageText || "");
|
|
2966
|
+
if (threadKey && textKey) {
|
|
2967
|
+
const key = `${threadKey}\u0001${textKey}`;
|
|
2968
|
+
const existingMs = userMessageIndex.get(key);
|
|
2969
|
+
const itemMs = Number(item.createdAtMs || 0);
|
|
2970
|
+
if (
|
|
2971
|
+
existingMs !== undefined &&
|
|
2972
|
+
Math.abs(existingMs - itemMs) <= userMessageDedupWindowMs
|
|
2973
|
+
) {
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
userMessageIndex.set(key, itemMs);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2312
2979
|
const prov = item.provider || "codex";
|
|
2313
2980
|
if (saturatedProviders.has(prov)) {
|
|
2314
2981
|
continue;
|
|
@@ -2352,6 +3019,7 @@ function migrateRecentCodeEventsState({ config, runtime, state }) {
|
|
|
2352
3019
|
const previousItems = Array.isArray(state.recentCodeEvents) ? normalizeCodeEvents(state.recentCodeEvents, config.maxCodeEvents) : [];
|
|
2353
3020
|
runtime.recentCodeEvents = nextItems;
|
|
2354
3021
|
state.recentCodeEvents = nextItems;
|
|
3022
|
+
invalidateDiffThreadGroupsCache();
|
|
2355
3023
|
return JSON.stringify(nextItems) !== JSON.stringify(previousItems);
|
|
2356
3024
|
}
|
|
2357
3025
|
|
|
@@ -2383,6 +3051,9 @@ function normalizeTimelineEntry(raw) {
|
|
|
2383
3051
|
if (!isPlainObject(raw)) {
|
|
2384
3052
|
return null;
|
|
2385
3053
|
}
|
|
3054
|
+
if (shouldHideInternalTimelineItem(raw)) {
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
2386
3057
|
|
|
2387
3058
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
2388
3059
|
const kind = cleanText(raw.kind ?? "");
|
|
@@ -2420,14 +3091,15 @@ function normalizeTimelineEntry(raw) {
|
|
|
2420
3091
|
raw.detailText,
|
|
2421
3092
|
raw.message
|
|
2422
3093
|
);
|
|
3094
|
+
const rawTitle = cleanText(raw.title ?? "");
|
|
2423
3095
|
const title =
|
|
2424
|
-
|
|
3096
|
+
(!isFallbackTimelineTitle(rawTitle, kind, threadId) ? rawTitle : "") ||
|
|
2425
3097
|
(kind === "file_event" ? fileEventTitle(DEFAULT_LOCALE, fileEventType) : "") ||
|
|
2426
3098
|
threadLabel ||
|
|
2427
3099
|
kindTitle(DEFAULT_LOCALE, kind);
|
|
2428
3100
|
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
2429
3101
|
|
|
2430
|
-
|
|
3102
|
+
const normalized = {
|
|
2431
3103
|
stableId,
|
|
2432
3104
|
token: cleanText(raw.token ?? "") || historyToken(stableId),
|
|
2433
3105
|
kind,
|
|
@@ -2473,6 +3145,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
2473
3145
|
...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
|
|
2474
3146
|
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
2475
3147
|
};
|
|
3148
|
+
return shouldHideInternalTimelineItem(normalized) ? null : normalized;
|
|
2476
3149
|
}
|
|
2477
3150
|
|
|
2478
3151
|
function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
@@ -2563,6 +3236,9 @@ function recordCodeEvent({ config, runtime, state, entry }) {
|
|
|
2563
3236
|
);
|
|
2564
3237
|
runtime.recentCodeEvents = nextItems;
|
|
2565
3238
|
state.recentCodeEvents = nextItems;
|
|
3239
|
+
if (changed) {
|
|
3240
|
+
invalidateDiffThreadGroupsCache();
|
|
3241
|
+
}
|
|
2566
3242
|
return changed;
|
|
2567
3243
|
}
|
|
2568
3244
|
|
|
@@ -2614,6 +3290,9 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
|
|
|
2614
3290
|
);
|
|
2615
3291
|
runtime.recentCodeEvents = nextItems;
|
|
2616
3292
|
state.recentCodeEvents = nextItems;
|
|
3293
|
+
if (changed) {
|
|
3294
|
+
invalidateDiffThreadGroupsCache();
|
|
3295
|
+
}
|
|
2617
3296
|
return changed;
|
|
2618
3297
|
}
|
|
2619
3298
|
|
|
@@ -2840,13 +3519,21 @@ function pendingChoiceStableId(userInputRequest) {
|
|
|
2840
3519
|
return `choice:${userInputRequest.requestKey}`;
|
|
2841
3520
|
}
|
|
2842
3521
|
|
|
2843
|
-
function buildAppItemUrl(config, kind, token) {
|
|
3522
|
+
function buildAppItemUrl(config, kind, token, options = {}) {
|
|
2844
3523
|
const url = new URL(`${config.nativeApprovalPublicBaseUrl}/app`);
|
|
2845
3524
|
url.searchParams.set("item", `${kind}:${token}`);
|
|
3525
|
+
const tab = cleanText(options.tab || "");
|
|
3526
|
+
const subtab = cleanText(options.subtab || "");
|
|
3527
|
+
if (tab) {
|
|
3528
|
+
url.searchParams.set("tab", tab);
|
|
3529
|
+
}
|
|
3530
|
+
if (subtab) {
|
|
3531
|
+
url.searchParams.set("subtab", subtab);
|
|
3532
|
+
}
|
|
2846
3533
|
return url.toString();
|
|
2847
3534
|
}
|
|
2848
3535
|
|
|
2849
|
-
function buildPushPayload({ config, kind, token, stableId, title, body }) {
|
|
3536
|
+
function buildPushPayload({ config, kind, token, stableId, title, body, tab = "", subtab = "" }) {
|
|
2850
3537
|
return {
|
|
2851
3538
|
title: withNotificationIcon(kind, title),
|
|
2852
3539
|
body: formatNotificationBody(body, config.completionDetailThresholdChars) || body || title,
|
|
@@ -2855,7 +3542,7 @@ function buildPushPayload({ config, kind, token, stableId, title, body }) {
|
|
|
2855
3542
|
kind,
|
|
2856
3543
|
token,
|
|
2857
3544
|
stableId,
|
|
2858
|
-
url: buildAppItemUrl(config, kind, token),
|
|
3545
|
+
url: buildAppItemUrl(config, kind, token, { tab, subtab }),
|
|
2859
3546
|
},
|
|
2860
3547
|
};
|
|
2861
3548
|
}
|
|
@@ -2975,7 +3662,7 @@ function pushDeliveryKey(deviceId, stableId) {
|
|
|
2975
3662
|
return `${cleanText(deviceId || "")}:${cleanText(stableId || "")}`;
|
|
2976
3663
|
}
|
|
2977
3664
|
|
|
2978
|
-
async function deliverWebPushItem({ config, state, kind, token, stableId, title, body, buildLocalizedContent = null }) {
|
|
3665
|
+
async function deliverWebPushItem({ config, state, kind, token, stableId, title, body, tab = "", subtab = "", buildLocalizedContent = null }) {
|
|
2979
3666
|
if (!config.webPushEnabled || config.dryRun) {
|
|
2980
3667
|
return false;
|
|
2981
3668
|
}
|
|
@@ -3010,6 +3697,8 @@ async function deliverWebPushItem({ config, state, kind, token, stableId, title,
|
|
|
3010
3697
|
stableId,
|
|
3011
3698
|
title: localizedContent?.title || title,
|
|
3012
3699
|
body: localizedContent?.body || body,
|
|
3700
|
+
tab,
|
|
3701
|
+
subtab,
|
|
3013
3702
|
})
|
|
3014
3703
|
);
|
|
3015
3704
|
await webPush.sendNotification(
|
|
@@ -3093,18 +3782,24 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3093
3782
|
}
|
|
3094
3783
|
|
|
3095
3784
|
if (config.webUiEnabled) {
|
|
3785
|
+
let claudeTranscriptChanged = false;
|
|
3096
3786
|
if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
|
|
3097
3787
|
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
|
|
3098
3788
|
runtime.lastClaudeScanAt = now;
|
|
3099
3789
|
}
|
|
3790
|
+
let claudeSessionTitlesChanged = false;
|
|
3100
3791
|
if (now - runtime.lastClaudeSessionTitleScanAt >= config.directoryScanIntervalMs) {
|
|
3101
|
-
await refreshClaudeSessionTitles(runtime);
|
|
3792
|
+
claudeSessionTitlesChanged = await refreshClaudeSessionTitles(runtime);
|
|
3102
3793
|
runtime.lastClaudeSessionTitleScanAt = now;
|
|
3103
3794
|
}
|
|
3104
3795
|
for (const filePath of runtime.claudeKnownFiles) {
|
|
3105
3796
|
const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
|
|
3797
|
+
claudeTranscriptChanged = claudeTranscriptChanged || changed;
|
|
3106
3798
|
dirty = dirty || changed;
|
|
3107
3799
|
}
|
|
3800
|
+
if (claudeTranscriptChanged || claudeSessionTitlesChanged) {
|
|
3801
|
+
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
3802
|
+
}
|
|
3108
3803
|
}
|
|
3109
3804
|
|
|
3110
3805
|
if (config.webUiEnabled) {
|
|
@@ -3330,6 +4025,7 @@ function fileEventCallIdFromStableId(stableId) {
|
|
|
3330
4025
|
}
|
|
3331
4026
|
|
|
3332
4027
|
async function refreshClaudeSessionTitles(runtime) {
|
|
4028
|
+
let changed = false;
|
|
3333
4029
|
// Read ~/Library/Application Support/Claude/claude-code-sessions/*/*/local_*.json
|
|
3334
4030
|
// Each file maps cliSessionId → title (auto-generated by Claude Desktop).
|
|
3335
4031
|
const baseDir = path.join(
|
|
@@ -3367,7 +4063,10 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
3367
4063
|
const cliSessionId = cleanText(data?.cliSessionId || "");
|
|
3368
4064
|
const title = cleanText(data?.title || "");
|
|
3369
4065
|
if (cliSessionId && title) {
|
|
3370
|
-
runtime.claudeSessionTitles.
|
|
4066
|
+
if (runtime.claudeSessionTitles.get(cliSessionId) !== title) {
|
|
4067
|
+
runtime.claudeSessionTitles.set(cliSessionId, title);
|
|
4068
|
+
changed = true;
|
|
4069
|
+
}
|
|
3371
4070
|
}
|
|
3372
4071
|
} catch {
|
|
3373
4072
|
// skip unreadable/invalid
|
|
@@ -3378,6 +4077,49 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
3378
4077
|
} catch {
|
|
3379
4078
|
// base dir missing — Claude Desktop not installed
|
|
3380
4079
|
}
|
|
4080
|
+
|
|
4081
|
+
for (const filePath of runtime.claudeKnownFiles ?? []) {
|
|
4082
|
+
let lines;
|
|
4083
|
+
try {
|
|
4084
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
4085
|
+
lines = raw.split("\n");
|
|
4086
|
+
} catch {
|
|
4087
|
+
continue;
|
|
4088
|
+
}
|
|
4089
|
+
for (const rawLine of lines) {
|
|
4090
|
+
const trimmed = rawLine.trim();
|
|
4091
|
+
if (!trimmed) continue;
|
|
4092
|
+
let record;
|
|
4093
|
+
try {
|
|
4094
|
+
record = JSON.parse(trimmed);
|
|
4095
|
+
} catch {
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4098
|
+
if (record?.entrypoint !== "sdk-cli" || record?.type !== "user") {
|
|
4099
|
+
continue;
|
|
4100
|
+
}
|
|
4101
|
+
const threadId = cleanText(record?.sessionId || "");
|
|
4102
|
+
const message = record?.message || {};
|
|
4103
|
+
let text = "";
|
|
4104
|
+
if (typeof message.content === "string") {
|
|
4105
|
+
text = message.content;
|
|
4106
|
+
} else if (Array.isArray(message.content)) {
|
|
4107
|
+
for (const block of message.content) {
|
|
4108
|
+
if (block?.type === "text" && block.text) {
|
|
4109
|
+
text += block.text;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
const derivedTitle = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
4114
|
+
if (threadId && derivedTitle && runtime.claudeSessionTitles.get(threadId) !== derivedTitle) {
|
|
4115
|
+
runtime.claudeSessionTitles.set(threadId, derivedTitle);
|
|
4116
|
+
changed = true;
|
|
4117
|
+
}
|
|
4118
|
+
break;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
return changed;
|
|
3381
4123
|
}
|
|
3382
4124
|
|
|
3383
4125
|
async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
@@ -3497,8 +4239,10 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3497
4239
|
fileState.threadLabel = path.basename(record.cwd);
|
|
3498
4240
|
}
|
|
3499
4241
|
|
|
3500
|
-
//
|
|
3501
|
-
|
|
4242
|
+
// Accept both Claude Desktop sessions and the current sdk-cli sessions
|
|
4243
|
+
// that Claude emits for agent-driven work. The latter regressed the
|
|
4244
|
+
// timeline/completed views because they were silently skipped here.
|
|
4245
|
+
if (record.entrypoint !== "claude-desktop" && record.entrypoint !== "sdk-cli") continue;
|
|
3502
4246
|
|
|
3503
4247
|
const type = record.type;
|
|
3504
4248
|
if (type !== "user" && type !== "assistant") continue;
|
|
@@ -3526,6 +4270,15 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3526
4270
|
}
|
|
3527
4271
|
text = cleanText(text);
|
|
3528
4272
|
if (!text) continue;
|
|
4273
|
+
if (type === "user" && record.entrypoint === "sdk-cli") {
|
|
4274
|
+
const derivedThreadLabel = deriveClaudeSdkCliThreadLabel(text, threadId);
|
|
4275
|
+
if (derivedThreadLabel) {
|
|
4276
|
+
fileState.threadLabel = derivedThreadLabel;
|
|
4277
|
+
if (threadId) {
|
|
4278
|
+
runtime.claudeSessionTitles.set(threadId, derivedThreadLabel);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
3529
4282
|
// Skip Claude Code internal system messages (task notifications, system
|
|
3530
4283
|
// reminders, etc.) — these are XML-like tags injected into the
|
|
3531
4284
|
// conversation that should not appear on the user-facing timeline.
|
|
@@ -3587,6 +4340,7 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3587
4340
|
config,
|
|
3588
4341
|
state,
|
|
3589
4342
|
kind: "assistant_final",
|
|
4343
|
+
tab: "timeline",
|
|
3590
4344
|
token: entry.token,
|
|
3591
4345
|
stableId: entry.stableId,
|
|
3592
4346
|
title: threadLabel || "Claude",
|
|
@@ -3608,6 +4362,129 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
3608
4362
|
return dirty;
|
|
3609
4363
|
}
|
|
3610
4364
|
|
|
4365
|
+
async function loadRecoveryBackupState(stateFile) {
|
|
4366
|
+
const dir = path.dirname(stateFile);
|
|
4367
|
+
const base = path.basename(stateFile);
|
|
4368
|
+
let entries;
|
|
4369
|
+
try {
|
|
4370
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
4371
|
+
} catch {
|
|
4372
|
+
return null;
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
const candidates = [];
|
|
4376
|
+
for (const entry of entries) {
|
|
4377
|
+
if (!entry.isFile()) continue;
|
|
4378
|
+
if (!entry.name.startsWith(`${base}.bak-`)) continue;
|
|
4379
|
+
const filePath = path.join(dir, entry.name);
|
|
4380
|
+
try {
|
|
4381
|
+
const stat = await fs.stat(filePath);
|
|
4382
|
+
candidates.push({ filePath, mtimeMs: Number(stat.mtimeMs) || 0 });
|
|
4383
|
+
} catch {
|
|
4384
|
+
// ignore unreadable backup
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
4389
|
+
for (const candidate of candidates) {
|
|
4390
|
+
try {
|
|
4391
|
+
const raw = await fs.readFile(candidate.filePath, "utf8");
|
|
4392
|
+
return JSON.parse(raw);
|
|
4393
|
+
} catch {
|
|
4394
|
+
// try next backup
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
return null;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
function providerSetForItems(items) {
|
|
4401
|
+
return new Set(
|
|
4402
|
+
(Array.isArray(items) ? items : [])
|
|
4403
|
+
.map((item) => normalizeProvider(item?.provider))
|
|
4404
|
+
.filter(Boolean)
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
function missingRecoveryProviders(items) {
|
|
4409
|
+
const present = providerSetForItems(items);
|
|
4410
|
+
return ["claude", "a2a"].filter((provider) => !present.has(provider));
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
function selectRecoveryItems(items, missingProviders) {
|
|
4414
|
+
const wanted = new Set(missingProviders);
|
|
4415
|
+
return (Array.isArray(items) ? items : []).filter((item) => wanted.has(normalizeProvider(item?.provider)));
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
async function recoverMissingProviderStateFromBackup({ config, runtime, state }) {
|
|
4419
|
+
const missingHistoryProviders = missingRecoveryProviders(state.recentHistoryItems ?? runtime.recentHistoryItems);
|
|
4420
|
+
const missingTimelineProviders = missingRecoveryProviders(state.recentTimelineEntries ?? runtime.recentTimelineEntries);
|
|
4421
|
+
const shouldRecoverCodeEvents = !Array.isArray(state.recentCodeEvents) || state.recentCodeEvents.length === 0;
|
|
4422
|
+
|
|
4423
|
+
if (missingHistoryProviders.length === 0 && missingTimelineProviders.length === 0 && !shouldRecoverCodeEvents) {
|
|
4424
|
+
return false;
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
const backup = await loadRecoveryBackupState(config.stateFile);
|
|
4428
|
+
if (!backup) {
|
|
4429
|
+
return false;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
let changed = false;
|
|
4433
|
+
|
|
4434
|
+
if (missingHistoryProviders.length > 0) {
|
|
4435
|
+
const mergedHistory = normalizeHistoryItems(
|
|
4436
|
+
[
|
|
4437
|
+
...(state.recentHistoryItems ?? runtime.recentHistoryItems ?? []),
|
|
4438
|
+
...selectRecoveryItems(backup.recentHistoryItems, missingHistoryProviders),
|
|
4439
|
+
],
|
|
4440
|
+
config.maxHistoryItems
|
|
4441
|
+
);
|
|
4442
|
+
if (JSON.stringify(mergedHistory) !== JSON.stringify(state.recentHistoryItems ?? runtime.recentHistoryItems ?? [])) {
|
|
4443
|
+
state.recentHistoryItems = mergedHistory;
|
|
4444
|
+
runtime.recentHistoryItems = mergedHistory;
|
|
4445
|
+
changed = true;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
if (missingTimelineProviders.length > 0) {
|
|
4450
|
+
const mergedTimeline = normalizeTimelineEntries(
|
|
4451
|
+
[
|
|
4452
|
+
...(state.recentTimelineEntries ?? runtime.recentTimelineEntries ?? []),
|
|
4453
|
+
...selectRecoveryItems(backup.recentTimelineEntries, missingTimelineProviders),
|
|
4454
|
+
],
|
|
4455
|
+
config.maxTimelineEntries
|
|
4456
|
+
);
|
|
4457
|
+
if (JSON.stringify(mergedTimeline) !== JSON.stringify(state.recentTimelineEntries ?? runtime.recentTimelineEntries ?? [])) {
|
|
4458
|
+
state.recentTimelineEntries = mergedTimeline;
|
|
4459
|
+
runtime.recentTimelineEntries = mergedTimeline;
|
|
4460
|
+
changed = true;
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
if (shouldRecoverCodeEvents) {
|
|
4465
|
+
const mergedCodeEvents = normalizeCodeEvents(
|
|
4466
|
+
[
|
|
4467
|
+
...(state.recentCodeEvents ?? runtime.recentCodeEvents ?? []),
|
|
4468
|
+
...(Array.isArray(backup.recentCodeEvents) ? backup.recentCodeEvents : []),
|
|
4469
|
+
],
|
|
4470
|
+
config.maxCodeEvents
|
|
4471
|
+
);
|
|
4472
|
+
if (JSON.stringify(mergedCodeEvents) !== JSON.stringify(state.recentCodeEvents ?? runtime.recentCodeEvents ?? [])) {
|
|
4473
|
+
state.recentCodeEvents = mergedCodeEvents;
|
|
4474
|
+
runtime.recentCodeEvents = mergedCodeEvents;
|
|
4475
|
+
invalidateDiffThreadGroupsCache();
|
|
4476
|
+
changed = true;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
if (changed) {
|
|
4481
|
+
console.log(
|
|
4482
|
+
`[state-recovery] recovered history=${missingHistoryProviders.join(",") || "none"} timeline=${missingTimelineProviders.join(",") || "none"} code=${shouldRecoverCodeEvents ? "yes" : "no"}`
|
|
4483
|
+
);
|
|
4484
|
+
}
|
|
4485
|
+
return changed;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
3611
4488
|
async function processSqliteCompletionLog({ config, runtime, state, now }) {
|
|
3612
4489
|
const logsDbFile = cleanText(runtime.logsDbFile || config.codexLogsDbFile || "");
|
|
3613
4490
|
if (!logsDbFile) {
|
|
@@ -4800,6 +5677,12 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
4800
5677
|
return false;
|
|
4801
5678
|
}
|
|
4802
5679
|
|
|
5680
|
+
if (shouldSuppressInternalScannedEvent(event)) {
|
|
5681
|
+
state.seenEvents[event.id] = event.timestampMs || Date.now();
|
|
5682
|
+
trimSeenEvents(state.seenEvents, config.maxSeenEvents);
|
|
5683
|
+
return true;
|
|
5684
|
+
}
|
|
5685
|
+
|
|
4803
5686
|
let dirty = false;
|
|
4804
5687
|
|
|
4805
5688
|
if (event.kind === "task_complete") {
|
|
@@ -4826,6 +5709,8 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
4826
5709
|
config,
|
|
4827
5710
|
state,
|
|
4828
5711
|
kind: "completion",
|
|
5712
|
+
tab: "inbox",
|
|
5713
|
+
subtab: "completed",
|
|
4829
5714
|
token: historyToken(event.id),
|
|
4830
5715
|
stableId: event.id,
|
|
4831
5716
|
title: event.title,
|
|
@@ -5134,6 +6019,28 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5134
6019
|
request,
|
|
5135
6020
|
approval: existing,
|
|
5136
6021
|
});
|
|
6022
|
+
const existingAutoPilotWriteCandidate =
|
|
6023
|
+
existing.kind === "file"
|
|
6024
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval: existing })
|
|
6025
|
+
: null;
|
|
6026
|
+
if (existingAutoPilotWriteCandidate) {
|
|
6027
|
+
try {
|
|
6028
|
+
await autoApproveTrustedWrite({
|
|
6029
|
+
config,
|
|
6030
|
+
runtime,
|
|
6031
|
+
state,
|
|
6032
|
+
approval: existing,
|
|
6033
|
+
candidate: existingAutoPilotWriteCandidate,
|
|
6034
|
+
});
|
|
6035
|
+
continue;
|
|
6036
|
+
} catch (error) {
|
|
6037
|
+
existing.resolved = false;
|
|
6038
|
+
existing.resolving = false;
|
|
6039
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, existing);
|
|
6040
|
+
runtime.nativeApprovalsByToken.set(existing.token, existing);
|
|
6041
|
+
console.error(`[auto-pilot-write-error] ${requestKey} | ${error.message}`);
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
5137
6044
|
if (changed) {
|
|
5138
6045
|
const fileKeys = isPlainObject(existing.rawParams) ? Object.keys(existing.rawParams).join(",") : "";
|
|
5139
6046
|
console.log(
|
|
@@ -5156,6 +6063,57 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5156
6063
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
5157
6064
|
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
5158
6065
|
|
|
6066
|
+
const nativeAutoPilotMatch =
|
|
6067
|
+
approval.kind === "command"
|
|
6068
|
+
? maybeAutoApproveTrustedRead({
|
|
6069
|
+
state,
|
|
6070
|
+
commandText: approval.rawParams?.command ?? approval.rawParams?.cmd ?? "",
|
|
6071
|
+
cwd: approval.rawParams?.cwd ?? approval.rawParams?.grantRoot ?? "",
|
|
6072
|
+
workspaceRoot: approval.rawParams?.grantRoot ?? approval.rawParams?.cwd ?? "",
|
|
6073
|
+
})
|
|
6074
|
+
: null;
|
|
6075
|
+
if (nativeAutoPilotMatch) {
|
|
6076
|
+
try {
|
|
6077
|
+
await autoApproveTrustedRead({
|
|
6078
|
+
config,
|
|
6079
|
+
runtime,
|
|
6080
|
+
state,
|
|
6081
|
+
approval,
|
|
6082
|
+
policyMatch: nativeAutoPilotMatch,
|
|
6083
|
+
});
|
|
6084
|
+
continue;
|
|
6085
|
+
} catch (error) {
|
|
6086
|
+
approval.resolved = false;
|
|
6087
|
+
approval.resolving = false;
|
|
6088
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
6089
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
6090
|
+
console.error(`[auto-pilot-error] ${requestKey} | ${error.message}`);
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
const nativeAutoPilotWriteCandidate =
|
|
6095
|
+
approval.kind === "file"
|
|
6096
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval })
|
|
6097
|
+
: null;
|
|
6098
|
+
if (nativeAutoPilotWriteCandidate) {
|
|
6099
|
+
try {
|
|
6100
|
+
await autoApproveTrustedWrite({
|
|
6101
|
+
config,
|
|
6102
|
+
runtime,
|
|
6103
|
+
state,
|
|
6104
|
+
approval,
|
|
6105
|
+
candidate: nativeAutoPilotWriteCandidate,
|
|
6106
|
+
});
|
|
6107
|
+
continue;
|
|
6108
|
+
} catch (error) {
|
|
6109
|
+
approval.resolved = false;
|
|
6110
|
+
approval.resolving = false;
|
|
6111
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
6112
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
6113
|
+
console.error(`[auto-pilot-write-error] ${requestKey} | ${error.message}`);
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
|
|
5159
6117
|
if (previousKeys.has(requestKey)) {
|
|
5160
6118
|
const fileKeys = isPlainObject(approval.rawParams) ? Object.keys(approval.rawParams).join(",") : "";
|
|
5161
6119
|
console.log(
|
|
@@ -5187,6 +6145,8 @@ async function syncNativeApprovals({ config, runtime, state, conversationId, pre
|
|
|
5187
6145
|
config,
|
|
5188
6146
|
state,
|
|
5189
6147
|
kind: "approval",
|
|
6148
|
+
tab: "inbox",
|
|
6149
|
+
subtab: "pending",
|
|
5190
6150
|
token: approval.token,
|
|
5191
6151
|
stableId: pendingApprovalStableId(approval),
|
|
5192
6152
|
title: approval.title,
|
|
@@ -5310,6 +6270,8 @@ async function syncPlanImplementationRequests({
|
|
|
5310
6270
|
config,
|
|
5311
6271
|
state,
|
|
5312
6272
|
kind: "plan",
|
|
6273
|
+
tab: "inbox",
|
|
6274
|
+
subtab: "pending",
|
|
5313
6275
|
token: planRequest.token,
|
|
5314
6276
|
stableId: pendingPlanStableId(planRequest),
|
|
5315
6277
|
title: planRequest.title,
|
|
@@ -5515,6 +6477,8 @@ async function syncGenericUserInputRequests({
|
|
|
5515
6477
|
config,
|
|
5516
6478
|
state,
|
|
5517
6479
|
kind: "choice",
|
|
6480
|
+
tab: "inbox",
|
|
6481
|
+
subtab: "pending",
|
|
5518
6482
|
token: userInputRequest.token,
|
|
5519
6483
|
stableId: pendingChoiceStableId(userInputRequest),
|
|
5520
6484
|
title: userInputRequest.title,
|
|
@@ -5578,6 +6542,150 @@ async function createNativeApproval({ config, runtime, conversationId, request,
|
|
|
5578
6542
|
};
|
|
5579
6543
|
}
|
|
5580
6544
|
|
|
6545
|
+
function createX402PaymentApproval({ config, body, now = Date.now() }) {
|
|
6546
|
+
const payment = normalizeX402PaymentApprovalBody(body);
|
|
6547
|
+
if (!payment) {
|
|
6548
|
+
return null;
|
|
6549
|
+
}
|
|
6550
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
6551
|
+
const requestId = cleanText(body.paymentRequestId || body.requestId || crypto.randomUUID());
|
|
6552
|
+
const requestKey = `payment:${requestId}`;
|
|
6553
|
+
const title = payment.amountUsdc
|
|
6554
|
+
? `Payment approval — ${payment.amountUsdc} USDC`
|
|
6555
|
+
: "Payment approval";
|
|
6556
|
+
return {
|
|
6557
|
+
token,
|
|
6558
|
+
requestKey,
|
|
6559
|
+
conversationId: "payments",
|
|
6560
|
+
requestId,
|
|
6561
|
+
ownerClientId: null,
|
|
6562
|
+
kind: "payment",
|
|
6563
|
+
threadLabel: "x402 payment",
|
|
6564
|
+
title,
|
|
6565
|
+
messageText: formatX402PaymentApprovalMessage(payment),
|
|
6566
|
+
reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
|
|
6567
|
+
rawParams: payment,
|
|
6568
|
+
cwd: "",
|
|
6569
|
+
workspaceRoot: "",
|
|
6570
|
+
fileRefs: [],
|
|
6571
|
+
diffText: "",
|
|
6572
|
+
diffAvailable: false,
|
|
6573
|
+
diffSource: "",
|
|
6574
|
+
diffAddedLines: 0,
|
|
6575
|
+
diffRemovedLines: 0,
|
|
6576
|
+
createdAtMs: now,
|
|
6577
|
+
resolved: false,
|
|
6578
|
+
resolving: false,
|
|
6579
|
+
resolveClaudeWaiter: null,
|
|
6580
|
+
provider: "viveworker",
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
|
|
6585
|
+
function createHazbaseWalletPaymentApproval({ config, body, now = Date.now() }) {
|
|
6586
|
+
const payment = normalizeX402PaymentApprovalBody(body);
|
|
6587
|
+
if (!payment) return null;
|
|
6588
|
+
const paymentRequestId = cleanText(body.paymentRequestId || body.requestId || "");
|
|
6589
|
+
if (!/^[a-zA-Z0-9:_-]{8,160}$/u.test(paymentRequestId)) return null;
|
|
6590
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
6591
|
+
const requestKey = `hazbase_wallet_payment:${paymentRequestId}`;
|
|
6592
|
+
const title = payment.amountUsdc
|
|
6593
|
+
? `Hazbase wallet payment — ${payment.amountUsdc} USDC`
|
|
6594
|
+
: "Hazbase wallet payment";
|
|
6595
|
+
return {
|
|
6596
|
+
token,
|
|
6597
|
+
requestKey,
|
|
6598
|
+
conversationId: "payments",
|
|
6599
|
+
requestId: paymentRequestId,
|
|
6600
|
+
paymentRequestId,
|
|
6601
|
+
ownerClientId: null,
|
|
6602
|
+
kind: "hazbase_wallet_payment",
|
|
6603
|
+
threadLabel: "x402 payment",
|
|
6604
|
+
title,
|
|
6605
|
+
messageText: formatHazbaseWalletPaymentApprovalMessage(payment),
|
|
6606
|
+
reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
|
|
6607
|
+
rawParams: payment,
|
|
6608
|
+
cwd: "",
|
|
6609
|
+
workspaceRoot: "",
|
|
6610
|
+
fileRefs: [],
|
|
6611
|
+
diffText: "",
|
|
6612
|
+
diffAvailable: false,
|
|
6613
|
+
diffSource: "",
|
|
6614
|
+
diffAddedLines: 0,
|
|
6615
|
+
diffRemovedLines: 0,
|
|
6616
|
+
createdAtMs: now,
|
|
6617
|
+
resolved: false,
|
|
6618
|
+
resolving: false,
|
|
6619
|
+
resolveClaudeWaiter: null,
|
|
6620
|
+
provider: "viveworker",
|
|
6621
|
+
};
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
function formatHazbaseWalletPaymentApprovalMessage(payment) {
|
|
6625
|
+
const lines = [
|
|
6626
|
+
"Hazbase Smart Wallet payment requested.",
|
|
6627
|
+
"",
|
|
6628
|
+
`Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
|
|
6629
|
+
`Network: ${payment.network} (chainId ${payment.chainId})`,
|
|
6630
|
+
`Pay to: ${payment.payTo}`,
|
|
6631
|
+
`Asset: ${payment.asset}`,
|
|
6632
|
+
`Resource: ${payment.resource}`,
|
|
6633
|
+
"",
|
|
6634
|
+
"Approving will ask for your passkey, then hazBase will submit a gasless Smart Wallet payment.",
|
|
6635
|
+
];
|
|
6636
|
+
if (payment.description) lines.splice(7, 0, `Description: ${payment.description}`);
|
|
6637
|
+
if (payment.url && payment.url !== payment.resource) lines.splice(-1, 0, `URL: ${payment.url}`);
|
|
6638
|
+
return lines.join("\n");
|
|
6639
|
+
}
|
|
6640
|
+
|
|
6641
|
+
function normalizeX402PaymentApprovalBody(body) {
|
|
6642
|
+
if (!isPlainObject(body)) return null;
|
|
6643
|
+
const payment = isPlainObject(body.payment) ? body.payment : {};
|
|
6644
|
+
const network = cleanText(payment.network || "");
|
|
6645
|
+
const chainId = Number(payment.chainId) || 0;
|
|
6646
|
+
const amountAtomic = cleanText(payment.amountAtomic || "");
|
|
6647
|
+
const amountUsdc = cleanText(payment.amountUsdc || "");
|
|
6648
|
+
const payTo = cleanText(payment.payTo || "");
|
|
6649
|
+
const asset = cleanText(payment.asset || "");
|
|
6650
|
+
const resource = cleanText(payment.resource || body.url || "");
|
|
6651
|
+
const description = cleanText(payment.description || "");
|
|
6652
|
+
const url = cleanText(body.url || resource);
|
|
6653
|
+
if (!network || !chainId || !amountAtomic || !payTo || !asset || !resource) {
|
|
6654
|
+
return null;
|
|
6655
|
+
}
|
|
6656
|
+
return {
|
|
6657
|
+
url,
|
|
6658
|
+
network,
|
|
6659
|
+
chainId,
|
|
6660
|
+
amountAtomic,
|
|
6661
|
+
amountUsdc,
|
|
6662
|
+
payTo,
|
|
6663
|
+
asset,
|
|
6664
|
+
resource,
|
|
6665
|
+
description,
|
|
6666
|
+
};
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
function formatX402PaymentApprovalMessage(payment) {
|
|
6670
|
+
const lines = [
|
|
6671
|
+
"x402 payment approval requested.",
|
|
6672
|
+
"",
|
|
6673
|
+
`Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
|
|
6674
|
+
`Network: ${payment.network} (chainId ${payment.chainId})`,
|
|
6675
|
+
`Pay to: ${payment.payTo}`,
|
|
6676
|
+
`Asset: ${payment.asset}`,
|
|
6677
|
+
`Resource: ${payment.resource}`,
|
|
6678
|
+
];
|
|
6679
|
+
if (payment.description) {
|
|
6680
|
+
lines.push(`Description: ${payment.description}`);
|
|
6681
|
+
}
|
|
6682
|
+
if (payment.url && payment.url !== payment.resource) {
|
|
6683
|
+
lines.push(`URL: ${payment.url}`);
|
|
6684
|
+
}
|
|
6685
|
+
lines.push("", "Approve only if the amount, recipient, network, and resource match what you expect.");
|
|
6686
|
+
return lines.join("\n");
|
|
6687
|
+
}
|
|
6688
|
+
|
|
5581
6689
|
async function buildNativeApprovalPayload({ config, runtime, conversationId, request, token }) {
|
|
5582
6690
|
const kind = nativeApprovalKind(request.method);
|
|
5583
6691
|
if (!kind) {
|
|
@@ -5633,6 +6741,8 @@ async function buildNativeApprovalPayload({ config, runtime, conversationId, req
|
|
|
5633
6741
|
ownerClientId: runtime.threadOwnerClientIds.get(conversationId) ?? null,
|
|
5634
6742
|
approvalIds,
|
|
5635
6743
|
rawParams,
|
|
6744
|
+
cwd: cleanText(rawParams?.cwd ?? rawParams?.grantRoot ?? ""),
|
|
6745
|
+
workspaceRoot: cleanText(rawParams?.grantRoot ?? rawParams?.cwd ?? ""),
|
|
5636
6746
|
fileRefs: normalizeTimelineFileRefs(mergedDelta?.fileRefs ?? []),
|
|
5637
6747
|
diffText: normalizeTimelineDiffText(mergedDelta?.diffText ?? ""),
|
|
5638
6748
|
diffAvailable: Boolean(mergedDelta?.diffAvailable),
|
|
@@ -5669,6 +6779,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5669
6779
|
ownerClientId: approval.ownerClientId,
|
|
5670
6780
|
approvalIds: approval.approvalIds,
|
|
5671
6781
|
rawParams: approval.rawParams,
|
|
6782
|
+
cwd: approval.cwd,
|
|
6783
|
+
workspaceRoot: approval.workspaceRoot,
|
|
5672
6784
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
5673
6785
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
5674
6786
|
diffAvailable: Boolean(approval.diffAvailable),
|
|
@@ -5688,6 +6800,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5688
6800
|
approval.ownerClientId = payload.ownerClientId;
|
|
5689
6801
|
approval.approvalIds = payload.approvalIds;
|
|
5690
6802
|
approval.rawParams = payload.rawParams;
|
|
6803
|
+
approval.cwd = payload.cwd;
|
|
6804
|
+
approval.workspaceRoot = payload.workspaceRoot;
|
|
5691
6805
|
approval.fileRefs = payload.fileRefs;
|
|
5692
6806
|
approval.diffText = payload.diffText;
|
|
5693
6807
|
approval.diffAvailable = payload.diffAvailable;
|
|
@@ -5707,6 +6821,8 @@ async function refreshNativeApprovalFromRequest({ config, runtime, conversationI
|
|
|
5707
6821
|
ownerClientId: approval.ownerClientId,
|
|
5708
6822
|
approvalIds: approval.approvalIds,
|
|
5709
6823
|
rawParams: approval.rawParams,
|
|
6824
|
+
cwd: approval.cwd,
|
|
6825
|
+
workspaceRoot: approval.workspaceRoot,
|
|
5710
6826
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
5711
6827
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
5712
6828
|
diffAvailable: Boolean(approval.diffAvailable),
|
|
@@ -7767,6 +8883,121 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
|
|
|
7767
8883
|
return shortId(normalizedConversationId) || t(DEFAULT_LOCALE, "server.fallback.codexTask", { provider: providerDisplayName(DEFAULT_LOCALE, "codex") });
|
|
7768
8884
|
}
|
|
7769
8885
|
|
|
8886
|
+
function isFallbackConversationLabel(label, conversationId) {
|
|
8887
|
+
const normalizedLabel = cleanText(label || "");
|
|
8888
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
8889
|
+
if (!normalizedLabel) {
|
|
8890
|
+
return true;
|
|
8891
|
+
}
|
|
8892
|
+
if (normalizedConversationId && normalizedLabel === shortId(normalizedConversationId)) {
|
|
8893
|
+
return true;
|
|
8894
|
+
}
|
|
8895
|
+
return false;
|
|
8896
|
+
}
|
|
8897
|
+
|
|
8898
|
+
function deriveClaudeSdkCliThreadLabel(messageText, conversationId = "") {
|
|
8899
|
+
const cleaned = stripNotificationMarkup(stripEnvironmentContextBlocks(messageText || ""));
|
|
8900
|
+
const single = cleanText(cleaned);
|
|
8901
|
+
if (!single || /^\d+$/u.test(single)) {
|
|
8902
|
+
return "";
|
|
8903
|
+
}
|
|
8904
|
+
|
|
8905
|
+
if (/^You are scoring Moltbook posts? for an AI agent\b/iu.test(single)) {
|
|
8906
|
+
return "Moltbook scoring";
|
|
8907
|
+
}
|
|
8908
|
+
if (/^Codex from another agent:/iu.test(single)) {
|
|
8909
|
+
return truncate(cleanText(single.replace(/^Codex from another agent:\s*/iu, "")), 90) || "Cross-agent task";
|
|
8910
|
+
}
|
|
8911
|
+
|
|
8912
|
+
const firstSentence = cleanText(single.split(/(?<=[.!?。!?])\s+/u)[0] || "");
|
|
8913
|
+
const candidate = firstSentence || single;
|
|
8914
|
+
if (!candidate || candidate === shortId(conversationId)) {
|
|
8915
|
+
return "";
|
|
8916
|
+
}
|
|
8917
|
+
return truncate(candidate, 90);
|
|
8918
|
+
}
|
|
8919
|
+
|
|
8920
|
+
function isHiddenClaudeInternalScoringText(text) {
|
|
8921
|
+
const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
|
|
8922
|
+
if (!single) {
|
|
8923
|
+
return false;
|
|
8924
|
+
}
|
|
8925
|
+
return /^You are scoring Moltbook posts? for an AI agent\b/iu.test(single);
|
|
8926
|
+
}
|
|
8927
|
+
|
|
8928
|
+
function isHiddenCodexApprovalAssessmentText(text) {
|
|
8929
|
+
const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
|
|
8930
|
+
if (!single) {
|
|
8931
|
+
return false;
|
|
8932
|
+
}
|
|
8933
|
+
return /\bThe following is the Codex agent history(?: added since your last approval assessment)?\b/iu.test(single);
|
|
8934
|
+
}
|
|
8935
|
+
|
|
8936
|
+
function shouldHideInternalTimelineItem(item) {
|
|
8937
|
+
return shouldHideClaudeInternalItem(item) || shouldHideCodexInternalApprovalItem(item);
|
|
8938
|
+
}
|
|
8939
|
+
|
|
8940
|
+
function shouldHideClaudeInternalItem(item) {
|
|
8941
|
+
if (!isPlainObject(item)) {
|
|
8942
|
+
return false;
|
|
8943
|
+
}
|
|
8944
|
+
if (normalizeProvider(item.provider) !== "claude") {
|
|
8945
|
+
return false;
|
|
8946
|
+
}
|
|
8947
|
+
const threadLabel = cleanText(item.threadLabel ?? "");
|
|
8948
|
+
if (threadLabel === "Moltbook scoring") {
|
|
8949
|
+
return true;
|
|
8950
|
+
}
|
|
8951
|
+
return (
|
|
8952
|
+
isHiddenClaudeInternalScoringText(item.messageText) ||
|
|
8953
|
+
isHiddenClaudeInternalScoringText(item.summary) ||
|
|
8954
|
+
isHiddenClaudeInternalScoringText(item.detailText) ||
|
|
8955
|
+
isHiddenClaudeInternalScoringText(item.message)
|
|
8956
|
+
);
|
|
8957
|
+
}
|
|
8958
|
+
|
|
8959
|
+
function shouldSuppressInternalScannedEvent(event) {
|
|
8960
|
+
if (!isPlainObject(event)) {
|
|
8961
|
+
return false;
|
|
8962
|
+
}
|
|
8963
|
+
const provider = normalizeProvider(event.provider || "codex");
|
|
8964
|
+
if (provider !== "codex") {
|
|
8965
|
+
return false;
|
|
8966
|
+
}
|
|
8967
|
+
|
|
8968
|
+
return shouldHideCodexInternalApprovalItem({
|
|
8969
|
+
provider,
|
|
8970
|
+
kind: cleanText(event.kind || ""),
|
|
8971
|
+
title: event.title,
|
|
8972
|
+
threadLabel: event.threadLabel,
|
|
8973
|
+
summary: event.summary ?? event.message,
|
|
8974
|
+
messageText: event.messageText ?? event.detailText ?? event.message,
|
|
8975
|
+
detailText: event.detailText,
|
|
8976
|
+
message: event.message,
|
|
8977
|
+
});
|
|
8978
|
+
}
|
|
8979
|
+
|
|
8980
|
+
function shouldHideCodexInternalApprovalItem(item) {
|
|
8981
|
+
if (!isPlainObject(item)) {
|
|
8982
|
+
return false;
|
|
8983
|
+
}
|
|
8984
|
+
if (normalizeProvider(item.provider) !== "codex") {
|
|
8985
|
+
return false;
|
|
8986
|
+
}
|
|
8987
|
+
|
|
8988
|
+
const title = cleanText(item.title ?? "");
|
|
8989
|
+
const threadLabel = cleanText(item.threadLabel ?? "");
|
|
8990
|
+
if (isHiddenCodexApprovalAssessmentText(title) || isHiddenCodexApprovalAssessmentText(threadLabel)) {
|
|
8991
|
+
return true;
|
|
8992
|
+
}
|
|
8993
|
+
return (
|
|
8994
|
+
isHiddenCodexApprovalAssessmentText(item.messageText) ||
|
|
8995
|
+
isHiddenCodexApprovalAssessmentText(item.summary) ||
|
|
8996
|
+
isHiddenCodexApprovalAssessmentText(item.detailText) ||
|
|
8997
|
+
isHiddenCodexApprovalAssessmentText(item.message)
|
|
8998
|
+
);
|
|
8999
|
+
}
|
|
9000
|
+
|
|
7770
9001
|
function threadStateArchiveStatus(threadState) {
|
|
7771
9002
|
if (!isPlainObject(threadState)) {
|
|
7772
9003
|
return "";
|
|
@@ -8724,6 +9955,8 @@ function markDevicePaired(state, config, deviceId, metadata = {}, now = Date.now
|
|
|
8724
9955
|
return JSON.stringify(previous ?? null) !== JSON.stringify(next);
|
|
8725
9956
|
}
|
|
8726
9957
|
|
|
9958
|
+
const TOUCH_DEVICE_TRUST_DEBOUNCE_MS = 60_000;
|
|
9959
|
+
|
|
8727
9960
|
function touchDeviceTrust(state, config, deviceId, now = Date.now()) {
|
|
8728
9961
|
const normalizedDeviceId = cleanText(deviceId || "");
|
|
8729
9962
|
const current = getActiveDeviceTrustRecord(state, config, normalizedDeviceId, now);
|
|
@@ -8731,6 +9964,20 @@ function touchDeviceTrust(state, config, deviceId, now = Date.now()) {
|
|
|
8731
9964
|
return false;
|
|
8732
9965
|
}
|
|
8733
9966
|
|
|
9967
|
+
// Debounce: `lastAuthenticatedAtMs` is bumped to `now` on every
|
|
9968
|
+
// authenticated request. Without this guard, every `/api/session`
|
|
9969
|
+
// (and `/api/bootstrap`) call flags the record as "changed" and the
|
|
9970
|
+
// caller runs a full `saveState`, which synchronously stringifies an
|
|
9971
|
+
// 8+ MB state.json on the main thread and starves the event loop —
|
|
9972
|
+
// driving TLS handshake latency for any in-flight connection into the
|
|
9973
|
+
// seconds range. Skip the write when we've touched it recently; the
|
|
9974
|
+
// next real change (pairing, revocation, locale update, etc.) still
|
|
9975
|
+
// goes through unchanged.
|
|
9976
|
+
const previousTouch = Number(current.lastAuthenticatedAtMs) || 0;
|
|
9977
|
+
if (previousTouch > 0 && now - previousTouch < TOUCH_DEVICE_TRUST_DEBOUNCE_MS) {
|
|
9978
|
+
return false;
|
|
9979
|
+
}
|
|
9980
|
+
|
|
8734
9981
|
const next = {
|
|
8735
9982
|
...current,
|
|
8736
9983
|
lastAuthenticatedAtMs: now,
|
|
@@ -8853,11 +10100,27 @@ function buildDeviceSummary({ config, state, deviceId, record, currentDeviceId,
|
|
|
8853
10100
|
};
|
|
8854
10101
|
}
|
|
8855
10102
|
|
|
8856
|
-
|
|
10103
|
+
// Shared between `/api/session` and `/api/bootstrap` so both return an
|
|
10104
|
+
// identical session shape.
|
|
10105
|
+
function buildSessionPayload({ config, state, session }) {
|
|
8857
10106
|
return {
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
10107
|
+
authenticated: Boolean(session.authenticated),
|
|
10108
|
+
expiresAtMs: Number(session.expiresAtMs) || 0,
|
|
10109
|
+
pairingAvailable: isPairingAvailableForState(config, state),
|
|
10110
|
+
webPushEnabled: config.webPushEnabled,
|
|
10111
|
+
httpsEnabled: config.nativeApprovalPublicBaseUrl.startsWith("https://"),
|
|
10112
|
+
appVersion: appPackageVersion,
|
|
10113
|
+
deviceId: session.deviceId || null,
|
|
10114
|
+
temporaryPairing: session.temporaryPairing === true,
|
|
10115
|
+
...buildSessionLocalePayload(config, state, session.deviceId),
|
|
10116
|
+
};
|
|
10117
|
+
}
|
|
10118
|
+
|
|
10119
|
+
function buildDevicesResponse({ config, state, session, locale }) {
|
|
10120
|
+
return {
|
|
10121
|
+
devices: activeTrustedDevices(state, config).map(({ deviceId, record }) =>
|
|
10122
|
+
buildDeviceSummary({
|
|
10123
|
+
config,
|
|
8861
10124
|
state,
|
|
8862
10125
|
deviceId,
|
|
8863
10126
|
record,
|
|
@@ -9448,7 +10711,9 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
9448
10711
|
token: approval.token,
|
|
9449
10712
|
threadId: cleanText(approval.conversationId || ""),
|
|
9450
10713
|
threadLabel: approval.threadLabel || "",
|
|
9451
|
-
title:
|
|
10714
|
+
title: cleanText(approval.kind || "") === "payment"
|
|
10715
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
10716
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
9452
10717
|
summary: formatNotificationBody(approval.messageText, 100) || approval.messageText,
|
|
9453
10718
|
primaryLabel: t(locale, "server.action.review"),
|
|
9454
10719
|
createdAtMs: Number(approval.createdAtMs) || now,
|
|
@@ -9608,7 +10873,7 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
9608
10873
|
}
|
|
9609
10874
|
|
|
9610
10875
|
async function buildDiffInboxItems(runtime, state, config, locale) {
|
|
9611
|
-
return (await
|
|
10876
|
+
return (await getDiffThreadGroupsCached(runtime, state, config)).map((group) => ({
|
|
9612
10877
|
kind: "diff_thread",
|
|
9613
10878
|
token: group.token,
|
|
9614
10879
|
threadId: group.threadId,
|
|
@@ -9767,14 +11032,24 @@ async function buildDiffThreadGroups(runtime, state, config) {
|
|
|
9767
11032
|
.sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
|
|
9768
11033
|
}
|
|
9769
11034
|
|
|
9770
|
-
|
|
11035
|
+
// Split into two halves so `/api/inbox` (pending + completed) returns
|
|
11036
|
+
// without waiting on the diff build, which spawns `git` subprocesses per
|
|
11037
|
+
// tracked repo inside `buildCurrentUnstagedChangesForRepo`. The diff half
|
|
11038
|
+
// is served from `/api/inbox/diff` and fetched separately by the PWA so
|
|
11039
|
+
// it doesn't block first paint of the inbox/completed lists.
|
|
11040
|
+
function buildInboxFastResponse(runtime, state, config, locale) {
|
|
9771
11041
|
return {
|
|
9772
11042
|
pending: buildPendingInboxItems(runtime, state, config, locale),
|
|
9773
|
-
diff: await buildDiffInboxItems(runtime, state, config, locale),
|
|
9774
11043
|
completed: buildCompletedInboxItems(runtime, state, config, locale),
|
|
9775
11044
|
};
|
|
9776
11045
|
}
|
|
9777
11046
|
|
|
11047
|
+
async function buildInboxDiffResponse(runtime, state, config, locale) {
|
|
11048
|
+
return {
|
|
11049
|
+
diff: await buildDiffInboxItems(runtime, state, config, locale),
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
11052
|
+
|
|
9778
11053
|
function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
9779
11054
|
const now = Date.now();
|
|
9780
11055
|
const items = [];
|
|
@@ -9790,7 +11065,9 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
9790
11065
|
kind: "approval",
|
|
9791
11066
|
threadId: cleanText(approval.conversationId || ""),
|
|
9792
11067
|
threadLabel: approval.threadLabel,
|
|
9793
|
-
title:
|
|
11068
|
+
title: cleanText(approval.kind || "") === "payment"
|
|
11069
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
11070
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
9794
11071
|
summary: formatNotificationBody(approval.messageText, 180) || approval.messageText,
|
|
9795
11072
|
messageText: approval.messageText,
|
|
9796
11073
|
outcome: "pending",
|
|
@@ -9988,15 +11265,18 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
9988
11265
|
};
|
|
9989
11266
|
}
|
|
9990
11267
|
|
|
9991
|
-
function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
11268
|
+
function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
9992
11269
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
9993
11270
|
const approvalKind = cleanText(approval.kind || "");
|
|
11271
|
+
const autoPilotReview = buildAutoPilotManualReview(runtime, state, approval, locale);
|
|
9994
11272
|
const detail = {
|
|
9995
11273
|
kind: "approval",
|
|
9996
11274
|
approvalKind,
|
|
9997
11275
|
provider: normalizeProvider(approval.provider),
|
|
9998
11276
|
token: approval.token,
|
|
9999
|
-
title:
|
|
11277
|
+
title: approvalKind === "payment"
|
|
11278
|
+
? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
|
|
11279
|
+
: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
10000
11280
|
threadLabel: approval.threadLabel || "",
|
|
10001
11281
|
createdAtMs: Number(approval.createdAtMs) || 0,
|
|
10002
11282
|
messageHtml: renderMessageHtml(approval.messageText, `<p>${escapeHtml(t(locale, "detail.approvalRequested"))}</p>`),
|
|
@@ -10007,13 +11287,18 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
|
10007
11287
|
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
10008
11288
|
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
10009
11289
|
previousContext,
|
|
11290
|
+
autoPilotReview,
|
|
10010
11291
|
readOnly: approval.readOnly === true,
|
|
10011
11292
|
actions: approval.readOnly === true
|
|
10012
11293
|
? []
|
|
10013
|
-
:
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
11294
|
+
: approvalKind === "hazbase_wallet_payment"
|
|
11295
|
+
? [
|
|
11296
|
+
{ label: t(locale, "server.action.payWithWallet"), tone: "primary", url: `/api/payments/x402/hazbase-wallet/${encodeURIComponent(approval.token)}/pay`, body: { hazbaseReauth: true } },
|
|
11297
|
+
]
|
|
11298
|
+
: [
|
|
11299
|
+
{ label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
|
|
11300
|
+
{ label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
|
|
11301
|
+
],
|
|
10017
11302
|
};
|
|
10018
11303
|
if (approvalKind === "plan") {
|
|
10019
11304
|
const planText = String(approval.planText || "");
|
|
@@ -10031,6 +11316,193 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
|
10031
11316
|
return detail;
|
|
10032
11317
|
}
|
|
10033
11318
|
|
|
11319
|
+
function buildAutoPilotManualReview(runtime, state, approval, locale) {
|
|
11320
|
+
if (!isPlainObject(approval) || approval.readOnly === true) {
|
|
11321
|
+
return null;
|
|
11322
|
+
}
|
|
11323
|
+
if (cleanText(approval.kind || "") !== "file") {
|
|
11324
|
+
return null;
|
|
11325
|
+
}
|
|
11326
|
+
return buildAutoPilotWriteManualReview(runtime, state, approval, locale);
|
|
11327
|
+
}
|
|
11328
|
+
|
|
11329
|
+
function buildAutoPilotWriteManualReview(runtime, state, approval, locale) {
|
|
11330
|
+
const writesEnabled = isAutoPilotTrustedWritesEnabled(state);
|
|
11331
|
+
if (!writesEnabled) {
|
|
11332
|
+
return {
|
|
11333
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11334
|
+
body: t(locale, "detail.autoPilot.writeDisabled"),
|
|
11335
|
+
};
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11338
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
11339
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
11340
|
+
if (!cwd || !workspaceRoot) {
|
|
11341
|
+
return {
|
|
11342
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11343
|
+
body: t(locale, "detail.autoPilot.writeMissingWorkspace"),
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
|
|
11347
|
+
const diffText = normalizeTimelineDiffText(approval?.diffText ?? "");
|
|
11348
|
+
if (!diffText) {
|
|
11349
|
+
return {
|
|
11350
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11351
|
+
body: t(locale, "detail.autoPilot.writeMissingDiff"),
|
|
11352
|
+
};
|
|
11353
|
+
}
|
|
11354
|
+
|
|
11355
|
+
const rawFileRefs = normalizeTimelineFileRefs(approval?.fileRefs ?? []);
|
|
11356
|
+
if (rawFileRefs.length === 0) {
|
|
11357
|
+
return {
|
|
11358
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11359
|
+
body: t(locale, "detail.autoPilot.writeMissingFiles"),
|
|
11360
|
+
};
|
|
11361
|
+
}
|
|
11362
|
+
|
|
11363
|
+
const resolvedFileRefs = resolveTrustedWriteFileRefs({
|
|
11364
|
+
fileRefs: approval?.fileRefs ?? [],
|
|
11365
|
+
cwd,
|
|
11366
|
+
workspaceRoot,
|
|
11367
|
+
});
|
|
11368
|
+
if (!resolvedFileRefs) {
|
|
11369
|
+
return {
|
|
11370
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11371
|
+
body: t(locale, "detail.autoPilot.writeInvalidFiles"),
|
|
11372
|
+
};
|
|
11373
|
+
}
|
|
11374
|
+
|
|
11375
|
+
const counts = diffLineCounts(diffText);
|
|
11376
|
+
const totalChangedLines =
|
|
11377
|
+
Math.max(0, Number(counts.addedLines) || 0) +
|
|
11378
|
+
Math.max(0, Number(counts.removedLines) || 0);
|
|
11379
|
+
if (totalChangedLines === 0) {
|
|
11380
|
+
return {
|
|
11381
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11382
|
+
body: t(locale, "detail.autoPilot.writeMissingDiff"),
|
|
11383
|
+
};
|
|
11384
|
+
}
|
|
11385
|
+
if (totalChangedLines > 120) {
|
|
11386
|
+
return {
|
|
11387
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11388
|
+
body: t(locale, "detail.autoPilot.writeDiffTooLarge"),
|
|
11389
|
+
};
|
|
11390
|
+
}
|
|
11391
|
+
|
|
11392
|
+
const diffSections = splitUnifiedDiffTextByFile(diffText);
|
|
11393
|
+
if (diffSections.length === 0) {
|
|
11394
|
+
return {
|
|
11395
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11396
|
+
body: t(locale, "detail.autoPilot.writeDiffUnreadable"),
|
|
11397
|
+
};
|
|
11398
|
+
}
|
|
11399
|
+
if (diffSections.length > 3) {
|
|
11400
|
+
return {
|
|
11401
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11402
|
+
body: t(locale, "detail.autoPilot.writeDiffTooLarge"),
|
|
11403
|
+
};
|
|
11404
|
+
}
|
|
11405
|
+
|
|
11406
|
+
if (
|
|
11407
|
+
/^(?: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)
|
|
11408
|
+
) {
|
|
11409
|
+
return {
|
|
11410
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11411
|
+
body: t(locale, "detail.autoPilot.writeDangerousDiff"),
|
|
11412
|
+
};
|
|
11413
|
+
}
|
|
11414
|
+
|
|
11415
|
+
const diffSectionRefs = resolveTrustedWriteDiffSectionFileRefs({
|
|
11416
|
+
diffSections,
|
|
11417
|
+
cwd,
|
|
11418
|
+
workspaceRoot,
|
|
11419
|
+
});
|
|
11420
|
+
if (!diffSectionRefs || !sameNormalizedFileRefSet(diffSectionRefs, resolvedFileRefs)) {
|
|
11421
|
+
return {
|
|
11422
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11423
|
+
body: t(locale, "detail.autoPilot.writeDiffMismatch"),
|
|
11424
|
+
};
|
|
11425
|
+
}
|
|
11426
|
+
|
|
11427
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
11428
|
+
const isContentOnly = resolvedFileRefs.every((fileRef) => isAutoPilotContentWritePath(fileRef));
|
|
11429
|
+
if (isContentOnly) {
|
|
11430
|
+
if (!lanes.content) {
|
|
11431
|
+
return {
|
|
11432
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11433
|
+
body: t(locale, "detail.autoPilot.writeContentDisabled"),
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
return {
|
|
11437
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11438
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11439
|
+
};
|
|
11440
|
+
}
|
|
11441
|
+
|
|
11442
|
+
const isUiTestsOnly = resolvedFileRefs.every((fileRef) => isAutoPilotUiTestWritePath(fileRef));
|
|
11443
|
+
if (isUiTestsOnly) {
|
|
11444
|
+
if (!lanes.uiTests) {
|
|
11445
|
+
return {
|
|
11446
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11447
|
+
body: t(locale, "detail.autoPilot.writeUiDisabled"),
|
|
11448
|
+
};
|
|
11449
|
+
}
|
|
11450
|
+
if (totalChangedLines > 80) {
|
|
11451
|
+
return {
|
|
11452
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11453
|
+
body: t(locale, "detail.autoPilot.writeUiTooLarge"),
|
|
11454
|
+
};
|
|
11455
|
+
}
|
|
11456
|
+
if (hasUnsafeUiOrTestWriteDiff(diffText)) {
|
|
11457
|
+
return {
|
|
11458
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11459
|
+
body: t(locale, "detail.autoPilot.writeUiUnsafe"),
|
|
11460
|
+
};
|
|
11461
|
+
}
|
|
11462
|
+
return {
|
|
11463
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11464
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11465
|
+
};
|
|
11466
|
+
}
|
|
11467
|
+
|
|
11468
|
+
const isSourceOnly = resolvedFileRefs.every((fileRef) => isAutoPilotSourceWritePath(fileRef));
|
|
11469
|
+
if (isSourceOnly) {
|
|
11470
|
+
if (!lanes.source) {
|
|
11471
|
+
return {
|
|
11472
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11473
|
+
body: t(locale, "detail.autoPilot.writeSourceDisabled"),
|
|
11474
|
+
};
|
|
11475
|
+
}
|
|
11476
|
+
if (totalChangedLines > 40) {
|
|
11477
|
+
return {
|
|
11478
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11479
|
+
body: t(locale, "detail.autoPilot.writeSourceTooLarge"),
|
|
11480
|
+
};
|
|
11481
|
+
}
|
|
11482
|
+
if (hasUnsafeSourceWriteDiff(diffText)) {
|
|
11483
|
+
return {
|
|
11484
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11485
|
+
body: t(locale, "detail.autoPilot.writeSourceUnsafe"),
|
|
11486
|
+
};
|
|
11487
|
+
}
|
|
11488
|
+
if (!hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs })) {
|
|
11489
|
+
return {
|
|
11490
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11491
|
+
body: t(locale, "detail.autoPilot.writeSourceContinuity"),
|
|
11492
|
+
};
|
|
11493
|
+
}
|
|
11494
|
+
return {
|
|
11495
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11496
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11497
|
+
};
|
|
11498
|
+
}
|
|
11499
|
+
|
|
11500
|
+
return {
|
|
11501
|
+
title: t(locale, "detail.autoPilot.manualTitle"),
|
|
11502
|
+
body: t(locale, "detail.autoPilot.writeNoLaneMatch"),
|
|
11503
|
+
};
|
|
11504
|
+
}
|
|
11505
|
+
|
|
10034
11506
|
function buildPreviousApprovalContext(runtime, approval) {
|
|
10035
11507
|
const threadId = cleanText(approval?.conversationId || "");
|
|
10036
11508
|
const approvalCreatedAtMs = Number(approval?.createdAtMs) || 0;
|
|
@@ -10993,142 +12465,668 @@ function findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerpri
|
|
|
10993
12465
|
return match;
|
|
10994
12466
|
}
|
|
10995
12467
|
|
|
10996
|
-
|
|
10997
|
-
if (
|
|
10998
|
-
|
|
10999
|
-
// ExitPlanMode cannot be truly auto-approved via permissionDecision: "allow"
|
|
11000
|
-
// (Claude still shows the native PC plan dialog). Instead, deny the tool
|
|
11001
|
-
// call with a reason that tells Claude the user already decided on mobile.
|
|
11002
|
-
approval.resolveClaudeWaiter({
|
|
11003
|
-
permissionDecision: "deny",
|
|
11004
|
-
permissionDecisionReason:
|
|
11005
|
-
decision === "accept"
|
|
11006
|
-
? "User approved the plan via the viveworker mobile app. Proceed with implementing the plan exactly as proposed and do not call ExitPlanMode again."
|
|
11007
|
-
: "User rejected the plan via the viveworker mobile app. Revise the plan based on the conversation context before calling ExitPlanMode again.",
|
|
11008
|
-
});
|
|
11009
|
-
} else {
|
|
11010
|
-
approval.resolveClaudeWaiter(decision);
|
|
11011
|
-
}
|
|
11012
|
-
} else if (approval.provider === "claude") {
|
|
11013
|
-
// notifyOnly Claude approvals have no long-poll waiter and no ipcClient
|
|
11014
|
-
// back-channel — the desktop user already answered. Nothing to send.
|
|
11015
|
-
} else {
|
|
11016
|
-
await runtime.ipcClient?.sendApprovalDecision(approval, decision);
|
|
11017
|
-
}
|
|
11018
|
-
approval.resolved = true;
|
|
11019
|
-
approval.resolving = false;
|
|
11020
|
-
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
11021
|
-
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
11022
|
-
const stateChanged = recordActionHistoryItem({
|
|
11023
|
-
config,
|
|
11024
|
-
runtime,
|
|
11025
|
-
state,
|
|
11026
|
-
kind: "approval",
|
|
11027
|
-
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
11028
|
-
token: approval.token,
|
|
11029
|
-
title: approval.title,
|
|
11030
|
-
threadLabel: approval.threadLabel || "",
|
|
11031
|
-
messageText: `${approvalDecisionMessage(decision, config.defaultLocale, approval.provider)}\n\n${approval.messageText}`,
|
|
11032
|
-
summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage(decision, config.defaultLocale, approval.provider),
|
|
11033
|
-
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
11034
|
-
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
11035
|
-
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
11036
|
-
diffAvailable: approval.diffAvailable === true || Boolean(approval.diffText),
|
|
11037
|
-
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
11038
|
-
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
11039
|
-
outcome: decision === "accept" ? "approved" : "rejected",
|
|
11040
|
-
provider: approval.provider || "codex",
|
|
11041
|
-
});
|
|
11042
|
-
if (stateChanged) {
|
|
11043
|
-
await saveState(config.stateFile, state);
|
|
12468
|
+
function maybeAutoApproveTrustedRead({ state, commandText, cwd, workspaceRoot }) {
|
|
12469
|
+
if (state?.autoPilotTrustedReads !== true) {
|
|
12470
|
+
return null;
|
|
11044
12471
|
}
|
|
11045
|
-
|
|
12472
|
+
return evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot });
|
|
11046
12473
|
}
|
|
11047
12474
|
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
12475
|
+
function isAutoPilotTrustedWritesEnabled(state) {
|
|
12476
|
+
return hasAnyAutoPilotWriteLaneEnabled(state);
|
|
12477
|
+
}
|
|
12478
|
+
|
|
12479
|
+
function isDeniedTrustedWritePath(candidatePath) {
|
|
12480
|
+
const normalized = cleanText(candidatePath || "");
|
|
12481
|
+
if (!normalized) {
|
|
12482
|
+
return true;
|
|
11052
12483
|
}
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
12484
|
+
const lower = normalized.toLowerCase();
|
|
12485
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12486
|
+
const basename = segments[segments.length - 1] || "";
|
|
12487
|
+
if (isSensitiveCommandPath(normalized)) {
|
|
12488
|
+
return true;
|
|
11056
12489
|
}
|
|
11057
|
-
if (
|
|
11058
|
-
|
|
11059
|
-
if (entry) {
|
|
11060
|
-
return buildAmbientSuggestionsDetail(entry, locale);
|
|
11061
|
-
}
|
|
11062
|
-
const historicalSource = (runtime.recentHistoryItems ?? []).find((item) => {
|
|
11063
|
-
if (cleanText(item?.token || "") !== cleanText(token || "")) {
|
|
11064
|
-
return false;
|
|
11065
|
-
}
|
|
11066
|
-
const itemKind = cleanText(item?.kind || "");
|
|
11067
|
-
if (itemKind !== "assistant_final" && itemKind !== "completion") {
|
|
11068
|
-
return false;
|
|
11069
|
-
}
|
|
11070
|
-
return parseAmbientSuggestionsPayload(item?.messageText ?? "") !== null;
|
|
11071
|
-
});
|
|
11072
|
-
if (!historicalSource) {
|
|
11073
|
-
return null;
|
|
11074
|
-
}
|
|
11075
|
-
return buildAmbientSuggestionsDetail({
|
|
11076
|
-
token: historicalSource.token,
|
|
11077
|
-
threadId: historyItemThreadId(historicalSource),
|
|
11078
|
-
threadLabel: historicalSource.threadLabel,
|
|
11079
|
-
title: kindTitle(locale, "ambient_suggestions"),
|
|
11080
|
-
createdAtMs: historicalSource.createdAtMs,
|
|
11081
|
-
suggestions: parseAmbientSuggestionsPayload(historicalSource.messageText ?? "") ?? [],
|
|
11082
|
-
}, locale);
|
|
12490
|
+
if (segments.some((segment) => [".github", ".gitlab", ".terraform", ".claude", ".husky", ".vscode"].includes(segment))) {
|
|
12491
|
+
return true;
|
|
11083
12492
|
}
|
|
11084
|
-
if (
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
}
|
|
11111
|
-
return detail;
|
|
12493
|
+
if (
|
|
12494
|
+
[
|
|
12495
|
+
"package.json",
|
|
12496
|
+
"package-lock.json",
|
|
12497
|
+
"pnpm-lock.yaml",
|
|
12498
|
+
"yarn.lock",
|
|
12499
|
+
"bun.lockb",
|
|
12500
|
+
"cargo.toml",
|
|
12501
|
+
"cargo.lock",
|
|
12502
|
+
"gemfile",
|
|
12503
|
+
"gemfile.lock",
|
|
12504
|
+
"podfile",
|
|
12505
|
+
"podfile.lock",
|
|
12506
|
+
"composer.json",
|
|
12507
|
+
"composer.lock",
|
|
12508
|
+
"pipfile",
|
|
12509
|
+
"pipfile.lock",
|
|
12510
|
+
"poetry.lock",
|
|
12511
|
+
"requirements.txt",
|
|
12512
|
+
"dockerfile",
|
|
12513
|
+
"wrangler.toml",
|
|
12514
|
+
"tsconfig.json",
|
|
12515
|
+
"tsconfig.tsbuildinfo",
|
|
12516
|
+
].includes(basename)
|
|
12517
|
+
) {
|
|
12518
|
+
return true;
|
|
11112
12519
|
}
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
return
|
|
12520
|
+
return false;
|
|
12521
|
+
}
|
|
12522
|
+
|
|
12523
|
+
function isAutoPilotContentWritePath(candidatePath) {
|
|
12524
|
+
const normalized = cleanText(candidatePath || "");
|
|
12525
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12526
|
+
return false;
|
|
11120
12527
|
}
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
return historicalPlan ? buildHistoryDetail(historicalPlan, locale, runtime) : null;
|
|
12528
|
+
const lower = normalized.toLowerCase();
|
|
12529
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12530
|
+
const basename = segments[segments.length - 1] || "";
|
|
12531
|
+
const extension = path.extname(basename);
|
|
12532
|
+
if ([".md", ".mdx", ".txt", ".rst", ".adoc"].includes(extension)) {
|
|
12533
|
+
return true;
|
|
11128
12534
|
}
|
|
11129
|
-
if (
|
|
11130
|
-
|
|
11131
|
-
|
|
12535
|
+
if (["license", "notice", "copying", "readme", "changelog", "contributing"].includes(path.basename(basename, extension))) {
|
|
12536
|
+
return true;
|
|
12537
|
+
}
|
|
12538
|
+
if (segments.includes("i18n") && [".js", ".ts", ".json", ".yaml", ".yml"].includes(extension)) {
|
|
12539
|
+
return true;
|
|
12540
|
+
}
|
|
12541
|
+
if (segments.includes("messages") && extension === ".json") {
|
|
12542
|
+
return true;
|
|
12543
|
+
}
|
|
12544
|
+
return false;
|
|
12545
|
+
}
|
|
12546
|
+
|
|
12547
|
+
function isAutoPilotUiTestWritePath(candidatePath) {
|
|
12548
|
+
const normalized = cleanText(candidatePath || "");
|
|
12549
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12550
|
+
return false;
|
|
12551
|
+
}
|
|
12552
|
+
const lower = normalized.toLowerCase();
|
|
12553
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
12554
|
+
const basename = segments[segments.length - 1] || "";
|
|
12555
|
+
const extension = path.extname(basename);
|
|
12556
|
+
if ([".css", ".scss", ".sass", ".less", ".styl", ".pcss"].includes(extension)) {
|
|
12557
|
+
return true;
|
|
12558
|
+
}
|
|
12559
|
+
if (segments.includes("__tests__") || /\.(test|spec)\.[cm]?[jt]sx?$/u.test(basename)) {
|
|
12560
|
+
return true;
|
|
12561
|
+
}
|
|
12562
|
+
if ((segments.includes("web") || segments.includes("components")) && [".js", ".jsx", ".ts", ".tsx", ".html"].includes(extension)) {
|
|
12563
|
+
return true;
|
|
12564
|
+
}
|
|
12565
|
+
return false;
|
|
12566
|
+
}
|
|
12567
|
+
|
|
12568
|
+
function isAutoPilotSourceWritePath(candidatePath) {
|
|
12569
|
+
const normalized = cleanText(candidatePath || "");
|
|
12570
|
+
if (!normalized || isDeniedTrustedWritePath(normalized)) {
|
|
12571
|
+
return false;
|
|
12572
|
+
}
|
|
12573
|
+
if (isAutoPilotContentWritePath(normalized) || isAutoPilotUiTestWritePath(normalized)) {
|
|
12574
|
+
return false;
|
|
12575
|
+
}
|
|
12576
|
+
const extension = path.extname(normalized.toLowerCase());
|
|
12577
|
+
return [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(extension);
|
|
12578
|
+
}
|
|
12579
|
+
|
|
12580
|
+
function extractAddedDiffLines(diffText) {
|
|
12581
|
+
return normalizeTimelineDiffText(diffText)
|
|
12582
|
+
.split("\n")
|
|
12583
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"));
|
|
12584
|
+
}
|
|
12585
|
+
|
|
12586
|
+
function addedDiffLinesContain(diffText, pattern) {
|
|
12587
|
+
return extractAddedDiffLines(diffText).some((line) => pattern.test(line.slice(1)));
|
|
12588
|
+
}
|
|
12589
|
+
|
|
12590
|
+
function hasUnsafeUiOrTestWriteDiff(diffText) {
|
|
12591
|
+
const patterns = [
|
|
12592
|
+
/\bprocess\.env\b/u,
|
|
12593
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
12594
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
12595
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
12596
|
+
/\bcrypto\b/u,
|
|
12597
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
12598
|
+
];
|
|
12599
|
+
return (
|
|
12600
|
+
addedDiffLinesContain(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
12601
|
+
addedDiffLinesContain(diffText, /\brequire\s*\(/u) ||
|
|
12602
|
+
patterns.some((pattern) => addedDiffLinesContain(diffText, pattern))
|
|
12603
|
+
);
|
|
12604
|
+
}
|
|
12605
|
+
|
|
12606
|
+
function hasUnsafeSourceWriteDiff(diffText) {
|
|
12607
|
+
const patterns = [
|
|
12608
|
+
/\bprocess\.env\b/u,
|
|
12609
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
12610
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
12611
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
12612
|
+
/\b(?:net|tls|dgram|http2?)\b/u,
|
|
12613
|
+
/\bcrypto\b/u,
|
|
12614
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
12615
|
+
];
|
|
12616
|
+
return (
|
|
12617
|
+
addedDiffLinesContain(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
12618
|
+
addedDiffLinesContain(diffText, /\brequire\s*\(/u) ||
|
|
12619
|
+
patterns.some((pattern) => addedDiffLinesContain(diffText, pattern))
|
|
12620
|
+
);
|
|
12621
|
+
}
|
|
12622
|
+
|
|
12623
|
+
const AUTO_PILOT_SOURCE_CONTINUITY_WINDOW_MS = 15 * 60 * 1000;
|
|
12624
|
+
|
|
12625
|
+
function isReadContinuityTimelineEntry(entry) {
|
|
12626
|
+
if (!isPlainObject(entry)) {
|
|
12627
|
+
return false;
|
|
12628
|
+
}
|
|
12629
|
+
if (cleanText(entry.kind || "") === "file_event" && cleanText(entry.fileEventType || "") === "read") {
|
|
12630
|
+
return true;
|
|
12631
|
+
}
|
|
12632
|
+
return (
|
|
12633
|
+
cleanText(entry.kind || "") === "approval" &&
|
|
12634
|
+
cleanText(entry.outcome || "") === "approved" &&
|
|
12635
|
+
!normalizeTimelineDiffText(entry.diffText ?? "") &&
|
|
12636
|
+
normalizeTimelineFileRefs(entry.fileRefs ?? []).length > 0
|
|
12637
|
+
);
|
|
12638
|
+
}
|
|
12639
|
+
|
|
12640
|
+
function resolveRecentReadContinuityFileRefs({ entry, cwd, workspaceRoot }) {
|
|
12641
|
+
const normalizedRefs = normalizeTimelineFileRefs(entry?.fileRefs ?? []);
|
|
12642
|
+
if (normalizedRefs.length === 0) {
|
|
12643
|
+
return [];
|
|
12644
|
+
}
|
|
12645
|
+
const resolvedRefs = [];
|
|
12646
|
+
for (const fileRef of normalizedRefs) {
|
|
12647
|
+
const resolved = resolveTrustedReadTargetPath({
|
|
12648
|
+
token: fileRef,
|
|
12649
|
+
cwd,
|
|
12650
|
+
workspaceRoot,
|
|
12651
|
+
});
|
|
12652
|
+
if (resolved) {
|
|
12653
|
+
resolvedRefs.push(resolved);
|
|
12654
|
+
}
|
|
12655
|
+
}
|
|
12656
|
+
return normalizeTimelineFileRefs(resolvedRefs);
|
|
12657
|
+
}
|
|
12658
|
+
|
|
12659
|
+
function hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs }) {
|
|
12660
|
+
const normalizedTargets = normalizeTimelineFileRefs(resolvedFileRefs ?? []);
|
|
12661
|
+
if (normalizedTargets.length !== 1) {
|
|
12662
|
+
return false;
|
|
12663
|
+
}
|
|
12664
|
+
const targetFileRef = normalizedTargets[0];
|
|
12665
|
+
const conversationId = cleanText(approval?.conversationId || "");
|
|
12666
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
12667
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
12668
|
+
if (!cwd || !workspaceRoot) {
|
|
12669
|
+
return false;
|
|
12670
|
+
}
|
|
12671
|
+
const timelineEntries =
|
|
12672
|
+
Array.isArray(runtime?.recentTimelineEntries) && runtime.recentTimelineEntries.length > 0
|
|
12673
|
+
? runtime.recentTimelineEntries
|
|
12674
|
+
: normalizeTimelineEntries(state?.recentTimelineEntries ?? [], 40);
|
|
12675
|
+
const now = Date.now();
|
|
12676
|
+
for (const entry of timelineEntries) {
|
|
12677
|
+
if (!isReadContinuityTimelineEntry(entry)) {
|
|
12678
|
+
continue;
|
|
12679
|
+
}
|
|
12680
|
+
const createdAtMs = Number(entry?.createdAtMs) || 0;
|
|
12681
|
+
if (!createdAtMs || now - createdAtMs > AUTO_PILOT_SOURCE_CONTINUITY_WINDOW_MS) {
|
|
12682
|
+
continue;
|
|
12683
|
+
}
|
|
12684
|
+
const entryThreadId = cleanText(entry?.threadId || extractConversationIdFromStableId(entry?.stableId) || "");
|
|
12685
|
+
if (conversationId && entryThreadId && entryThreadId !== conversationId) {
|
|
12686
|
+
continue;
|
|
12687
|
+
}
|
|
12688
|
+
const entryFileRefs = resolveRecentReadContinuityFileRefs({
|
|
12689
|
+
entry,
|
|
12690
|
+
cwd,
|
|
12691
|
+
workspaceRoot,
|
|
12692
|
+
});
|
|
12693
|
+
if (entryFileRefs.includes(targetFileRef)) {
|
|
12694
|
+
return true;
|
|
12695
|
+
}
|
|
12696
|
+
}
|
|
12697
|
+
return false;
|
|
12698
|
+
}
|
|
12699
|
+
|
|
12700
|
+
function classifyTrustedWriteLane({ runtime, state, approval, resolvedFileRefs, diffText, totalChangedLines }) {
|
|
12701
|
+
const lanes = autoPilotWriteLaneState(state);
|
|
12702
|
+
if (
|
|
12703
|
+
lanes.content &&
|
|
12704
|
+
resolvedFileRefs.length >= 1 &&
|
|
12705
|
+
resolvedFileRefs.length <= 3 &&
|
|
12706
|
+
totalChangedLines <= 120 &&
|
|
12707
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotContentWritePath(fileRef))
|
|
12708
|
+
) {
|
|
12709
|
+
return "content";
|
|
12710
|
+
}
|
|
12711
|
+
if (
|
|
12712
|
+
lanes.uiTests &&
|
|
12713
|
+
resolvedFileRefs.length >= 1 &&
|
|
12714
|
+
resolvedFileRefs.length <= 2 &&
|
|
12715
|
+
totalChangedLines <= 80 &&
|
|
12716
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotUiTestWritePath(fileRef)) &&
|
|
12717
|
+
!hasUnsafeUiOrTestWriteDiff(diffText)
|
|
12718
|
+
) {
|
|
12719
|
+
return "ui_tests";
|
|
12720
|
+
}
|
|
12721
|
+
if (
|
|
12722
|
+
lanes.source &&
|
|
12723
|
+
resolvedFileRefs.length === 1 &&
|
|
12724
|
+
totalChangedLines <= 40 &&
|
|
12725
|
+
resolvedFileRefs.every((fileRef) => isAutoPilotSourceWritePath(fileRef)) &&
|
|
12726
|
+
!hasUnsafeSourceWriteDiff(diffText) &&
|
|
12727
|
+
hasRecentSourceReadContinuity({ runtime, state, approval, resolvedFileRefs })
|
|
12728
|
+
) {
|
|
12729
|
+
return "source";
|
|
12730
|
+
}
|
|
12731
|
+
return "";
|
|
12732
|
+
}
|
|
12733
|
+
|
|
12734
|
+
function resolveTrustedWriteFileRefs({ fileRefs, cwd, workspaceRoot }) {
|
|
12735
|
+
const normalizedRefs = normalizeTimelineFileRefs(fileRefs ?? []);
|
|
12736
|
+
if (normalizedRefs.length === 0 || normalizedRefs.length > 3) {
|
|
12737
|
+
return null;
|
|
12738
|
+
}
|
|
12739
|
+
const resolvedRefs = [];
|
|
12740
|
+
for (const fileRef of normalizedRefs) {
|
|
12741
|
+
const resolved = resolveTrustedReadTargetPath({
|
|
12742
|
+
token: fileRef,
|
|
12743
|
+
cwd,
|
|
12744
|
+
workspaceRoot,
|
|
12745
|
+
});
|
|
12746
|
+
if (!resolved || isDeniedTrustedWritePath(resolved)) {
|
|
12747
|
+
return null;
|
|
12748
|
+
}
|
|
12749
|
+
resolvedRefs.push(resolved);
|
|
12750
|
+
}
|
|
12751
|
+
return normalizeTimelineFileRefs(resolvedRefs);
|
|
12752
|
+
}
|
|
12753
|
+
|
|
12754
|
+
function sameNormalizedFileRefSet(leftRefs, rightRefs) {
|
|
12755
|
+
const left = normalizeTimelineFileRefs(leftRefs ?? []);
|
|
12756
|
+
const right = normalizeTimelineFileRefs(rightRefs ?? []);
|
|
12757
|
+
if (left.length !== right.length) {
|
|
12758
|
+
return false;
|
|
12759
|
+
}
|
|
12760
|
+
const rightSet = new Set(right);
|
|
12761
|
+
return left.every((value) => rightSet.has(value));
|
|
12762
|
+
}
|
|
12763
|
+
|
|
12764
|
+
function resolveTrustedWriteDiffSectionFileRefs({ diffSections, cwd, workspaceRoot }) {
|
|
12765
|
+
const resolvedSections = [];
|
|
12766
|
+
for (const section of diffSections) {
|
|
12767
|
+
const paths = extractUnifiedDiffSectionPaths(section);
|
|
12768
|
+
const sectionFileRef = cleanText(paths.newFileRef || paths.oldFileRef || "");
|
|
12769
|
+
if (!sectionFileRef) {
|
|
12770
|
+
return null;
|
|
12771
|
+
}
|
|
12772
|
+
if (paths.oldFileRef && paths.newFileRef && cleanText(paths.oldFileRef) !== cleanText(paths.newFileRef)) {
|
|
12773
|
+
return null;
|
|
12774
|
+
}
|
|
12775
|
+
const resolved = resolveTrustedDiffSectionPath({
|
|
12776
|
+
token: sectionFileRef,
|
|
12777
|
+
cwd,
|
|
12778
|
+
workspaceRoot,
|
|
12779
|
+
});
|
|
12780
|
+
if (!resolved || isDeniedTrustedWritePath(resolved)) {
|
|
12781
|
+
return null;
|
|
12782
|
+
}
|
|
12783
|
+
resolvedSections.push(resolved);
|
|
12784
|
+
}
|
|
12785
|
+
return normalizeTimelineFileRefs(resolvedSections);
|
|
12786
|
+
}
|
|
12787
|
+
|
|
12788
|
+
function evaluateTrustedWriteApprovalCandidate({ runtime, state, approval }) {
|
|
12789
|
+
const cwd = cleanText(approval?.cwd || approval?.rawParams?.cwd || approval?.rawParams?.grantRoot || "");
|
|
12790
|
+
const workspaceRoot = cleanText(approval?.workspaceRoot || approval?.rawParams?.grantRoot || cwd);
|
|
12791
|
+
const resolvedFileRefs = resolveTrustedWriteFileRefs({
|
|
12792
|
+
fileRefs: approval?.fileRefs ?? [],
|
|
12793
|
+
cwd,
|
|
12794
|
+
workspaceRoot,
|
|
12795
|
+
});
|
|
12796
|
+
const diffText = normalizeTimelineDiffText(approval?.diffText ?? "");
|
|
12797
|
+
if (!cwd || !workspaceRoot || !resolvedFileRefs || !diffText) {
|
|
12798
|
+
return null;
|
|
12799
|
+
}
|
|
12800
|
+
const counts = diffLineCounts(diffText);
|
|
12801
|
+
const totalChangedLines =
|
|
12802
|
+
Math.max(0, Number(counts.addedLines) || 0) +
|
|
12803
|
+
Math.max(0, Number(counts.removedLines) || 0);
|
|
12804
|
+
if (totalChangedLines === 0 || totalChangedLines > 120) {
|
|
12805
|
+
return null;
|
|
12806
|
+
}
|
|
12807
|
+
const diffSections = splitUnifiedDiffTextByFile(diffText);
|
|
12808
|
+
if (diffSections.length === 0 || diffSections.length > 3) {
|
|
12809
|
+
return null;
|
|
12810
|
+
}
|
|
12811
|
+
if (
|
|
12812
|
+
/^(?: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)
|
|
12813
|
+
) {
|
|
12814
|
+
return null;
|
|
12815
|
+
}
|
|
12816
|
+
const diffSectionRefs = resolveTrustedWriteDiffSectionFileRefs({
|
|
12817
|
+
diffSections,
|
|
12818
|
+
cwd,
|
|
12819
|
+
workspaceRoot,
|
|
12820
|
+
});
|
|
12821
|
+
if (!diffSectionRefs || !sameNormalizedFileRefSet(diffSectionRefs, resolvedFileRefs)) {
|
|
12822
|
+
return null;
|
|
12823
|
+
}
|
|
12824
|
+
const lane = classifyTrustedWriteLane({
|
|
12825
|
+
runtime,
|
|
12826
|
+
state: state ?? approval?.stateSnapshot ?? null,
|
|
12827
|
+
approval,
|
|
12828
|
+
resolvedFileRefs: diffSectionRefs,
|
|
12829
|
+
diffText,
|
|
12830
|
+
totalChangedLines,
|
|
12831
|
+
});
|
|
12832
|
+
if (!lane) {
|
|
12833
|
+
return null;
|
|
12834
|
+
}
|
|
12835
|
+
return {
|
|
12836
|
+
fileRefs: diffSectionRefs,
|
|
12837
|
+
diffText,
|
|
12838
|
+
diffAddedLines: Math.max(0, Number(counts.addedLines) || 0),
|
|
12839
|
+
diffRemovedLines: Math.max(0, Number(counts.removedLines) || 0),
|
|
12840
|
+
totalChangedLines,
|
|
12841
|
+
lane,
|
|
12842
|
+
};
|
|
12843
|
+
}
|
|
12844
|
+
|
|
12845
|
+
function maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval }) {
|
|
12846
|
+
if (!isAutoPilotTrustedWritesEnabled(state)) {
|
|
12847
|
+
return null;
|
|
12848
|
+
}
|
|
12849
|
+
if (cleanText(approval?.kind || "") !== "file") {
|
|
12850
|
+
return null;
|
|
12851
|
+
}
|
|
12852
|
+
return evaluateTrustedWriteApprovalCandidate({
|
|
12853
|
+
runtime,
|
|
12854
|
+
state,
|
|
12855
|
+
approval: { ...approval, stateSnapshot: state },
|
|
12856
|
+
});
|
|
12857
|
+
}
|
|
12858
|
+
|
|
12859
|
+
function autoPilotTrustedWriteMessage(locale, approval, candidate) {
|
|
12860
|
+
const prefix = t(locale, "server.message.autoPilotTrustedWriteApproved");
|
|
12861
|
+
const fileBlock = candidate?.fileRefs?.length
|
|
12862
|
+
? `${t(locale, "server.message.filesLabel")}\n\`\`\`text\n${candidate.fileRefs.join("\n")}\n\`\`\``
|
|
12863
|
+
: "";
|
|
12864
|
+
const sizeLine =
|
|
12865
|
+
candidate && Number.isFinite(candidate.diffAddedLines) && Number.isFinite(candidate.diffRemovedLines)
|
|
12866
|
+
? `${t(locale, "server.message.diffSummaryLabel")} +${candidate.diffAddedLines} / -${candidate.diffRemovedLines}`
|
|
12867
|
+
: "";
|
|
12868
|
+
const originalMessage = cleanText(approval?.messageText || "");
|
|
12869
|
+
return [prefix, sizeLine, fileBlock, originalMessage].filter(Boolean).join("\n\n");
|
|
12870
|
+
}
|
|
12871
|
+
|
|
12872
|
+
async function recordAutoApprovedTrustedRead({
|
|
12873
|
+
config,
|
|
12874
|
+
runtime,
|
|
12875
|
+
state,
|
|
12876
|
+
approval,
|
|
12877
|
+
policyMatch,
|
|
12878
|
+
}) {
|
|
12879
|
+
const stateChanged = recordActionHistoryItem({
|
|
12880
|
+
config,
|
|
12881
|
+
runtime,
|
|
12882
|
+
state,
|
|
12883
|
+
kind: "approval",
|
|
12884
|
+
stableId: `approval:${approval.requestKey}:autopilot`,
|
|
12885
|
+
token: approval.token,
|
|
12886
|
+
title: approval.title,
|
|
12887
|
+
threadLabel: approval.threadLabel || "",
|
|
12888
|
+
messageText: autoPilotApprovalMessage(config.defaultLocale, policyMatch.commandText),
|
|
12889
|
+
summary: t(config.defaultLocale, "server.message.autoPilotTrustedReadSummary"),
|
|
12890
|
+
fileRefs: normalizeTimelineFileRefs(policyMatch.fileRefs ?? approval.fileRefs ?? []),
|
|
12891
|
+
diffText: "",
|
|
12892
|
+
diffSource: "",
|
|
12893
|
+
diffAvailable: false,
|
|
12894
|
+
diffAddedLines: 0,
|
|
12895
|
+
diffRemovedLines: 0,
|
|
12896
|
+
outcome: "approved",
|
|
12897
|
+
provider: approval.provider || "codex",
|
|
12898
|
+
});
|
|
12899
|
+
if (stateChanged) {
|
|
12900
|
+
await saveState(config.stateFile, state);
|
|
12901
|
+
}
|
|
12902
|
+
}
|
|
12903
|
+
|
|
12904
|
+
async function recordAutoApprovedTrustedWrite({
|
|
12905
|
+
config,
|
|
12906
|
+
runtime,
|
|
12907
|
+
state,
|
|
12908
|
+
approval,
|
|
12909
|
+
candidate,
|
|
12910
|
+
}) {
|
|
12911
|
+
const stateChanged = recordActionHistoryItem({
|
|
12912
|
+
config,
|
|
12913
|
+
runtime,
|
|
12914
|
+
state,
|
|
12915
|
+
kind: "approval",
|
|
12916
|
+
stableId: `approval:${approval.requestKey}:autopilot-write:${candidate.lane || "content"}`,
|
|
12917
|
+
token: approval.token,
|
|
12918
|
+
title: approval.title,
|
|
12919
|
+
threadLabel: approval.threadLabel || "",
|
|
12920
|
+
messageText: autoPilotTrustedWriteMessage(config.defaultLocale, approval, candidate),
|
|
12921
|
+
summary: t(config.defaultLocale, "server.message.autoPilotTrustedWriteSummary"),
|
|
12922
|
+
fileRefs: candidate.fileRefs,
|
|
12923
|
+
diffText: candidate.diffText,
|
|
12924
|
+
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
12925
|
+
diffAvailable: true,
|
|
12926
|
+
diffAddedLines: candidate.diffAddedLines,
|
|
12927
|
+
diffRemovedLines: candidate.diffRemovedLines,
|
|
12928
|
+
outcome: "approved",
|
|
12929
|
+
provider: approval.provider || "codex",
|
|
12930
|
+
});
|
|
12931
|
+
if (stateChanged) {
|
|
12932
|
+
await saveState(config.stateFile, state);
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
|
|
12936
|
+
async function autoApproveTrustedRead({
|
|
12937
|
+
config,
|
|
12938
|
+
runtime,
|
|
12939
|
+
state,
|
|
12940
|
+
approval,
|
|
12941
|
+
policyMatch,
|
|
12942
|
+
}) {
|
|
12943
|
+
if (approval.provider === "claude" && approval.resolveClaudeWaiter) {
|
|
12944
|
+
approval.resolveClaudeWaiter({
|
|
12945
|
+
permissionDecision: "allow",
|
|
12946
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedReadReason"),
|
|
12947
|
+
});
|
|
12948
|
+
} else if (approval.provider !== "claude") {
|
|
12949
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, "accept");
|
|
12950
|
+
}
|
|
12951
|
+
approval.resolved = true;
|
|
12952
|
+
approval.resolving = false;
|
|
12953
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
12954
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
12955
|
+
await recordAutoApprovedTrustedRead({
|
|
12956
|
+
config,
|
|
12957
|
+
runtime,
|
|
12958
|
+
state,
|
|
12959
|
+
approval,
|
|
12960
|
+
policyMatch,
|
|
12961
|
+
});
|
|
12962
|
+
console.log(`[auto-pilot-approved] ${approval.requestKey} | ${policyMatch.command}`);
|
|
12963
|
+
}
|
|
12964
|
+
|
|
12965
|
+
async function autoApproveTrustedWrite({
|
|
12966
|
+
config,
|
|
12967
|
+
runtime,
|
|
12968
|
+
state,
|
|
12969
|
+
approval,
|
|
12970
|
+
candidate,
|
|
12971
|
+
}) {
|
|
12972
|
+
if (approval.provider === "claude" && approval.resolveClaudeWaiter) {
|
|
12973
|
+
approval.resolveClaudeWaiter({
|
|
12974
|
+
permissionDecision: "allow",
|
|
12975
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedWriteReason"),
|
|
12976
|
+
});
|
|
12977
|
+
} else if (approval.provider !== "claude") {
|
|
12978
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, "accept");
|
|
12979
|
+
}
|
|
12980
|
+
approval.resolved = true;
|
|
12981
|
+
approval.resolving = false;
|
|
12982
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
12983
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
12984
|
+
await recordAutoApprovedTrustedWrite({
|
|
12985
|
+
config,
|
|
12986
|
+
runtime,
|
|
12987
|
+
state,
|
|
12988
|
+
approval,
|
|
12989
|
+
candidate,
|
|
12990
|
+
});
|
|
12991
|
+
console.log(`[auto-pilot-approved-write] ${approval.requestKey} | files=${candidate.fileRefs.length} | lines=${candidate.totalChangedLines}`);
|
|
12992
|
+
}
|
|
12993
|
+
|
|
12994
|
+
async function handleNativeApprovalDecision({ config, runtime, state, approval, decision }) {
|
|
12995
|
+
if (approval.resolveClaudeWaiter) {
|
|
12996
|
+
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
12997
|
+
// ExitPlanMode cannot be truly auto-approved via permissionDecision: "allow"
|
|
12998
|
+
// (Claude still shows the native PC plan dialog). Instead, deny the tool
|
|
12999
|
+
// call with a reason that tells Claude the user already decided on mobile.
|
|
13000
|
+
approval.resolveClaudeWaiter({
|
|
13001
|
+
permissionDecision: "deny",
|
|
13002
|
+
permissionDecisionReason:
|
|
13003
|
+
decision === "accept"
|
|
13004
|
+
? "User approved the plan via the viveworker mobile app. Proceed with implementing the plan exactly as proposed and do not call ExitPlanMode again."
|
|
13005
|
+
: "User rejected the plan via the viveworker mobile app. Revise the plan based on the conversation context before calling ExitPlanMode again.",
|
|
13006
|
+
});
|
|
13007
|
+
} else {
|
|
13008
|
+
approval.resolveClaudeWaiter(decision);
|
|
13009
|
+
}
|
|
13010
|
+
} else if (approval.provider === "claude") {
|
|
13011
|
+
// notifyOnly Claude approvals have no long-poll waiter and no ipcClient
|
|
13012
|
+
// back-channel — the desktop user already answered. Nothing to send.
|
|
13013
|
+
} else {
|
|
13014
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, decision);
|
|
13015
|
+
}
|
|
13016
|
+
approval.resolved = true;
|
|
13017
|
+
approval.resolving = false;
|
|
13018
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
13019
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
13020
|
+
const stateChanged = recordActionHistoryItem({
|
|
13021
|
+
config,
|
|
13022
|
+
runtime,
|
|
13023
|
+
state,
|
|
13024
|
+
kind: "approval",
|
|
13025
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
13026
|
+
token: approval.token,
|
|
13027
|
+
title: approval.title,
|
|
13028
|
+
threadLabel: approval.threadLabel || "",
|
|
13029
|
+
messageText: `${approvalDecisionMessage(decision, config.defaultLocale, approval.provider)}\n\n${approval.messageText}`,
|
|
13030
|
+
summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage(decision, config.defaultLocale, approval.provider),
|
|
13031
|
+
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
13032
|
+
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
13033
|
+
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
13034
|
+
diffAvailable: approval.diffAvailable === true || Boolean(approval.diffText),
|
|
13035
|
+
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
13036
|
+
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
13037
|
+
outcome: decision === "accept" ? "approved" : "rejected",
|
|
13038
|
+
provider: approval.provider || "codex",
|
|
13039
|
+
});
|
|
13040
|
+
if (stateChanged) {
|
|
13041
|
+
await saveState(config.stateFile, state);
|
|
13042
|
+
}
|
|
13043
|
+
console.log(`[native-decision] ${approval.requestKey} | ${decision}`);
|
|
13044
|
+
}
|
|
13045
|
+
|
|
13046
|
+
async function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
13047
|
+
if (kind === "diff_thread") {
|
|
13048
|
+
const group = (await getDiffThreadGroupsCached(runtime, state, config)).find((entry) => entry.token === token);
|
|
13049
|
+
return group ? buildDiffThreadDetail(group, locale) : null;
|
|
13050
|
+
}
|
|
13051
|
+
if (kind === "file_event") {
|
|
13052
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13053
|
+
return entry ? buildTimelineFileEventDetail(entry, locale) : null;
|
|
13054
|
+
}
|
|
13055
|
+
if (kind === "ambient_suggestions") {
|
|
13056
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13057
|
+
if (entry) {
|
|
13058
|
+
return buildAmbientSuggestionsDetail(entry, locale);
|
|
13059
|
+
}
|
|
13060
|
+
const historicalSource = (runtime.recentHistoryItems ?? []).find((item) => {
|
|
13061
|
+
if (cleanText(item?.token || "") !== cleanText(token || "")) {
|
|
13062
|
+
return false;
|
|
13063
|
+
}
|
|
13064
|
+
const itemKind = cleanText(item?.kind || "");
|
|
13065
|
+
if (itemKind !== "assistant_final" && itemKind !== "completion") {
|
|
13066
|
+
return false;
|
|
13067
|
+
}
|
|
13068
|
+
return parseAmbientSuggestionsPayload(item?.messageText ?? "") !== null;
|
|
13069
|
+
});
|
|
13070
|
+
if (!historicalSource) {
|
|
13071
|
+
return null;
|
|
13072
|
+
}
|
|
13073
|
+
return buildAmbientSuggestionsDetail({
|
|
13074
|
+
token: historicalSource.token,
|
|
13075
|
+
threadId: historyItemThreadId(historicalSource),
|
|
13076
|
+
threadLabel: historicalSource.threadLabel,
|
|
13077
|
+
title: kindTitle(locale, "ambient_suggestions"),
|
|
13078
|
+
createdAtMs: historicalSource.createdAtMs,
|
|
13079
|
+
suggestions: parseAmbientSuggestionsPayload(historicalSource.messageText ?? "") ?? [],
|
|
13080
|
+
}, locale);
|
|
13081
|
+
}
|
|
13082
|
+
if (timelineMessageKinds.has(kind)) {
|
|
13083
|
+
const entry = timelineEntryByToken(runtime, token, kind);
|
|
13084
|
+
if (!entry) return null;
|
|
13085
|
+
const detail = buildTimelineMessageDetail(entry, locale, runtime);
|
|
13086
|
+
// Add reply support for Codex assistant_final entries only (replaces completion reply).
|
|
13087
|
+
// Check directly against timeline entries (not history items) to avoid
|
|
13088
|
+
// maxHistoryItems eviction issues.
|
|
13089
|
+
if (kind === "assistant_final") {
|
|
13090
|
+
const entryProvider = normalizeProvider(entry.provider);
|
|
13091
|
+
if (entryProvider === "codex" && runtime?.ipcClient) {
|
|
13092
|
+
const entryThreadId = cleanText(entry.threadId || "");
|
|
13093
|
+
const entryTs = Number(entry.createdAtMs) || 0;
|
|
13094
|
+
let isLatestForThread = true;
|
|
13095
|
+
for (const other of runtime.recentTimelineEntries) {
|
|
13096
|
+
if (other.kind === "assistant_final" &&
|
|
13097
|
+
cleanText(other.threadId || "") === entryThreadId &&
|
|
13098
|
+
(Number(other.createdAtMs) || 0) > entryTs) {
|
|
13099
|
+
isLatestForThread = false;
|
|
13100
|
+
break;
|
|
13101
|
+
}
|
|
13102
|
+
}
|
|
13103
|
+
if (isLatestForThread) {
|
|
13104
|
+
detail.reply = { enabled: true, supportsPlanMode: true, supportsImages: true };
|
|
13105
|
+
detail.provider = entryProvider;
|
|
13106
|
+
}
|
|
13107
|
+
}
|
|
13108
|
+
}
|
|
13109
|
+
return detail;
|
|
13110
|
+
}
|
|
13111
|
+
if (kind === "approval") {
|
|
13112
|
+
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
13113
|
+
if (approval) {
|
|
13114
|
+
return buildPendingApprovalDetail(runtime, state, approval, locale);
|
|
13115
|
+
}
|
|
13116
|
+
const historicalApproval = historyItemByToken(runtime, kind, token);
|
|
13117
|
+
return historicalApproval ? buildHistoryDetail(historicalApproval, locale, runtime) : null;
|
|
13118
|
+
}
|
|
13119
|
+
if (kind === "plan") {
|
|
13120
|
+
const planRequest = runtime.planRequestsByToken.get(token);
|
|
13121
|
+
if (planRequest && !planRequest.resolved && !isPlanRequestExpired(planRequest)) {
|
|
13122
|
+
return buildPendingPlanDetail(planRequest, locale);
|
|
13123
|
+
}
|
|
13124
|
+
const historicalPlan = historyItemByToken(runtime, kind, token);
|
|
13125
|
+
return historicalPlan ? buildHistoryDetail(historicalPlan, locale, runtime) : null;
|
|
13126
|
+
}
|
|
13127
|
+
if (kind === "choice") {
|
|
13128
|
+
const userInputRequest = findLatestPersistedUserInputRequest({ config, runtime, state, token });
|
|
13129
|
+
if (userInputRequest && !userInputRequest.resolved && !isGenericUserInputRequestExpired(userInputRequest)) {
|
|
11132
13130
|
return buildChoiceDetail(userInputRequest, config, locale);
|
|
11133
13131
|
}
|
|
11134
13132
|
const historicalChoice = historyItemByToken(runtime, kind, token);
|
|
@@ -11343,21 +13341,81 @@ function contentTypeForFile(filePath) {
|
|
|
11343
13341
|
}
|
|
11344
13342
|
}
|
|
11345
13343
|
|
|
11346
|
-
|
|
13344
|
+
// Per-file cache of {raw, gzip, mtimeMs}. Gzip of the ~464KB app shell
|
|
13345
|
+
// (app.js 278KB + app.css 76KB + i18n.js 110KB) takes a few ms to compute,
|
|
13346
|
+
// and without this the bridge would recompress on every request. Keyed by
|
|
13347
|
+
// resolved filePath so /app and /index.html share a cache slot.
|
|
13348
|
+
const WEB_ASSET_CACHE = new Map();
|
|
13349
|
+
const GZIPPABLE_EXTENSIONS = new Set([
|
|
13350
|
+
".js",
|
|
13351
|
+
".mjs",
|
|
13352
|
+
".css",
|
|
13353
|
+
".html",
|
|
13354
|
+
".htm",
|
|
13355
|
+
".json",
|
|
13356
|
+
".webmanifest",
|
|
13357
|
+
".svg",
|
|
13358
|
+
".txt",
|
|
13359
|
+
".map",
|
|
13360
|
+
]);
|
|
13361
|
+
const MIN_GZIP_BYTES = 512;
|
|
13362
|
+
|
|
13363
|
+
async function loadWebAssetEntry(filePath) {
|
|
13364
|
+
const stat = await fs.stat(filePath);
|
|
13365
|
+
const cached = WEB_ASSET_CACHE.get(filePath);
|
|
13366
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
13367
|
+
return cached;
|
|
13368
|
+
}
|
|
13369
|
+
const raw = await fs.readFile(filePath);
|
|
13370
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
13371
|
+
let gzip = null;
|
|
13372
|
+
if (GZIPPABLE_EXTENSIONS.has(extension) && raw.length >= MIN_GZIP_BYTES) {
|
|
13373
|
+
try {
|
|
13374
|
+
gzip = zlib.gzipSync(raw, { level: 6 });
|
|
13375
|
+
} catch {
|
|
13376
|
+
gzip = null;
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13379
|
+
const entry = {
|
|
13380
|
+
raw,
|
|
13381
|
+
gzip,
|
|
13382
|
+
mtimeMs: stat.mtimeMs,
|
|
13383
|
+
size: stat.size,
|
|
13384
|
+
contentType: contentTypeForFile(filePath),
|
|
13385
|
+
};
|
|
13386
|
+
WEB_ASSET_CACHE.set(filePath, entry);
|
|
13387
|
+
return entry;
|
|
13388
|
+
}
|
|
13389
|
+
|
|
13390
|
+
function clientAcceptsGzip(req) {
|
|
13391
|
+
const header = req?.headers?.["accept-encoding"];
|
|
13392
|
+
if (!header) return false;
|
|
13393
|
+
const value = Array.isArray(header) ? header.join(",") : String(header);
|
|
13394
|
+
return /\bgzip\b/i.test(value);
|
|
13395
|
+
}
|
|
13396
|
+
|
|
13397
|
+
async function serveWebAsset(res, urlPath, req) {
|
|
11347
13398
|
const filePath = resolveWebAsset(urlPath);
|
|
11348
13399
|
if (!filePath) {
|
|
11349
13400
|
return false;
|
|
11350
13401
|
}
|
|
11351
13402
|
|
|
11352
13403
|
try {
|
|
11353
|
-
const
|
|
13404
|
+
const entry = await loadWebAssetEntry(filePath);
|
|
13405
|
+
const wantsGzip = entry.gzip && clientAcceptsGzip(req);
|
|
11354
13406
|
res.statusCode = 200;
|
|
11355
|
-
res.setHeader("Content-Type",
|
|
13407
|
+
res.setHeader("Content-Type", entry.contentType);
|
|
11356
13408
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
11357
13409
|
if (urlPath === "/sw.js") {
|
|
11358
13410
|
res.setHeader("Service-Worker-Allowed", "/");
|
|
11359
13411
|
}
|
|
11360
|
-
|
|
13412
|
+
if (wantsGzip) {
|
|
13413
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
13414
|
+
res.setHeader("Vary", "Accept-Encoding");
|
|
13415
|
+
res.end(entry.gzip);
|
|
13416
|
+
} else {
|
|
13417
|
+
res.end(entry.raw);
|
|
13418
|
+
}
|
|
11361
13419
|
return true;
|
|
11362
13420
|
} catch {
|
|
11363
13421
|
return false;
|
|
@@ -11489,10 +13547,11 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11489
13547
|
url.pathname === "/app.js" ||
|
|
11490
13548
|
url.pathname === "/app.css" ||
|
|
11491
13549
|
url.pathname === "/i18n.js" ||
|
|
13550
|
+
url.pathname === "/hazbase-passkey.js" ||
|
|
11492
13551
|
url.pathname === "/sw.js" ||
|
|
11493
13552
|
url.pathname.startsWith("/icons/")
|
|
11494
13553
|
) {
|
|
11495
|
-
const served = await serveWebAsset(res, url.pathname);
|
|
13554
|
+
const served = await serveWebAsset(res, url.pathname, req);
|
|
11496
13555
|
if (served) {
|
|
11497
13556
|
return;
|
|
11498
13557
|
}
|
|
@@ -11632,16 +13691,47 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11632
13691
|
if (session.authenticated && session.restoredFromDevice) {
|
|
11633
13692
|
setSessionCookie(res, config);
|
|
11634
13693
|
}
|
|
13694
|
+
return writeJson(res, 200, buildSessionPayload({ config, state, session }));
|
|
13695
|
+
}
|
|
13696
|
+
|
|
13697
|
+
if (url.pathname === "/api/version/status" && req.method === "GET") {
|
|
13698
|
+
const session = requireApiSession(req, res, config, state);
|
|
13699
|
+
if (!session) return;
|
|
13700
|
+
return writeJson(res, 200, await getNpmVersionStatus(runtime, config));
|
|
13701
|
+
}
|
|
13702
|
+
|
|
13703
|
+
// Collapses the PWA's boot-time fan-out (session + inbox + timeline
|
|
13704
|
+
// + devices) into a single HTTPS round-trip. iOS Safari tears down
|
|
13705
|
+
// connections aggressively, so each parallel fetch pays its own
|
|
13706
|
+
// TLS handshake — and `JSON.stringify(state)` hiccups on other
|
|
13707
|
+
// requests make those handshakes bursty. One endpoint = one
|
|
13708
|
+
// handshake + one response, which drops the "Completed list is
|
|
13709
|
+
// blank for several seconds after launch" latency floor.
|
|
13710
|
+
if (url.pathname === "/api/bootstrap" && req.method === "GET") {
|
|
13711
|
+
const session = readSession(req, config, state);
|
|
13712
|
+
const localeInfo = resolveDeviceLocaleInfo(config, state, session.deviceId);
|
|
13713
|
+
if (session.authenticated && session.deviceId) {
|
|
13714
|
+
const trustChanged = touchDeviceTrust(state, config, session.deviceId);
|
|
13715
|
+
const metadataChanged = updateCurrentDeviceSnapshot(state, config, session.deviceId, {
|
|
13716
|
+
userAgent: requestUserAgent(req),
|
|
13717
|
+
lastLocale: localeInfo.locale,
|
|
13718
|
+
});
|
|
13719
|
+
if (trustChanged || metadataChanged) {
|
|
13720
|
+
await saveState(config.stateFile, state);
|
|
13721
|
+
}
|
|
13722
|
+
}
|
|
13723
|
+
if (session.authenticated && session.restoredFromDevice) {
|
|
13724
|
+
setSessionCookie(res, config);
|
|
13725
|
+
}
|
|
13726
|
+
const sessionPayload = buildSessionPayload({ config, state, session });
|
|
13727
|
+
if (!session.authenticated) {
|
|
13728
|
+
return writeJson(res, 200, { session: sessionPayload });
|
|
13729
|
+
}
|
|
11635
13730
|
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),
|
|
13731
|
+
session: sessionPayload,
|
|
13732
|
+
inbox: buildInboxFastResponse(runtime, state, config, localeInfo.locale),
|
|
13733
|
+
timeline: buildTimelineResponse(runtime, state, config, localeInfo.locale),
|
|
13734
|
+
devices: buildDevicesResponse({ config, state, session, locale: localeInfo.locale }),
|
|
11645
13735
|
});
|
|
11646
13736
|
}
|
|
11647
13737
|
|
|
@@ -11756,7 +13846,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11756
13846
|
lastLocale: resolveDeviceLocaleInfo(config, state, session.deviceId).locale,
|
|
11757
13847
|
}) || changed;
|
|
11758
13848
|
if (changed) {
|
|
11759
|
-
|
|
13849
|
+
scheduleSaveState(config, state);
|
|
11760
13850
|
}
|
|
11761
13851
|
return writeJson(res, 200, {
|
|
11762
13852
|
ok: true,
|
|
@@ -11778,12 +13868,80 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11778
13868
|
const enabled = payload?.enabled === true;
|
|
11779
13869
|
if (state.claudeAwayMode !== enabled) {
|
|
11780
13870
|
state.claudeAwayMode = enabled;
|
|
11781
|
-
|
|
13871
|
+
scheduleSaveState(config, state);
|
|
11782
13872
|
}
|
|
11783
13873
|
await syncClaudeAwayModeSentinel(config, enabled);
|
|
11784
13874
|
return writeJson(res, 200, { ok: true, enabled });
|
|
11785
13875
|
}
|
|
11786
13876
|
|
|
13877
|
+
if (url.pathname === "/api/settings/auto-pilot" && req.method === "POST") {
|
|
13878
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
13879
|
+
if (!session) {
|
|
13880
|
+
return;
|
|
13881
|
+
}
|
|
13882
|
+
let payload;
|
|
13883
|
+
try {
|
|
13884
|
+
payload = await parseJsonBody(req);
|
|
13885
|
+
} catch {
|
|
13886
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
13887
|
+
}
|
|
13888
|
+
const trustedReadsEnabled =
|
|
13889
|
+
typeof payload?.trustedReadsEnabled === "boolean"
|
|
13890
|
+
? payload.trustedReadsEnabled === true
|
|
13891
|
+
: state.autoPilotTrustedReads === true;
|
|
13892
|
+
const hasLaneUpdate =
|
|
13893
|
+
typeof payload?.writeLaneContentEnabled === "boolean" ||
|
|
13894
|
+
typeof payload?.writeLaneUiTestsEnabled === "boolean" ||
|
|
13895
|
+
typeof payload?.writeLaneSourceEnabled === "boolean";
|
|
13896
|
+
const legacyTrustedWritesEnabled =
|
|
13897
|
+
typeof payload?.trustedWritesEnabled === "boolean"
|
|
13898
|
+
? payload.trustedWritesEnabled === true
|
|
13899
|
+
: typeof payload?.trustedWritesShadowEnabled === "boolean"
|
|
13900
|
+
? payload.trustedWritesShadowEnabled === true
|
|
13901
|
+
: null;
|
|
13902
|
+
const currentLanes = autoPilotWriteLaneState(state);
|
|
13903
|
+
const writeLaneContentEnabled =
|
|
13904
|
+
typeof payload?.writeLaneContentEnabled === "boolean"
|
|
13905
|
+
? payload.writeLaneContentEnabled === true
|
|
13906
|
+
: !hasLaneUpdate && legacyTrustedWritesEnabled != null
|
|
13907
|
+
? legacyTrustedWritesEnabled
|
|
13908
|
+
: currentLanes.content;
|
|
13909
|
+
const writeLaneUiTestsEnabled =
|
|
13910
|
+
typeof payload?.writeLaneUiTestsEnabled === "boolean"
|
|
13911
|
+
? payload.writeLaneUiTestsEnabled === true
|
|
13912
|
+
: currentLanes.uiTests;
|
|
13913
|
+
const writeLaneSourceEnabled =
|
|
13914
|
+
typeof payload?.writeLaneSourceEnabled === "boolean"
|
|
13915
|
+
? payload.writeLaneSourceEnabled === true
|
|
13916
|
+
: currentLanes.source;
|
|
13917
|
+
const trustedWritesEnabled =
|
|
13918
|
+
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled;
|
|
13919
|
+
if (
|
|
13920
|
+
state.autoPilotTrustedReads !== trustedReadsEnabled ||
|
|
13921
|
+
state.autoPilotWriteLaneContent !== writeLaneContentEnabled ||
|
|
13922
|
+
state.autoPilotWriteLaneUiTests !== writeLaneUiTestsEnabled ||
|
|
13923
|
+
state.autoPilotWriteLaneSource !== writeLaneSourceEnabled ||
|
|
13924
|
+
isAutoPilotTrustedWritesEnabled(state) !== trustedWritesEnabled
|
|
13925
|
+
) {
|
|
13926
|
+
state.autoPilotTrustedReads = trustedReadsEnabled;
|
|
13927
|
+
state.autoPilotTrustedWrites = trustedWritesEnabled;
|
|
13928
|
+
state.autoPilotTrustedWritesShadow = false;
|
|
13929
|
+
state.autoPilotWriteLaneContent = writeLaneContentEnabled;
|
|
13930
|
+
state.autoPilotWriteLaneUiTests = writeLaneUiTestsEnabled;
|
|
13931
|
+
state.autoPilotWriteLaneSource = writeLaneSourceEnabled;
|
|
13932
|
+
await saveState(config.stateFile, state);
|
|
13933
|
+
}
|
|
13934
|
+
return writeJson(res, 200, {
|
|
13935
|
+
ok: true,
|
|
13936
|
+
trustedReadsEnabled,
|
|
13937
|
+
trustedWritesEnabled,
|
|
13938
|
+
trustedWritesShadowEnabled: false,
|
|
13939
|
+
writeLaneContentEnabled,
|
|
13940
|
+
writeLaneUiTestsEnabled,
|
|
13941
|
+
writeLaneSourceEnabled,
|
|
13942
|
+
});
|
|
13943
|
+
}
|
|
13944
|
+
|
|
11787
13945
|
if (url.pathname === "/api/settings/a2a-executor" && req.method === "POST") {
|
|
11788
13946
|
const session = requireMutatingApiSession(req, res, config, state);
|
|
11789
13947
|
if (!session) return;
|
|
@@ -11797,7 +13955,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11797
13955
|
const preference = valid.has(payload?.preference) ? payload.preference : "auto";
|
|
11798
13956
|
if (state.a2aExecutorPreference !== preference) {
|
|
11799
13957
|
state.a2aExecutorPreference = preference;
|
|
11800
|
-
|
|
13958
|
+
scheduleSaveState(config, state);
|
|
11801
13959
|
}
|
|
11802
13960
|
return writeJson(res, 200, { ok: true, preference });
|
|
11803
13961
|
}
|
|
@@ -11967,6 +14125,314 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11967
14125
|
});
|
|
11968
14126
|
}
|
|
11969
14127
|
|
|
14128
|
+
|
|
14129
|
+
if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
|
|
14130
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14131
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
14132
|
+
if (!hookAuth) {
|
|
14133
|
+
const session = requireApiSession(req, res, config, state);
|
|
14134
|
+
if (!session) return;
|
|
14135
|
+
}
|
|
14136
|
+
if (!config.hazbaseApiUrl) {
|
|
14137
|
+
return writeJson(res, 200, { enabled: false });
|
|
14138
|
+
}
|
|
14139
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14140
|
+
const [paymentsResult, chainsResult] = await Promise.allSettled([
|
|
14141
|
+
fetchHazbaseMetadata(config, "/api/meta/payments"),
|
|
14142
|
+
fetchHazbaseMetadata(config, "/api/meta/chains"),
|
|
14143
|
+
]);
|
|
14144
|
+
const payments = paymentsResult.status === "fulfilled" ? paymentsResult.value : null;
|
|
14145
|
+
const chains = chainsResult.status === "fulfilled" ? chainsResult.value : null;
|
|
14146
|
+
const errors = [];
|
|
14147
|
+
if (paymentsResult.status === "rejected") {
|
|
14148
|
+
errors.push(paymentsResult.reason?.message || String(paymentsResult.reason || "payments metadata unavailable"));
|
|
14149
|
+
}
|
|
14150
|
+
if (chainsResult.status === "rejected") {
|
|
14151
|
+
errors.push(chainsResult.reason?.message || String(chainsResult.reason || "chains metadata unavailable"));
|
|
14152
|
+
}
|
|
14153
|
+
const supportedChains = Array.isArray(chains?.chains)
|
|
14154
|
+
? chains.chains.filter((entry) => Number(entry.chainId) === 8453 || Number(entry.chainId) === 84532)
|
|
14155
|
+
: [];
|
|
14156
|
+
const payoutAddresses = {
|
|
14157
|
+
8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
|
|
14158
|
+
84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
|
|
14159
|
+
};
|
|
14160
|
+
const signedIn = Boolean(hazbase.accessToken) && !hazbase.sessionInvalid;
|
|
14161
|
+
return writeJson(res, 200, {
|
|
14162
|
+
enabled: true,
|
|
14163
|
+
apiUrl: config.hazbaseApiUrl,
|
|
14164
|
+
signedIn,
|
|
14165
|
+
sessionStatus: hazbase.sessionInvalid ? "expired" : signedIn ? "signed_in" : "signed_out",
|
|
14166
|
+
sessionInvalid: hazbase.sessionInvalid,
|
|
14167
|
+
sessionInvalidReason: hazbase.sessionInvalidReason,
|
|
14168
|
+
sessionInvalidAt: hazbase.sessionInvalidAt,
|
|
14169
|
+
email: hazbase.email,
|
|
14170
|
+
userId: hazbase.userId,
|
|
14171
|
+
sessionId: hazbase.sessionId,
|
|
14172
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
14173
|
+
credentialId: hazbase.credentialId,
|
|
14174
|
+
highTrustExpiresAt: hazbase.highTrustExpiresAt,
|
|
14175
|
+
accounts: hazbase.accounts,
|
|
14176
|
+
supportedPayments: payments?.networks || [],
|
|
14177
|
+
supportedChains,
|
|
14178
|
+
defaultPaymentNetwork: payments?.defaultNetwork || "base-sepolia",
|
|
14179
|
+
payoutAddresses,
|
|
14180
|
+
error: errors.join(" | "),
|
|
14181
|
+
});
|
|
14182
|
+
}
|
|
14183
|
+
|
|
14184
|
+
if (url.pathname === "/api/hazbase/payout-address" && req.method === "GET") {
|
|
14185
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14186
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
14187
|
+
if (!hookAuth) {
|
|
14188
|
+
const session = requireApiSession(req, res, config, state);
|
|
14189
|
+
if (!session) return;
|
|
14190
|
+
}
|
|
14191
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14192
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14193
|
+
const chainId = Number(url.searchParams.get("chainId") || 0);
|
|
14194
|
+
if (chainId !== 8453 && chainId !== 84532) {
|
|
14195
|
+
return writeJson(res, 400, { error: "unsupported-chain" });
|
|
14196
|
+
}
|
|
14197
|
+
const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
|
|
14198
|
+
if (!account?.smartAccountAddress) {
|
|
14199
|
+
return writeJson(res, 409, { error: "hazbase-wallet-account-missing" });
|
|
14200
|
+
}
|
|
14201
|
+
return writeJson(res, 200, {
|
|
14202
|
+
chainId,
|
|
14203
|
+
payoutMethod: "hazbase_wallet",
|
|
14204
|
+
payoutAddress: account.smartAccountAddress,
|
|
14205
|
+
smartAccountAddress: account.smartAccountAddress,
|
|
14206
|
+
accountVariant: account.accountVariant || "",
|
|
14207
|
+
relayMode: account.relayMode || "",
|
|
14208
|
+
});
|
|
14209
|
+
}
|
|
14210
|
+
|
|
14211
|
+
if (url.pathname === "/api/hazbase/request-otp" && req.method === "POST") {
|
|
14212
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14213
|
+
if (!session) return;
|
|
14214
|
+
const body = await parseJsonBody(req);
|
|
14215
|
+
const email = cleanText(body?.email || "");
|
|
14216
|
+
if (!email) return writeJson(res, 400, { error: "email-required" });
|
|
14217
|
+
const result = await requestHazbaseEmailOtp({ email });
|
|
14218
|
+
state.hazbase = { ...normalizeHazbaseState(state.hazbase), email };
|
|
14219
|
+
await saveState(config.stateFile, state);
|
|
14220
|
+
return writeJson(res, 200, result);
|
|
14221
|
+
}
|
|
14222
|
+
|
|
14223
|
+
if (url.pathname === "/api/hazbase/verify-otp" && req.method === "POST") {
|
|
14224
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14225
|
+
if (!session) return;
|
|
14226
|
+
const body = await parseJsonBody(req);
|
|
14227
|
+
const email = cleanText(body?.email || state.hazbase?.email || "");
|
|
14228
|
+
const code = cleanText(body?.code || "");
|
|
14229
|
+
if (!email || !code) return writeJson(res, 400, { error: "otp-required" });
|
|
14230
|
+
const result = await verifyHazbaseEmailOtp({ email, code });
|
|
14231
|
+
const previousHazbase = normalizeHazbaseState(state.hazbase);
|
|
14232
|
+
state.hazbase = {
|
|
14233
|
+
...previousHazbase,
|
|
14234
|
+
email: result.email || email,
|
|
14235
|
+
accessToken: cleanText(result.accessToken || ""),
|
|
14236
|
+
sessionId: cleanText(result.sessionId || ""),
|
|
14237
|
+
userId: cleanText(result.userId || ""),
|
|
14238
|
+
accounts: mergeHazbaseAccountSummaries(previousHazbase.accounts, Array.isArray(result.accounts) ? result.accounts : []),
|
|
14239
|
+
highTrustToken: "",
|
|
14240
|
+
highTrustExpiresAt: "",
|
|
14241
|
+
sessionInvalid: false,
|
|
14242
|
+
sessionInvalidReason: "",
|
|
14243
|
+
sessionInvalidAt: "",
|
|
14244
|
+
};
|
|
14245
|
+
await saveState(config.stateFile, state);
|
|
14246
|
+
return writeJson(res, 200, result);
|
|
14247
|
+
}
|
|
14248
|
+
|
|
14249
|
+
if (url.pathname === "/api/hazbase/passkey/register/challenge" && req.method === "POST") {
|
|
14250
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14251
|
+
if (!session) return;
|
|
14252
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14253
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14254
|
+
const body = await parseJsonBody(req);
|
|
14255
|
+
let partnerOrigin;
|
|
14256
|
+
try {
|
|
14257
|
+
partnerOrigin = deriveHazbasePartnerPasskeyOrigin(req, config);
|
|
14258
|
+
} catch (error) {
|
|
14259
|
+
return writeJson(res, 400, {
|
|
14260
|
+
error: error?.code || "hazbase-passkey-local-host-required",
|
|
14261
|
+
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14262
|
+
});
|
|
14263
|
+
}
|
|
14264
|
+
let result;
|
|
14265
|
+
try {
|
|
14266
|
+
result = await requestPasskeyRegistrationChallenge({
|
|
14267
|
+
emailSession: hazbase.accessToken,
|
|
14268
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14269
|
+
partnerOrigin,
|
|
14270
|
+
});
|
|
14271
|
+
} catch (error) {
|
|
14272
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14273
|
+
throw error;
|
|
14274
|
+
}
|
|
14275
|
+
return writeJson(res, 200, result);
|
|
14276
|
+
}
|
|
14277
|
+
|
|
14278
|
+
if (url.pathname === "/api/hazbase/passkey/register/complete" && req.method === "POST") {
|
|
14279
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14280
|
+
if (!session) return;
|
|
14281
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14282
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14283
|
+
const body = await parseJsonBody(req);
|
|
14284
|
+
let result;
|
|
14285
|
+
try {
|
|
14286
|
+
result = await completePasskeyRegistration({
|
|
14287
|
+
emailSession: hazbase.accessToken,
|
|
14288
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14289
|
+
credential: body?.credential,
|
|
14290
|
+
deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
|
|
14291
|
+
});
|
|
14292
|
+
} catch (error) {
|
|
14293
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14294
|
+
throw error;
|
|
14295
|
+
}
|
|
14296
|
+
state.hazbase = {
|
|
14297
|
+
...hazbase,
|
|
14298
|
+
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14299
|
+
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14300
|
+
sessionInvalid: false,
|
|
14301
|
+
sessionInvalidReason: "",
|
|
14302
|
+
sessionInvalidAt: "",
|
|
14303
|
+
};
|
|
14304
|
+
await saveState(config.stateFile, state);
|
|
14305
|
+
return writeJson(res, 200, result);
|
|
14306
|
+
}
|
|
14307
|
+
|
|
14308
|
+
if (url.pathname === "/api/hazbase/passkey/assert/challenge" && req.method === "POST") {
|
|
14309
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14310
|
+
if (!session) return;
|
|
14311
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14312
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14313
|
+
const body = await parseJsonBody(req);
|
|
14314
|
+
let partnerOrigin;
|
|
14315
|
+
try {
|
|
14316
|
+
partnerOrigin = deriveHazbasePartnerPasskeyOrigin(req, config);
|
|
14317
|
+
} catch (error) {
|
|
14318
|
+
return writeJson(res, 400, {
|
|
14319
|
+
error: error?.code || "hazbase-passkey-local-host-required",
|
|
14320
|
+
message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
|
|
14321
|
+
});
|
|
14322
|
+
}
|
|
14323
|
+
let result;
|
|
14324
|
+
try {
|
|
14325
|
+
result = await requestPasskeyAssertionChallenge({
|
|
14326
|
+
emailSession: hazbase.accessToken,
|
|
14327
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14328
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14329
|
+
partnerOrigin,
|
|
14330
|
+
});
|
|
14331
|
+
} catch (error) {
|
|
14332
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14333
|
+
throw error;
|
|
14334
|
+
}
|
|
14335
|
+
return writeJson(res, 200, result);
|
|
14336
|
+
}
|
|
14337
|
+
|
|
14338
|
+
if (url.pathname === "/api/hazbase/passkey/assert/complete" && req.method === "POST") {
|
|
14339
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14340
|
+
if (!session) return;
|
|
14341
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14342
|
+
if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
|
|
14343
|
+
const body = await parseJsonBody(req);
|
|
14344
|
+
let result;
|
|
14345
|
+
try {
|
|
14346
|
+
result = await completePasskeyAssertion({
|
|
14347
|
+
emailSession: hazbase.accessToken,
|
|
14348
|
+
challengeId: cleanText(body?.challengeId || ""),
|
|
14349
|
+
credential: body?.credential,
|
|
14350
|
+
purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
|
|
14351
|
+
deviceBindingId: hazbase.deviceBindingId || undefined,
|
|
14352
|
+
});
|
|
14353
|
+
} catch (error) {
|
|
14354
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14355
|
+
throw error;
|
|
14356
|
+
}
|
|
14357
|
+
state.hazbase = {
|
|
14358
|
+
...hazbase,
|
|
14359
|
+
deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
|
|
14360
|
+
credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
|
|
14361
|
+
highTrustToken: cleanText(result.highTrustToken || ""),
|
|
14362
|
+
highTrustExpiresAt: cleanText(result.highTrustExpiresAt || ""),
|
|
14363
|
+
sessionInvalid: false,
|
|
14364
|
+
sessionInvalidReason: "",
|
|
14365
|
+
sessionInvalidAt: "",
|
|
14366
|
+
};
|
|
14367
|
+
await saveState(config.stateFile, state);
|
|
14368
|
+
return writeJson(res, 200, result);
|
|
14369
|
+
}
|
|
14370
|
+
|
|
14371
|
+
if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST") {
|
|
14372
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14373
|
+
if (!session) return;
|
|
14374
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
14375
|
+
if (!hazbase.accessToken || !hazbase.deviceBindingId || !hazbase.highTrustToken) {
|
|
14376
|
+
return writeJson(res, 409, { error: "hazbase-bootstrap-prerequisites-missing" });
|
|
14377
|
+
}
|
|
14378
|
+
const body = await parseJsonBody(req);
|
|
14379
|
+
const chainId = Number(body?.chainId || 0);
|
|
14380
|
+
if (chainId !== 8453 && chainId !== 84532) {
|
|
14381
|
+
return writeJson(res, 400, { error: "unsupported-chain" });
|
|
14382
|
+
}
|
|
14383
|
+
let result;
|
|
14384
|
+
try {
|
|
14385
|
+
result = await bootstrapPasskeyAccount({
|
|
14386
|
+
emailSession: hazbase.accessToken,
|
|
14387
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
14388
|
+
highTrustToken: hazbase.highTrustToken,
|
|
14389
|
+
chainId,
|
|
14390
|
+
});
|
|
14391
|
+
} catch (error) {
|
|
14392
|
+
if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
|
|
14393
|
+
throw error;
|
|
14394
|
+
}
|
|
14395
|
+
const nextAccounts = mergeHazbaseAccountSummaries(hazbase.accounts, [{
|
|
14396
|
+
smartAccountAddress: result.smartAccountAddress,
|
|
14397
|
+
chainId: result.chainId,
|
|
14398
|
+
accountVariant: result.accountVariant,
|
|
14399
|
+
relayMode: result.relayMode,
|
|
14400
|
+
primaryDeviceBindingId: result.deviceBindingId,
|
|
14401
|
+
}]);
|
|
14402
|
+
state.hazbase = {
|
|
14403
|
+
...hazbase,
|
|
14404
|
+
accounts: nextAccounts,
|
|
14405
|
+
sessionInvalid: false,
|
|
14406
|
+
sessionInvalidReason: "",
|
|
14407
|
+
sessionInvalidAt: "",
|
|
14408
|
+
};
|
|
14409
|
+
await saveState(config.stateFile, state);
|
|
14410
|
+
return writeJson(res, 200, result);
|
|
14411
|
+
}
|
|
14412
|
+
|
|
14413
|
+
if (url.pathname === "/api/hazbase/session/refresh" && req.method === "POST") {
|
|
14414
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14415
|
+
const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
|
|
14416
|
+
if (!hookAuth) {
|
|
14417
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14418
|
+
if (!session) return;
|
|
14419
|
+
}
|
|
14420
|
+
markHazbaseSessionInvalid(state, "manual_refresh_requested");
|
|
14421
|
+
await saveState(config.stateFile, state);
|
|
14422
|
+
return writeJson(res, 200, {
|
|
14423
|
+
ok: true,
|
|
14424
|
+
status: "session_refresh_required",
|
|
14425
|
+
});
|
|
14426
|
+
}
|
|
14427
|
+
|
|
14428
|
+
if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
14429
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14430
|
+
if (!session) return;
|
|
14431
|
+
state.hazbase = normalizeHazbaseState(null);
|
|
14432
|
+
await saveState(config.stateFile, state);
|
|
14433
|
+
return writeJson(res, 200, { ok: true });
|
|
14434
|
+
}
|
|
14435
|
+
|
|
11970
14436
|
// Toggle acceptPublicTasks on the A2A relay.
|
|
11971
14437
|
if (url.pathname === "/api/a2a/public-tasks" && req.method === "POST") {
|
|
11972
14438
|
const session = requireApiSession(req, res, config, state);
|
|
@@ -11975,11 +14441,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
11975
14441
|
const accept = body?.accept === true;
|
|
11976
14442
|
config.a2aAcceptPublicTasks = accept;
|
|
11977
14443
|
state.a2aAcceptPublicTasks = accept;
|
|
11978
|
-
|
|
11979
|
-
await saveState(config.stateFile, state);
|
|
11980
|
-
} catch (error) {
|
|
11981
|
-
console.error(`[a2a-public-tasks-save] ${error.message}`);
|
|
11982
|
-
}
|
|
14444
|
+
scheduleSaveState(config, state);
|
|
11983
14445
|
// Re-register with relay to propagate the flag.
|
|
11984
14446
|
if (config.a2aRelayUrl && config.a2aRelayUserId) {
|
|
11985
14447
|
try {
|
|
@@ -12154,6 +14616,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12154
14616
|
await deliverWebPushItem({
|
|
12155
14617
|
config, state,
|
|
12156
14618
|
kind: "thread_share",
|
|
14619
|
+
tab: "inbox",
|
|
14620
|
+
subtab: "pending",
|
|
12157
14621
|
token,
|
|
12158
14622
|
stableId: `thread_share:${shareId}`,
|
|
12159
14623
|
title: `Thread Share: ${sourceLabel || sourceTool || "agent"} → ${targetLabel || targetTool}`,
|
|
@@ -12251,6 +14715,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12251
14715
|
await deliverWebPushItem({
|
|
12252
14716
|
config, state,
|
|
12253
14717
|
kind: "thread_share",
|
|
14718
|
+
tab: "inbox",
|
|
14719
|
+
subtab: "completed",
|
|
12254
14720
|
token: share.token,
|
|
12255
14721
|
stableId: `thread_share_fallback:${share.shareId}`,
|
|
12256
14722
|
title: "Thread Share: target unreachable",
|
|
@@ -12393,7 +14859,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12393
14859
|
lastLocale: resolveDeviceLocaleInfo(config, state, session.deviceId).locale,
|
|
12394
14860
|
});
|
|
12395
14861
|
if (changed || metadataChanged) {
|
|
12396
|
-
|
|
14862
|
+
scheduleSaveState(config, state);
|
|
12397
14863
|
}
|
|
12398
14864
|
return writeJson(res, 200, {
|
|
12399
14865
|
ok: true,
|
|
@@ -12416,7 +14882,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12416
14882
|
changed = deletePushSubscriptionsForDevice(state, session.deviceId) || changed;
|
|
12417
14883
|
}
|
|
12418
14884
|
if (changed) {
|
|
12419
|
-
|
|
14885
|
+
scheduleSaveState(config, state);
|
|
12420
14886
|
}
|
|
12421
14887
|
return writeJson(res, 200, { ok: true, subscribed: false });
|
|
12422
14888
|
}
|
|
@@ -12428,13 +14894,13 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12428
14894
|
}
|
|
12429
14895
|
try {
|
|
12430
14896
|
await sendPushTestToDevice({ config, state, session });
|
|
12431
|
-
|
|
14897
|
+
scheduleSaveState(config, state);
|
|
12432
14898
|
return writeJson(res, 200, { ok: true });
|
|
12433
14899
|
} catch (error) {
|
|
12434
14900
|
const statusCode = Number(error?.statusCode) || 0;
|
|
12435
14901
|
if (statusCode === 404 || statusCode === 410) {
|
|
12436
14902
|
deletePushSubscriptionsForDevice(state, session.deviceId);
|
|
12437
|
-
|
|
14903
|
+
scheduleSaveState(config, state);
|
|
12438
14904
|
return writeJson(res, 410, { error: "push-subscription-expired" });
|
|
12439
14905
|
}
|
|
12440
14906
|
return writeJson(res, 500, {
|
|
@@ -12442,43 +14908,201 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12442
14908
|
ipcError: error.ipcError ?? null,
|
|
12443
14909
|
});
|
|
12444
14910
|
}
|
|
12445
|
-
}
|
|
12446
|
-
|
|
12447
|
-
if (url.pathname === "/api/devices" && req.method === "GET") {
|
|
12448
|
-
const session = requireApiSession(req, res, config, state);
|
|
12449
|
-
if (!session) {
|
|
12450
|
-
return;
|
|
14911
|
+
}
|
|
14912
|
+
|
|
14913
|
+
if (url.pathname === "/api/devices" && req.method === "GET") {
|
|
14914
|
+
const session = requireApiSession(req, res, config, state);
|
|
14915
|
+
if (!session) {
|
|
14916
|
+
return;
|
|
14917
|
+
}
|
|
14918
|
+
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
14919
|
+
return writeJson(res, 200, buildDevicesResponse({ config, state, session, locale }));
|
|
14920
|
+
}
|
|
14921
|
+
|
|
14922
|
+
const apiDeviceRevokeMatch = url.pathname.match(/^\/api\/devices\/([^/]+)\/revoke$/u);
|
|
14923
|
+
if (apiDeviceRevokeMatch && req.method === "POST") {
|
|
14924
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
14925
|
+
if (!session) {
|
|
14926
|
+
return;
|
|
14927
|
+
}
|
|
14928
|
+
const deviceId = decodeURIComponent(apiDeviceRevokeMatch[1]);
|
|
14929
|
+
const existing = getActiveDeviceTrustRecord(state, config, deviceId);
|
|
14930
|
+
if (!existing) {
|
|
14931
|
+
return writeJson(res, 404, { error: "device-not-found" });
|
|
14932
|
+
}
|
|
14933
|
+
let changed = false;
|
|
14934
|
+
changed = revokeDeviceTrust(state, config, deviceId) || changed;
|
|
14935
|
+
changed = deletePushSubscriptionsForDevice(state, deviceId) || changed;
|
|
14936
|
+
if (changed) {
|
|
14937
|
+
await saveState(config.stateFile, state);
|
|
14938
|
+
}
|
|
14939
|
+
const currentDeviceRevoked = deviceId === session.deviceId;
|
|
14940
|
+
if (currentDeviceRevoked) {
|
|
14941
|
+
clearAuthCookies(res, config);
|
|
14942
|
+
}
|
|
14943
|
+
return writeJson(res, 200, {
|
|
14944
|
+
ok: true,
|
|
14945
|
+
deviceId,
|
|
14946
|
+
currentDeviceRevoked,
|
|
14947
|
+
});
|
|
14948
|
+
}
|
|
14949
|
+
|
|
14950
|
+
|
|
14951
|
+
if (url.pathname === "/api/payments/x402/hazbase-wallet" && req.method === "POST") {
|
|
14952
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
14953
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
14954
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
14955
|
+
}
|
|
14956
|
+
|
|
14957
|
+
let body;
|
|
14958
|
+
try {
|
|
14959
|
+
body = await parseJsonBody(req);
|
|
14960
|
+
} catch {
|
|
14961
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
14962
|
+
}
|
|
14963
|
+
|
|
14964
|
+
const approval = createHazbaseWalletPaymentApproval({ config, body });
|
|
14965
|
+
if (!approval) {
|
|
14966
|
+
return writeJson(res, 400, { error: "invalid-hazbase-wallet-payment-request" });
|
|
14967
|
+
}
|
|
14968
|
+
if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
|
|
14969
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-already-pending" });
|
|
14970
|
+
}
|
|
14971
|
+
|
|
14972
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
14973
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
14974
|
+
|
|
14975
|
+
deliverWebPushItem({
|
|
14976
|
+
config,
|
|
14977
|
+
state,
|
|
14978
|
+
kind: "approval",
|
|
14979
|
+
tab: "inbox",
|
|
14980
|
+
subtab: "pending",
|
|
14981
|
+
token: approval.token,
|
|
14982
|
+
stableId: pendingApprovalStableId(approval),
|
|
14983
|
+
title: approval.title,
|
|
14984
|
+
body: approval.messageText,
|
|
14985
|
+
buildLocalizedContent: () => ({ title: approval.title, body: approval.messageText }),
|
|
14986
|
+
}).catch((error) => {
|
|
14987
|
+
console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
|
|
14988
|
+
});
|
|
14989
|
+
|
|
14990
|
+
const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
|
|
14991
|
+
const decisionPromise = new Promise((resolve) => {
|
|
14992
|
+
approval.resolveClaudeWaiter = resolve;
|
|
14993
|
+
});
|
|
14994
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
14995
|
+
setTimeout(() => resolve({ paid: false, decision: "timeout", error: "hazbase-wallet-payment-timeout" }), waitMs);
|
|
14996
|
+
});
|
|
14997
|
+
const decision = await Promise.race([decisionPromise, timeoutPromise]);
|
|
14998
|
+
|
|
14999
|
+
if (!approval.resolved) {
|
|
15000
|
+
approval.resolved = true;
|
|
15001
|
+
approval.resolving = false;
|
|
15002
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
15003
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
15004
|
+
}
|
|
15005
|
+
|
|
15006
|
+
if (decision && typeof decision === "object" && decision.paid === true) {
|
|
15007
|
+
return writeJson(res, 200, decision);
|
|
15008
|
+
}
|
|
15009
|
+
const error = decision?.error || decision?.decision || "hazbase-wallet-payment-declined";
|
|
15010
|
+
const statusCode = error === "hazbase-wallet-payment-timeout" ? 408 : 403;
|
|
15011
|
+
return writeJson(res, statusCode, { paid: false, error, decision: decision?.decision || "decline" });
|
|
15012
|
+
}
|
|
15013
|
+
|
|
15014
|
+
const hazbaseWalletPayMatch = url.pathname.match(/^\/api\/payments\/x402\/hazbase-wallet\/([a-f0-9]+)\/pay$/u);
|
|
15015
|
+
if (hazbaseWalletPayMatch && req.method === "POST") {
|
|
15016
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
15017
|
+
if (!session) return;
|
|
15018
|
+
const token = hazbaseWalletPayMatch[1];
|
|
15019
|
+
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
15020
|
+
if (!approval || approval.kind !== "hazbase_wallet_payment") {
|
|
15021
|
+
return writeJson(res, 404, { error: "approval-not-found" });
|
|
15022
|
+
}
|
|
15023
|
+
if (approval.resolved || approval.resolving) {
|
|
15024
|
+
return writeJson(res, 409, { error: "approval-already-handled" });
|
|
15025
|
+
}
|
|
15026
|
+
approval.resolving = true;
|
|
15027
|
+
try {
|
|
15028
|
+
const result = await completeHazbaseWalletPaymentApproval({ config, runtime, state, approval });
|
|
15029
|
+
return writeJson(res, 200, result);
|
|
15030
|
+
} catch (error) {
|
|
15031
|
+
approval.resolving = false;
|
|
15032
|
+
console.error(`[hazbase-wallet-payment-error] ${approval.requestKey} | ${error?.stack || error?.message || error}`);
|
|
15033
|
+
return writeJson(res, Number(error?.statusCode) || 500, { error: error?.code || error?.message || "hazbase-wallet-payment-failed" });
|
|
12451
15034
|
}
|
|
12452
|
-
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
12453
|
-
return writeJson(res, 200, buildDevicesResponse({ config, state, session, locale }));
|
|
12454
15035
|
}
|
|
12455
15036
|
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
return;
|
|
15037
|
+
if (url.pathname === "/api/payments/x402/approval" && req.method === "POST") {
|
|
15038
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
15039
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
15040
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
12461
15041
|
}
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
15042
|
+
|
|
15043
|
+
let body;
|
|
15044
|
+
try {
|
|
15045
|
+
body = await parseJsonBody(req);
|
|
15046
|
+
} catch {
|
|
15047
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
12466
15048
|
}
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
await saveState(config.stateFile, state);
|
|
15049
|
+
|
|
15050
|
+
const approval = createX402PaymentApproval({ config, body });
|
|
15051
|
+
if (!approval) {
|
|
15052
|
+
return writeJson(res, 400, { error: "invalid-payment-approval-request" });
|
|
12472
15053
|
}
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
clearAuthCookies(res, config);
|
|
15054
|
+
if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
|
|
15055
|
+
return writeJson(res, 409, { error: "payment-approval-already-pending" });
|
|
12476
15056
|
}
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
15057
|
+
|
|
15058
|
+
runtime.nativeApprovalsByToken.set(approval.token, approval);
|
|
15059
|
+
runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
|
|
15060
|
+
|
|
15061
|
+
deliverWebPushItem({
|
|
15062
|
+
config,
|
|
15063
|
+
state,
|
|
15064
|
+
kind: "approval",
|
|
15065
|
+
tab: "inbox",
|
|
15066
|
+
subtab: "pending",
|
|
15067
|
+
token: approval.token,
|
|
15068
|
+
stableId: pendingApprovalStableId(approval),
|
|
15069
|
+
title: approval.title,
|
|
15070
|
+
body: approval.messageText,
|
|
15071
|
+
buildLocalizedContent: () => ({
|
|
15072
|
+
title: approval.title,
|
|
15073
|
+
body: approval.messageText,
|
|
15074
|
+
}),
|
|
15075
|
+
}).catch((error) => {
|
|
15076
|
+
console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
15077
|
+
});
|
|
15078
|
+
|
|
15079
|
+
const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
|
|
15080
|
+
const decisionPromise = new Promise((resolve) => {
|
|
15081
|
+
approval.resolveClaudeWaiter = resolve;
|
|
15082
|
+
});
|
|
15083
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
15084
|
+
setTimeout(() => resolve("timeout"), waitMs);
|
|
12481
15085
|
});
|
|
15086
|
+
const decision = await Promise.race([decisionPromise, timeoutPromise]);
|
|
15087
|
+
|
|
15088
|
+
if (!approval.resolved) {
|
|
15089
|
+
approval.resolved = true;
|
|
15090
|
+
approval.resolving = false;
|
|
15091
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
15092
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
15093
|
+
}
|
|
15094
|
+
|
|
15095
|
+
if (decision === "accept") {
|
|
15096
|
+
return writeJson(res, 200, {
|
|
15097
|
+
approved: true,
|
|
15098
|
+
decision,
|
|
15099
|
+
approvedPayment: approval.rawParams,
|
|
15100
|
+
});
|
|
15101
|
+
}
|
|
15102
|
+
if (decision === "timeout") {
|
|
15103
|
+
return writeJson(res, 408, { approved: false, decision, error: "payment-approval-timeout" });
|
|
15104
|
+
}
|
|
15105
|
+
return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
|
|
12482
15106
|
}
|
|
12483
15107
|
|
|
12484
15108
|
if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
|
|
@@ -12640,6 +15264,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12640
15264
|
diffSource: String(body.diffSource || "claude_permission_request"),
|
|
12641
15265
|
diffAddedLines: Math.max(0, Number(body.diffAddedLines) || 0),
|
|
12642
15266
|
diffRemovedLines: Math.max(0, Number(body.diffRemovedLines) || 0),
|
|
15267
|
+
cwd: body.cwd ? String(body.cwd) : "",
|
|
15268
|
+
workspaceRoot: body.cwd ? String(body.cwd) : "",
|
|
12643
15269
|
createdAtMs: Number(body.createdAtMs) || Date.now(),
|
|
12644
15270
|
resolved: false,
|
|
12645
15271
|
resolving: false,
|
|
@@ -12653,6 +15279,59 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12653
15279
|
readOnly: body.notifyOnly === true,
|
|
12654
15280
|
};
|
|
12655
15281
|
|
|
15282
|
+
const claudeAutoPilotMatch =
|
|
15283
|
+
approval.kind === "command"
|
|
15284
|
+
? maybeAutoApproveTrustedRead({
|
|
15285
|
+
state,
|
|
15286
|
+
commandText: body?.toolInput?.command ?? "",
|
|
15287
|
+
cwd: body.cwd ? String(body.cwd) : "",
|
|
15288
|
+
workspaceRoot: body.cwd ? String(body.cwd) : "",
|
|
15289
|
+
})
|
|
15290
|
+
: null;
|
|
15291
|
+
if (claudeAutoPilotMatch) {
|
|
15292
|
+
try {
|
|
15293
|
+
await recordAutoApprovedTrustedRead({
|
|
15294
|
+
config,
|
|
15295
|
+
runtime,
|
|
15296
|
+
state,
|
|
15297
|
+
approval,
|
|
15298
|
+
policyMatch: claudeAutoPilotMatch,
|
|
15299
|
+
});
|
|
15300
|
+
console.log(`[auto-pilot-approved] ${approval.requestKey} | ${claudeAutoPilotMatch.command}`);
|
|
15301
|
+
return writeJson(res, 200, {
|
|
15302
|
+
permissionDecision: "allow",
|
|
15303
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedReadReason"),
|
|
15304
|
+
});
|
|
15305
|
+
} catch (error) {
|
|
15306
|
+
console.error(`[auto-pilot-error] ${approval.requestKey} | ${error.message}`);
|
|
15307
|
+
}
|
|
15308
|
+
}
|
|
15309
|
+
|
|
15310
|
+
const claudeAutoPilotWriteCandidate =
|
|
15311
|
+
approval.kind === "file"
|
|
15312
|
+
? maybeAutoApproveTrustedWriteCandidate({ runtime, state, approval })
|
|
15313
|
+
: null;
|
|
15314
|
+
if (claudeAutoPilotWriteCandidate) {
|
|
15315
|
+
try {
|
|
15316
|
+
await recordAutoApprovedTrustedWrite({
|
|
15317
|
+
config,
|
|
15318
|
+
runtime,
|
|
15319
|
+
state,
|
|
15320
|
+
approval,
|
|
15321
|
+
candidate: claudeAutoPilotWriteCandidate,
|
|
15322
|
+
});
|
|
15323
|
+
console.log(
|
|
15324
|
+
`[auto-pilot-approved-write] ${approval.requestKey} | files=${claudeAutoPilotWriteCandidate.fileRefs.length} | lines=${claudeAutoPilotWriteCandidate.totalChangedLines}`
|
|
15325
|
+
);
|
|
15326
|
+
return writeJson(res, 200, {
|
|
15327
|
+
permissionDecision: "allow",
|
|
15328
|
+
permissionDecisionReason: t(config.defaultLocale, "server.message.autoPilotTrustedWriteReason"),
|
|
15329
|
+
});
|
|
15330
|
+
} catch (error) {
|
|
15331
|
+
console.error(`[auto-pilot-write-error] ${approval.requestKey} | ${error.message}`);
|
|
15332
|
+
}
|
|
15333
|
+
}
|
|
15334
|
+
|
|
12656
15335
|
runtime.nativeApprovalsByToken.set(token, approval);
|
|
12657
15336
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
12658
15337
|
|
|
@@ -12660,6 +15339,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12660
15339
|
config,
|
|
12661
15340
|
state,
|
|
12662
15341
|
kind: "approval",
|
|
15342
|
+
tab: "inbox",
|
|
15343
|
+
subtab: "pending",
|
|
12663
15344
|
token: approval.token,
|
|
12664
15345
|
stableId: pendingApprovalStableId(approval),
|
|
12665
15346
|
title: approval.title,
|
|
@@ -12789,6 +15470,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12789
15470
|
config,
|
|
12790
15471
|
state,
|
|
12791
15472
|
kind: "moltbook_reply",
|
|
15473
|
+
tab: "inbox",
|
|
15474
|
+
subtab: "pending",
|
|
12792
15475
|
token,
|
|
12793
15476
|
stableId: `moltbook_reply:${item.sourceId}`,
|
|
12794
15477
|
title: item.title,
|
|
@@ -12970,6 +15653,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
12970
15653
|
config,
|
|
12971
15654
|
state,
|
|
12972
15655
|
kind: "moltbook_draft",
|
|
15656
|
+
tab: "inbox",
|
|
15657
|
+
subtab: "pending",
|
|
12973
15658
|
token,
|
|
12974
15659
|
stableId: `moltbook_draft:${sourceId}`,
|
|
12975
15660
|
title: pushTitle,
|
|
@@ -13066,6 +15751,8 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13066
15751
|
try {
|
|
13067
15752
|
await deliverWebPushItem({
|
|
13068
15753
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
15754
|
+
tab: "inbox",
|
|
15755
|
+
subtab: "completed",
|
|
13069
15756
|
stableId: `moltbook_draft_failed:${draft.sourceId}`,
|
|
13070
15757
|
title: "Draft post failed",
|
|
13071
15758
|
body: String(postError.message || "").slice(0, 160),
|
|
@@ -13089,7 +15776,16 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13089
15776
|
return;
|
|
13090
15777
|
}
|
|
13091
15778
|
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
13092
|
-
return writeJson(res, 200,
|
|
15779
|
+
return writeJson(res, 200, buildInboxFastResponse(runtime, state, config, locale));
|
|
15780
|
+
}
|
|
15781
|
+
|
|
15782
|
+
if (url.pathname === "/api/inbox/diff" && req.method === "GET") {
|
|
15783
|
+
const session = requireApiSession(req, res, config, state);
|
|
15784
|
+
if (!session) {
|
|
15785
|
+
return;
|
|
15786
|
+
}
|
|
15787
|
+
const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
|
|
15788
|
+
return writeJson(res, 200, await buildInboxDiffResponse(runtime, state, config, locale));
|
|
13093
15789
|
}
|
|
13094
15790
|
|
|
13095
15791
|
if (url.pathname === "/api/timeline" && req.method === "GET") {
|
|
@@ -13226,6 +15922,10 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13226
15922
|
if (approval.resolved || approval.resolving) {
|
|
13227
15923
|
return writeJson(res, 409, { error: "approval-already-handled" });
|
|
13228
15924
|
}
|
|
15925
|
+
if (approval.kind === "hazbase_wallet_payment") {
|
|
15926
|
+
console.warn(`[hazbase-wallet-payment-generic-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
15927
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
|
|
15928
|
+
}
|
|
13229
15929
|
|
|
13230
15930
|
approval.resolving = true;
|
|
13231
15931
|
try {
|
|
@@ -13820,6 +16520,21 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
13820
16520
|
if (approval.resolved || approval.resolving) {
|
|
13821
16521
|
return writeApprovalHandled(res, req, approval.title, 409, config.defaultLocale);
|
|
13822
16522
|
}
|
|
16523
|
+
if (approval.kind === "hazbase_wallet_payment") {
|
|
16524
|
+
console.warn(`[hazbase-wallet-payment-native-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
16525
|
+
if (requestWantsHtml(req)) {
|
|
16526
|
+
return writeHtml(
|
|
16527
|
+
res,
|
|
16528
|
+
409,
|
|
16529
|
+
renderStatusPage({
|
|
16530
|
+
title: approval.title,
|
|
16531
|
+
body: "This approval must be completed from the viveworker app with the wallet payment button.",
|
|
16532
|
+
tone: "warn",
|
|
16533
|
+
})
|
|
16534
|
+
);
|
|
16535
|
+
}
|
|
16536
|
+
return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
|
|
16537
|
+
}
|
|
13823
16538
|
|
|
13824
16539
|
approval.resolving = true;
|
|
13825
16540
|
try {
|
|
@@ -15169,6 +17884,8 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
15169
17884
|
const pushTitle = draft.draftType === "original_post" ? finalTitle : `Reply → ${draft.postTitle || "Moltbook"}`;
|
|
15170
17885
|
await deliverWebPushItem({
|
|
15171
17886
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
17887
|
+
tab: "inbox",
|
|
17888
|
+
subtab: "completed",
|
|
15172
17889
|
stableId: `moltbook_draft_posted:${draft.sourceId}`,
|
|
15173
17890
|
title: "Moltbook posted",
|
|
15174
17891
|
body: truncate(singleLine(pushTitle), 160),
|
|
@@ -15201,6 +17918,12 @@ function buildConfig(cli) {
|
|
|
15201
17918
|
a2aShareUrl: stripTrailingSlash(
|
|
15202
17919
|
process.env.VIVEWORKER_SHARE_URL || "https://share.viveworker.com"
|
|
15203
17920
|
),
|
|
17921
|
+
hazbaseApiUrl: stripTrailingSlash(process.env.HAZBASE_API_URL || "https://api.hazbase.com"),
|
|
17922
|
+
hazbaseClientKey: cleanText(process.env.HAZBASE_CLIENT_KEY || "viveworker-internal"),
|
|
17923
|
+
hazbaseDeviceLabel: cleanText(process.env.HAZBASE_DEVICE_LABEL || "viveworker"),
|
|
17924
|
+
npmPackageName: cleanText(process.env.VIVEWORKER_NPM_PACKAGE || "viveworker"),
|
|
17925
|
+
npmDistTag: cleanText(process.env.VIVEWORKER_NPM_DIST_TAG || "latest"),
|
|
17926
|
+
npmRegistryUrl: stripTrailingSlash(process.env.NPM_REGISTRY_URL || "https://registry.npmjs.org"),
|
|
15204
17927
|
webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
|
|
15205
17928
|
authRequired: boolEnv("AUTH_REQUIRED", true),
|
|
15206
17929
|
webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
|
|
@@ -15288,6 +18011,386 @@ function buildConfig(cli) {
|
|
|
15288
18011
|
};
|
|
15289
18012
|
}
|
|
15290
18013
|
|
|
18014
|
+
|
|
18015
|
+
function normalizeHazbaseState(raw) {
|
|
18016
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
18017
|
+
return {
|
|
18018
|
+
email: cleanText(value.email ?? ""),
|
|
18019
|
+
accessToken: cleanText(value.accessToken ?? ""),
|
|
18020
|
+
sessionId: cleanText(value.sessionId ?? ""),
|
|
18021
|
+
userId: cleanText(value.userId ?? ""),
|
|
18022
|
+
deviceBindingId: cleanText(value.deviceBindingId ?? ""),
|
|
18023
|
+
credentialId: cleanText(value.credentialId ?? ""),
|
|
18024
|
+
highTrustToken: cleanText(value.highTrustToken ?? ""),
|
|
18025
|
+
highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
|
|
18026
|
+
accounts: Array.isArray(value.accounts) ? value.accounts : [],
|
|
18027
|
+
sessionInvalid: value.sessionInvalid === true,
|
|
18028
|
+
sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
|
|
18029
|
+
sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
|
|
18030
|
+
};
|
|
18031
|
+
}
|
|
18032
|
+
|
|
18033
|
+
function isHazbaseInvalidAppSessionError(error) {
|
|
18034
|
+
const details = error?.details && typeof error.details === "object" ? error.details : {};
|
|
18035
|
+
const statusCode = Number(error?.statusCode || error?.status || details.statusCode || 0);
|
|
18036
|
+
const text = [
|
|
18037
|
+
error?.code,
|
|
18038
|
+
error?.message,
|
|
18039
|
+
details.errorCode,
|
|
18040
|
+
details.code,
|
|
18041
|
+
details.error,
|
|
18042
|
+
details.message,
|
|
18043
|
+
]
|
|
18044
|
+
.map((entry) => cleanText(entry || "").toLowerCase())
|
|
18045
|
+
.filter(Boolean)
|
|
18046
|
+
.join(" ");
|
|
18047
|
+
return statusCode === 401 && text.includes("invalid app session");
|
|
18048
|
+
}
|
|
18049
|
+
|
|
18050
|
+
function markHazbaseSessionInvalid(state, reason = "invalid_app_session") {
|
|
18051
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
18052
|
+
state.hazbase = {
|
|
18053
|
+
...hazbase,
|
|
18054
|
+
accessToken: "",
|
|
18055
|
+
sessionId: "",
|
|
18056
|
+
highTrustToken: "",
|
|
18057
|
+
highTrustExpiresAt: "",
|
|
18058
|
+
sessionInvalid: true,
|
|
18059
|
+
sessionInvalidReason: cleanText(reason || "invalid_app_session"),
|
|
18060
|
+
sessionInvalidAt: new Date().toISOString(),
|
|
18061
|
+
};
|
|
18062
|
+
return state.hazbase;
|
|
18063
|
+
}
|
|
18064
|
+
|
|
18065
|
+
async function maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res }) {
|
|
18066
|
+
if (!isHazbaseInvalidAppSessionError(error)) {
|
|
18067
|
+
return false;
|
|
18068
|
+
}
|
|
18069
|
+
markHazbaseSessionInvalid(state);
|
|
18070
|
+
await saveState(config.stateFile, state);
|
|
18071
|
+
return writeJson(res, 401, {
|
|
18072
|
+
error: "hazbase-session-expired",
|
|
18073
|
+
message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
|
|
18074
|
+
});
|
|
18075
|
+
}
|
|
18076
|
+
|
|
18077
|
+
function hazbasePasskeyLocalHostError() {
|
|
18078
|
+
const error = new Error("Open viveworker on its .local hostname to use hazBase passkeys.");
|
|
18079
|
+
error.code = "hazbase-passkey-local-host-required";
|
|
18080
|
+
return error;
|
|
18081
|
+
}
|
|
18082
|
+
|
|
18083
|
+
function deriveHazbasePartnerPasskeyOrigin(req, config) {
|
|
18084
|
+
const rawOrigin = cleanText(req.headers.origin || "");
|
|
18085
|
+
const rawHost = cleanText(req.headers.host || "");
|
|
18086
|
+
const candidateOrigin = rawOrigin || (rawHost ? `https://${rawHost}` : "");
|
|
18087
|
+
if (!candidateOrigin) {
|
|
18088
|
+
throw hazbasePasskeyLocalHostError();
|
|
18089
|
+
}
|
|
18090
|
+
|
|
18091
|
+
let parsed;
|
|
18092
|
+
try {
|
|
18093
|
+
parsed = new URL(candidateOrigin);
|
|
18094
|
+
} catch {
|
|
18095
|
+
throw hazbasePasskeyLocalHostError();
|
|
18096
|
+
}
|
|
18097
|
+
|
|
18098
|
+
const hostname = cleanText(parsed.hostname || "").toLowerCase();
|
|
18099
|
+
if (parsed.protocol !== "https:" || !hostname || net.isIP(hostname) || !hostname.endsWith(".local")) {
|
|
18100
|
+
throw hazbasePasskeyLocalHostError();
|
|
18101
|
+
}
|
|
18102
|
+
|
|
18103
|
+
return {
|
|
18104
|
+
origin: `${parsed.protocol}//${parsed.host}`.toLowerCase(),
|
|
18105
|
+
rpId: hostname,
|
|
18106
|
+
clientKey: config.hazbaseClientKey || "viveworker-internal",
|
|
18107
|
+
};
|
|
18108
|
+
}
|
|
18109
|
+
|
|
18110
|
+
function configureHazbaseClient(config) {
|
|
18111
|
+
try {
|
|
18112
|
+
setHazbaseClientKey(config.hazbaseClientKey || "viveworker-internal");
|
|
18113
|
+
setHazbaseApiEndpoint(config.hazbaseApiUrl || "https://api.hazbase.com");
|
|
18114
|
+
} catch (error) {
|
|
18115
|
+
console.error(`[hazbase-config] ${error.message}`);
|
|
18116
|
+
}
|
|
18117
|
+
}
|
|
18118
|
+
|
|
18119
|
+
async function fetchHazbaseMetadata(config, endpoint, timeoutMs = HAZBASE_METADATA_TIMEOUT_MS) {
|
|
18120
|
+
const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
|
|
18121
|
+
if (!baseUrl) return null;
|
|
18122
|
+
const timeout = Math.max(250, Number(timeoutMs) || HAZBASE_METADATA_TIMEOUT_MS);
|
|
18123
|
+
const controller = new AbortController();
|
|
18124
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
18125
|
+
timer.unref?.();
|
|
18126
|
+
try {
|
|
18127
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
18128
|
+
method: "GET",
|
|
18129
|
+
headers: { Accept: "application/json" },
|
|
18130
|
+
signal: controller.signal,
|
|
18131
|
+
});
|
|
18132
|
+
if (!response.ok) {
|
|
18133
|
+
const errorText = await response.text().catch(() => "");
|
|
18134
|
+
throw new Error(`${endpoint} failed: ${cleanText(errorText || response.statusText || "upstream error")}`);
|
|
18135
|
+
}
|
|
18136
|
+
const payload = await response.json().catch(() => null);
|
|
18137
|
+
return payload?.data ?? payload ?? null;
|
|
18138
|
+
} catch (error) {
|
|
18139
|
+
if (error?.name === "AbortError") {
|
|
18140
|
+
throw new Error(`${endpoint} timed out after ${timeout}ms`);
|
|
18141
|
+
}
|
|
18142
|
+
throw error;
|
|
18143
|
+
} finally {
|
|
18144
|
+
clearTimeout(timer);
|
|
18145
|
+
}
|
|
18146
|
+
}
|
|
18147
|
+
|
|
18148
|
+
async function getNpmVersionStatus(runtime, config) {
|
|
18149
|
+
const now = Date.now();
|
|
18150
|
+
const cached = runtime.npmVersionStatusCache;
|
|
18151
|
+
if (cached && now - Number(cached.checkedAtMs || 0) < NPM_VERSION_CHECK_CACHE_TTL_MS) {
|
|
18152
|
+
return cached;
|
|
18153
|
+
}
|
|
18154
|
+
if (runtime.npmVersionStatusPending) {
|
|
18155
|
+
return runtime.npmVersionStatusPending;
|
|
18156
|
+
}
|
|
18157
|
+
runtime.npmVersionStatusPending = fetchNpmVersionStatus(config)
|
|
18158
|
+
.then((status) => {
|
|
18159
|
+
runtime.npmVersionStatusCache = status;
|
|
18160
|
+
return status;
|
|
18161
|
+
})
|
|
18162
|
+
.catch((error) => {
|
|
18163
|
+
const fallback = {
|
|
18164
|
+
packageName: config.npmPackageName || "viveworker",
|
|
18165
|
+
distTag: config.npmDistTag || "latest",
|
|
18166
|
+
currentVersion: appPackageVersion,
|
|
18167
|
+
latestVersion: "",
|
|
18168
|
+
updateAvailable: false,
|
|
18169
|
+
checkedAtMs: Date.now(),
|
|
18170
|
+
error: cleanText(error?.message || "npm version check failed"),
|
|
18171
|
+
};
|
|
18172
|
+
runtime.npmVersionStatusCache = fallback;
|
|
18173
|
+
return fallback;
|
|
18174
|
+
})
|
|
18175
|
+
.finally(() => {
|
|
18176
|
+
runtime.npmVersionStatusPending = null;
|
|
18177
|
+
});
|
|
18178
|
+
return runtime.npmVersionStatusPending;
|
|
18179
|
+
}
|
|
18180
|
+
|
|
18181
|
+
async function fetchNpmVersionStatus(config) {
|
|
18182
|
+
const packageName = cleanText(config.npmPackageName || "viveworker") || "viveworker";
|
|
18183
|
+
const distTag = cleanText(config.npmDistTag || "latest") || "latest";
|
|
18184
|
+
const registryBaseUrl = stripTrailingSlash(config.npmRegistryUrl || "https://registry.npmjs.org");
|
|
18185
|
+
const endpoint = `${registryBaseUrl}/${encodeURIComponent(packageName).replace(/^%40/u, "@")}/${encodeURIComponent(distTag)}`;
|
|
18186
|
+
const controller = new AbortController();
|
|
18187
|
+
const timer = setTimeout(() => controller.abort(), NPM_VERSION_CHECK_TIMEOUT_MS);
|
|
18188
|
+
timer.unref?.();
|
|
18189
|
+
try {
|
|
18190
|
+
const response = await fetch(endpoint, {
|
|
18191
|
+
method: "GET",
|
|
18192
|
+
headers: { Accept: "application/json" },
|
|
18193
|
+
signal: controller.signal,
|
|
18194
|
+
});
|
|
18195
|
+
if (!response.ok) {
|
|
18196
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
18197
|
+
}
|
|
18198
|
+
const payload = await response.json().catch(() => null);
|
|
18199
|
+
const latestVersion = cleanText(payload?.version || "");
|
|
18200
|
+
if (!latestVersion) {
|
|
18201
|
+
throw new Error("npm registry response did not include a version");
|
|
18202
|
+
}
|
|
18203
|
+
return {
|
|
18204
|
+
packageName,
|
|
18205
|
+
distTag,
|
|
18206
|
+
currentVersion: appPackageVersion,
|
|
18207
|
+
latestVersion,
|
|
18208
|
+
updateAvailable: comparePackageVersions(latestVersion, appPackageVersion) > 0,
|
|
18209
|
+
checkedAtMs: Date.now(),
|
|
18210
|
+
error: "",
|
|
18211
|
+
};
|
|
18212
|
+
} catch (error) {
|
|
18213
|
+
if (error?.name === "AbortError") {
|
|
18214
|
+
throw new Error("npm version check timed out");
|
|
18215
|
+
}
|
|
18216
|
+
throw error;
|
|
18217
|
+
} finally {
|
|
18218
|
+
clearTimeout(timer);
|
|
18219
|
+
}
|
|
18220
|
+
}
|
|
18221
|
+
|
|
18222
|
+
function comparePackageVersions(left, right) {
|
|
18223
|
+
const a = parsePackageVersion(left);
|
|
18224
|
+
const b = parsePackageVersion(right);
|
|
18225
|
+
for (let index = 0; index < 3; index += 1) {
|
|
18226
|
+
if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1;
|
|
18227
|
+
}
|
|
18228
|
+
if (!a.prerelease.length && b.prerelease.length) return 1;
|
|
18229
|
+
if (a.prerelease.length && !b.prerelease.length) return -1;
|
|
18230
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
18231
|
+
for (let index = 0; index < length; index += 1) {
|
|
18232
|
+
const av = a.prerelease[index];
|
|
18233
|
+
const bv = b.prerelease[index];
|
|
18234
|
+
if (av == null && bv == null) return 0;
|
|
18235
|
+
if (av == null) return -1;
|
|
18236
|
+
if (bv == null) return 1;
|
|
18237
|
+
const an = /^\d+$/u.test(av) ? Number(av) : null;
|
|
18238
|
+
const bn = /^\d+$/u.test(bv) ? Number(bv) : null;
|
|
18239
|
+
if (an != null && bn != null && an !== bn) return an > bn ? 1 : -1;
|
|
18240
|
+
if (an != null && bn == null) return -1;
|
|
18241
|
+
if (an == null && bn != null) return 1;
|
|
18242
|
+
const cmp = av.localeCompare(bv);
|
|
18243
|
+
if (cmp !== 0) return cmp > 0 ? 1 : -1;
|
|
18244
|
+
}
|
|
18245
|
+
return 0;
|
|
18246
|
+
}
|
|
18247
|
+
|
|
18248
|
+
function parsePackageVersion(value) {
|
|
18249
|
+
const normalized = cleanText(value || "").replace(/^v/iu, "").split("+")[0];
|
|
18250
|
+
const [core, prerelease = ""] = normalized.split("-", 2);
|
|
18251
|
+
const parts = core.split(".").map((part) => Number(part)).map((part) => (Number.isFinite(part) && part >= 0 ? part : 0));
|
|
18252
|
+
while (parts.length < 3) parts.push(0);
|
|
18253
|
+
return {
|
|
18254
|
+
parts: parts.slice(0, 3),
|
|
18255
|
+
prerelease: prerelease ? prerelease.split(".").map((part) => cleanText(part)).filter(Boolean) : [],
|
|
18256
|
+
};
|
|
18257
|
+
}
|
|
18258
|
+
|
|
18259
|
+
function mergeHazbaseAccountSummaries(existing, incoming) {
|
|
18260
|
+
const merged = new Map();
|
|
18261
|
+
for (const entry of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(incoming) ? incoming : [])]) {
|
|
18262
|
+
if (!entry || !entry.smartAccountAddress) continue;
|
|
18263
|
+
const key = `${Number(entry.chainId) || 0}:${String(entry.smartAccountAddress).toLowerCase()}`;
|
|
18264
|
+
merged.set(key, entry);
|
|
18265
|
+
}
|
|
18266
|
+
return [...merged.values()];
|
|
18267
|
+
}
|
|
18268
|
+
|
|
18269
|
+
function resolveHazbaseAccountForChain(accounts, chainId) {
|
|
18270
|
+
const normalizedChainId = Number(chainId || 0);
|
|
18271
|
+
if (!normalizedChainId) return null;
|
|
18272
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
18273
|
+
return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
|
|
18274
|
+
}
|
|
18275
|
+
|
|
18276
|
+
|
|
18277
|
+
async function completeHazbaseWalletPaymentApproval({ config, runtime, state, approval }) {
|
|
18278
|
+
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
18279
|
+
if (!hazbase.accessToken) throw codedError("hazbase-auth-required", 401);
|
|
18280
|
+
if (!hazbase.deviceBindingId) throw codedError("hazbase-passkey-required", 409);
|
|
18281
|
+
if (!hazbase.highTrustToken) throw codedError("hazbase-reauth-required", 409);
|
|
18282
|
+
const chainId = Number(approval.rawParams?.chainId || 0);
|
|
18283
|
+
const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
|
|
18284
|
+
if (!account?.smartAccountAddress) throw codedError("hazbase-wallet-account-missing", 409);
|
|
18285
|
+
|
|
18286
|
+
let result;
|
|
18287
|
+
try {
|
|
18288
|
+
result = await postHazbaseJson(config, "/api/payments/x402/hazbase-wallet/pay", {
|
|
18289
|
+
paymentRequestId: approval.paymentRequestId,
|
|
18290
|
+
deviceBindingId: hazbase.deviceBindingId,
|
|
18291
|
+
highTrustToken: hazbase.highTrustToken,
|
|
18292
|
+
smartAccountAddress: account.smartAccountAddress,
|
|
18293
|
+
waitForReceipt: true,
|
|
18294
|
+
}, hazbase.accessToken, 130_000);
|
|
18295
|
+
} catch (error) {
|
|
18296
|
+
if (isHazbaseInvalidAppSessionError(error)) {
|
|
18297
|
+
markHazbaseSessionInvalid(state);
|
|
18298
|
+
await saveState(config.stateFile, state);
|
|
18299
|
+
throw codedError("hazbase-session-expired", 401, {
|
|
18300
|
+
message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
|
|
18301
|
+
});
|
|
18302
|
+
}
|
|
18303
|
+
throw error;
|
|
18304
|
+
}
|
|
18305
|
+
|
|
18306
|
+
if (!result?.xPayment) {
|
|
18307
|
+
throw codedError(result?.errorCode || result?.error || "hazbase-wallet-payment-failed", 502);
|
|
18308
|
+
}
|
|
18309
|
+
|
|
18310
|
+
const payload = {
|
|
18311
|
+
paid: true,
|
|
18312
|
+
decision: "accept",
|
|
18313
|
+
paymentRequestId: approval.paymentRequestId,
|
|
18314
|
+
xPayment: cleanText(result.xPayment || ""),
|
|
18315
|
+
payer: cleanText(result.payer || account.smartAccountAddress || ""),
|
|
18316
|
+
chainId: Number(result.chainId || chainId),
|
|
18317
|
+
network: cleanText(result.network || approval.rawParams?.network || ""),
|
|
18318
|
+
relayMode: cleanText(result.relayMode || ""),
|
|
18319
|
+
submittedUserOpHash: cleanText(result.submittedUserOpHash || ""),
|
|
18320
|
+
transactionHash: cleanText(result.transactionHash || ""),
|
|
18321
|
+
payment: result,
|
|
18322
|
+
};
|
|
18323
|
+
|
|
18324
|
+
approval.resolved = true;
|
|
18325
|
+
approval.resolving = false;
|
|
18326
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
18327
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
18328
|
+
approval.resolveClaudeWaiter?.(payload);
|
|
18329
|
+
const stateChanged = recordActionHistoryItem({
|
|
18330
|
+
config,
|
|
18331
|
+
runtime,
|
|
18332
|
+
state,
|
|
18333
|
+
kind: "approval",
|
|
18334
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
18335
|
+
token: approval.token,
|
|
18336
|
+
title: approval.title,
|
|
18337
|
+
threadLabel: approval.threadLabel || "",
|
|
18338
|
+
messageText: `${approvalDecisionMessage("accept", config.defaultLocale, approval.provider)}\n\n${approval.messageText}\n\nTransaction: ${payload.transactionHash || payload.submittedUserOpHash}`,
|
|
18339
|
+
summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage("accept", config.defaultLocale, approval.provider),
|
|
18340
|
+
fileRefs: [],
|
|
18341
|
+
diffText: "",
|
|
18342
|
+
diffSource: "",
|
|
18343
|
+
diffAvailable: false,
|
|
18344
|
+
diffAddedLines: 0,
|
|
18345
|
+
diffRemovedLines: 0,
|
|
18346
|
+
outcome: "approved",
|
|
18347
|
+
provider: approval.provider || "viveworker",
|
|
18348
|
+
});
|
|
18349
|
+
if (stateChanged) await saveState(config.stateFile, state);
|
|
18350
|
+
console.log(`[hazbase-wallet-payment] ${approval.requestKey} | ${payload.transactionHash || payload.submittedUserOpHash}`);
|
|
18351
|
+
return { ok: true, ...payload };
|
|
18352
|
+
}
|
|
18353
|
+
|
|
18354
|
+
async function postHazbaseJson(config, endpoint, body, bearerToken, timeoutMs = 30_000) {
|
|
18355
|
+
const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
|
|
18356
|
+
if (!baseUrl) throw codedError("hazbase-api-not-configured", 503);
|
|
18357
|
+
const controller = new AbortController();
|
|
18358
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18359
|
+
timer.unref?.();
|
|
18360
|
+
try {
|
|
18361
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
18362
|
+
method: "POST",
|
|
18363
|
+
headers: {
|
|
18364
|
+
"content-type": "application/json",
|
|
18365
|
+
accept: "application/json",
|
|
18366
|
+
authorization: `Bearer ${bearerToken}`,
|
|
18367
|
+
"x-request-id": `viveworker_${crypto.randomUUID()}`,
|
|
18368
|
+
},
|
|
18369
|
+
body: JSON.stringify(body),
|
|
18370
|
+
signal: controller.signal,
|
|
18371
|
+
});
|
|
18372
|
+
const payload = await response.json().catch(() => null);
|
|
18373
|
+
const data = payload?.data || payload || {};
|
|
18374
|
+
if (!response.ok) {
|
|
18375
|
+
throw codedError(data?.errorCode || data?.code || data?.error || `hazbase-api-${response.status}`, response.status, data);
|
|
18376
|
+
}
|
|
18377
|
+
return data;
|
|
18378
|
+
} catch (error) {
|
|
18379
|
+
if (error?.name === "AbortError") throw codedError("hazbase-api-timeout", 504);
|
|
18380
|
+
throw error;
|
|
18381
|
+
} finally {
|
|
18382
|
+
clearTimeout(timer);
|
|
18383
|
+
}
|
|
18384
|
+
}
|
|
18385
|
+
|
|
18386
|
+
function codedError(code, statusCode = 500, details = null) {
|
|
18387
|
+
const error = new Error(code);
|
|
18388
|
+
error.code = code;
|
|
18389
|
+
error.statusCode = statusCode;
|
|
18390
|
+
error.details = details;
|
|
18391
|
+
return error;
|
|
18392
|
+
}
|
|
18393
|
+
|
|
15291
18394
|
function validateConfig(config) {
|
|
15292
18395
|
let publicBaseUrl = null;
|
|
15293
18396
|
try {
|
|
@@ -15443,6 +18546,23 @@ async function loadState(stateFile) {
|
|
|
15443
18546
|
try {
|
|
15444
18547
|
const raw = await fs.readFile(stateFile, "utf8");
|
|
15445
18548
|
const parsed = JSON.parse(raw);
|
|
18549
|
+
const hasExplicitWriteLaneState =
|
|
18550
|
+
typeof parsed.autoPilotWriteLaneContent === "boolean" ||
|
|
18551
|
+
typeof parsed.autoPilotWriteLaneUiTests === "boolean" ||
|
|
18552
|
+
typeof parsed.autoPilotWriteLaneSource === "boolean";
|
|
18553
|
+
const legacyTrustedWritesEnabled = parsed.autoPilotTrustedWrites === true || parsed.autoPilotTrustedWritesShadow === true;
|
|
18554
|
+
const autoPilotWriteLaneContent =
|
|
18555
|
+
typeof parsed.autoPilotWriteLaneContent === "boolean"
|
|
18556
|
+
? parsed.autoPilotWriteLaneContent === true
|
|
18557
|
+
: !hasExplicitWriteLaneState && legacyTrustedWritesEnabled;
|
|
18558
|
+
const autoPilotWriteLaneUiTests =
|
|
18559
|
+
typeof parsed.autoPilotWriteLaneUiTests === "boolean"
|
|
18560
|
+
? parsed.autoPilotWriteLaneUiTests === true
|
|
18561
|
+
: false;
|
|
18562
|
+
const autoPilotWriteLaneSource =
|
|
18563
|
+
typeof parsed.autoPilotWriteLaneSource === "boolean"
|
|
18564
|
+
? parsed.autoPilotWriteLaneSource === true
|
|
18565
|
+
: false;
|
|
15446
18566
|
return {
|
|
15447
18567
|
fileOffsets: parsed.fileOffsets ?? {},
|
|
15448
18568
|
seenEvents: parsed.seenEvents ?? {},
|
|
@@ -15468,7 +18588,14 @@ async function loadState(stateFile) {
|
|
|
15468
18588
|
pairingConsumedAt: Number(parsed.pairingConsumedAt) || 0,
|
|
15469
18589
|
pairingConsumedCredential: cleanText(parsed.pairingConsumedCredential ?? ""),
|
|
15470
18590
|
claudeAwayMode: parsed.claudeAwayMode === true,
|
|
18591
|
+
autoPilotTrustedReads: parsed.autoPilotTrustedReads === true,
|
|
18592
|
+
autoPilotTrustedWrites: autoPilotWriteLaneContent || autoPilotWriteLaneUiTests || autoPilotWriteLaneSource,
|
|
18593
|
+
autoPilotTrustedWritesShadow: false,
|
|
18594
|
+
autoPilotWriteLaneContent,
|
|
18595
|
+
autoPilotWriteLaneUiTests,
|
|
18596
|
+
autoPilotWriteLaneSource,
|
|
15471
18597
|
a2aAcceptPublicTasks: parsed.a2aAcceptPublicTasks === true,
|
|
18598
|
+
hazbase: normalizeHazbaseState(parsed.hazbase),
|
|
15472
18599
|
};
|
|
15473
18600
|
} catch {
|
|
15474
18601
|
return {
|
|
@@ -15496,6 +18623,13 @@ async function loadState(stateFile) {
|
|
|
15496
18623
|
pairingConsumedAt: 0,
|
|
15497
18624
|
pairingConsumedCredential: "",
|
|
15498
18625
|
claudeAwayMode: false,
|
|
18626
|
+
autoPilotTrustedReads: false,
|
|
18627
|
+
autoPilotTrustedWrites: false,
|
|
18628
|
+
autoPilotTrustedWritesShadow: false,
|
|
18629
|
+
autoPilotWriteLaneContent: false,
|
|
18630
|
+
autoPilotWriteLaneUiTests: false,
|
|
18631
|
+
autoPilotWriteLaneSource: false,
|
|
18632
|
+
hazbase: normalizeHazbaseState(null),
|
|
15499
18633
|
};
|
|
15500
18634
|
}
|
|
15501
18635
|
}
|
|
@@ -15520,7 +18654,11 @@ async function syncClaudeAwayModeSentinel(config, enabled) {
|
|
|
15520
18654
|
}
|
|
15521
18655
|
|
|
15522
18656
|
async function saveState(stateFile, state) {
|
|
15523
|
-
|
|
18657
|
+
// No indent. The state file is ~8 MB and humans don't read it; pretty-print
|
|
18658
|
+
// just costs a few extra ms of stringify and ~10% more bytes on disk. The
|
|
18659
|
+
// single newline at the end keeps diffs/hexdumps working on the off chance
|
|
18660
|
+
// someone cats the file.
|
|
18661
|
+
const output = JSON.stringify(state);
|
|
15524
18662
|
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
15525
18663
|
await fs.writeFile(stateFile, `${output}\n`, "utf8");
|
|
15526
18664
|
}
|
|
@@ -16128,6 +19266,7 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
16128
19266
|
) {
|
|
16129
19267
|
runtime.recentCodeEvents = nextCodeEvents;
|
|
16130
19268
|
state.recentCodeEvents = nextCodeEvents;
|
|
19269
|
+
invalidateDiffThreadGroupsCache();
|
|
16131
19270
|
changed = true;
|
|
16132
19271
|
}
|
|
16133
19272
|
|
|
@@ -16318,8 +19457,8 @@ function extractTitleOnlyJsonTitle(value) {
|
|
|
16318
19457
|
}
|
|
16319
19458
|
|
|
16320
19459
|
function preferTitleOnlyJsonThreadLabel(rawThreadLabel, conversationId, ...candidates) {
|
|
16321
|
-
|
|
16322
|
-
if (preferredThreadLabel) {
|
|
19460
|
+
const preferredThreadLabel = sanitizeResolvedThreadLabel(rawThreadLabel, conversationId);
|
|
19461
|
+
if (preferredThreadLabel && !isFallbackConversationLabel(preferredThreadLabel, conversationId)) {
|
|
16323
19462
|
return preferredThreadLabel;
|
|
16324
19463
|
}
|
|
16325
19464
|
for (const candidate of candidates) {
|
|
@@ -16327,8 +19466,12 @@ function preferTitleOnlyJsonThreadLabel(rawThreadLabel, conversationId, ...candi
|
|
|
16327
19466
|
if (titleOnlyJsonTitle) {
|
|
16328
19467
|
return titleOnlyJsonTitle;
|
|
16329
19468
|
}
|
|
19469
|
+
const derivedSdkCliLabel = deriveClaudeSdkCliThreadLabel(candidate, conversationId);
|
|
19470
|
+
if (derivedSdkCliLabel) {
|
|
19471
|
+
return derivedSdkCliLabel;
|
|
19472
|
+
}
|
|
16330
19473
|
}
|
|
16331
|
-
return cleanText(rawThreadLabel || "");
|
|
19474
|
+
return cleanText(preferredThreadLabel || rawThreadLabel || "");
|
|
16332
19475
|
}
|
|
16333
19476
|
|
|
16334
19477
|
function stripMarkdownLinks(value) {
|
|
@@ -16400,6 +19543,25 @@ function shortId(value) {
|
|
|
16400
19543
|
return text.length > 8 ? text.slice(0, 8) : text;
|
|
16401
19544
|
}
|
|
16402
19545
|
|
|
19546
|
+
function isFallbackTimelineTitle(rawTitle, kind, threadId) {
|
|
19547
|
+
const normalizedTitle = cleanText(rawTitle || "");
|
|
19548
|
+
const normalizedKind = cleanText(kind || "");
|
|
19549
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
19550
|
+
if (!normalizedTitle) {
|
|
19551
|
+
return true;
|
|
19552
|
+
}
|
|
19553
|
+
if (normalizedThreadId && normalizedTitle === shortId(normalizedThreadId)) {
|
|
19554
|
+
return true;
|
|
19555
|
+
}
|
|
19556
|
+
if (normalizedTitle === kindTitle(DEFAULT_LOCALE, normalizedKind)) {
|
|
19557
|
+
return true;
|
|
19558
|
+
}
|
|
19559
|
+
if (normalizedThreadId && normalizedTitle === formatTitle(kindTitle(DEFAULT_LOCALE, normalizedKind), shortId(normalizedThreadId))) {
|
|
19560
|
+
return true;
|
|
19561
|
+
}
|
|
19562
|
+
return false;
|
|
19563
|
+
}
|
|
19564
|
+
|
|
16403
19565
|
function sleep(ms) {
|
|
16404
19566
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16405
19567
|
}
|
|
@@ -16554,6 +19716,7 @@ async function main() {
|
|
|
16554
19716
|
migratedRecentCodeEventsStateChanged ||
|
|
16555
19717
|
restoredPendingUserInputStateChanged ||
|
|
16556
19718
|
backfilledMoltbookInboxChanged ||
|
|
19719
|
+
recoveredMissingProviderStateChanged ||
|
|
16557
19720
|
refreshResolvedThreadLabels({ config, runtime, state })
|
|
16558
19721
|
) {
|
|
16559
19722
|
await saveState(config.stateFile, state);
|
|
@@ -16660,7 +19823,16 @@ async function main() {
|
|
|
16660
19823
|
try {
|
|
16661
19824
|
const dirty = await scanOnce({ config, runtime, state });
|
|
16662
19825
|
if (dirty) {
|
|
16663
|
-
|
|
19826
|
+
// Fire-and-forget via the debouncer rather than awaiting — the
|
|
19827
|
+
// ~8 MB state file used to block the scan loop for ~50-100 ms on
|
|
19828
|
+
// every dirty iteration, which directly delays the next rollout /
|
|
19829
|
+
// history.jsonl read and therefore how quickly new user_message
|
|
19830
|
+
// entries reach the phone. runtime/state.recentTimelineEntries are
|
|
19831
|
+
// already updated synchronously inside recordTimelineEntry, so the
|
|
19832
|
+
// /api/timeline response sees fresh data the moment the client
|
|
19833
|
+
// next polls, regardless of when the disk write lands. Pending
|
|
19834
|
+
// writes are drained in the `finally` block below on SIGINT/SIGTERM.
|
|
19835
|
+
scheduleSaveState(config, state);
|
|
16664
19836
|
}
|
|
16665
19837
|
} catch (error) {
|
|
16666
19838
|
console.error(`[scan-error] ${error.message}`);
|
|
@@ -16737,5 +19909,10 @@ async function main() {
|
|
|
16737
19909
|
if (approvalServer) {
|
|
16738
19910
|
await stopHttpServer(approvalServer);
|
|
16739
19911
|
}
|
|
19912
|
+
|
|
19913
|
+
// Drain any state mutations that were queued via scheduleSaveState but
|
|
19914
|
+
// not yet written to disk — otherwise SIGINT/SIGTERM mid-debounce would
|
|
19915
|
+
// silently drop the last ~500 ms of settings/notification changes.
|
|
19916
|
+
await flushPendingStateWrite(config, state);
|
|
16740
19917
|
}
|
|
16741
19918
|
}
|