ahp-inspector 1.4.3 → 1.5.1

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/dist/index.js CHANGED
@@ -2664,6 +2664,7 @@ import { Buffer as Buffer2 } from "buffer";
2664
2664
  import { basename as basename6 } from "path";
2665
2665
 
2666
2666
  // ../core/src/correlator.ts
2667
+ var MAX_PENDING = 1e4;
2667
2668
  var Correlator = class {
2668
2669
  #store;
2669
2670
  #pendingRequests = /* @__PURE__ */ new Map();
@@ -2677,6 +2678,14 @@ var Correlator = class {
2677
2678
  this.#store = store;
2678
2679
  this.#unsubscribe = store.subscribe((range) => this.#onAppend(range));
2679
2680
  }
2681
+ /** Outstanding requests awaiting a response (bounded by {@link MAX_PENDING}). */
2682
+ get pendingRequestCount() {
2683
+ return this.#pendingRequests.size;
2684
+ }
2685
+ /** Out-of-order responses awaiting a request (bounded by {@link MAX_PENDING}). */
2686
+ get pendingResponseCount() {
2687
+ return this.#pendingResponses.size;
2688
+ }
2680
2689
  pairOf(idx) {
2681
2690
  const v = this.pairIdx[idx];
2682
2691
  return v === void 0 || v < 0 ? null : v;
@@ -2748,6 +2757,16 @@ var Correlator = class {
2748
2757
  if (displaced !== void 0) {
2749
2758
  this.status[displaced] = "orphan";
2750
2759
  this.#changedIndexes.add(displaced);
2760
+ } else if (this.#pendingRequests.size >= MAX_PENDING) {
2761
+ const oldestKey = this.#pendingRequests.keys().next().value;
2762
+ if (oldestKey !== void 0) {
2763
+ const oldIdx = this.#pendingRequests.get(oldestKey);
2764
+ this.#pendingRequests.delete(oldestKey);
2765
+ if (oldIdx !== void 0) {
2766
+ this.status[oldIdx] = "unmatched";
2767
+ this.#changedIndexes.add(oldIdx);
2768
+ }
2769
+ }
2751
2770
  }
2752
2771
  this.#pendingRequests.set(key, idx);
2753
2772
  this.status[idx] = "pending";
@@ -2765,6 +2784,9 @@ var Correlator = class {
2765
2784
  if (displaced !== void 0) {
2766
2785
  this.status[displaced] = "orphan";
2767
2786
  this.#changedIndexes.add(displaced);
2787
+ } else if (this.#pendingResponses.size >= MAX_PENDING) {
2788
+ const oldestKey = this.#pendingResponses.keys().next().value;
2789
+ if (oldestKey !== void 0) this.#pendingResponses.delete(oldestKey);
2768
2790
  }
2769
2791
  this.#pendingResponses.set(key, idx);
2770
2792
  }
@@ -3025,7 +3047,7 @@ function refreshSummaryStatus(state) {
3025
3047
  }
3026
3048
  return { ...state, summary: { ...state.summary, status } };
3027
3049
  }
3028
- function endTurn(state, turnId, turnState, terminalStatus, error) {
3050
+ function endTurn(state, turnId, turnState, terminalStatus, error, now = Date.now) {
3029
3051
  if (!state.activeTurn || state.activeTurn.id !== turnId) {
3030
3052
  return state;
3031
3053
  }
@@ -3061,7 +3083,7 @@ function endTurn(state, turnId, turnState, terminalStatus, error) {
3061
3083
  ...state,
3062
3084
  turns: [...state.turns, turn],
3063
3085
  activeTurn: void 0,
3064
- summary: { ...state.summary, modifiedAt: Date.now() }
3086
+ summary: { ...state.summary, modifiedAt: now() }
3065
3087
  };
3066
3088
  delete next.inputRequests;
3067
3089
  return {
@@ -3069,7 +3091,7 @@ function endTurn(state, turnId, turnState, terminalStatus, error) {
3069
3091
  summary: { ...next.summary, status: summaryStatus(next, terminalStatus) }
3070
3092
  };
3071
3093
  }
3072
- function upsertInputRequest(state, request) {
3094
+ function upsertInputRequest(state, request, now = Date.now) {
3073
3095
  const existing = state.inputRequests ?? [];
3074
3096
  const idx = existing.findIndex((r) => r.id === request.id);
3075
3097
  const inputRequests = [...existing];
@@ -3085,7 +3107,7 @@ function upsertInputRequest(state, request) {
3085
3107
  summary: {
3086
3108
  ...next.summary,
3087
3109
  status: withStatusFlag(summaryStatus(next), 32 /* IsRead */, false),
3088
- modifiedAt: Date.now()
3110
+ modifiedAt: now()
3089
3111
  }
3090
3112
  };
3091
3113
  }
@@ -3138,7 +3160,7 @@ function updateResponsePart(state, turnId, partId, updater) {
3138
3160
  activeTurn: { ...activeTurn, responseParts }
3139
3161
  };
3140
3162
  }
3141
- function sessionReducer(state, action, log) {
3163
+ function sessionReducer(state, action, log, now = Date.now) {
3142
3164
  switch (action.type) {
3143
3165
  // ── Lifecycle ──────────────────────────────────────────────────────────
3144
3166
  case "session/ready" /* SessionReady */:
@@ -3169,7 +3191,7 @@ function sessionReducer(state, action, log) {
3169
3191
  summary: {
3170
3192
  ...next.summary,
3171
3193
  status: withStatusFlag(summaryStatus(next), 32 /* IsRead */, false),
3172
- modifiedAt: Date.now()
3194
+ modifiedAt: now()
3173
3195
  }
3174
3196
  };
3175
3197
  if (action.queuedMessageId) {
@@ -3202,11 +3224,11 @@ function sessionReducer(state, action, log) {
3202
3224
  }
3203
3225
  };
3204
3226
  case "session/turnComplete" /* SessionTurnComplete */:
3205
- return endTurn(state, action.turnId, "complete" /* Complete */);
3227
+ return endTurn(state, action.turnId, "complete" /* Complete */, void 0, void 0, now);
3206
3228
  case "session/turnCancelled" /* SessionTurnCancelled */:
3207
- return endTurn(state, action.turnId, "cancelled" /* Cancelled */);
3229
+ return endTurn(state, action.turnId, "cancelled" /* Cancelled */, void 0, void 0, now);
3208
3230
  case "session/error" /* SessionError */:
3209
- return endTurn(state, action.turnId, "error" /* Error */, 2 /* Error */, action.error);
3231
+ return endTurn(state, action.turnId, "error" /* Error */, 2 /* Error */, action.error, now);
3210
3232
  // ── Tool Call State Machine ───────────────────────────────────────────
3211
3233
  case "session/toolCallStart" /* SessionToolCallStart */:
3212
3234
  if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
@@ -3378,7 +3400,7 @@ function sessionReducer(state, action, log) {
3378
3400
  case "session/titleChanged" /* SessionTitleChanged */:
3379
3401
  return {
3380
3402
  ...state,
3381
- summary: { ...state.summary, title: action.title, modifiedAt: Date.now() }
3403
+ summary: { ...state.summary, title: action.title, modifiedAt: now() }
3382
3404
  };
3383
3405
  case "session/usage" /* SessionUsage */:
3384
3406
  if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
@@ -3398,7 +3420,7 @@ function sessionReducer(state, action, log) {
3398
3420
  case "session/modelChanged" /* SessionModelChanged */:
3399
3421
  return {
3400
3422
  ...state,
3401
- summary: { ...state.summary, model: action.model, modifiedAt: Date.now() }
3423
+ summary: { ...state.summary, model: action.model, modifiedAt: now() }
3402
3424
  };
3403
3425
  case "session/isReadChanged" /* SessionIsReadChanged */:
3404
3426
  return {
@@ -3439,7 +3461,7 @@ function sessionReducer(state, action, log) {
3439
3461
  },
3440
3462
  summary: {
3441
3463
  ...state.summary,
3442
- modifiedAt: Date.now()
3464
+ modifiedAt: now()
3443
3465
  }
3444
3466
  };
3445
3467
  }
@@ -3457,7 +3479,7 @@ function sessionReducer(state, action, log) {
3457
3479
  },
3458
3480
  summary: {
3459
3481
  ...state.summary,
3460
- modifiedAt: Date.now()
3482
+ modifiedAt: now()
3461
3483
  }
3462
3484
  };
3463
3485
  }
@@ -3540,7 +3562,7 @@ function sessionReducer(state, action, log) {
3540
3562
  ...state,
3541
3563
  turns,
3542
3564
  activeTurn: void 0,
3543
- summary: { ...state.summary, modifiedAt: Date.now() }
3565
+ summary: { ...state.summary, modifiedAt: now() }
3544
3566
  };
3545
3567
  delete next.inputRequests;
3546
3568
  return {
@@ -3550,7 +3572,7 @@ function sessionReducer(state, action, log) {
3550
3572
  }
3551
3573
  // ── Session Input Requests ─────────────────────────────────────────────
3552
3574
  case "session/inputRequested" /* SessionInputRequested */:
3553
- return upsertInputRequest(state, action.request);
3575
+ return upsertInputRequest(state, action.request, now);
3554
3576
  case "session/inputAnswerChanged" /* SessionInputAnswerChanged */: {
3555
3577
  const existing = state.inputRequests;
3556
3578
  const idx = existing?.findIndex((request2) => request2.id === action.requestId) ?? -1;
@@ -3572,7 +3594,7 @@ function sessionReducer(state, action, log) {
3572
3594
  return {
3573
3595
  ...state,
3574
3596
  inputRequests: updated,
3575
- summary: { ...state.summary, modifiedAt: Date.now() }
3597
+ summary: { ...state.summary, modifiedAt: now() }
3576
3598
  };
3577
3599
  }
3578
3600
  case "session/inputCompleted" /* SessionInputCompleted */: {
@@ -3591,7 +3613,7 @@ function sessionReducer(state, action, log) {
3591
3613
  }
3592
3614
  return {
3593
3615
  ...next,
3594
- summary: { ...next.summary, status: summaryStatus(next), modifiedAt: Date.now() }
3616
+ summary: { ...next.summary, status: summaryStatus(next), modifiedAt: now() }
3595
3617
  };
3596
3618
  }
3597
3619
  // ── Pending Messages ──────────────────────────────────────────────────
@@ -4004,15 +4026,20 @@ function applyEnvelope(ctx, envelope, eventIdx, eventTs) {
4004
4026
  details: { actionType: actionTypeOf(envelope.action) }
4005
4027
  });
4006
4028
  };
4007
- resource.state = withEventTime(eventTs, () => {
4029
+ resource.state = (() => {
4008
4030
  if (target.kind === "root") {
4009
4031
  return rootReducer(resource.state, envelope.action, log);
4010
4032
  }
4011
4033
  if (target.kind === "session") {
4012
- return sessionReducer(resource.state, envelope.action, log);
4034
+ return sessionReducer(
4035
+ resource.state,
4036
+ envelope.action,
4037
+ log,
4038
+ () => eventTs
4039
+ );
4013
4040
  }
4014
4041
  return terminalReducer(resource.state, envelope.action, log);
4015
- });
4042
+ })();
4016
4043
  resource.lastAppliedEventIdx = eventIdx;
4017
4044
  resource.lastServerSeq = envelope.serverSeq;
4018
4045
  linkAcceptedIntent(ctx, envelope);
@@ -4088,15 +4115,6 @@ function linkAcceptedIntent(ctx, envelope) {
4088
4115
  }
4089
4116
  ctx.intents[idx] = { ...intent, acceptedByServerSeq: envelope.serverSeq };
4090
4117
  }
4091
- function withEventTime(eventTs, fn) {
4092
- const original = Date.now;
4093
- Date.now = () => eventTs;
4094
- try {
4095
- return fn();
4096
- } finally {
4097
- Date.now = original;
4098
- }
4099
- }
4100
4118
  function readSnapshot(value) {
4101
4119
  if (!isRecord(value) || typeof value.resource !== "string" || typeof value.fromSeq !== "number") {
4102
4120
  return null;
@@ -4680,9 +4698,15 @@ function formatTs(ms) {
4680
4698
  function formatSessionShort(sessionId) {
4681
4699
  const watched = formatResourceWatchChannel(sessionId);
4682
4700
  if (watched) return watched;
4683
- const parts = sessionId.split(/[/:]+/).filter(Boolean);
4684
- let label = parts.at(-1) ?? sessionId;
4685
- label = label.replace(/^session[-_:]?/i, "");
4701
+ return shortenIdLabel(sessionId, /^session[-_:]?/i);
4702
+ }
4703
+ function formatTurnShort(turnId) {
4704
+ return shortenIdLabel(turnId, /^turn[-_:]?/i);
4705
+ }
4706
+ function shortenIdLabel(id, stripPrefix) {
4707
+ const parts = id.split(/[/:]+/).filter(Boolean);
4708
+ let label = parts.at(-1) ?? id;
4709
+ label = label.replace(stripPrefix, "");
4686
4710
  label = label.replace(/[-_]\d{4}[-_]\d{2}[-_]\d{2}$/u, "");
4687
4711
  if (/^[0-9a-f]{16,}$/iu.test(label)) return label.slice(-8);
4688
4712
  const uuidFirstSegment = label.match(/^[0-9a-f]{8}(?=-[0-9a-f]{4}-)/iu)?.[0];
@@ -4788,7 +4812,7 @@ function projectRow(event, idx, status, latencyMs, extras = DEFAULT_EXTRAS, pair
4788
4812
  sessionId: session,
4789
4813
  sessionShort: session ? formatSessionShort(session) : null,
4790
4814
  turnId: turn,
4791
- turnShort: turn ? turn.slice(-6) : null,
4815
+ turnShort: turn ? formatTurnShort(turn) : null,
4792
4816
  keyId: idStr ? idStr.length > 12 ? idStr.slice(0, 12) : idStr : null,
4793
4817
  status,
4794
4818
  latencyMs,
@@ -5182,25 +5206,44 @@ async function createAppState(opts) {
5182
5206
  };
5183
5207
  }
5184
5208
 
5209
+ // ../server/src/origin.ts
5210
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
5211
+ function isAllowedOrigin(origin) {
5212
+ if (origin === void 0 || origin === null || origin === "") return true;
5213
+ if (origin === "null") return true;
5214
+ if (origin.startsWith("vscode-webview://")) return true;
5215
+ let url;
5216
+ try {
5217
+ url = new URL(origin);
5218
+ } catch {
5219
+ return false;
5220
+ }
5221
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
5222
+ return LOOPBACK_HOSTS.has(url.hostname);
5223
+ }
5224
+
5185
5225
  // ../server/src/cors.ts
5226
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
5186
5227
  var corsMiddleware = async (c, next) => {
5187
5228
  const origin = c.req.header("origin");
5229
+ const allowed = isAllowedOrigin(origin);
5188
5230
  if (c.req.method === "OPTIONS") {
5189
5231
  const reqHeaders = c.req.header("access-control-request-headers") ?? "*";
5190
5232
  const reqMethod = c.req.header("access-control-request-method") ?? "GET";
5191
- return new Response(null, {
5192
- status: 204,
5193
- headers: {
5194
- "access-control-allow-origin": origin ?? "*",
5195
- "access-control-allow-methods": `${reqMethod}, GET, POST, OPTIONS`,
5196
- "access-control-allow-headers": reqHeaders,
5197
- "access-control-max-age": "600",
5198
- vary: "Origin"
5199
- }
5200
- });
5233
+ const headers = {
5234
+ "access-control-allow-methods": `${reqMethod}, GET, POST, OPTIONS`,
5235
+ "access-control-allow-headers": reqHeaders,
5236
+ "access-control-max-age": "600",
5237
+ vary: "Origin"
5238
+ };
5239
+ if (allowed) headers["access-control-allow-origin"] = origin ?? "*";
5240
+ return new Response(null, { status: 204, headers });
5241
+ }
5242
+ if (origin !== void 0 && !allowed && MUTATING_METHODS.has(c.req.method)) {
5243
+ return c.json({ code: "forbidden-origin", message: "forbidden-origin" }, 403);
5201
5244
  }
5202
5245
  await next();
5203
- if (origin) {
5246
+ if (allowed && origin !== void 0) {
5204
5247
  c.res.headers.set("access-control-allow-origin", origin);
5205
5248
  c.res.headers.append("vary", "Origin");
5206
5249
  }
@@ -7973,7 +8016,7 @@ function registerSearchRoutes(app, sessions) {
7973
8016
  // ../server/src/session-routes.ts
7974
8017
  function registerSessionRoutes(app, sessions) {
7975
8018
  app.get("/api/sessions/discover", async (c) => {
7976
- const r = await discoverVsCodeLogs();
8019
+ const r = await sessions.discover();
7977
8020
  return c.json({ candidates: r.candidates, truncated: r.truncated });
7978
8021
  });
7979
8022
  app.post("/api/sessions/open", async (c) => {
@@ -8167,7 +8210,9 @@ var streamSSE = (c, cb, onError) => {
8167
8210
  // ../server/src/sse-routes.ts
8168
8211
  var SNAPSHOT_CHUNK = 2e3;
8169
8212
  var PING_INTERVAL_MS = 2e4;
8213
+ var MAX_SSE_CONNECTIONS = 8;
8170
8214
  function registerLogRoutes(app, sessions) {
8215
+ let activeStreams = 0;
8171
8216
  app.get("/api/log/meta", (c) => {
8172
8217
  const a = sessions.current();
8173
8218
  if (!a) return c.body(null, 204);
@@ -8178,127 +8223,135 @@ function registerLogRoutes(app, sessions) {
8178
8223
  if (!initial) {
8179
8224
  return c.json({ code: "no-active-log" }, 409);
8180
8225
  }
8226
+ if (activeStreams >= MAX_SSE_CONNECTIONS) {
8227
+ return c.json({ code: "too-many-streams" }, 503);
8228
+ }
8229
+ activeStreams++;
8181
8230
  return streamSSE(c, async (stream2) => {
8182
- const a = initial;
8183
- const queue = [];
8184
- let queueStart = 0;
8185
- let pumping = false;
8186
- let snapshotStreaming = true;
8187
- const queuedFrameCount = () => queue.length - queueStart;
8188
- const pump = async () => {
8189
- if (pumping || snapshotStreaming) return;
8190
- pumping = true;
8191
- try {
8192
- while (queuedFrameCount() > 0 && !stream2.aborted && !stream2.closed) {
8193
- const msg = queue[queueStart++];
8194
- if (!msg) break;
8195
- try {
8196
- await stream2.writeSSE({ event: msg.kind, data: JSON.stringify(msg) });
8197
- } catch {
8198
- return;
8231
+ try {
8232
+ const a = initial;
8233
+ const queue = [];
8234
+ let queueStart = 0;
8235
+ let pumping = false;
8236
+ let snapshotStreaming = true;
8237
+ const queuedFrameCount = () => queue.length - queueStart;
8238
+ const pump = async () => {
8239
+ if (pumping || snapshotStreaming) return;
8240
+ pumping = true;
8241
+ try {
8242
+ while (queuedFrameCount() > 0 && !stream2.aborted && !stream2.closed) {
8243
+ const msg = queue[queueStart++];
8244
+ if (!msg) break;
8245
+ try {
8246
+ await stream2.writeSSE({ event: msg.kind, data: JSON.stringify(msg) });
8247
+ } catch {
8248
+ return;
8249
+ }
8199
8250
  }
8251
+ } finally {
8252
+ if (queueStart === queue.length) {
8253
+ queue.length = 0;
8254
+ queueStart = 0;
8255
+ }
8256
+ pumping = false;
8200
8257
  }
8201
- } finally {
8202
- if (queueStart === queue.length) {
8203
- queue.length = 0;
8204
- queueStart = 0;
8258
+ };
8259
+ const off = a.appState.subscribe((msg) => {
8260
+ queue.push(msg);
8261
+ void pump();
8262
+ });
8263
+ const snap = a.appState.snapshot();
8264
+ await stream2.writeSSE({
8265
+ event: "snapshot-begin",
8266
+ data: JSON.stringify({ meta: snap.meta, total: snap.rows.length })
8267
+ });
8268
+ for (let i = 0; i < snap.rows.length; i += SNAPSHOT_CHUNK) {
8269
+ if (stream2.aborted || stream2.closed) {
8270
+ off();
8271
+ return;
8205
8272
  }
8206
- pumping = false;
8273
+ const chunk = snap.rows.slice(i, i + SNAPSHOT_CHUNK);
8274
+ await stream2.writeSSE({
8275
+ event: "snapshot-chunk",
8276
+ data: JSON.stringify({ rows: chunk, from: i })
8277
+ });
8278
+ await stream2.sleep(0);
8207
8279
  }
8208
- };
8209
- const off = a.appState.subscribe((msg) => {
8210
- queue.push(msg);
8211
- void pump();
8212
- });
8213
- const snap = a.appState.snapshot();
8214
- await stream2.writeSSE({
8215
- event: "snapshot-begin",
8216
- data: JSON.stringify({ meta: snap.meta, total: snap.rows.length })
8217
- });
8218
- for (let i = 0; i < snap.rows.length; i += SNAPSHOT_CHUNK) {
8219
8280
  if (stream2.aborted || stream2.closed) {
8220
8281
  off();
8221
8282
  return;
8222
8283
  }
8223
- const chunk = snap.rows.slice(i, i + SNAPSHOT_CHUNK);
8224
- await stream2.writeSSE({
8225
- event: "snapshot-chunk",
8226
- data: JSON.stringify({ rows: chunk, from: i })
8227
- });
8228
- await stream2.sleep(0);
8229
- }
8230
- if (stream2.aborted || stream2.closed) {
8231
- off();
8232
- return;
8233
- }
8234
- await stream2.writeSSE({ event: "snapshot-end", data: "{}" });
8235
- if (snap.loadProgress.phase !== "idle") {
8236
- await stream2.writeSSE({
8237
- event: snap.loadProgress.kind,
8238
- data: JSON.stringify(snap.loadProgress)
8284
+ await stream2.writeSSE({ event: "snapshot-end", data: "{}" });
8285
+ if (snap.loadProgress.phase !== "idle") {
8286
+ await stream2.writeSSE({
8287
+ event: snap.loadProgress.kind,
8288
+ data: JSON.stringify(snap.loadProgress)
8289
+ });
8290
+ }
8291
+ snapshotStreaming = false;
8292
+ if (queuedFrameCount() > 0) {
8293
+ const queuedRows = queue.slice(queueStart).reduce((total, msg) => total + (msg.kind === "append" ? msg.rows.length : 0), 0);
8294
+ const backlog = {
8295
+ kind: "stream-backlog",
8296
+ queuedFrames: queuedFrameCount(),
8297
+ queuedRows
8298
+ };
8299
+ await stream2.writeSSE({ event: backlog.kind, data: JSON.stringify(backlog) });
8300
+ await pump();
8301
+ const clearedBacklog = {
8302
+ kind: "stream-backlog",
8303
+ queuedFrames: 0,
8304
+ queuedRows: 0
8305
+ };
8306
+ await stream2.writeSSE({
8307
+ event: clearedBacklog.kind,
8308
+ data: JSON.stringify(clearedBacklog)
8309
+ });
8310
+ } else {
8311
+ await pump();
8312
+ }
8313
+ let endRequested = false;
8314
+ const offChange = sessions.onChange(async () => {
8315
+ if (endRequested) return;
8316
+ endRequested = true;
8317
+ try {
8318
+ await stream2.writeSSE({ event: "log-reset", data: "{}" });
8319
+ } catch {
8320
+ }
8239
8321
  });
8240
- }
8241
- snapshotStreaming = false;
8242
- if (queuedFrameCount() > 0) {
8243
- const queuedRows = queue.slice(queueStart).reduce((total, msg) => total + (msg.kind === "append" ? msg.rows.length : 0), 0);
8244
- const backlog = {
8245
- kind: "stream-backlog",
8246
- queuedFrames: queuedFrameCount(),
8247
- queuedRows
8248
- };
8249
- await stream2.writeSSE({ event: backlog.kind, data: JSON.stringify(backlog) });
8250
- await pump();
8251
- const clearedBacklog = {
8252
- kind: "stream-backlog",
8253
- queuedFrames: 0,
8254
- queuedRows: 0
8255
- };
8256
- await stream2.writeSSE({
8257
- event: clearedBacklog.kind,
8258
- data: JSON.stringify(clearedBacklog)
8322
+ const pinger = setInterval(() => {
8323
+ if (stream2.aborted || stream2.closed) return;
8324
+ stream2.writeSSE({ event: "ping", data: "{}" }).catch(() => {
8325
+ });
8326
+ }, PING_INTERVAL_MS);
8327
+ await new Promise((resolveWait) => {
8328
+ if (stream2.aborted || stream2.closed) {
8329
+ resolveWait();
8330
+ return;
8331
+ }
8332
+ stream2.onAbort(() => resolveWait());
8333
+ const tick = setInterval(() => {
8334
+ if (endRequested) {
8335
+ clearInterval(tick);
8336
+ resolveWait();
8337
+ }
8338
+ }, 50);
8259
8339
  });
8260
- } else {
8261
- await pump();
8262
- }
8263
- let endRequested = false;
8264
- const offChange = sessions.onChange(async () => {
8265
- if (endRequested) return;
8266
- endRequested = true;
8340
+ clearInterval(pinger);
8267
8341
  try {
8268
- await stream2.writeSSE({ event: "log-reset", data: "{}" });
8342
+ off();
8269
8343
  } catch {
8270
8344
  }
8271
- });
8272
- const pinger = setInterval(() => {
8273
- if (stream2.aborted || stream2.closed) return;
8274
- stream2.writeSSE({ event: "ping", data: "{}" }).catch(() => {
8275
- });
8276
- }, PING_INTERVAL_MS);
8277
- await new Promise((resolveWait) => {
8278
- if (stream2.aborted || stream2.closed) {
8279
- resolveWait();
8280
- return;
8345
+ try {
8346
+ offChange();
8347
+ } catch {
8281
8348
  }
8282
- stream2.onAbort(() => resolveWait());
8283
- const tick = setInterval(() => {
8284
- if (endRequested) {
8285
- clearInterval(tick);
8286
- resolveWait();
8287
- }
8288
- }, 50);
8289
- });
8290
- clearInterval(pinger);
8291
- try {
8292
- off();
8293
- } catch {
8294
- }
8295
- try {
8296
- offChange();
8297
- } catch {
8298
- }
8299
- try {
8300
- await stream2.writeSSE({ event: "bye", data: "{}" });
8301
- } catch {
8349
+ try {
8350
+ await stream2.writeSSE({ event: "bye", data: "{}" });
8351
+ } catch {
8352
+ }
8353
+ } finally {
8354
+ activeStreams--;
8302
8355
  }
8303
8356
  });
8304
8357
  });
@@ -8653,6 +8706,41 @@ import { tmpdir } from "os";
8653
8706
  import { extname as extname2, join as join6, basename as pathBasename } from "path";
8654
8707
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
8655
8708
  var UPLOAD_DIR_PREFIX = "ahp-inspector-upload-";
8709
+ var BodyTooLargeError = class extends Error {
8710
+ };
8711
+ async function readBodyCapped(body, maxBytes) {
8712
+ if (!body) return new Uint8Array(0);
8713
+ const reader = body.getReader();
8714
+ const chunks = [];
8715
+ let total = 0;
8716
+ for (; ; ) {
8717
+ let result;
8718
+ try {
8719
+ result = await reader.read();
8720
+ } catch {
8721
+ reader.releaseLock();
8722
+ throw new Error("read-failed");
8723
+ }
8724
+ if (result.done) break;
8725
+ const value = result.value;
8726
+ if (!value) continue;
8727
+ total += value.byteLength;
8728
+ if (total > maxBytes) {
8729
+ await reader.cancel().catch(() => {
8730
+ });
8731
+ throw new BodyTooLargeError();
8732
+ }
8733
+ chunks.push(value);
8734
+ }
8735
+ reader.releaseLock();
8736
+ const out = new Uint8Array(total);
8737
+ let offset = 0;
8738
+ for (const chunk of chunks) {
8739
+ out.set(chunk, offset);
8740
+ offset += chunk.byteLength;
8741
+ }
8742
+ return out;
8743
+ }
8656
8744
  function sanitizeFilename(raw2) {
8657
8745
  let decoded;
8658
8746
  try {
@@ -8703,8 +8791,9 @@ function createUploadStore() {
8703
8791
  }
8704
8792
  return { write, cleanupAllExcept, disposeAll };
8705
8793
  }
8706
- function registerUploadRoutes(app, sessions) {
8794
+ function registerUploadRoutes(app, sessions, opts) {
8707
8795
  const store = createUploadStore();
8796
+ const maxBytes = opts?.maxUploadBytes ?? MAX_UPLOAD_BYTES;
8708
8797
  const unsubscribe = sessions.onChange((active) => {
8709
8798
  if (active === null) {
8710
8799
  void store.disposeAll();
@@ -8725,24 +8814,24 @@ function registerUploadRoutes(app, sessions) {
8725
8814
  }
8726
8815
  const lengthHeader = c.req.header("content-length");
8727
8816
  const declaredLength = lengthHeader ? Number.parseInt(lengthHeader, 10) : Number.NaN;
8728
- if (Number.isFinite(declaredLength) && declaredLength > MAX_UPLOAD_BYTES) {
8817
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
8729
8818
  return c.json({ code: "too-large", message: "too-large" }, 413);
8730
8819
  }
8731
- let buf;
8820
+ let bytes;
8732
8821
  try {
8733
- buf = await c.req.arrayBuffer();
8734
- } catch {
8822
+ bytes = await readBodyCapped(c.req.raw.body, maxBytes);
8823
+ } catch (err) {
8824
+ if (err instanceof BodyTooLargeError) {
8825
+ return c.json({ code: "too-large", message: "too-large" }, 413);
8826
+ }
8735
8827
  return c.json({ code: "bad-request", message: "could not read body" }, 400);
8736
8828
  }
8737
- if (buf.byteLength === 0) {
8829
+ if (bytes.byteLength === 0) {
8738
8830
  return c.json({ code: "bad-request", message: "empty body" }, 400);
8739
8831
  }
8740
- if (buf.byteLength > MAX_UPLOAD_BYTES) {
8741
- return c.json({ code: "too-large", message: "too-large" }, 413);
8742
- }
8743
8832
  let tempPath;
8744
8833
  try {
8745
- tempPath = await store.write(safeName, new Uint8Array(buf));
8834
+ tempPath = await store.write(safeName, bytes);
8746
8835
  } catch {
8747
8836
  return c.json({ code: "io-error", message: "io-error" }, 500);
8748
8837
  }
@@ -8875,6 +8964,7 @@ function createLogSessionManager(opts) {
8875
8964
  }
8876
8965
  return {
8877
8966
  current: () => active,
8967
+ discover: () => opts.host.discoverLogs(),
8878
8968
  async open(input) {
8879
8969
  const next = chain.then(async () => {
8880
8970
  if ("path" in input) return doOpen(input.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ahp-inspector",
3
- "version": "1.4.3",
3
+ "version": "1.5.1",
4
4
  "type": "module",
5
5
  "description": "Local-first viewer for VS Code Agent Host Protocol (AHP) JSONL logs.",
6
6
  "keywords": [