viveworker 0.7.0-beta.2 → 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.
@@ -56,6 +56,8 @@ const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
56
56
  const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
57
57
  const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
58
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;
59
61
 
60
62
  // Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
61
63
  // per tracked repo (`git diff --name-status`, `git status --porcelain`,
@@ -265,6 +267,11 @@ const runtime = {
265
267
  // deliberately NOT persisted via `state` since the cache is worthless after
266
268
  // a restart and would just bloat the state file.
267
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,
268
275
  };
269
276
  const state = await loadState(config.stateFile);
270
277
  runtime.threadRegistry = await loadThreadRegistry(config.threadRegistryFile);
@@ -673,7 +680,7 @@ function withNotificationIcon(kind, title) {
673
680
 
674
681
  function normalizeTimelineOutcome(value) {
675
682
  const normalized = cleanText(value || "").toLowerCase();
676
- return ["pending", "approved", "rejected", "implemented", "dismissed", "submitted"].includes(normalized)
683
+ return ["pending", "approved", "rejected", "failed", "implemented", "dismissed", "submitted"].includes(normalized)
677
684
  ? normalized
678
685
  : "";
679
686
  }
@@ -2809,6 +2816,7 @@ function providerDisplayName(locale, provider) {
2809
2816
  if (p === "claude") return t(locale, "common.claude");
2810
2817
  if (p === "moltbook") return "Moltbook";
2811
2818
  if (p === "a2a") return "A2A";
2819
+ if (p === "viveworker") return t(locale, "common.appName");
2812
2820
  return t(locale, "common.codex");
2813
2821
  }
2814
2822
 
@@ -2848,7 +2856,7 @@ function normalizeHistoryItem(raw) {
2848
2856
  if (!isPlainObject(raw)) {
2849
2857
  return null;
2850
2858
  }
2851
- if (shouldHideClaudeInternalItem(raw)) {
2859
+ if (shouldHideInternalTimelineItem(raw)) {
2852
2860
  return null;
2853
2861
  }
2854
2862
 
@@ -2903,7 +2911,7 @@ function normalizeHistoryItem(raw) {
2903
2911
  ...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
2904
2912
  ...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
2905
2913
  };
2906
- return shouldHideClaudeInternalItem(normalized) ? null : normalized;
2914
+ return shouldHideInternalTimelineItem(normalized) ? null : normalized;
2907
2915
  }
2908
2916
 
2909
2917
  function historyToken(stableId) {
@@ -2985,6 +2993,70 @@ function normalizeTimelineEntries(rawItems, maxItems) {
2985
2993
  return deduped;
2986
2994
  }
2987
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
+
2988
3060
  function isCodeEventEntry(raw) {
2989
3061
  if (!isPlainObject(raw)) {
2990
3062
  return false;
@@ -3043,7 +3115,7 @@ function normalizeTimelineEntry(raw) {
3043
3115
  if (!isPlainObject(raw)) {
3044
3116
  return null;
3045
3117
  }
3046
- if (shouldHideClaudeInternalItem(raw)) {
3118
+ if (shouldHideInternalTimelineItem(raw)) {
3047
3119
  return null;
3048
3120
  }
3049
3121
 
@@ -3137,7 +3209,7 @@ function normalizeTimelineEntry(raw) {
3137
3209
  ...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
3138
3210
  ...(suggestions.length > 0 ? { suggestions } : {}),
3139
3211
  };
3140
- return shouldHideClaudeInternalItem(normalized) ? null : normalized;
3212
+ return shouldHideInternalTimelineItem(normalized) ? null : normalized;
3141
3213
  }
3142
3214
 
3143
3215
  function recordTimelineEntry({ config, runtime, state, entry }) {
@@ -3150,35 +3222,21 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
3150
3222
  [normalized, ...runtime.recentTimelineEntries.filter((item) => item.stableId !== normalized.stableId)],
3151
3223
  config.maxTimelineEntries
3152
3224
  );
3153
- const changed =
3154
- JSON.stringify(
3155
- nextItems.map((item) => [
3156
- item.stableId,
3157
- item.title,
3158
- item.createdAtMs,
3159
- item.diffAvailable,
3160
- item.diffSource,
3161
- item.diffAddedLines,
3162
- item.diffRemovedLines,
3163
- item.diffText,
3164
- item.previousFileRefs,
3165
- item.cwd,
3166
- ])
3167
- ) !==
3168
- JSON.stringify(
3169
- runtime.recentTimelineEntries.map((item) => [
3170
- item.stableId,
3171
- item.title,
3172
- item.createdAtMs,
3173
- item.diffAvailable,
3174
- item.diffSource,
3175
- item.diffAddedLines,
3176
- item.diffRemovedLines,
3177
- item.diffText,
3178
- item.previousFileRefs,
3179
- item.cwd,
3180
- ])
3181
- );
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
+ ]);
3182
3240
  runtime.recentTimelineEntries = nextItems;
3183
3241
  state.recentTimelineEntries = nextItems;
3184
3242
  return changed;
@@ -3197,35 +3255,19 @@ function recordCodeEvent({ config, runtime, state, entry }) {
3197
3255
  [normalized, ...runtime.recentCodeEvents.filter((item) => item.stableId !== normalized.stableId)],
3198
3256
  config.maxCodeEvents
3199
3257
  );
3200
- const changed =
3201
- JSON.stringify(
3202
- nextItems.map((item) => [
3203
- item.stableId,
3204
- item.title,
3205
- item.createdAtMs,
3206
- item.diffAvailable,
3207
- item.diffSource,
3208
- item.diffAddedLines,
3209
- item.diffRemovedLines,
3210
- item.diffText,
3211
- item.previousFileRefs,
3212
- item.cwd,
3213
- ])
3214
- ) !==
3215
- JSON.stringify(
3216
- runtime.recentCodeEvents.map((item) => [
3217
- item.stableId,
3218
- item.title,
3219
- item.createdAtMs,
3220
- item.diffAvailable,
3221
- item.diffSource,
3222
- item.diffAddedLines,
3223
- item.diffRemovedLines,
3224
- item.diffText,
3225
- item.previousFileRefs,
3226
- item.cwd,
3227
- ])
3228
- );
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
+ ]);
3229
3271
  runtime.recentCodeEvents = nextItems;
3230
3272
  state.recentCodeEvents = nextItems;
3231
3273
  if (changed) {
@@ -3249,37 +3291,19 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
3249
3291
  ],
3250
3292
  config.maxCodeEvents
3251
3293
  );
3252
- const changed =
3253
- JSON.stringify(
3254
- nextItems.map((item) => [
3255
- item.stableId,
3256
- item.title,
3257
- item.createdAtMs,
3258
- item.threadLabel,
3259
- item.diffAvailable,
3260
- item.diffSource,
3261
- item.diffAddedLines,
3262
- item.diffRemovedLines,
3263
- item.diffText,
3264
- item.previousFileRefs,
3265
- item.cwd,
3266
- ])
3267
- ) !==
3268
- JSON.stringify(
3269
- runtime.recentCodeEvents.map((item) => [
3270
- item.stableId,
3271
- item.title,
3272
- item.createdAtMs,
3273
- item.threadLabel,
3274
- item.diffAvailable,
3275
- item.diffSource,
3276
- item.diffAddedLines,
3277
- item.diffRemovedLines,
3278
- item.diffText,
3279
- item.previousFileRefs,
3280
- item.cwd,
3281
- ])
3282
- );
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
+ ]);
3283
3307
  runtime.recentCodeEvents = nextItems;
3284
3308
  state.recentCodeEvents = nextItems;
3285
3309
  if (changed) {
@@ -3345,9 +3369,11 @@ function recordHistoryItem({ config, runtime, state, item }) {
3345
3369
  [normalized, ...runtime.recentHistoryItems.filter((entry) => entry.stableId !== normalized.stableId)],
3346
3370
  config.maxHistoryItems
3347
3371
  );
3348
- const changed =
3349
- JSON.stringify(nextItems.map((entry) => [entry.stableId, entry.title, entry.createdAtMs])) !==
3350
- JSON.stringify(runtime.recentHistoryItems.map((entry) => [entry.stableId, entry.title, entry.createdAtMs]));
3372
+ const changed = timelineProjectionChanged(nextItems, runtime.recentHistoryItems, [
3373
+ "stableId",
3374
+ "title",
3375
+ "createdAtMs",
3376
+ ]);
3351
3377
  runtime.recentHistoryItems = nextItems;
3352
3378
  state.recentHistoryItems = nextItems;
3353
3379
  return changed;
@@ -3776,7 +3802,7 @@ async function scanOnce({ config, runtime, state }) {
3776
3802
  if (config.webUiEnabled) {
3777
3803
  let claudeTranscriptChanged = false;
3778
3804
  if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
3779
- runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
3805
+ runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir, config.claudeTranscriptMaxAgeMs);
3780
3806
  runtime.lastClaudeScanAt = now;
3781
3807
  }
3782
3808
  let claudeSessionTitlesChanged = false;
@@ -4114,7 +4140,16 @@ async function refreshClaudeSessionTitles(runtime) {
4114
4140
  return changed;
4115
4141
  }
4116
4142
 
4117
- 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;
4118
4153
  try {
4119
4154
  const result = [];
4120
4155
  const entries = await fs.readdir(claudeProjectsDir, { withFileTypes: true });
@@ -4125,7 +4160,17 @@ async function listClaudeTranscriptFiles(claudeProjectsDir) {
4125
4160
  const files = await fs.readdir(projectPath, { withFileTypes: true });
4126
4161
  for (const file of files) {
4127
4162
  if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
4128
- result.push(path.join(projectPath, file.name));
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);
4129
4174
  }
4130
4175
  } catch {
4131
4176
  // skip unreadable project dirs
@@ -5669,6 +5714,12 @@ async function processScannedEvent({ config, runtime, state, event }) {
5669
5714
  return false;
5670
5715
  }
5671
5716
 
5717
+ if (shouldSuppressInternalScannedEvent(event)) {
5718
+ state.seenEvents[event.id] = event.timestampMs || Date.now();
5719
+ trimSeenEvents(state.seenEvents, config.maxSeenEvents);
5720
+ return true;
5721
+ }
5722
+
5672
5723
  let dirty = false;
5673
5724
 
5674
5725
  if (event.kind === "task_complete") {
@@ -6528,6 +6579,150 @@ async function createNativeApproval({ config, runtime, conversationId, request,
6528
6579
  };
6529
6580
  }
6530
6581
 
6582
+ function createX402PaymentApproval({ config, body, now = Date.now() }) {
6583
+ const payment = normalizeX402PaymentApprovalBody(body);
6584
+ if (!payment) {
6585
+ return null;
6586
+ }
6587
+ const token = crypto.randomBytes(18).toString("hex");
6588
+ const requestId = cleanText(body.paymentRequestId || body.requestId || crypto.randomUUID());
6589
+ const requestKey = `payment:${requestId}`;
6590
+ const title = payment.amountUsdc
6591
+ ? `Payment approval — ${payment.amountUsdc} USDC`
6592
+ : "Payment approval";
6593
+ return {
6594
+ token,
6595
+ requestKey,
6596
+ conversationId: "payments",
6597
+ requestId,
6598
+ ownerClientId: null,
6599
+ kind: "payment",
6600
+ threadLabel: "x402 payment",
6601
+ title,
6602
+ messageText: formatX402PaymentApprovalMessage(payment),
6603
+ reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
6604
+ rawParams: payment,
6605
+ cwd: "",
6606
+ workspaceRoot: "",
6607
+ fileRefs: [],
6608
+ diffText: "",
6609
+ diffAvailable: false,
6610
+ diffSource: "",
6611
+ diffAddedLines: 0,
6612
+ diffRemovedLines: 0,
6613
+ createdAtMs: now,
6614
+ resolved: false,
6615
+ resolving: false,
6616
+ resolveClaudeWaiter: null,
6617
+ provider: "viveworker",
6618
+ };
6619
+ }
6620
+
6621
+
6622
+ function createHazbaseWalletPaymentApproval({ config, body, now = Date.now() }) {
6623
+ const payment = normalizeX402PaymentApprovalBody(body);
6624
+ if (!payment) return null;
6625
+ const paymentRequestId = cleanText(body.paymentRequestId || body.requestId || "");
6626
+ if (!/^[a-zA-Z0-9:_-]{8,160}$/u.test(paymentRequestId)) return null;
6627
+ const token = crypto.randomBytes(18).toString("hex");
6628
+ const requestKey = `hazbase_wallet_payment:${paymentRequestId}`;
6629
+ const title = payment.amountUsdc
6630
+ ? `Hazbase wallet payment — ${payment.amountUsdc} USDC`
6631
+ : "Hazbase wallet payment";
6632
+ return {
6633
+ token,
6634
+ requestKey,
6635
+ conversationId: "payments",
6636
+ requestId: paymentRequestId,
6637
+ paymentRequestId,
6638
+ ownerClientId: null,
6639
+ kind: "hazbase_wallet_payment",
6640
+ threadLabel: "x402 payment",
6641
+ title,
6642
+ messageText: formatHazbaseWalletPaymentApprovalMessage(payment),
6643
+ reviewUrl: `${config.nativeApprovalPublicBaseUrl}/native-approvals/${token}`,
6644
+ rawParams: payment,
6645
+ cwd: "",
6646
+ workspaceRoot: "",
6647
+ fileRefs: [],
6648
+ diffText: "",
6649
+ diffAvailable: false,
6650
+ diffSource: "",
6651
+ diffAddedLines: 0,
6652
+ diffRemovedLines: 0,
6653
+ createdAtMs: now,
6654
+ resolved: false,
6655
+ resolving: false,
6656
+ resolveClaudeWaiter: null,
6657
+ provider: "viveworker",
6658
+ };
6659
+ }
6660
+
6661
+ function formatHazbaseWalletPaymentApprovalMessage(payment) {
6662
+ const lines = [
6663
+ "Hazbase Smart Wallet payment requested.",
6664
+ "",
6665
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
6666
+ `Network: ${payment.network} (chainId ${payment.chainId})`,
6667
+ `Pay to: ${payment.payTo}`,
6668
+ `Asset: ${payment.asset}`,
6669
+ `Resource: ${payment.resource}`,
6670
+ "",
6671
+ "Approving will ask for your passkey, then hazBase will submit a gasless Smart Wallet payment.",
6672
+ ];
6673
+ if (payment.description) lines.splice(7, 0, `Description: ${payment.description}`);
6674
+ if (payment.url && payment.url !== payment.resource) lines.splice(-1, 0, `URL: ${payment.url}`);
6675
+ return lines.join("\n");
6676
+ }
6677
+
6678
+ function normalizeX402PaymentApprovalBody(body) {
6679
+ if (!isPlainObject(body)) return null;
6680
+ const payment = isPlainObject(body.payment) ? body.payment : {};
6681
+ const network = cleanText(payment.network || "");
6682
+ const chainId = Number(payment.chainId) || 0;
6683
+ const amountAtomic = cleanText(payment.amountAtomic || "");
6684
+ const amountUsdc = cleanText(payment.amountUsdc || "");
6685
+ const payTo = cleanText(payment.payTo || "");
6686
+ const asset = cleanText(payment.asset || "");
6687
+ const resource = cleanText(payment.resource || body.url || "");
6688
+ const description = cleanText(payment.description || "");
6689
+ const url = cleanText(body.url || resource);
6690
+ if (!network || !chainId || !amountAtomic || !payTo || !asset || !resource) {
6691
+ return null;
6692
+ }
6693
+ return {
6694
+ url,
6695
+ network,
6696
+ chainId,
6697
+ amountAtomic,
6698
+ amountUsdc,
6699
+ payTo,
6700
+ asset,
6701
+ resource,
6702
+ description,
6703
+ };
6704
+ }
6705
+
6706
+ function formatX402PaymentApprovalMessage(payment) {
6707
+ const lines = [
6708
+ "x402 payment approval requested.",
6709
+ "",
6710
+ `Amount: ${payment.amountUsdc || payment.amountAtomic} USDC`,
6711
+ `Network: ${payment.network} (chainId ${payment.chainId})`,
6712
+ `Pay to: ${payment.payTo}`,
6713
+ `Asset: ${payment.asset}`,
6714
+ `Resource: ${payment.resource}`,
6715
+ ];
6716
+ if (payment.description) {
6717
+ lines.push(`Description: ${payment.description}`);
6718
+ }
6719
+ if (payment.url && payment.url !== payment.resource) {
6720
+ lines.push(`URL: ${payment.url}`);
6721
+ }
6722
+ lines.push("", "Approve only if the amount, recipient, network, and resource match what you expect.");
6723
+ return lines.join("\n");
6724
+ }
6725
+
6531
6726
  async function buildNativeApprovalPayload({ config, runtime, conversationId, request, token }) {
6532
6727
  const kind = nativeApprovalKind(request.method);
6533
6728
  if (!kind) {
@@ -8767,6 +8962,18 @@ function isHiddenClaudeInternalScoringText(text) {
8767
8962
  return /^You are scoring Moltbook posts? for an AI agent\b/iu.test(single);
8768
8963
  }
8769
8964
 
8965
+ function isHiddenCodexApprovalAssessmentText(text) {
8966
+ const single = cleanText(stripNotificationMarkup(stripEnvironmentContextBlocks(text || "")));
8967
+ if (!single) {
8968
+ return false;
8969
+ }
8970
+ return /\bThe following is the Codex agent history(?: added since your last approval assessment)?\b/iu.test(single);
8971
+ }
8972
+
8973
+ function shouldHideInternalTimelineItem(item) {
8974
+ return shouldHideClaudeInternalItem(item) || shouldHideCodexInternalApprovalItem(item);
8975
+ }
8976
+
8770
8977
  function shouldHideClaudeInternalItem(item) {
8771
8978
  if (!isPlainObject(item)) {
8772
8979
  return false;
@@ -8786,6 +8993,48 @@ function shouldHideClaudeInternalItem(item) {
8786
8993
  );
8787
8994
  }
8788
8995
 
8996
+ function shouldSuppressInternalScannedEvent(event) {
8997
+ if (!isPlainObject(event)) {
8998
+ return false;
8999
+ }
9000
+ const provider = normalizeProvider(event.provider || "codex");
9001
+ if (provider !== "codex") {
9002
+ return false;
9003
+ }
9004
+
9005
+ return shouldHideCodexInternalApprovalItem({
9006
+ provider,
9007
+ kind: cleanText(event.kind || ""),
9008
+ title: event.title,
9009
+ threadLabel: event.threadLabel,
9010
+ summary: event.summary ?? event.message,
9011
+ messageText: event.messageText ?? event.detailText ?? event.message,
9012
+ detailText: event.detailText,
9013
+ message: event.message,
9014
+ });
9015
+ }
9016
+
9017
+ function shouldHideCodexInternalApprovalItem(item) {
9018
+ if (!isPlainObject(item)) {
9019
+ return false;
9020
+ }
9021
+ if (normalizeProvider(item.provider) !== "codex") {
9022
+ return false;
9023
+ }
9024
+
9025
+ const title = cleanText(item.title ?? "");
9026
+ const threadLabel = cleanText(item.threadLabel ?? "");
9027
+ if (isHiddenCodexApprovalAssessmentText(title) || isHiddenCodexApprovalAssessmentText(threadLabel)) {
9028
+ return true;
9029
+ }
9030
+ return (
9031
+ isHiddenCodexApprovalAssessmentText(item.messageText) ||
9032
+ isHiddenCodexApprovalAssessmentText(item.summary) ||
9033
+ isHiddenCodexApprovalAssessmentText(item.detailText) ||
9034
+ isHiddenCodexApprovalAssessmentText(item.message)
9035
+ );
9036
+ }
9037
+
8789
9038
  function threadStateArchiveStatus(threadState) {
8790
9039
  if (!isPlainObject(threadState)) {
8791
9040
  return "";
@@ -10499,7 +10748,9 @@ function buildPendingInboxItems(runtime, state, config, locale) {
10499
10748
  token: approval.token,
10500
10749
  threadId: cleanText(approval.conversationId || ""),
10501
10750
  threadLabel: approval.threadLabel || "",
10502
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10751
+ title: cleanText(approval.kind || "") === "payment"
10752
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
10753
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10503
10754
  summary: formatNotificationBody(approval.messageText, 100) || approval.messageText,
10504
10755
  primaryLabel: t(locale, "server.action.review"),
10505
10756
  createdAtMs: Number(approval.createdAtMs) || now,
@@ -10851,7 +11102,9 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
10851
11102
  kind: "approval",
10852
11103
  threadId: cleanText(approval.conversationId || ""),
10853
11104
  threadLabel: approval.threadLabel,
10854
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11105
+ title: cleanText(approval.kind || "") === "payment"
11106
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
11107
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
10855
11108
  summary: formatNotificationBody(approval.messageText, 180) || approval.messageText,
10856
11109
  messageText: approval.messageText,
10857
11110
  outcome: "pending",
@@ -11058,7 +11311,9 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
11058
11311
  approvalKind,
11059
11312
  provider: normalizeProvider(approval.provider),
11060
11313
  token: approval.token,
11061
- title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11314
+ title: approvalKind === "payment"
11315
+ ? cleanText(approval.title || "") || formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel)
11316
+ : formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
11062
11317
  threadLabel: approval.threadLabel || "",
11063
11318
  createdAtMs: Number(approval.createdAtMs) || 0,
11064
11319
  messageHtml: renderMessageHtml(approval.messageText, `<p>${escapeHtml(t(locale, "detail.approvalRequested"))}</p>`),
@@ -11073,10 +11328,15 @@ function buildPendingApprovalDetail(runtime, state, approval, locale) {
11073
11328
  readOnly: approval.readOnly === true,
11074
11329
  actions: approval.readOnly === true
11075
11330
  ? []
11076
- : [
11077
- { label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
11078
- { label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
11079
- ],
11331
+ : approvalKind === "hazbase_wallet_payment"
11332
+ ? [
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: {} },
11335
+ ]
11336
+ : [
11337
+ { label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
11338
+ { label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
11339
+ ],
11080
11340
  };
11081
11341
  if (approvalKind === "plan") {
11082
11342
  const planText = String(approval.planText || "");
@@ -13258,9 +13518,93 @@ function buildWebAppHtml({ pairToken }) {
13258
13518
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
13259
13519
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
13260
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>
13261
13597
  <title>viveworker</title>
13262
13598
  </head>
13263
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>
13264
13608
  <div id="app"></div>
13265
13609
  <script type="module" src="/app.js"></script>
13266
13610
  </body>
@@ -13472,6 +13816,12 @@ function createNativeApprovalServer({ config, runtime, state }) {
13472
13816
  return writeJson(res, 200, buildSessionPayload({ config, state, session }));
13473
13817
  }
13474
13818
 
13819
+ if (url.pathname === "/api/version/status" && req.method === "GET") {
13820
+ const session = requireApiSession(req, res, config, state);
13821
+ if (!session) return;
13822
+ return writeJson(res, 200, await getNpmVersionStatus(runtime, config));
13823
+ }
13824
+
13475
13825
  // Collapses the PWA's boot-time fan-out (session + inbox + timeline
13476
13826
  // + devices) into a single HTTPS round-trip. iOS Safari tears down
13477
13827
  // connections aggressively, so each parallel fetch pays its own
@@ -13929,10 +14279,15 @@ if (url.pathname === "/api/hazbase/status" && req.method === "GET") {
13929
14279
  8453: resolveHazbaseAccountForChain(hazbase.accounts, 8453)?.smartAccountAddress || "",
13930
14280
  84532: resolveHazbaseAccountForChain(hazbase.accounts, 84532)?.smartAccountAddress || "",
13931
14281
  };
14282
+ const signedIn = Boolean(hazbase.accessToken) && !hazbase.sessionInvalid;
13932
14283
  return writeJson(res, 200, {
13933
14284
  enabled: true,
13934
14285
  apiUrl: config.hazbaseApiUrl,
13935
- signedIn: Boolean(hazbase.accessToken),
14286
+ signedIn,
14287
+ sessionStatus: hazbase.sessionInvalid ? "expired" : signedIn ? "signed_in" : "signed_out",
14288
+ sessionInvalid: hazbase.sessionInvalid,
14289
+ sessionInvalidReason: hazbase.sessionInvalidReason,
14290
+ sessionInvalidAt: hazbase.sessionInvalidAt,
13936
14291
  email: hazbase.email,
13937
14292
  userId: hazbase.userId,
13938
14293
  sessionId: hazbase.sessionId,
@@ -13995,15 +14350,19 @@ if (url.pathname === "/api/hazbase/verify-otp" && req.method === "POST") {
13995
14350
  const code = cleanText(body?.code || "");
13996
14351
  if (!email || !code) return writeJson(res, 400, { error: "otp-required" });
13997
14352
  const result = await verifyHazbaseEmailOtp({ email, code });
14353
+ const previousHazbase = normalizeHazbaseState(state.hazbase);
13998
14354
  state.hazbase = {
13999
- ...normalizeHazbaseState(state.hazbase),
14355
+ ...previousHazbase,
14000
14356
  email: result.email || email,
14001
14357
  accessToken: cleanText(result.accessToken || ""),
14002
14358
  sessionId: cleanText(result.sessionId || ""),
14003
14359
  userId: cleanText(result.userId || ""),
14004
- accounts: Array.isArray(result.accounts) ? result.accounts : [],
14360
+ accounts: mergeHazbaseAccountSummaries(previousHazbase.accounts, Array.isArray(result.accounts) ? result.accounts : []),
14005
14361
  highTrustToken: "",
14006
14362
  highTrustExpiresAt: "",
14363
+ sessionInvalid: false,
14364
+ sessionInvalidReason: "",
14365
+ sessionInvalidAt: "",
14007
14366
  };
14008
14367
  await saveState(config.stateFile, state);
14009
14368
  return writeJson(res, 200, result);
@@ -14024,11 +14383,17 @@ if (url.pathname === "/api/hazbase/passkey/register/challenge" && req.method ===
14024
14383
  message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
14025
14384
  });
14026
14385
  }
14027
- const result = await requestPasskeyRegistrationChallenge({
14028
- emailSession: hazbase.accessToken,
14029
- deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14030
- partnerOrigin,
14031
- });
14386
+ let result;
14387
+ try {
14388
+ result = await requestPasskeyRegistrationChallenge({
14389
+ emailSession: hazbase.accessToken,
14390
+ deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14391
+ partnerOrigin,
14392
+ });
14393
+ } catch (error) {
14394
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14395
+ throw error;
14396
+ }
14032
14397
  return writeJson(res, 200, result);
14033
14398
  }
14034
14399
 
@@ -14038,16 +14403,25 @@ if (url.pathname === "/api/hazbase/passkey/register/complete" && req.method ===
14038
14403
  const hazbase = normalizeHazbaseState(state.hazbase);
14039
14404
  if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
14040
14405
  const body = await parseJsonBody(req);
14041
- const result = await completePasskeyRegistration({
14042
- emailSession: hazbase.accessToken,
14043
- challengeId: cleanText(body?.challengeId || ""),
14044
- credential: body?.credential,
14045
- deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14046
- });
14406
+ let result;
14407
+ try {
14408
+ result = await completePasskeyRegistration({
14409
+ emailSession: hazbase.accessToken,
14410
+ challengeId: cleanText(body?.challengeId || ""),
14411
+ credential: body?.credential,
14412
+ deviceLabel: cleanText(body?.deviceLabel || config.hazbaseDeviceLabel || "") || undefined,
14413
+ });
14414
+ } catch (error) {
14415
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14416
+ throw error;
14417
+ }
14047
14418
  state.hazbase = {
14048
14419
  ...hazbase,
14049
14420
  deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
14050
14421
  credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
14422
+ sessionInvalid: false,
14423
+ sessionInvalidReason: "",
14424
+ sessionInvalidAt: "",
14051
14425
  };
14052
14426
  await saveState(config.stateFile, state);
14053
14427
  return writeJson(res, 200, result);
@@ -14068,12 +14442,18 @@ if (url.pathname === "/api/hazbase/passkey/assert/challenge" && req.method === "
14068
14442
  message: error?.message || "Open viveworker on its .local hostname to use hazBase passkeys.",
14069
14443
  });
14070
14444
  }
14071
- const result = await requestPasskeyAssertionChallenge({
14072
- emailSession: hazbase.accessToken,
14073
- purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14074
- deviceBindingId: hazbase.deviceBindingId || undefined,
14075
- partnerOrigin,
14076
- });
14445
+ let result;
14446
+ try {
14447
+ result = await requestPasskeyAssertionChallenge({
14448
+ emailSession: hazbase.accessToken,
14449
+ purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14450
+ deviceBindingId: hazbase.deviceBindingId || undefined,
14451
+ partnerOrigin,
14452
+ });
14453
+ } catch (error) {
14454
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14455
+ throw error;
14456
+ }
14077
14457
  return writeJson(res, 200, result);
14078
14458
  }
14079
14459
 
@@ -14083,19 +14463,28 @@ if (url.pathname === "/api/hazbase/passkey/assert/complete" && req.method === "P
14083
14463
  const hazbase = normalizeHazbaseState(state.hazbase);
14084
14464
  if (!hazbase.accessToken) return writeJson(res, 401, { error: "hazbase-auth-required" });
14085
14465
  const body = await parseJsonBody(req);
14086
- const result = await completePasskeyAssertion({
14087
- emailSession: hazbase.accessToken,
14088
- challengeId: cleanText(body?.challengeId || ""),
14089
- credential: body?.credential,
14090
- purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14091
- deviceBindingId: hazbase.deviceBindingId || undefined,
14092
- });
14466
+ let result;
14467
+ try {
14468
+ result = await completePasskeyAssertion({
14469
+ emailSession: hazbase.accessToken,
14470
+ challengeId: cleanText(body?.challengeId || ""),
14471
+ credential: body?.credential,
14472
+ purpose: cleanText(body?.purpose || "bootstrap") || "bootstrap",
14473
+ deviceBindingId: hazbase.deviceBindingId || undefined,
14474
+ });
14475
+ } catch (error) {
14476
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14477
+ throw error;
14478
+ }
14093
14479
  state.hazbase = {
14094
14480
  ...hazbase,
14095
14481
  deviceBindingId: cleanText(result.deviceBindingId || hazbase.deviceBindingId || ""),
14096
14482
  credentialId: cleanText(result.credentialId || hazbase.credentialId || ""),
14097
14483
  highTrustToken: cleanText(result.highTrustToken || ""),
14098
14484
  highTrustExpiresAt: cleanText(result.highTrustExpiresAt || ""),
14485
+ sessionInvalid: false,
14486
+ sessionInvalidReason: "",
14487
+ sessionInvalidAt: "",
14099
14488
  };
14100
14489
  await saveState(config.stateFile, state);
14101
14490
  return writeJson(res, 200, result);
@@ -14113,12 +14502,18 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
14113
14502
  if (chainId !== 8453 && chainId !== 84532) {
14114
14503
  return writeJson(res, 400, { error: "unsupported-chain" });
14115
14504
  }
14116
- const result = await bootstrapPasskeyAccount({
14117
- emailSession: hazbase.accessToken,
14118
- deviceBindingId: hazbase.deviceBindingId,
14119
- highTrustToken: hazbase.highTrustToken,
14120
- chainId,
14121
- });
14505
+ let result;
14506
+ try {
14507
+ result = await bootstrapPasskeyAccount({
14508
+ emailSession: hazbase.accessToken,
14509
+ deviceBindingId: hazbase.deviceBindingId,
14510
+ highTrustToken: hazbase.highTrustToken,
14511
+ chainId,
14512
+ });
14513
+ } catch (error) {
14514
+ if (await maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res })) return;
14515
+ throw error;
14516
+ }
14122
14517
  const nextAccounts = mergeHazbaseAccountSummaries(hazbase.accounts, [{
14123
14518
  smartAccountAddress: result.smartAccountAddress,
14124
14519
  chainId: result.chainId,
@@ -14129,11 +14524,29 @@ if (url.pathname === "/api/hazbase/account/bootstrap" && req.method === "POST")
14129
14524
  state.hazbase = {
14130
14525
  ...hazbase,
14131
14526
  accounts: nextAccounts,
14527
+ sessionInvalid: false,
14528
+ sessionInvalidReason: "",
14529
+ sessionInvalidAt: "",
14132
14530
  };
14133
14531
  await saveState(config.stateFile, state);
14134
14532
  return writeJson(res, 200, result);
14135
14533
  }
14136
14534
 
14535
+ if (url.pathname === "/api/hazbase/session/refresh" && req.method === "POST") {
14536
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
14537
+ const hookAuth = config.sessionSecret && hookSecret === config.sessionSecret;
14538
+ if (!hookAuth) {
14539
+ const session = requireMutatingApiSession(req, res, config, state);
14540
+ if (!session) return;
14541
+ }
14542
+ markHazbaseSessionInvalid(state, "manual_refresh_requested");
14543
+ await saveState(config.stateFile, state);
14544
+ return writeJson(res, 200, {
14545
+ ok: true,
14546
+ status: "session_refresh_required",
14547
+ });
14548
+ }
14549
+
14137
14550
  if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
14138
14551
  const session = requireMutatingApiSession(req, res, config, state);
14139
14552
  if (!session) return;
@@ -14656,6 +15069,168 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
14656
15069
  });
14657
15070
  }
14658
15071
 
15072
+
15073
+ if (url.pathname === "/api/payments/x402/hazbase-wallet" && req.method === "POST") {
15074
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
15075
+ if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
15076
+ return writeJson(res, 401, { error: "unauthorized" });
15077
+ }
15078
+
15079
+ let body;
15080
+ try {
15081
+ body = await parseJsonBody(req);
15082
+ } catch {
15083
+ return writeJson(res, 400, { error: "invalid-json-body" });
15084
+ }
15085
+
15086
+ const approval = createHazbaseWalletPaymentApproval({ config, body });
15087
+ if (!approval) {
15088
+ return writeJson(res, 400, { error: "invalid-hazbase-wallet-payment-request" });
15089
+ }
15090
+ if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
15091
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-already-pending" });
15092
+ }
15093
+
15094
+ runtime.nativeApprovalsByToken.set(approval.token, approval);
15095
+ runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
15096
+
15097
+ deliverWebPushItem({
15098
+ config,
15099
+ state,
15100
+ kind: "approval",
15101
+ tab: "inbox",
15102
+ subtab: "pending",
15103
+ token: approval.token,
15104
+ stableId: pendingApprovalStableId(approval),
15105
+ title: approval.title,
15106
+ body: approval.messageText,
15107
+ buildLocalizedContent: () => ({ title: approval.title, body: approval.messageText }),
15108
+ }).catch((error) => {
15109
+ console.error(`[hazbase-wallet-payment-push] ${approval.requestKey} | ${error.message}`);
15110
+ });
15111
+
15112
+ const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
15113
+ const decisionPromise = new Promise((resolve) => {
15114
+ approval.resolveClaudeWaiter = resolve;
15115
+ });
15116
+ const timeoutPromise = new Promise((resolve) => {
15117
+ setTimeout(() => resolve({ paid: false, decision: "timeout", error: "hazbase-wallet-payment-timeout" }), waitMs);
15118
+ });
15119
+ const decision = await Promise.race([decisionPromise, timeoutPromise]);
15120
+
15121
+ if (!approval.resolved) {
15122
+ approval.resolved = true;
15123
+ approval.resolving = false;
15124
+ runtime.nativeApprovalsByToken.delete(approval.token);
15125
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
15126
+ }
15127
+
15128
+ if (decision && typeof decision === "object" && decision.paid === true) {
15129
+ return writeJson(res, 200, decision);
15130
+ }
15131
+ const error = decision?.error || decision?.decision || "hazbase-wallet-payment-declined";
15132
+ const statusCode = error === "hazbase-wallet-payment-timeout" ? 408 : 403;
15133
+ return writeJson(res, statusCode, { paid: false, error, decision: decision?.decision || "decline" });
15134
+ }
15135
+
15136
+ const hazbaseWalletPayMatch = url.pathname.match(/^\/api\/payments\/x402\/hazbase-wallet\/([a-f0-9]+)\/pay$/u);
15137
+ if (hazbaseWalletPayMatch && req.method === "POST") {
15138
+ const session = requireMutatingApiSession(req, res, config, state);
15139
+ if (!session) return;
15140
+ const token = hazbaseWalletPayMatch[1];
15141
+ const approval = runtime.nativeApprovalsByToken.get(token);
15142
+ if (!approval || approval.kind !== "hazbase_wallet_payment") {
15143
+ return writeJson(res, 404, { error: "approval-not-found" });
15144
+ }
15145
+ if (approval.resolved || approval.resolving) {
15146
+ return writeJson(res, 409, { error: "approval-already-handled" });
15147
+ }
15148
+ approval.resolving = true;
15149
+ try {
15150
+ const result = await completeHazbaseWalletPaymentApproval({ config, runtime, state, approval });
15151
+ return writeJson(res, 200, result);
15152
+ } catch (error) {
15153
+ const failure = await finalizeHazbaseWalletPaymentFailure({ config, runtime, state, approval, error });
15154
+ console.error(`[hazbase-wallet-payment-error] ${approval.requestKey} | ${error?.stack || error?.message || error}`);
15155
+ return writeJson(res, Number(error?.statusCode) || 500, {
15156
+ error: failure.error,
15157
+ approvalFinalized: true,
15158
+ retryable: true,
15159
+ });
15160
+ }
15161
+ }
15162
+
15163
+ if (url.pathname === "/api/payments/x402/approval" && req.method === "POST") {
15164
+ const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
15165
+ if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
15166
+ return writeJson(res, 401, { error: "unauthorized" });
15167
+ }
15168
+
15169
+ let body;
15170
+ try {
15171
+ body = await parseJsonBody(req);
15172
+ } catch {
15173
+ return writeJson(res, 400, { error: "invalid-json-body" });
15174
+ }
15175
+
15176
+ const approval = createX402PaymentApproval({ config, body });
15177
+ if (!approval) {
15178
+ return writeJson(res, 400, { error: "invalid-payment-approval-request" });
15179
+ }
15180
+ if (runtime.nativeApprovalsByRequestKey.has(approval.requestKey)) {
15181
+ return writeJson(res, 409, { error: "payment-approval-already-pending" });
15182
+ }
15183
+
15184
+ runtime.nativeApprovalsByToken.set(approval.token, approval);
15185
+ runtime.nativeApprovalsByRequestKey.set(approval.requestKey, approval);
15186
+
15187
+ deliverWebPushItem({
15188
+ config,
15189
+ state,
15190
+ kind: "approval",
15191
+ tab: "inbox",
15192
+ subtab: "pending",
15193
+ token: approval.token,
15194
+ stableId: pendingApprovalStableId(approval),
15195
+ title: approval.title,
15196
+ body: approval.messageText,
15197
+ buildLocalizedContent: () => ({
15198
+ title: approval.title,
15199
+ body: approval.messageText,
15200
+ }),
15201
+ }).catch((error) => {
15202
+ console.error(`[payment-approval-push] ${approval.requestKey} | ${error.message}`);
15203
+ });
15204
+
15205
+ const waitMs = Math.max(10_000, Math.min(900_000, Number(body.timeoutMs) || 600_000));
15206
+ const decisionPromise = new Promise((resolve) => {
15207
+ approval.resolveClaudeWaiter = resolve;
15208
+ });
15209
+ const timeoutPromise = new Promise((resolve) => {
15210
+ setTimeout(() => resolve("timeout"), waitMs);
15211
+ });
15212
+ const decision = await Promise.race([decisionPromise, timeoutPromise]);
15213
+
15214
+ if (!approval.resolved) {
15215
+ approval.resolved = true;
15216
+ approval.resolving = false;
15217
+ runtime.nativeApprovalsByToken.delete(approval.token);
15218
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
15219
+ }
15220
+
15221
+ if (decision === "accept") {
15222
+ return writeJson(res, 200, {
15223
+ approved: true,
15224
+ decision,
15225
+ approvedPayment: approval.rawParams,
15226
+ });
15227
+ }
15228
+ if (decision === "timeout") {
15229
+ return writeJson(res, 408, { approved: false, decision, error: "payment-approval-timeout" });
15230
+ }
15231
+ return writeJson(res, 403, { approved: false, decision: "decline", error: "payment-approval-declined" });
15232
+ }
15233
+
14659
15234
  if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
14660
15235
  const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
14661
15236
  if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
@@ -15473,6 +16048,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
15473
16048
  if (approval.resolved || approval.resolving) {
15474
16049
  return writeJson(res, 409, { error: "approval-already-handled" });
15475
16050
  }
16051
+ if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
16052
+ console.warn(`[hazbase-wallet-payment-generic-decision-blocked] ${approval.requestKey} | ${decision}`);
16053
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
16054
+ }
15476
16055
 
15477
16056
  approval.resolving = true;
15478
16057
  try {
@@ -15483,6 +16062,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
15483
16062
  return writeJson(res, 200, { ok: true, decision });
15484
16063
  } catch (error) {
15485
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
+ }
15486
16069
  return writeJson(res, 500, { error: error.message });
15487
16070
  }
15488
16071
  }
@@ -16067,6 +16650,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
16067
16650
  if (approval.resolved || approval.resolving) {
16068
16651
  return writeApprovalHandled(res, req, approval.title, 409, config.defaultLocale);
16069
16652
  }
16653
+ if (approval.kind === "hazbase_wallet_payment" && decision !== "decline") {
16654
+ console.warn(`[hazbase-wallet-payment-native-decision-blocked] ${approval.requestKey} | ${decision}`);
16655
+ if (requestWantsHtml(req)) {
16656
+ return writeHtml(
16657
+ res,
16658
+ 409,
16659
+ renderStatusPage({
16660
+ title: approval.title,
16661
+ body: "This approval must be completed from the viveworker app with the wallet payment button.",
16662
+ tone: "warn",
16663
+ })
16664
+ );
16665
+ }
16666
+ return writeJson(res, 409, { error: "hazbase-wallet-payment-requires-wallet-pay-route" });
16667
+ }
16070
16668
 
16071
16669
  approval.resolving = true;
16072
16670
  try {
@@ -16091,6 +16689,10 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
16091
16689
  });
16092
16690
  } catch (error) {
16093
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
+ }
16094
16696
  if (requestWantsHtml(req)) {
16095
16697
  return writeHtml(
16096
16698
  res,
@@ -16105,6 +16707,9 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
16105
16707
  return writeJson(res, 500, { error: error.message });
16106
16708
  }
16107
16709
  } catch (error) {
16710
+ if (res.headersSent || res.writableEnded) {
16711
+ return;
16712
+ }
16108
16713
  if (requestWantsHtml(req)) {
16109
16714
  return writeHtml(
16110
16715
  res,
@@ -17453,6 +18058,9 @@ function buildConfig(cli) {
17453
18058
  hazbaseApiUrl: stripTrailingSlash(process.env.HAZBASE_API_URL || "https://api.hazbase.com"),
17454
18059
  hazbaseClientKey: cleanText(process.env.HAZBASE_CLIENT_KEY || "viveworker-internal"),
17455
18060
  hazbaseDeviceLabel: cleanText(process.env.HAZBASE_DEVICE_LABEL || "viveworker"),
18061
+ npmPackageName: cleanText(process.env.VIVEWORKER_NPM_PACKAGE || "viveworker"),
18062
+ npmDistTag: cleanText(process.env.VIVEWORKER_NPM_DIST_TAG || "latest"),
18063
+ npmRegistryUrl: stripTrailingSlash(process.env.NPM_REGISTRY_URL || "https://registry.npmjs.org"),
17456
18064
  webUiEnabled: boolEnv("WEB_UI_ENABLED", true),
17457
18065
  authRequired: boolEnv("AUTH_REQUIRED", true),
17458
18066
  webPushEnabled: boolEnv("WEB_PUSH_ENABLED", false),
@@ -17498,6 +18106,12 @@ function buildConfig(cli) {
17498
18106
  maxCodeEvents: numberEnv("MAX_CODE_EVENTS", 1000),
17499
18107
  maxTimelineThreads: numberEnv("MAX_TIMELINE_THREADS", 20),
17500
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,
17501
18115
  maxMessageChars: numberEnv("MAX_MESSAGE_CHARS", 320),
17502
18116
  maxCommandChars: numberEnv("MAX_COMMAND_CHARS", 220),
17503
18117
  maxJustificationChars: numberEnv("MAX_JUSTIFICATION_CHARS", 220),
@@ -17541,21 +18155,92 @@ function buildConfig(cli) {
17541
18155
  }
17542
18156
 
17543
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
+
17544
18169
  function normalizeHazbaseState(raw) {
17545
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);
17546
18173
  return {
17547
18174
  email: cleanText(value.email ?? ""),
17548
18175
  accessToken: cleanText(value.accessToken ?? ""),
17549
18176
  sessionId: cleanText(value.sessionId ?? ""),
17550
18177
  userId: cleanText(value.userId ?? ""),
17551
- deviceBindingId: cleanText(value.deviceBindingId ?? ""),
18178
+ deviceBindingId,
17552
18179
  credentialId: cleanText(value.credentialId ?? ""),
17553
18180
  highTrustToken: cleanText(value.highTrustToken ?? ""),
17554
18181
  highTrustExpiresAt: cleanText(value.highTrustExpiresAt ?? ""),
17555
- accounts: Array.isArray(value.accounts) ? value.accounts : [],
18182
+ accounts,
18183
+ sessionInvalid: value.sessionInvalid === true,
18184
+ sessionInvalidReason: cleanText(value.sessionInvalidReason ?? ""),
18185
+ sessionInvalidAt: cleanText(value.sessionInvalidAt ?? ""),
17556
18186
  };
17557
18187
  }
17558
18188
 
18189
+ function isHazbaseInvalidAppSessionError(error) {
18190
+ const details = error?.details && typeof error.details === "object" ? error.details : {};
18191
+ const rawText = [
18192
+ error?.code,
18193
+ error?.message,
18194
+ details.errorCode,
18195
+ details.code,
18196
+ details.error,
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,
18207
+ ]
18208
+ .map((entry) => cleanText(entry || "").toLowerCase())
18209
+ .filter(Boolean)
18210
+ .join(" ");
18211
+ return statusCode === 401 && (
18212
+ text.includes("invalid app session") ||
18213
+ text.includes("invalid_app_session")
18214
+ );
18215
+ }
18216
+
18217
+ function markHazbaseSessionInvalid(state, reason = "invalid_app_session") {
18218
+ const hazbase = normalizeHazbaseState(state.hazbase);
18219
+ state.hazbase = {
18220
+ ...hazbase,
18221
+ accessToken: "",
18222
+ sessionId: "",
18223
+ highTrustToken: "",
18224
+ highTrustExpiresAt: "",
18225
+ sessionInvalid: true,
18226
+ sessionInvalidReason: cleanText(reason || "invalid_app_session"),
18227
+ sessionInvalidAt: new Date().toISOString(),
18228
+ };
18229
+ return state.hazbase;
18230
+ }
18231
+
18232
+ async function maybeWriteHazbaseSessionExpiredResponse({ error, config, state, res }) {
18233
+ if (!isHazbaseInvalidAppSessionError(error)) {
18234
+ return false;
18235
+ }
18236
+ markHazbaseSessionInvalid(state);
18237
+ await saveState(config.stateFile, state);
18238
+ return writeJson(res, 401, {
18239
+ error: "hazbase-session-expired",
18240
+ message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
18241
+ });
18242
+ }
18243
+
17559
18244
  function hazbasePasskeyLocalHostError() {
17560
18245
  const error = new Error("Open viveworker on its .local hostname to use hazBase passkeys.");
17561
18246
  error.code = "hazbase-passkey-local-host-required";
@@ -17627,6 +18312,117 @@ async function fetchHazbaseMetadata(config, endpoint, timeoutMs = HAZBASE_METADA
17627
18312
  }
17628
18313
  }
17629
18314
 
18315
+ async function getNpmVersionStatus(runtime, config) {
18316
+ const now = Date.now();
18317
+ const cached = runtime.npmVersionStatusCache;
18318
+ if (cached && now - Number(cached.checkedAtMs || 0) < NPM_VERSION_CHECK_CACHE_TTL_MS) {
18319
+ return cached;
18320
+ }
18321
+ if (runtime.npmVersionStatusPending) {
18322
+ return runtime.npmVersionStatusPending;
18323
+ }
18324
+ runtime.npmVersionStatusPending = fetchNpmVersionStatus(config)
18325
+ .then((status) => {
18326
+ runtime.npmVersionStatusCache = status;
18327
+ return status;
18328
+ })
18329
+ .catch((error) => {
18330
+ const fallback = {
18331
+ packageName: config.npmPackageName || "viveworker",
18332
+ distTag: config.npmDistTag || "latest",
18333
+ currentVersion: appPackageVersion,
18334
+ latestVersion: "",
18335
+ updateAvailable: false,
18336
+ checkedAtMs: Date.now(),
18337
+ error: cleanText(error?.message || "npm version check failed"),
18338
+ };
18339
+ runtime.npmVersionStatusCache = fallback;
18340
+ return fallback;
18341
+ })
18342
+ .finally(() => {
18343
+ runtime.npmVersionStatusPending = null;
18344
+ });
18345
+ return runtime.npmVersionStatusPending;
18346
+ }
18347
+
18348
+ async function fetchNpmVersionStatus(config) {
18349
+ const packageName = cleanText(config.npmPackageName || "viveworker") || "viveworker";
18350
+ const distTag = cleanText(config.npmDistTag || "latest") || "latest";
18351
+ const registryBaseUrl = stripTrailingSlash(config.npmRegistryUrl || "https://registry.npmjs.org");
18352
+ const endpoint = `${registryBaseUrl}/${encodeURIComponent(packageName).replace(/^%40/u, "@")}/${encodeURIComponent(distTag)}`;
18353
+ const controller = new AbortController();
18354
+ const timer = setTimeout(() => controller.abort(), NPM_VERSION_CHECK_TIMEOUT_MS);
18355
+ timer.unref?.();
18356
+ try {
18357
+ const response = await fetch(endpoint, {
18358
+ method: "GET",
18359
+ headers: { Accept: "application/json" },
18360
+ signal: controller.signal,
18361
+ });
18362
+ if (!response.ok) {
18363
+ throw new Error(`npm registry returned ${response.status}`);
18364
+ }
18365
+ const payload = await response.json().catch(() => null);
18366
+ const latestVersion = cleanText(payload?.version || "");
18367
+ if (!latestVersion) {
18368
+ throw new Error("npm registry response did not include a version");
18369
+ }
18370
+ return {
18371
+ packageName,
18372
+ distTag,
18373
+ currentVersion: appPackageVersion,
18374
+ latestVersion,
18375
+ updateAvailable: comparePackageVersions(latestVersion, appPackageVersion) > 0,
18376
+ checkedAtMs: Date.now(),
18377
+ error: "",
18378
+ };
18379
+ } catch (error) {
18380
+ if (error?.name === "AbortError") {
18381
+ throw new Error("npm version check timed out");
18382
+ }
18383
+ throw error;
18384
+ } finally {
18385
+ clearTimeout(timer);
18386
+ }
18387
+ }
18388
+
18389
+ function comparePackageVersions(left, right) {
18390
+ const a = parsePackageVersion(left);
18391
+ const b = parsePackageVersion(right);
18392
+ for (let index = 0; index < 3; index += 1) {
18393
+ if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1;
18394
+ }
18395
+ if (!a.prerelease.length && b.prerelease.length) return 1;
18396
+ if (a.prerelease.length && !b.prerelease.length) return -1;
18397
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
18398
+ for (let index = 0; index < length; index += 1) {
18399
+ const av = a.prerelease[index];
18400
+ const bv = b.prerelease[index];
18401
+ if (av == null && bv == null) return 0;
18402
+ if (av == null) return -1;
18403
+ if (bv == null) return 1;
18404
+ const an = /^\d+$/u.test(av) ? Number(av) : null;
18405
+ const bn = /^\d+$/u.test(bv) ? Number(bv) : null;
18406
+ if (an != null && bn != null && an !== bn) return an > bn ? 1 : -1;
18407
+ if (an != null && bn == null) return -1;
18408
+ if (an == null && bn != null) return 1;
18409
+ const cmp = av.localeCompare(bv);
18410
+ if (cmp !== 0) return cmp > 0 ? 1 : -1;
18411
+ }
18412
+ return 0;
18413
+ }
18414
+
18415
+ function parsePackageVersion(value) {
18416
+ const normalized = cleanText(value || "").replace(/^v/iu, "").split("+")[0];
18417
+ const [core, prerelease = ""] = normalized.split("-", 2);
18418
+ const parts = core.split(".").map((part) => Number(part)).map((part) => (Number.isFinite(part) && part >= 0 ? part : 0));
18419
+ while (parts.length < 3) parts.push(0);
18420
+ return {
18421
+ parts: parts.slice(0, 3),
18422
+ prerelease: prerelease ? prerelease.split(".").map((part) => cleanText(part)).filter(Boolean) : [],
18423
+ };
18424
+ }
18425
+
17630
18426
  function mergeHazbaseAccountSummaries(existing, incoming) {
17631
18427
  const merged = new Map();
17632
18428
  for (const entry of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(incoming) ? incoming : [])]) {
@@ -17644,6 +18440,169 @@ function resolveHazbaseAccountForChain(accounts, chainId) {
17644
18440
  return list.find((entry) => Number(entry?.chainId) === normalizedChainId && cleanText(entry?.smartAccountAddress || "")) || null;
17645
18441
  }
17646
18442
 
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
+
18489
+ async function completeHazbaseWalletPaymentApproval({ config, runtime, state, approval }) {
18490
+ const hazbase = normalizeHazbaseState(state.hazbase);
18491
+ if (!hazbase.accessToken) throw codedError("hazbase-auth-required", 401);
18492
+ if (!hazbase.deviceBindingId) throw codedError("hazbase-passkey-required", 409);
18493
+ if (!hazbase.highTrustToken) throw codedError("hazbase-reauth-required", 409);
18494
+ const chainId = Number(approval.rawParams?.chainId || 0);
18495
+ const account = resolveHazbaseAccountForChain(hazbase.accounts, chainId);
18496
+ if (!account?.smartAccountAddress) throw codedError("hazbase-wallet-account-missing", 409);
18497
+
18498
+ let result;
18499
+ try {
18500
+ result = await postHazbaseJson(config, "/api/payments/x402/hazbase-wallet/pay", {
18501
+ paymentRequestId: approval.paymentRequestId,
18502
+ deviceBindingId: hazbase.deviceBindingId,
18503
+ highTrustToken: hazbase.highTrustToken,
18504
+ smartAccountAddress: account.smartAccountAddress,
18505
+ waitForReceipt: true,
18506
+ }, hazbase.accessToken, 130_000);
18507
+ } catch (error) {
18508
+ if (isHazbaseInvalidAppSessionError(error)) {
18509
+ markHazbaseSessionInvalid(state);
18510
+ await saveState(config.stateFile, state);
18511
+ throw codedError("hazbase-session-expired", 401, {
18512
+ message: "Hazbase wallet session expired. Sign in again from Wallet settings.",
18513
+ });
18514
+ }
18515
+ throw error;
18516
+ }
18517
+
18518
+ if (!result?.xPayment) {
18519
+ throw codedError(result?.errorCode || result?.error || "hazbase-wallet-payment-failed", 502);
18520
+ }
18521
+
18522
+ const payload = {
18523
+ paid: true,
18524
+ decision: "accept",
18525
+ paymentRequestId: approval.paymentRequestId,
18526
+ xPayment: cleanText(result.xPayment || ""),
18527
+ payer: cleanText(result.payer || account.smartAccountAddress || ""),
18528
+ chainId: Number(result.chainId || chainId),
18529
+ network: cleanText(result.network || approval.rawParams?.network || ""),
18530
+ relayMode: cleanText(result.relayMode || ""),
18531
+ submittedUserOpHash: cleanText(result.submittedUserOpHash || ""),
18532
+ transactionHash: cleanText(result.transactionHash || ""),
18533
+ payment: result,
18534
+ };
18535
+
18536
+ approval.resolved = true;
18537
+ approval.resolving = false;
18538
+ runtime.nativeApprovalsByToken.delete(approval.token);
18539
+ runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
18540
+ approval.resolveClaudeWaiter?.(payload);
18541
+ const stateChanged = recordActionHistoryItem({
18542
+ config,
18543
+ runtime,
18544
+ state,
18545
+ kind: "approval",
18546
+ stableId: `approval:${approval.requestKey}:${Date.now()}`,
18547
+ token: approval.token,
18548
+ title: approval.title,
18549
+ threadLabel: approval.threadLabel || "",
18550
+ messageText: `${approvalDecisionMessage("accept", config.defaultLocale, approval.provider)}\n\n${approval.messageText}\n\nTransaction: ${payload.transactionHash || payload.submittedUserOpHash}`,
18551
+ summary: formatNotificationBody(approval.messageText, 160) || approvalDecisionMessage("accept", config.defaultLocale, approval.provider),
18552
+ fileRefs: [],
18553
+ diffText: "",
18554
+ diffSource: "",
18555
+ diffAvailable: false,
18556
+ diffAddedLines: 0,
18557
+ diffRemovedLines: 0,
18558
+ outcome: "approved",
18559
+ provider: approval.provider || "viveworker",
18560
+ });
18561
+ if (stateChanged) await saveState(config.stateFile, state);
18562
+ console.log(`[hazbase-wallet-payment] ${approval.requestKey} | ${payload.transactionHash || payload.submittedUserOpHash}`);
18563
+ return { ok: true, ...payload };
18564
+ }
18565
+
18566
+ async function postHazbaseJson(config, endpoint, body, bearerToken, timeoutMs = 30_000) {
18567
+ const baseUrl = stripTrailingSlash(config?.hazbaseApiUrl || "");
18568
+ if (!baseUrl) throw codedError("hazbase-api-not-configured", 503);
18569
+ const controller = new AbortController();
18570
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
18571
+ timer.unref?.();
18572
+ try {
18573
+ const response = await fetch(`${baseUrl}${endpoint}`, {
18574
+ method: "POST",
18575
+ headers: {
18576
+ "content-type": "application/json",
18577
+ accept: "application/json",
18578
+ authorization: `Bearer ${bearerToken}`,
18579
+ "x-request-id": `viveworker_${crypto.randomUUID()}`,
18580
+ },
18581
+ body: JSON.stringify(body),
18582
+ signal: controller.signal,
18583
+ });
18584
+ const payload = await response.json().catch(() => null);
18585
+ const data = payload?.data || payload || {};
18586
+ if (!response.ok) {
18587
+ throw codedError(data?.errorCode || data?.code || data?.error || `hazbase-api-${response.status}`, response.status, data);
18588
+ }
18589
+ return data;
18590
+ } catch (error) {
18591
+ if (error?.name === "AbortError") throw codedError("hazbase-api-timeout", 504);
18592
+ throw error;
18593
+ } finally {
18594
+ clearTimeout(timer);
18595
+ }
18596
+ }
18597
+
18598
+ function codedError(code, statusCode = 500, details = null) {
18599
+ const error = new Error(code);
18600
+ error.code = code;
18601
+ error.statusCode = statusCode;
18602
+ error.details = details;
18603
+ return error;
18604
+ }
18605
+
17647
18606
  function validateConfig(config) {
17648
18607
  let publicBaseUrl = null;
17649
18608
  try {
@@ -18436,8 +19395,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
18436
19395
  );
18437
19396
 
18438
19397
  if (
18439
- JSON.stringify(nextHistoryItems.map((item) => [item.stableId, item.title, item.threadLabel])) !==
18440
- JSON.stringify(runtime.recentHistoryItems.map((item) => [item.stableId, item.title, item.threadLabel]))
19398
+ timelineProjectionChanged(nextHistoryItems, runtime.recentHistoryItems, [
19399
+ "stableId",
19400
+ "title",
19401
+ "threadLabel",
19402
+ ])
18441
19403
  ) {
18442
19404
  runtime.recentHistoryItems = nextHistoryItems;
18443
19405
  state.recentHistoryItems = nextHistoryItems;
@@ -18478,8 +19440,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
18478
19440
  );
18479
19441
 
18480
19442
  if (
18481
- JSON.stringify(nextTimelineEntries.map((entry) => [entry.stableId, entry.title, entry.threadLabel])) !==
18482
- JSON.stringify(runtime.recentTimelineEntries.map((entry) => [entry.stableId, entry.title, entry.threadLabel]))
19443
+ timelineProjectionChanged(nextTimelineEntries, runtime.recentTimelineEntries, [
19444
+ "stableId",
19445
+ "title",
19446
+ "threadLabel",
19447
+ ])
18483
19448
  ) {
18484
19449
  runtime.recentTimelineEntries = nextTimelineEntries;
18485
19450
  state.recentTimelineEntries = nextTimelineEntries;
@@ -18514,8 +19479,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
18514
19479
  );
18515
19480
 
18516
19481
  if (
18517
- JSON.stringify(nextCodeEvents.map((entry) => [entry.stableId, entry.title, entry.threadLabel])) !==
18518
- JSON.stringify(runtime.recentCodeEvents.map((entry) => [entry.stableId, entry.title, entry.threadLabel]))
19482
+ timelineProjectionChanged(nextCodeEvents, runtime.recentCodeEvents, [
19483
+ "stableId",
19484
+ "title",
19485
+ "threadLabel",
19486
+ ])
18519
19487
  ) {
18520
19488
  runtime.recentCodeEvents = nextCodeEvents;
18521
19489
  state.recentCodeEvents = nextCodeEvents;
@@ -18950,6 +19918,37 @@ async function main() {
18950
19918
  process.on("SIGINT", handleSignal);
18951
19919
  process.on("SIGTERM", handleSignal);
18952
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
+
18953
19952
  try {
18954
19953
  if (config.webPushEnabled) {
18955
19954
  webPush.setVapidDetails(