viveworker 0.7.0-beta.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/viveworker-bridge.mjs +363 -111
- package/web/app.css +97 -4
- package/web/app.js +80 -46
- package/web/i18n.js +20 -8
- package/web/index.html +84 -0
- package/web/sw.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.7.0
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Open mobile control surface for Codex, Claude, Thread Sharing, File Share, Moltbook, and A2A tasks on your trusted LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -680,7 +680,7 @@ function withNotificationIcon(kind, title) {
|
|
|
680
680
|
|
|
681
681
|
function normalizeTimelineOutcome(value) {
|
|
682
682
|
const normalized = cleanText(value || "").toLowerCase();
|
|
683
|
-
return ["pending", "approved", "rejected", "implemented", "dismissed", "submitted"].includes(normalized)
|
|
683
|
+
return ["pending", "approved", "rejected", "failed", "implemented", "dismissed", "submitted"].includes(normalized)
|
|
684
684
|
? normalized
|
|
685
685
|
: "";
|
|
686
686
|
}
|
|
@@ -2993,6 +2993,70 @@ function normalizeTimelineEntries(rawItems, maxItems) {
|
|
|
2993
2993
|
return deduped;
|
|
2994
2994
|
}
|
|
2995
2995
|
|
|
2996
|
+
// Cheap structural diff used by record* helpers to decide whether a rebuilt
|
|
2997
|
+
// timeline / history / code-events array actually changed in a way worth
|
|
2998
|
+
// persisting. The previous implementation `JSON.stringify(a) !== JSON.stringify(b)`
|
|
2999
|
+
// allocated several megabytes of intermediate strings on every call when the
|
|
3000
|
+
// projection included `diffText` (5+ KB per file_event × ~250 entries × 2
|
|
3001
|
+
// sides), which dominated scavenger GC time in busy sessions and showed up as
|
|
3002
|
+
// a 25% CPU sink in `JsonStringifier::Serialize_*` under V8 sampling. Walking
|
|
3003
|
+
// the arrays in lockstep and bailing on the first mismatched primitive lets
|
|
3004
|
+
// the common "fresh entry prepended" case exit on the very first stableId
|
|
3005
|
+
// compare with zero allocations.
|
|
3006
|
+
//
|
|
3007
|
+
// Field semantics:
|
|
3008
|
+
// - String / number / boolean fields use value-equality via `===` (works
|
|
3009
|
+
// across object reconstruction by `normalizeTimelineEntry`).
|
|
3010
|
+
// - `Array` fields (e.g. `previousFileRefs`) shallow-compare lengths and the
|
|
3011
|
+
// `path` of each element rather than serializing.
|
|
3012
|
+
// - `null` / `undefined` are treated as equal to each other but distinct from
|
|
3013
|
+
// any defined value.
|
|
3014
|
+
function timelineProjectionChanged(nextItems, previousItems, fieldKeys) {
|
|
3015
|
+
const a = Array.isArray(nextItems) ? nextItems : [];
|
|
3016
|
+
const b = Array.isArray(previousItems) ? previousItems : [];
|
|
3017
|
+
if (a.length !== b.length) {
|
|
3018
|
+
return true;
|
|
3019
|
+
}
|
|
3020
|
+
for (let i = 0; i < a.length; i++) {
|
|
3021
|
+
const ai = a[i];
|
|
3022
|
+
const bi = b[i];
|
|
3023
|
+
if (ai === bi) continue;
|
|
3024
|
+
if (!ai || !bi) return true;
|
|
3025
|
+
for (let k = 0; k < fieldKeys.length; k++) {
|
|
3026
|
+
const key = fieldKeys[k];
|
|
3027
|
+
const av = ai[key];
|
|
3028
|
+
const bv = bi[key];
|
|
3029
|
+
if (av === bv) continue;
|
|
3030
|
+
if (av == null && bv == null) continue;
|
|
3031
|
+
if (Array.isArray(av) && Array.isArray(bv)) {
|
|
3032
|
+
if (av.length !== bv.length) return true;
|
|
3033
|
+
let arrayDiffers = false;
|
|
3034
|
+
for (let j = 0; j < av.length; j++) {
|
|
3035
|
+
const aj = av[j];
|
|
3036
|
+
const bj = bv[j];
|
|
3037
|
+
if (aj === bj) continue;
|
|
3038
|
+
if (!aj || !bj) {
|
|
3039
|
+
arrayDiffers = true;
|
|
3040
|
+
break;
|
|
3041
|
+
}
|
|
3042
|
+
// Most array-of-object fields here are fileRef-shaped: identity is
|
|
3043
|
+
// captured by `path`. Falling back to JSON for unrecognised shapes
|
|
3044
|
+
// is unnecessary — a path mismatch (or a null path either side) is
|
|
3045
|
+
// sufficient to mark the projection changed.
|
|
3046
|
+
if (aj.path !== bj.path) {
|
|
3047
|
+
arrayDiffers = true;
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
if (arrayDiffers) return true;
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return false;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2996
3060
|
function isCodeEventEntry(raw) {
|
|
2997
3061
|
if (!isPlainObject(raw)) {
|
|
2998
3062
|
return false;
|
|
@@ -3158,35 +3222,21 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
|
|
|
3158
3222
|
[normalized, ...runtime.recentTimelineEntries.filter((item) => item.stableId !== normalized.stableId)],
|
|
3159
3223
|
config.maxTimelineEntries
|
|
3160
3224
|
);
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
JSON.stringify(
|
|
3177
|
-
runtime.recentTimelineEntries.map((item) => [
|
|
3178
|
-
item.stableId,
|
|
3179
|
-
item.title,
|
|
3180
|
-
item.createdAtMs,
|
|
3181
|
-
item.diffAvailable,
|
|
3182
|
-
item.diffSource,
|
|
3183
|
-
item.diffAddedLines,
|
|
3184
|
-
item.diffRemovedLines,
|
|
3185
|
-
item.diffText,
|
|
3186
|
-
item.previousFileRefs,
|
|
3187
|
-
item.cwd,
|
|
3188
|
-
])
|
|
3189
|
-
);
|
|
3225
|
+
// Note: diffText is intentionally omitted from the projection — the
|
|
3226
|
+
// line-count fields (`diffAddedLines` / `diffRemovedLines`) are a sufficient
|
|
3227
|
+
// proxy for "diff content changed" and avoid materialising a megabyte-class
|
|
3228
|
+
// intermediate string on every record call.
|
|
3229
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentTimelineEntries, [
|
|
3230
|
+
"stableId",
|
|
3231
|
+
"title",
|
|
3232
|
+
"createdAtMs",
|
|
3233
|
+
"diffAvailable",
|
|
3234
|
+
"diffSource",
|
|
3235
|
+
"diffAddedLines",
|
|
3236
|
+
"diffRemovedLines",
|
|
3237
|
+
"previousFileRefs",
|
|
3238
|
+
"cwd",
|
|
3239
|
+
]);
|
|
3190
3240
|
runtime.recentTimelineEntries = nextItems;
|
|
3191
3241
|
state.recentTimelineEntries = nextItems;
|
|
3192
3242
|
return changed;
|
|
@@ -3205,35 +3255,19 @@ function recordCodeEvent({ config, runtime, state, entry }) {
|
|
|
3205
3255
|
[normalized, ...runtime.recentCodeEvents.filter((item) => item.stableId !== normalized.stableId)],
|
|
3206
3256
|
config.maxCodeEvents
|
|
3207
3257
|
);
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
])
|
|
3222
|
-
) !==
|
|
3223
|
-
JSON.stringify(
|
|
3224
|
-
runtime.recentCodeEvents.map((item) => [
|
|
3225
|
-
item.stableId,
|
|
3226
|
-
item.title,
|
|
3227
|
-
item.createdAtMs,
|
|
3228
|
-
item.diffAvailable,
|
|
3229
|
-
item.diffSource,
|
|
3230
|
-
item.diffAddedLines,
|
|
3231
|
-
item.diffRemovedLines,
|
|
3232
|
-
item.diffText,
|
|
3233
|
-
item.previousFileRefs,
|
|
3234
|
-
item.cwd,
|
|
3235
|
-
])
|
|
3236
|
-
);
|
|
3258
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText` from
|
|
3259
|
+
// the projection — same megabyte-stringify hot path; same line-count proxy.
|
|
3260
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
3261
|
+
"stableId",
|
|
3262
|
+
"title",
|
|
3263
|
+
"createdAtMs",
|
|
3264
|
+
"diffAvailable",
|
|
3265
|
+
"diffSource",
|
|
3266
|
+
"diffAddedLines",
|
|
3267
|
+
"diffRemovedLines",
|
|
3268
|
+
"previousFileRefs",
|
|
3269
|
+
"cwd",
|
|
3270
|
+
]);
|
|
3237
3271
|
runtime.recentCodeEvents = nextItems;
|
|
3238
3272
|
state.recentCodeEvents = nextItems;
|
|
3239
3273
|
if (changed) {
|
|
@@ -3257,37 +3291,19 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
|
|
|
3257
3291
|
],
|
|
3258
3292
|
config.maxCodeEvents
|
|
3259
3293
|
);
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
item.cwd,
|
|
3274
|
-
])
|
|
3275
|
-
) !==
|
|
3276
|
-
JSON.stringify(
|
|
3277
|
-
runtime.recentCodeEvents.map((item) => [
|
|
3278
|
-
item.stableId,
|
|
3279
|
-
item.title,
|
|
3280
|
-
item.createdAtMs,
|
|
3281
|
-
item.threadLabel,
|
|
3282
|
-
item.diffAvailable,
|
|
3283
|
-
item.diffSource,
|
|
3284
|
-
item.diffAddedLines,
|
|
3285
|
-
item.diffRemovedLines,
|
|
3286
|
-
item.diffText,
|
|
3287
|
-
item.previousFileRefs,
|
|
3288
|
-
item.cwd,
|
|
3289
|
-
])
|
|
3290
|
-
);
|
|
3294
|
+
// See `recordTimelineEntry` for the rationale on dropping `diffText`.
|
|
3295
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentCodeEvents, [
|
|
3296
|
+
"stableId",
|
|
3297
|
+
"title",
|
|
3298
|
+
"createdAtMs",
|
|
3299
|
+
"threadLabel",
|
|
3300
|
+
"diffAvailable",
|
|
3301
|
+
"diffSource",
|
|
3302
|
+
"diffAddedLines",
|
|
3303
|
+
"diffRemovedLines",
|
|
3304
|
+
"previousFileRefs",
|
|
3305
|
+
"cwd",
|
|
3306
|
+
]);
|
|
3291
3307
|
runtime.recentCodeEvents = nextItems;
|
|
3292
3308
|
state.recentCodeEvents = nextItems;
|
|
3293
3309
|
if (changed) {
|
|
@@ -3353,9 +3369,11 @@ function recordHistoryItem({ config, runtime, state, item }) {
|
|
|
3353
3369
|
[normalized, ...runtime.recentHistoryItems.filter((entry) => entry.stableId !== normalized.stableId)],
|
|
3354
3370
|
config.maxHistoryItems
|
|
3355
3371
|
);
|
|
3356
|
-
const changed =
|
|
3357
|
-
|
|
3358
|
-
|
|
3372
|
+
const changed = timelineProjectionChanged(nextItems, runtime.recentHistoryItems, [
|
|
3373
|
+
"stableId",
|
|
3374
|
+
"title",
|
|
3375
|
+
"createdAtMs",
|
|
3376
|
+
]);
|
|
3359
3377
|
runtime.recentHistoryItems = nextItems;
|
|
3360
3378
|
state.recentHistoryItems = nextItems;
|
|
3361
3379
|
return changed;
|
|
@@ -3784,7 +3802,7 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
3784
3802
|
if (config.webUiEnabled) {
|
|
3785
3803
|
let claudeTranscriptChanged = false;
|
|
3786
3804
|
if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
|
|
3787
|
-
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
|
|
3805
|
+
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir, config.claudeTranscriptMaxAgeMs);
|
|
3788
3806
|
runtime.lastClaudeScanAt = now;
|
|
3789
3807
|
}
|
|
3790
3808
|
let claudeSessionTitlesChanged = false;
|
|
@@ -4122,7 +4140,16 @@ async function refreshClaudeSessionTitles(runtime) {
|
|
|
4122
4140
|
return changed;
|
|
4123
4141
|
}
|
|
4124
4142
|
|
|
4125
|
-
async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
4143
|
+
async function listClaudeTranscriptFiles(claudeProjectsDir, maxAgeMs = 0) {
|
|
4144
|
+
// ~/.claude/projects can accumulate thousands of .jsonl files (one per
|
|
4145
|
+
// session, never garbage-collected by Claude Code itself). Returning all of
|
|
4146
|
+
// them means processClaudeTranscriptFile fs.stat()s each every poll
|
|
4147
|
+
// iteration — for a long-time user that's 3000+ stats every 2.5 s, all but
|
|
4148
|
+
// a handful of which are guaranteed-cold. Filter by mtime here so the
|
|
4149
|
+
// per-iteration working set tracks "active sessions", not "total sessions
|
|
4150
|
+
// ever". maxAgeMs=0 disables filtering for callers who explicitly want
|
|
4151
|
+
// everything (none in the bridge today).
|
|
4152
|
+
const cutoffMs = maxAgeMs > 0 ? Date.now() - maxAgeMs : 0;
|
|
4126
4153
|
try {
|
|
4127
4154
|
const result = [];
|
|
4128
4155
|
const entries = await fs.readdir(claudeProjectsDir, { withFileTypes: true });
|
|
@@ -4133,7 +4160,17 @@ async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
|
4133
4160
|
const files = await fs.readdir(projectPath, { withFileTypes: true });
|
|
4134
4161
|
for (const file of files) {
|
|
4135
4162
|
if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
|
|
4136
|
-
|
|
4163
|
+
const fullPath = path.join(projectPath, file.name);
|
|
4164
|
+
if (cutoffMs > 0) {
|
|
4165
|
+
try {
|
|
4166
|
+
const stat = await fs.stat(fullPath);
|
|
4167
|
+
if (stat.mtimeMs < cutoffMs) continue;
|
|
4168
|
+
} catch {
|
|
4169
|
+
// skip unreadable file (vanished or permissioned out)
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
result.push(fullPath);
|
|
4137
4174
|
}
|
|
4138
4175
|
} catch {
|
|
4139
4176
|
// skip unreadable project dirs
|
|
@@ -11294,6 +11331,7 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
|
11294
11331
|
: approvalKind === "hazbase_wallet_payment"
|
|
11295
11332
|
? [
|
|
11296
11333
|
{ label: t(locale, "server.action.payWithWallet"), tone: "primary", url: `/api/payments/x402/hazbase-wallet/${encodeURIComponent(approval.token)}/pay`, body: { hazbaseReauth: true } },
|
|
11334
|
+
{ label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
|
|
11297
11335
|
]
|
|
11298
11336
|
: [
|
|
11299
11337
|
{ label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
|
|
@@ -13480,9 +13518,93 @@ function buildWebAppHtml({ pairToken }) {
|
|
|
13480
13518
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
13481
13519
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
13482
13520
|
<link rel="stylesheet" href="/app.css">
|
|
13521
|
+
<style>
|
|
13522
|
+
.boot-splash {
|
|
13523
|
+
position: fixed;
|
|
13524
|
+
inset: 0;
|
|
13525
|
+
z-index: 9999;
|
|
13526
|
+
display: grid;
|
|
13527
|
+
place-items: center;
|
|
13528
|
+
padding: max(1.2rem, env(safe-area-inset-top)) 1rem max(1.2rem, env(safe-area-inset-bottom));
|
|
13529
|
+
color: #f5fbff;
|
|
13530
|
+
background:
|
|
13531
|
+
radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%),
|
|
13532
|
+
radial-gradient(circle at 78% 78%, rgba(79, 131, 216, 0.14), transparent 28%),
|
|
13533
|
+
linear-gradient(180deg, #081015 0%, #091015 100%);
|
|
13534
|
+
transition: opacity 220ms ease, visibility 220ms ease;
|
|
13535
|
+
}
|
|
13536
|
+
.boot-splash__card {
|
|
13537
|
+
width: min(20rem, 82vw);
|
|
13538
|
+
display: grid;
|
|
13539
|
+
justify-items: center;
|
|
13540
|
+
gap: 0.9rem;
|
|
13541
|
+
text-align: center;
|
|
13542
|
+
}
|
|
13543
|
+
.boot-splash__logo {
|
|
13544
|
+
width: clamp(5.4rem, 28vw, 7rem);
|
|
13545
|
+
height: clamp(5.4rem, 28vw, 7rem);
|
|
13546
|
+
border-radius: 28%;
|
|
13547
|
+
background:
|
|
13548
|
+
radial-gradient(circle at 76% 24%, rgba(125, 211, 252, 0.22), transparent 30%),
|
|
13549
|
+
linear-gradient(180deg, rgba(23, 52, 72, 0.96), rgba(9, 17, 23, 0.96));
|
|
13550
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
|
|
13551
|
+
}
|
|
13552
|
+
.boot-splash__title {
|
|
13553
|
+
margin: 0.25rem 0 0;
|
|
13554
|
+
font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
13555
|
+
font-size: clamp(1.65rem, 8vw, 2.4rem);
|
|
13556
|
+
line-height: 1;
|
|
13557
|
+
letter-spacing: -0.04em;
|
|
13558
|
+
}
|
|
13559
|
+
.boot-splash__status {
|
|
13560
|
+
margin: 0;
|
|
13561
|
+
color: rgba(205, 220, 231, 0.72);
|
|
13562
|
+
font-family: "Avenir Next", "SF Pro Rounded", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
13563
|
+
font-size: 0.9rem;
|
|
13564
|
+
letter-spacing: 0.02em;
|
|
13565
|
+
}
|
|
13566
|
+
.boot-splash__dots {
|
|
13567
|
+
display: inline-grid;
|
|
13568
|
+
grid-auto-flow: column;
|
|
13569
|
+
gap: 0.34rem;
|
|
13570
|
+
margin-top: 0.15rem;
|
|
13571
|
+
}
|
|
13572
|
+
.boot-splash__dots span {
|
|
13573
|
+
width: 0.42rem;
|
|
13574
|
+
height: 0.42rem;
|
|
13575
|
+
border-radius: 999px;
|
|
13576
|
+
background: rgba(142, 215, 255, 0.88);
|
|
13577
|
+
animation: viveworker-boot-dot 980ms ease-in-out infinite;
|
|
13578
|
+
}
|
|
13579
|
+
.boot-splash__dots span:nth-child(2) { animation-delay: 140ms; }
|
|
13580
|
+
.boot-splash__dots span:nth-child(3) { animation-delay: 280ms; }
|
|
13581
|
+
.viveworker-ready .boot-splash {
|
|
13582
|
+
opacity: 0;
|
|
13583
|
+
visibility: hidden;
|
|
13584
|
+
}
|
|
13585
|
+
@keyframes viveworker-boot-dot {
|
|
13586
|
+
0%, 80%, 100% { transform: translateY(0); opacity: 0.42; }
|
|
13587
|
+
40% { transform: translateY(-0.28rem); opacity: 1; }
|
|
13588
|
+
}
|
|
13589
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13590
|
+
.boot-splash,
|
|
13591
|
+
.boot-splash__dots span {
|
|
13592
|
+
transition: none;
|
|
13593
|
+
animation: none;
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
</style>
|
|
13483
13597
|
<title>viveworker</title>
|
|
13484
13598
|
</head>
|
|
13485
13599
|
<body>
|
|
13600
|
+
<div id="boot-splash" class="boot-splash" role="status" aria-live="polite" aria-label="viveworker is starting">
|
|
13601
|
+
<div class="boot-splash__card">
|
|
13602
|
+
<img class="boot-splash__logo" src="/icons/viveworker-v-pulse.svg" alt="" width="112" height="112" decoding="async">
|
|
13603
|
+
<h1 class="boot-splash__title">viveworker</h1>
|
|
13604
|
+
<p class="boot-splash__status">Starting</p>
|
|
13605
|
+
<span class="boot-splash__dots" aria-hidden="true"><span></span><span></span><span></span></span>
|
|
13606
|
+
</div>
|
|
13607
|
+
</div>
|
|
13486
13608
|
<div id="app"></div>
|
|
13487
13609
|
<script type="module" src="/app.js"></script>
|
|
13488
13610
|
</body>
|
|
@@ -15028,9 +15150,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15028
15150
|
const result = await completeHazbaseWalletPaymentApproval({ config, runtime, state, approval });
|
|
15029
15151
|
return writeJson(res, 200, result);
|
|
15030
15152
|
} catch (error) {
|
|
15031
|
-
|
|
15153
|
+
const failure = await finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error });
|
|
15032
15154
|
console.error(`[hazbase-wallet-payment-error] ${approval.requestKey} | ${error?.stack || error?.message || error}`);
|
|
15033
|
-
return writeJson(res, Number(error?.statusCode) || 500, {
|
|
15155
|
+
return writeJson(res, Number(error?.statusCode) || 500, {
|
|
15156
|
+
error: failure.error,
|
|
15157
|
+
approvalFinalized: true,
|
|
15158
|
+
retryable: true,
|
|
15159
|
+
});
|
|
15034
15160
|
}
|
|
15035
15161
|
}
|
|
15036
15162
|
|
|
@@ -15922,7 +16048,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15922
16048
|
if (approval.resolved || approval.resolving) {
|
|
15923
16049
|
return writeJson(res, 409, { error: "approval-already-handled" });
|
|
15924
16050
|
}
|
|
15925
|
-
if (approval.kind === "hazbase_wallet_payment") {
|
|
16051
|
+
if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
|
|
15926
16052
|
console.warn(`[hazbase-wallet-payment-generic-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
15927
16053
|
return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
|
|
15928
16054
|
}
|
|
@@ -15936,6 +16062,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
15936
16062
|
return writeJson(res, 200, { ok: true, decision });
|
|
15937
16063
|
} catch (error) {
|
|
15938
16064
|
approval.resolving = false;
|
|
16065
|
+
if (res.headersSent || res.writableEnded) {
|
|
16066
|
+
console.error(`[approval-decision-post-write-error] ${approval.requestKey} | ${error.message}`);
|
|
16067
|
+
return;
|
|
16068
|
+
}
|
|
15939
16069
|
return writeJson(res, 500, { error: error.message });
|
|
15940
16070
|
}
|
|
15941
16071
|
}
|
|
@@ -16520,7 +16650,7 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16520
16650
|
if (approval.resolved || approval.resolving) {
|
|
16521
16651
|
return writeApprovalHandled(res, req, approval.title, 409, config.defaultLocale);
|
|
16522
16652
|
}
|
|
16523
|
-
if (approval.kind === "hazbase_wallet_payment") {
|
|
16653
|
+
if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
|
|
16524
16654
|
console.warn(`[hazbase-wallet-payment-native-decision-blocked] ${approval.requestKey} | ${decision}`);
|
|
16525
16655
|
if (requestWantsHtml(req)) {
|
|
16526
16656
|
return writeHtml(
|
|
@@ -16559,6 +16689,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16559
16689
|
});
|
|
16560
16690
|
} catch (error) {
|
|
16561
16691
|
approval.resolving = false;
|
|
16692
|
+
if (res.headersSent || res.writableEnded) {
|
|
16693
|
+
console.error(`[native-approval-decision-post-write-error] ${approval.requestKey} | ${error.message}`);
|
|
16694
|
+
return;
|
|
16695
|
+
}
|
|
16562
16696
|
if (requestWantsHtml(req)) {
|
|
16563
16697
|
return writeHtml(
|
|
16564
16698
|
res,
|
|
@@ -16573,6 +16707,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
16573
16707
|
return writeJson(res, 500, { error: error.message });
|
|
16574
16708
|
}
|
|
16575
16709
|
} catch (error) {
|
|
16710
|
+
if (res.headersSent || res.writableEnded) {
|
|
16711
|
+
return;
|
|
16712
|
+
}
|
|
16576
16713
|
if (requestWantsHtml(req)) {
|
|
16577
16714
|
return writeHtml(
|
|
16578
16715
|
res,
|
|
@@ -17969,6 +18106,12 @@ function buildConfig(cli) {
|
|
|
17969
18106
|
maxCodeEvents: numberEnv("MAX_CODE_EVENTS", 1000),
|
|
17970
18107
|
maxTimelineThreads: numberEnv("MAX_TIMELINE_THREADS", 20),
|
|
17971
18108
|
maxReadBytes: numberEnv("MAX_READ_BYTES", 2 * 1024 * 1024),
|
|
18109
|
+
// Cap how far back the bridge looks for Claude transcripts. Default 7 days
|
|
18110
|
+
// covers a long pause without dragging the long tail of dead sessions
|
|
18111
|
+
// (which can easily reach thousands of files) into the per-iteration
|
|
18112
|
+
// stat loop. Set to 0 to disable filtering and scan everything (the
|
|
18113
|
+
// legacy behavior).
|
|
18114
|
+
claudeTranscriptMaxAgeMs: numberEnv("CLAUDE_TRANSCRIPT_MAX_AGE_DAYS", 7) * 24 * 60 * 60 * 1000,
|
|
17972
18115
|
maxMessageChars: numberEnv("MAX_MESSAGE_CHARS", 320),
|
|
17973
18116
|
maxCommandChars: numberEnv("MAX_COMMAND_CHARS", 220),
|
|
17974
18117
|
maxJustificationChars: numberEnv("MAX_JUSTIFICATION_CHARS", 220),
|
|
@@ -18012,18 +18155,31 @@ function buildConfig(cli) {
|
|
|
18012
18155
|
}
|
|
18013
18156
|
|
|
18014
18157
|
|
|
18158
|
+
function inferHazbaseDeviceBindingIdFromAccounts(accounts) {
|
|
18159
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
18160
|
+
for (const entry of list) {
|
|
18161
|
+
const deviceBindingId = cleanText(
|
|
18162
|
+
entry?.primaryDeviceBindingId || entry?.deviceBindingId || entry?.ownerDeviceBindingId || ""
|
|
18163
|
+
);
|
|
18164
|
+
if (deviceBindingId) return deviceBindingId;
|
|
18165
|
+
}
|
|
18166
|
+
return "";
|
|
18167
|
+
}
|
|
18168
|
+
|
|
18015
18169
|
function normalizeHazbaseState(raw) {
|
|
18016
18170
|
const value = raw && typeof raw === "object" ? raw : {};
|
|
18171
|
+
const accounts = Array.isArray(value.accounts) ? value.accounts : [];
|
|
18172
|
+
const deviceBindingId = cleanText(value.deviceBindingId ?? "") || inferHazbaseDeviceBindingIdFromAccounts(accounts);
|
|
18017
18173
|
return {
|
|
18018
18174
|
email: cleanText(value.email ?? ""),
|
|
18019
18175
|
accessToken: cleanText(value.accessToken ?? ""),
|
|
18020
18176
|
sessionId: cleanText(value.sessionId ?? ""),
|
|
18021
18177
|
userId: cleanText(value.userId ?? ""),
|
|
18022
|
-
deviceBindingId
|
|
18178
|
+
deviceBindingId,
|
|
18023
18179
|
credentialId: cleanText(value.credentialId ?? ""),
|
|
18024
18180
|
highTrustToken: cleanText(value.highTrustToken ?? ""),
|
|
18025
18181
|
highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
|
|
18026
|
-
accounts
|
|
18182
|
+
accounts,
|
|
18027
18183
|
sessionInvalid: value.sessionInvalid === true,
|
|
18028
18184
|
sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
|
|
18029
18185
|
sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
|
|
@@ -18032,19 +18188,30 @@ function normalizeHazbaseState(raw) {
|
|
|
18032
18188
|
|
|
18033
18189
|
function isHazbaseInvalidAppSessionError(error) {
|
|
18034
18190
|
const details = error?.details && typeof error.details === "object" ? error.details : {};
|
|
18035
|
-
const
|
|
18036
|
-
const text = [
|
|
18191
|
+
const rawText = [
|
|
18037
18192
|
error?.code,
|
|
18038
18193
|
error?.message,
|
|
18039
18194
|
details.errorCode,
|
|
18040
18195
|
details.code,
|
|
18041
18196
|
details.error,
|
|
18042
18197
|
details.message,
|
|
18198
|
+
typeof error?.details === "string" ? error.details : "",
|
|
18199
|
+
]
|
|
18200
|
+
.map((entry) => cleanText(entry || ""))
|
|
18201
|
+
.filter(Boolean)
|
|
18202
|
+
.join(" ");
|
|
18203
|
+
const statusCode = Number(error?.statusCode || error?.status || details.statusCode || 0) ||
|
|
18204
|
+
(/statusCode["']?\s*[:=]\s*401/u.test(rawText) || /\b401\b/u.test(rawText) ? 401 : 0);
|
|
18205
|
+
const text = [
|
|
18206
|
+
rawText,
|
|
18043
18207
|
]
|
|
18044
18208
|
.map((entry) => cleanText(entry || "").toLowerCase())
|
|
18045
18209
|
.filter(Boolean)
|
|
18046
18210
|
.join(" ");
|
|
18047
|
-
return statusCode === 401 &&
|
|
18211
|
+
return statusCode === 401 && (
|
|
18212
|
+
text.includes("invalid app session") ||
|
|
18213
|
+
text.includes("invalid_app_session")
|
|
18214
|
+
);
|
|
18048
18215
|
}
|
|
18049
18216
|
|
|
18050
18217
|
function markHazbaseSessionInvalid(state, reason = "invalid_app_session") {
|
|
@@ -18274,6 +18441,51 @@ function resolveHazbaseAccountForChain(accounts, chainId) {
|
|
|
18274
18441
|
}
|
|
18275
18442
|
|
|
18276
18443
|
|
|
18444
|
+
async function finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error }) {
|
|
18445
|
+
const errorCode = cleanText(error?.code || error?.message || "hazbase-wallet-payment-failed") || "hazbase-wallet-payment-failed";
|
|
18446
|
+
if (approval.resolved) {
|
|
18447
|
+
return { paid: false, decision: "failed", error: errorCode };
|
|
18448
|
+
}
|
|
18449
|
+
|
|
18450
|
+
const payload = {
|
|
18451
|
+
paid: false,
|
|
18452
|
+
decision: "failed",
|
|
18453
|
+
error: errorCode,
|
|
18454
|
+
paymentRequestId: approval.paymentRequestId || approval.requestId || "",
|
|
18455
|
+
};
|
|
18456
|
+
|
|
18457
|
+
approval.resolved = true;
|
|
18458
|
+
approval.resolving = false;
|
|
18459
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
18460
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
18461
|
+
approval.resolveClaudeWaiter?.(payload);
|
|
18462
|
+
|
|
18463
|
+
const statusMessage = t(config.defaultLocale, "server.message.paymentFailed", { reason: errorCode });
|
|
18464
|
+
const stateChanged = recordActionHistoryItem({
|
|
18465
|
+
config,
|
|
18466
|
+
runtime,
|
|
18467
|
+
state,
|
|
18468
|
+
kind: "approval",
|
|
18469
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
18470
|
+
token: approval.token,
|
|
18471
|
+
title: approval.title,
|
|
18472
|
+
threadLabel: approval.threadLabel || "",
|
|
18473
|
+
messageText: `${statusMessage}\n\n${approval.messageText}`,
|
|
18474
|
+
summary: statusMessage,
|
|
18475
|
+
fileRefs: [],
|
|
18476
|
+
diffText: "",
|
|
18477
|
+
diffSource: "",
|
|
18478
|
+
diffAvailable: false,
|
|
18479
|
+
diffAddedLines: 0,
|
|
18480
|
+
diffRemovedLines: 0,
|
|
18481
|
+
outcome: "failed",
|
|
18482
|
+
provider: approval.provider || "viveworker",
|
|
18483
|
+
});
|
|
18484
|
+
if (stateChanged) await saveState(config.stateFile, state);
|
|
18485
|
+
console.log(`[hazbase-wallet-payment-failed] ${approval.requestKey} | ${errorCode}`);
|
|
18486
|
+
return payload;
|
|
18487
|
+
}
|
|
18488
|
+
|
|
18277
18489
|
async function completeHazbaseWalletPaymentApproval({ config, runtime, state, approval }) {
|
|
18278
18490
|
const hazbase = normalizeHazbaseState(state.hazbase);
|
|
18279
18491
|
if (!hazbase.accessToken) throw codedError("hazbase-auth-required", 401);
|
|
@@ -19183,8 +19395,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
19183
19395
|
);
|
|
19184
19396
|
|
|
19185
19397
|
if (
|
|
19186
|
-
|
|
19187
|
-
|
|
19398
|
+
timelineProjectionChanged(nextHistoryItems, runtime.recentHistoryItems, [
|
|
19399
|
+
"stableId",
|
|
19400
|
+
"title",
|
|
19401
|
+
"threadLabel",
|
|
19402
|
+
])
|
|
19188
19403
|
) {
|
|
19189
19404
|
runtime.recentHistoryItems = nextHistoryItems;
|
|
19190
19405
|
state.recentHistoryItems = nextHistoryItems;
|
|
@@ -19225,8 +19440,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
19225
19440
|
);
|
|
19226
19441
|
|
|
19227
19442
|
if (
|
|
19228
|
-
|
|
19229
|
-
|
|
19443
|
+
timelineProjectionChanged(nextTimelineEntries, runtime.recentTimelineEntries, [
|
|
19444
|
+
"stableId",
|
|
19445
|
+
"title",
|
|
19446
|
+
"threadLabel",
|
|
19447
|
+
])
|
|
19230
19448
|
) {
|
|
19231
19449
|
runtime.recentTimelineEntries = nextTimelineEntries;
|
|
19232
19450
|
state.recentTimelineEntries = nextTimelineEntries;
|
|
@@ -19261,8 +19479,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
19261
19479
|
);
|
|
19262
19480
|
|
|
19263
19481
|
if (
|
|
19264
|
-
|
|
19265
|
-
|
|
19482
|
+
timelineProjectionChanged(nextCodeEvents, runtime.recentCodeEvents, [
|
|
19483
|
+
"stableId",
|
|
19484
|
+
"title",
|
|
19485
|
+
"threadLabel",
|
|
19486
|
+
])
|
|
19266
19487
|
) {
|
|
19267
19488
|
runtime.recentCodeEvents = nextCodeEvents;
|
|
19268
19489
|
state.recentCodeEvents = nextCodeEvents;
|
|
@@ -19697,6 +19918,37 @@ async function main() {
|
|
|
19697
19918
|
process.on("SIGINT", handleSignal);
|
|
19698
19919
|
process.on("SIGTERM", handleSignal);
|
|
19699
19920
|
|
|
19921
|
+
// Belt-and-suspenders: a stray ERR_HTTP_HEADERS_SENT inside an async
|
|
19922
|
+
// request handler used to escape as an UnhandledPromiseRejection and crash
|
|
19923
|
+
// the process under launchd, triggering a restart loop that then re-ran all
|
|
19924
|
+
// the startup backfills (state.json parse, rollout/transcript replay) and
|
|
19925
|
+
// burned CPU + memory for ~30s per cycle. The targeted catch-handler guards
|
|
19926
|
+
// around handleNativeApprovalDecision cover the known sites; this top-level
|
|
19927
|
+
// filter swallows the same class of error from any new site we haven't
|
|
19928
|
+
// audited yet so the process keeps serving instead of dying.
|
|
19929
|
+
const isBenignHttpError = (err) =>
|
|
19930
|
+
err && typeof err === "object" && (
|
|
19931
|
+
err.code === "ERR_HTTP_HEADERS_SENT" ||
|
|
19932
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
19933
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
19934
|
+
err.code === "ECONNRESET" ||
|
|
19935
|
+
err.code === "EPIPE"
|
|
19936
|
+
);
|
|
19937
|
+
process.on("uncaughtException", (err) => {
|
|
19938
|
+
if (isBenignHttpError(err)) {
|
|
19939
|
+
console.warn(`[uncaught-http] ${err.code} ${err.message}`);
|
|
19940
|
+
return;
|
|
19941
|
+
}
|
|
19942
|
+
console.error(`[uncaught] ${err?.stack || err}`);
|
|
19943
|
+
});
|
|
19944
|
+
process.on("unhandledRejection", (reason) => {
|
|
19945
|
+
if (isBenignHttpError(reason)) {
|
|
19946
|
+
console.warn(`[unhandled-http] ${reason.code} ${reason.message}`);
|
|
19947
|
+
return;
|
|
19948
|
+
}
|
|
19949
|
+
console.error(`[unhandled] ${reason?.stack || reason}`);
|
|
19950
|
+
});
|
|
19951
|
+
|
|
19700
19952
|
try {
|
|
19701
19953
|
if (config.webPushEnabled) {
|
|
19702
19954
|
webPush.setVapidDetails(
|