copilot-tap-extension 2.0.5 → 2.0.7

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.
@@ -2284,7 +2284,7 @@ var require_websocket = __commonJS({
2284
2284
  var tls = __require("tls");
2285
2285
  var { randomBytes: randomBytes3, createHash } = __require("crypto");
2286
2286
  var { Duplex, Readable } = __require("stream");
2287
- var { URL } = __require("url");
2287
+ var { URL: URL2 } = __require("url");
2288
2288
  var PerMessageDeflate2 = require_permessage_deflate();
2289
2289
  var Receiver2 = require_receiver();
2290
2290
  var Sender2 = require_sender();
@@ -2785,11 +2785,11 @@ var require_websocket = __commonJS({
2785
2785
  );
2786
2786
  }
2787
2787
  let parsedUrl;
2788
- if (address instanceof URL) {
2788
+ if (address instanceof URL2) {
2789
2789
  parsedUrl = address;
2790
2790
  } else {
2791
2791
  try {
2792
- parsedUrl = new URL(address);
2792
+ parsedUrl = new URL2(address);
2793
2793
  } catch {
2794
2794
  throw new SyntaxError(`Invalid URL: ${address}`);
2795
2795
  }
@@ -2926,7 +2926,7 @@ var require_websocket = __commonJS({
2926
2926
  req.abort();
2927
2927
  let addr;
2928
2928
  try {
2929
- addr = new URL(location, address);
2929
+ addr = new URL2(location, address);
2930
2930
  } catch (e) {
2931
2931
  const err = new SyntaxError(`Invalid URL: ${location}`);
2932
2932
  emitErrorAndClose(websocket, err);
@@ -4568,12 +4568,12 @@ function createEmitterTools(deps) {
4568
4568
  properties: {
4569
4569
  name: { type: "string", description: "Unique emitter name." },
4570
4570
  command: { type: "string", description: "Shell command to run (creates a CommandEmitter). Output goes through EventFilter rules to determine whether lines are kept, surfaced, or injected. Use for log tailing, process monitoring, or any external command with stdout." },
4571
- prompt: { type: "string", description: "Prompt to send to the agent (creates a PromptEmitter). Always injects \u2014 bypasses EventFilter entirely. Use for repeated agent tasks, status checks, or simple messages." },
4571
+ prompt: { type: "string", description: "Prompt to send to the agent (creates a PromptEmitter). Always injects \u2014 bypasses EventFilter entirely. Use for repeated agent tasks, status checks, simple messages, or autonomous goal loops." },
4572
4572
  description: { type: "string", description: "Short summary." },
4573
4573
  channel: { type: "string", description: "EventStream to receive accepted events." },
4574
4574
  cwd: { type: "string", description: "Optional subdirectory relative to the session cwd. Absolute paths and paths that escape the session cwd are rejected." },
4575
- every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d (maximum about 24 days). Use 'idle' for prompts that re-run whenever the session is idle. When omitted, commands run continuously and prompts run once." },
4576
- everySchedule: { type: "array", minItems: 1, items: { type: "string" }, description: "Optional backoff schedule \u2014 an ordered non-empty list of interval strings (e.g. ['10s','20s','30s','1m','2m','5m','10m']; each maximum about 24 days). The emitter uses each interval in sequence, then repeats the last one forever. Overrides 'every' when provided. Cannot be 'idle' entries." },
4575
+ every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d (maximum about 24 days). Use 'idle' for conservative prompt loops that re-run only when the session is idle. When omitted, commands run continuously and prompts run once." },
4576
+ everySchedule: { type: "array", minItems: 1, items: { type: "string" }, description: "Optional backoff schedule \u2014 an ordered non-empty list of interval strings (e.g. ['10s','20s','30s','1m','2m','5m','10m']; each maximum about 24 days). The emitter uses each interval in sequence, then repeats the last one forever. Overrides 'every' when provided. Cannot be 'idle' entries. Prefer this for autopilot-compatible goal loops that may need timed nudges while the session stays busy." },
4577
4577
  lifespan: policyLifespanParameter(),
4578
4578
  ownership: policyOwnershipParameter(),
4579
4579
  scope: policyScopeParameter(),
@@ -4583,7 +4583,7 @@ function createEmitterTools(deps) {
4583
4583
  eventFilter: EVENT_FILTER_PARAMETER_SCHEMA,
4584
4584
  subscribe: { type: "boolean", description: "Whether to attach a session injector to the stream as part of emitter creation." },
4585
4585
  delivery: { type: "string", description: "Session injector delivery mode. 'important'/'inject' only send inject-outcome lines, 'surface' surfaces surface outcomes and sends inject outcomes, 'all' surfaces all accepted lines while inject outcomes still push into the conversation, and 'keep'/'drop' store without proactive delivery." },
4586
- maxRuns: { type: "integer", description: "Maximum number of iterations before the emitter auto-completes. Useful for idle and timed loops." },
4586
+ maxRuns: { type: "integer", description: "Maximum number of real iterations before the emitter stops. Useful for idle and timed loops. For goal loops this is a budget limit, not proof that the objective is complete." },
4587
4587
  force: policyForceParameter("emitter")
4588
4588
  },
4589
4589
  required: ["name"]
@@ -4643,14 +4643,59 @@ function createEmitterTools(deps) {
4643
4643
  ];
4644
4644
  }
4645
4645
 
4646
+ // src/tools/diagnostics.mjs
4647
+ function renderCanvasOpenResult(result) {
4648
+ return [
4649
+ "Opened tap diagnostics canvas.",
4650
+ result?.title ? `title=${result.title}` : null,
4651
+ result?.status ? `status=${result.status}` : null,
4652
+ result?.url ? `url=${result.url}` : null,
4653
+ result?.instanceId ? `instanceId=${result.instanceId}` : null,
4654
+ result?.availability ? `availability=${result.availability}` : null
4655
+ ].filter(Boolean).join("\n");
4656
+ }
4657
+ function createDiagnosticsTools(deps) {
4658
+ const diagnostics2 = deps.diagnostics ?? deps.runtime;
4659
+ if (!diagnostics2 || typeof diagnostics2.openCanvas !== "function") {
4660
+ return [];
4661
+ }
4662
+ return [
4663
+ {
4664
+ name: "tap_open_diagnostics_canvas",
4665
+ description: "Opens or focuses the Tap diagnostics canvas, a live flight recorder for streams, emitters, providers, logs, injection queues, and session events.",
4666
+ parameters: {
4667
+ type: "object",
4668
+ properties: {
4669
+ instanceId: {
4670
+ type: "string",
4671
+ description: "Stable canvas instance id. Reusing the same id focuses the existing diagnostics canvas."
4672
+ },
4673
+ limit: {
4674
+ type: "integer",
4675
+ minimum: 10,
4676
+ maximum: 300,
4677
+ description: "Maximum retained rows to show per diagnostics section."
4678
+ }
4679
+ }
4680
+ },
4681
+ handler: wrapToolHandler("tap_open_diagnostics_canvas", {}, async (args) => {
4682
+ const result = await diagnostics2.openCanvas(args ?? {});
4683
+ return renderCanvasOpenResult(result);
4684
+ })
4685
+ }
4686
+ ];
4687
+ }
4688
+
4646
4689
  // src/tools/index.mjs
4647
4690
  function createTools(deps) {
4648
4691
  const source = deps?.tools ?? deps?.runtime?.tools ?? {};
4649
4692
  const streams = deps?.streams ?? source.streams ?? deps?.runtime;
4650
4693
  const emitters = deps?.emitters ?? source.emitters ?? deps?.runtime;
4694
+ const diagnostics2 = deps?.diagnostics ?? source.diagnostics ?? deps?.runtime;
4651
4695
  return [
4652
4696
  ...createStreamTools({ streams }),
4653
- ...createEmitterTools({ emitters })
4697
+ ...createEmitterTools({ emitters }),
4698
+ ...createDiagnosticsTools({ diagnostics: diagnostics2 })
4654
4699
  ];
4655
4700
  }
4656
4701
 
@@ -6199,6 +6244,9 @@ function createProviderConnection(ws, options, adapters = {}) {
6199
6244
  get tools() {
6200
6245
  return tools;
6201
6246
  },
6247
+ get pendingCallCount() {
6248
+ return pendingCalls.size;
6249
+ },
6202
6250
  sendToolCall: sendToolCallMsg,
6203
6251
  sendToolCancel: sendToolCancelMsg,
6204
6252
  sendLifecycle,
@@ -6300,6 +6348,20 @@ function createDefaultPathAdapter() {
6300
6348
  join: (...parts) => path2.join(...parts)
6301
6349
  };
6302
6350
  }
6351
+ function formatGatewayBindMessage(err, { host, port }) {
6352
+ const message = String(err?.message ?? err ?? "unknown error");
6353
+ const code = String(err?.code ?? "");
6354
+ const isPortInUse = code === "EADDRINUSE" || /\bEADDRINUSE\b|address already in use/i.test(message);
6355
+ if (!isPortInUse) {
6356
+ return `Provider gateway could not start on ${host}:${port}: ${message}`;
6357
+ }
6358
+ return [
6359
+ `Provider gateway mesh already has an owner at ${host}:${port}; this tap session is joining without binding another listener.`,
6360
+ "Core tap tools remain available.",
6361
+ "External providers should keep using the existing gateway.",
6362
+ "No action is needed unless provider tools are missing."
6363
+ ].join(" ");
6364
+ }
6303
6365
  function createProviderGateway(options = {}, adapters = {}) {
6304
6366
  const {
6305
6367
  tapTools,
@@ -6592,7 +6654,7 @@ function createProviderGateway(options = {}, adapters = {}) {
6592
6654
  }
6593
6655
  }
6594
6656
  function resetFailedStart(server, err) {
6595
- log(`Failed to start provider gateway on port ${GATEWAY_PORT}: ${err.message}`);
6657
+ log(formatGatewayBindMessage(err, { host, port: GATEWAY_PORT }));
6596
6658
  starting = false;
6597
6659
  if (wss === server) {
6598
6660
  wss = null;
@@ -6660,6 +6722,41 @@ function createProviderGateway(options = {}, adapters = {}) {
6660
6722
  const tap = currentTapTools || (typeof tapTools === "function" ? tapTools() : []);
6661
6723
  return registry.buildSessionTools(tap, dispatchToolCall);
6662
6724
  }
6725
+ function projectProviderTool(tool) {
6726
+ return {
6727
+ name: tool.name,
6728
+ description: tool.description ?? "",
6729
+ timeout: tool.timeout ?? null
6730
+ };
6731
+ }
6732
+ function getDiagnosticState() {
6733
+ const connections = [...connectionsByProviderId.values()];
6734
+ return {
6735
+ running,
6736
+ starting,
6737
+ host,
6738
+ port: GATEWAY_PORT,
6739
+ reloadPending,
6740
+ reloadTimerActive: Boolean(reloadTimer),
6741
+ tokenPresent: Boolean(token),
6742
+ providerTokenFilePath: providerTokenFilePath ? "[redacted]" : null,
6743
+ gracefulShutdownActive,
6744
+ connectionCount: connectionsByWs.size,
6745
+ boundConnectionCount: connections.filter((conn) => conn.state === CONNECTION_STATE.BOUND).length,
6746
+ providers: registry.listProviders().map((provider) => {
6747
+ const conn = connectionsByProviderId.get(provider.id);
6748
+ return {
6749
+ id: provider.id,
6750
+ name: provider.name,
6751
+ sessionId: provider.sessionId,
6752
+ state: conn?.state ?? "unknown",
6753
+ pendingCallCount: conn?.pendingCallCount ?? 0,
6754
+ toolCount: provider.tools.length,
6755
+ tools: provider.tools.map(projectProviderTool)
6756
+ };
6757
+ })
6758
+ };
6759
+ }
6663
6760
  function dispatchToolCall(providerId, toolName, callId, args) {
6664
6761
  const conn = connectionsByProviderId.get(providerId);
6665
6762
  if (!conn || conn.state === CONNECTION_STATE.DISCONNECTED) {
@@ -6696,6 +6793,7 @@ function createProviderGateway(options = {}, adapters = {}) {
6696
6793
  getToken,
6697
6794
  getRegistry,
6698
6795
  getAllTools,
6796
+ getDiagnosticState,
6699
6797
  dispatchToolCall,
6700
6798
  broadcastLifecycle,
6701
6799
  onToolsChanged,
@@ -8905,7 +9003,7 @@ function buildCompleteMessage(state) {
8905
9003
  return `Emitter '${state.name}' completed one run of ${state.emitterType} work.`;
8906
9004
  }
8907
9005
  if (state.maxRuns && state.runCount >= state.maxRuns) {
8908
- return `Emitter '${state.name}' completed ${state.runCount} of ${state.maxRuns} runs.`;
9006
+ return `Emitter '${state.name}' reached its run budget (${state.runCount} of ${state.maxRuns} runs). This stops the emitter; goal-style loops must use their final evidence summary to decide whether the objective is complete.`;
8909
9007
  }
8910
9008
  return null;
8911
9009
  }
@@ -9060,12 +9158,12 @@ function computeDeferredIterationTransition(currentState, state) {
9060
9158
  };
9061
9159
  }
9062
9160
  function computeFailedIterationTransition(currentState, state, event, result) {
9063
- if (isRunBudgetExhausted(state)) {
9064
- return computeRunBudgetExhaustedTransition(currentState, state, event, result);
9065
- }
9066
9161
  if (result.deferred) {
9067
9162
  return computeDeferredIterationTransition(currentState, state);
9068
9163
  }
9164
+ if (isRunBudgetExhausted(state)) {
9165
+ return computeRunBudgetExhaustedTransition(currentState, state, event, result);
9166
+ }
9069
9167
  if (state.runSchedule === RUN_SCHEDULE.ONE_TIME) {
9070
9168
  return {
9071
9169
  currentState,
@@ -9405,11 +9503,12 @@ async function runPromptIteration(emitter, context) {
9405
9503
  } catch (error) {
9406
9504
  const message = error?.message ?? String(error ?? "unknown error");
9407
9505
  const sessionNotAttached = isSessionNotAttachedMessage(message);
9506
+ const deferred = sessionNotAttached || (emitter.runSchedule === RUN_SCHEDULE.TIMED || emitter.runSchedule === RUN_SCHEDULE.IDLE) && /\bsession\.idle\b/i.test(message);
9408
9507
  return {
9409
9508
  ok: false,
9410
9509
  error: message,
9411
- deferred: sessionNotAttached || (emitter.runSchedule === RUN_SCHEDULE.TIMED || emitter.runSchedule === RUN_SCHEDULE.IDLE) && /\bsession\.idle\b/i.test(message),
9412
- consumeRun: sessionNotAttached ? false : true,
9510
+ deferred,
9511
+ consumeRun: deferred ? false : true,
9413
9512
  deferredReason: sessionNotAttached ? "session-not-attached" : null
9414
9513
  };
9415
9514
  }
@@ -10258,6 +10357,16 @@ function createSessionPort(initialSession = null) {
10258
10357
  }
10259
10358
  return session2.sendAndWait({ prompt });
10260
10359
  }
10360
+ async function openCanvas(params) {
10361
+ if (!session2) {
10362
+ throw new LifecycleError("Session is not attached; cannot open canvas.");
10363
+ }
10364
+ const canvasApi = session2.rpc?.canvas;
10365
+ if (!canvasApi || typeof canvasApi.open !== "function") {
10366
+ throw new LifecycleError("Canvas renderer API is not available in this Copilot session.");
10367
+ }
10368
+ return canvasApi.open(params);
10369
+ }
10261
10370
  function registerTools(tools) {
10262
10371
  if (!session2) return;
10263
10372
  try {
@@ -10293,6 +10402,7 @@ function createSessionPort(initialSession = null) {
10293
10402
  log,
10294
10403
  send,
10295
10404
  sendAndWait,
10405
+ openCanvas,
10296
10406
  registerTools,
10297
10407
  reloadExtension
10298
10408
  };
@@ -10536,7 +10646,23 @@ function createNotificationDispatcher({
10536
10646
  }
10537
10647
  return { cleared, generation };
10538
10648
  }
10539
- return { enqueue, clear, dispose: clear };
10649
+ function snapshot(options = {}) {
10650
+ const limit = Math.max(0, Math.min(Number(options.limit ?? 20) || 20, queueLimit));
10651
+ return {
10652
+ queueSize: queue.length,
10653
+ maxQueueSize: queueLimit,
10654
+ inFlight,
10655
+ retryScheduled: retryTimer !== null,
10656
+ generation,
10657
+ queued: queue.slice(0, limit).map((entry) => ({
10658
+ channel: entry.channel,
10659
+ monitorName: entry.monitorName,
10660
+ stream: entry.stream,
10661
+ text: String(entry.text ?? "").slice(0, 500)
10662
+ }))
10663
+ };
10664
+ }
10665
+ return { enqueue, clear, dispose: clear, snapshot };
10540
10666
  }
10541
10667
 
10542
10668
  // src/services/runtime-subsystems.mjs
@@ -10732,8 +10858,263 @@ function createStreamService(deps) {
10732
10858
  };
10733
10859
  }
10734
10860
 
10861
+ // src/diagnostics/store.mjs
10862
+ var DEFAULT_MAX_LOGS = 300;
10863
+ var DEFAULT_MAX_EVENTS = 300;
10864
+ var DEFAULT_MAX_RUNTIME_EVENTS = 300;
10865
+ var MAX_STRING_LENGTH = 1200;
10866
+ var MAX_COLLECTION_ITEMS = 40;
10867
+ var MAX_DEPTH = 4;
10868
+ var SECRET_KEY_PATTERN = /(?:token|secret|password|credential|authorization|api[-_]?key|reconnectToken|expectedToken)/i;
10869
+ function normalizePositiveInteger(value, fallback) {
10870
+ const number = Number(value);
10871
+ if (!Number.isFinite(number)) {
10872
+ return fallback;
10873
+ }
10874
+ return Math.max(0, Math.floor(number));
10875
+ }
10876
+ function createRingBuffer(maxEntries) {
10877
+ const entries = [];
10878
+ const limit = normalizePositiveInteger(maxEntries, 0);
10879
+ let total = 0;
10880
+ let dropped = 0;
10881
+ function append(entry) {
10882
+ total += 1;
10883
+ if (limit === 0) {
10884
+ dropped += 1;
10885
+ return null;
10886
+ }
10887
+ entries.push(entry);
10888
+ if (entries.length > limit) {
10889
+ const overflow = entries.length - limit;
10890
+ entries.splice(0, overflow);
10891
+ dropped += overflow;
10892
+ }
10893
+ return entry;
10894
+ }
10895
+ function snapshot(limitOverride) {
10896
+ const requested = normalizePositiveInteger(limitOverride, entries.length);
10897
+ return entries.slice(Math.max(0, entries.length - requested)).map((entry) => safeClone(entry));
10898
+ }
10899
+ function stats() {
10900
+ return {
10901
+ retained: entries.length,
10902
+ total,
10903
+ dropped,
10904
+ capacity: limit
10905
+ };
10906
+ }
10907
+ return { append, snapshot, stats };
10908
+ }
10909
+ function truncateString(value, maxLength = MAX_STRING_LENGTH) {
10910
+ const text = String(value ?? "");
10911
+ if (text.length <= maxLength) {
10912
+ return text;
10913
+ }
10914
+ return `${text.slice(0, maxLength)}... (${text.length - maxLength} chars truncated)`;
10915
+ }
10916
+ function safeClone(value, options = {}, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
10917
+ const maxDepth = normalizePositiveInteger(options.maxDepth, MAX_DEPTH);
10918
+ const maxStringLength = normalizePositiveInteger(options.maxStringLength, MAX_STRING_LENGTH);
10919
+ const maxCollectionItems = normalizePositiveInteger(options.maxCollectionItems, MAX_COLLECTION_ITEMS);
10920
+ if (value === null || value === void 0) {
10921
+ return value ?? null;
10922
+ }
10923
+ if (typeof value === "string") {
10924
+ return truncateString(value, maxStringLength);
10925
+ }
10926
+ if (typeof value === "number" || typeof value === "boolean") {
10927
+ return value;
10928
+ }
10929
+ if (typeof value === "bigint") {
10930
+ return String(value);
10931
+ }
10932
+ if (typeof value === "function" || typeof value === "symbol") {
10933
+ return `[${typeof value}]`;
10934
+ }
10935
+ if (value instanceof Error) {
10936
+ return {
10937
+ name: value.name,
10938
+ message: truncateString(value.message, maxStringLength),
10939
+ stack: truncateString(value.stack ?? "", maxStringLength)
10940
+ };
10941
+ }
10942
+ if (depth >= maxDepth) {
10943
+ return Array.isArray(value) ? `[Array(${value.length})]` : "[Object]";
10944
+ }
10945
+ if (typeof value !== "object") {
10946
+ return String(value);
10947
+ }
10948
+ if (seen.has(value)) {
10949
+ return "[Circular]";
10950
+ }
10951
+ seen.add(value);
10952
+ if (Array.isArray(value)) {
10953
+ const slice = value.slice(0, maxCollectionItems).map((item) => safeClone(item, options, depth + 1, seen));
10954
+ if (value.length > maxCollectionItems) {
10955
+ slice.push(`... ${value.length - maxCollectionItems} more items`);
10956
+ }
10957
+ return slice;
10958
+ }
10959
+ const output = {};
10960
+ const entries = Object.entries(value).slice(0, maxCollectionItems);
10961
+ for (const [key, item] of entries) {
10962
+ output[key] = SECRET_KEY_PATTERN.test(key) ? "[redacted]" : safeClone(item, options, depth + 1, seen);
10963
+ }
10964
+ const remaining = Object.keys(value).length - entries.length;
10965
+ if (remaining > 0) {
10966
+ output.__truncatedKeys = remaining;
10967
+ }
10968
+ return output;
10969
+ }
10970
+ function normalizeLevel(value) {
10971
+ const level = String(value ?? "info").trim().toLowerCase();
10972
+ if (level === "warning" || level === "warn") return "warning";
10973
+ if (level === "error") return "error";
10974
+ if (level === "debug") return "debug";
10975
+ return "info";
10976
+ }
10977
+ function createId(prefix, count) {
10978
+ return `${prefix}-${count.toString(36)}`;
10979
+ }
10980
+ function summarizeSessionEvent(event) {
10981
+ const data = event?.data && typeof event.data === "object" ? event.data : {};
10982
+ return {
10983
+ id: event?.id ?? null,
10984
+ timestamp: event?.timestamp ?? nowIso(),
10985
+ type: String(event?.type ?? "unknown"),
10986
+ ephemeral: event?.ephemeral === true,
10987
+ agentId: event?.agentId ?? null,
10988
+ dataKeys: Object.keys(data),
10989
+ data: safeClone(data, {
10990
+ maxDepth: 3,
10991
+ maxStringLength: 700,
10992
+ maxCollectionItems: 24
10993
+ })
10994
+ };
10995
+ }
10996
+ function createDiagnosticsStore(options = {}) {
10997
+ const logs = createRingBuffer(options.maxLogs ?? DEFAULT_MAX_LOGS);
10998
+ const sessionEvents = createRingBuffer(options.maxSessionEvents ?? DEFAULT_MAX_EVENTS);
10999
+ const runtimeEvents = createRingBuffer(options.maxRuntimeEvents ?? DEFAULT_MAX_RUNTIME_EVENTS);
11000
+ const sessionEventCounts = /* @__PURE__ */ new Map();
11001
+ let logCount = 0;
11002
+ let runtimeEventCount = 0;
11003
+ let sessionEventCount = 0;
11004
+ let cleanupSessionListener = () => {
11005
+ };
11006
+ function recordLog(source, message, options2 = {}) {
11007
+ logCount += 1;
11008
+ return logs.append({
11009
+ id: createId("log", logCount),
11010
+ timestamp: nowIso(),
11011
+ source: String(source ?? "tap"),
11012
+ level: normalizeLevel(options2.level),
11013
+ message: truncateString(message, options2.maxStringLength ?? MAX_STRING_LENGTH),
11014
+ metadata: safeClone(options2.metadata ?? null, {
11015
+ maxDepth: 3,
11016
+ maxStringLength: 700,
11017
+ maxCollectionItems: 20
11018
+ })
11019
+ });
11020
+ }
11021
+ function recordRuntimeEvent(type, message, metadata = {}) {
11022
+ runtimeEventCount += 1;
11023
+ return runtimeEvents.append({
11024
+ id: createId("evt", runtimeEventCount),
11025
+ timestamp: nowIso(),
11026
+ type: String(type ?? "runtime"),
11027
+ message: truncateString(message, MAX_STRING_LENGTH),
11028
+ metadata: safeClone(metadata, {
11029
+ maxDepth: 3,
11030
+ maxStringLength: 700,
11031
+ maxCollectionItems: 20
11032
+ })
11033
+ });
11034
+ }
11035
+ function recordSessionEvent(event) {
11036
+ sessionEventCount += 1;
11037
+ const type = String(event?.type ?? "unknown");
11038
+ sessionEventCounts.set(type, (sessionEventCounts.get(type) ?? 0) + 1);
11039
+ return sessionEvents.append({
11040
+ sequence: sessionEventCount,
11041
+ ...summarizeSessionEvent(event)
11042
+ });
11043
+ }
11044
+ function detachSession() {
11045
+ try {
11046
+ cleanupSessionListener();
11047
+ } catch {
11048
+ }
11049
+ cleanupSessionListener = () => {
11050
+ };
11051
+ }
11052
+ function attachSession(session2) {
11053
+ detachSession();
11054
+ if (!session2 || typeof session2.on !== "function") {
11055
+ recordRuntimeEvent("session-events", "Session event capture unavailable; no session listener attached.");
11056
+ return;
11057
+ }
11058
+ try {
11059
+ const unsubscribe = session2.on((event) => {
11060
+ recordSessionEvent(event);
11061
+ });
11062
+ cleanupSessionListener = typeof unsubscribe === "function" ? unsubscribe : () => {
11063
+ };
11064
+ recordRuntimeEvent("session-events", "Session event capture attached.");
11065
+ } catch (error) {
11066
+ recordLog("diagnostics", "Failed to attach session event capture.", {
11067
+ level: "warning",
11068
+ metadata: { error }
11069
+ });
11070
+ }
11071
+ }
11072
+ function snapshot(options2 = {}) {
11073
+ return {
11074
+ generatedAt: nowIso(),
11075
+ logs: logs.snapshot(options2.logLimit ?? 140),
11076
+ runtimeEvents: runtimeEvents.snapshot(options2.runtimeEventLimit ?? 140),
11077
+ sessionEvents: sessionEvents.snapshot(options2.sessionEventLimit ?? 140),
11078
+ sessionEventCounts: Object.fromEntries([...sessionEventCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
11079
+ stats: {
11080
+ logs: logs.stats(),
11081
+ runtimeEvents: runtimeEvents.stats(),
11082
+ sessionEvents: sessionEvents.stats()
11083
+ }
11084
+ };
11085
+ }
11086
+ return {
11087
+ log: recordLog,
11088
+ event: recordRuntimeEvent,
11089
+ attachSession,
11090
+ detachSession,
11091
+ snapshot
11092
+ };
11093
+ }
11094
+
11095
+ // src/canvas/consts.mjs
11096
+ var TAP_DIAGNOSTICS_CANVAS_ID = "tap-diagnostics";
11097
+
10735
11098
  // src/services/tap-runtime-service.mjs
11099
+ function trimStreamEntries(stream, limit) {
11100
+ const entryLimit = Math.max(0, Math.floor(Number(limit ?? 80) || 80));
11101
+ return {
11102
+ ...stream,
11103
+ entries: Array.isArray(stream.entries) ? stream.entries.slice(Math.max(0, stream.entries.length - entryLimit)) : []
11104
+ };
11105
+ }
11106
+ function projectToolForDiagnostics(tool) {
11107
+ return {
11108
+ name: tool.name,
11109
+ description: tool.description ?? "",
11110
+ providerId: tool.providerId ?? null,
11111
+ providerName: tool.providerName ?? null,
11112
+ skipPermission: tool.skipPermission === true,
11113
+ overridesBuiltInTool: tool.overridesBuiltInTool === true
11114
+ };
11115
+ }
10736
11116
  function createTapRuntimeService(options = {}) {
11117
+ const diagnosticsStore = options.diagnostics ?? createDiagnosticsStore();
10737
11118
  const {
10738
11119
  streams,
10739
11120
  configStore,
@@ -10780,6 +11161,7 @@ function createTapRuntimeService(options = {}) {
10780
11161
  sessionContext.attachSession(session2);
10781
11162
  sessionPort.attach(session2);
10782
11163
  sessionActivityBridge.attach(session2);
11164
+ diagnosticsStore.attachSession(session2);
10783
11165
  }
10784
11166
  function clearNotificationsForLifecycle(options2 = {}) {
10785
11167
  if (options2.clearNotifications === true && typeof notifications.clear === "function") {
@@ -10852,6 +11234,7 @@ function createTapRuntimeService(options = {}) {
10852
11234
  const providerCapabilities = {
10853
11235
  getSessionInfo,
10854
11236
  log: (msg) => {
11237
+ diagnosticsStore.log("gateway", msg);
10855
11238
  process.stderr.write(`[tap-gateway] ${msg}
10856
11239
  `);
10857
11240
  void sessionPort.log(msg);
@@ -10862,14 +11245,53 @@ function createTapRuntimeService(options = {}) {
10862
11245
  void sessionPort.reloadExtension();
10863
11246
  }
10864
11247
  };
11248
+ function getDiagnosticsSnapshot(options2 = {}, extra = {}) {
11249
+ const allTools = Array.isArray(extra.tools) ? extra.tools.map(projectToolForDiagnostics) : [];
11250
+ return {
11251
+ generatedAt: nowIso(),
11252
+ session: getSessionInfo(),
11253
+ baseCwd: sessionContext.getBaseCwd(),
11254
+ process: {
11255
+ pid: process.pid,
11256
+ platform: process.platform,
11257
+ uptimeSeconds: Math.round(process.uptime())
11258
+ },
11259
+ streams: streamCapabilities.listStreams().map((stream) => trimStreamEntries(stream, options2.streamEntryLimit)),
11260
+ emitters: emitterCapabilities.listEmitters(),
11261
+ gateway: extra.gateway ?? null,
11262
+ tools: allTools,
11263
+ notifications: typeof notifications.snapshot === "function" ? notifications.snapshot({ limit: options2.notificationLimit ?? 20 }) : null,
11264
+ diagnostics: diagnosticsStore.snapshot(options2)
11265
+ };
11266
+ }
11267
+ const diagnosticsCapabilities = {
11268
+ log: (source, message, options2 = {}) => diagnosticsStore.log(source, message, options2),
11269
+ event: (type, message, metadata = {}) => diagnosticsStore.event(type, message, metadata),
11270
+ snapshot: getDiagnosticsSnapshot,
11271
+ openCanvas: async (input = {}) => {
11272
+ const instanceId = String(input.instanceId ?? TAP_DIAGNOSTICS_CANVAS_ID).trim() || TAP_DIAGNOSTICS_CANVAS_ID;
11273
+ const limit = Number(input.limit);
11274
+ const canvasInput = Number.isFinite(limit) ? { limit: Math.max(10, Math.min(300, Math.floor(limit))) } : {};
11275
+ diagnosticsStore.event("canvas.open.requested", "Opening tap diagnostics canvas.", { instanceId, canvasInput });
11276
+ return sessionPort.openCanvas({
11277
+ canvasId: TAP_DIAGNOSTICS_CANVAS_ID,
11278
+ instanceId,
11279
+ input: canvasInput
11280
+ });
11281
+ },
11282
+ attachSession: diagnosticsStore.attachSession,
11283
+ detachSession: diagnosticsStore.detachSession
11284
+ };
10865
11285
  return {
10866
11286
  tools: {
10867
11287
  streams: streamCapabilities,
10868
- emitters: emitterCapabilities
11288
+ emitters: emitterCapabilities,
11289
+ diagnostics: diagnosticsCapabilities
10869
11290
  },
10870
11291
  hooks: hookCapabilities,
10871
11292
  session: sessionCapabilities,
10872
11293
  provider: providerCapabilities,
11294
+ diagnostics: diagnosticsCapabilities,
10873
11295
  getBaseCwd: sessionContext.getBaseCwd,
10874
11296
  getSessionInfo,
10875
11297
  attachSession,
@@ -10892,6 +11314,725 @@ function createTapRuntimeService(options = {}) {
10892
11314
  };
10893
11315
  }
10894
11316
 
11317
+ // src/canvas/diagnostics-canvas.mjs
11318
+ import { createServer } from "node:http";
11319
+ import { createCanvas } from "@github/copilot-sdk/extension";
11320
+ var DEFAULT_REFRESH_MS = 1200;
11321
+ function jsonResponse(res, status, data) {
11322
+ const body = JSON.stringify(data, null, 2);
11323
+ res.writeHead(status, {
11324
+ "Content-Type": "application/json; charset=utf-8",
11325
+ "Cache-Control": "no-store",
11326
+ "X-Content-Type-Options": "nosniff"
11327
+ });
11328
+ res.end(body);
11329
+ }
11330
+ function textResponse(res, status, body, contentType = "text/plain; charset=utf-8") {
11331
+ res.writeHead(status, {
11332
+ "Content-Type": contentType,
11333
+ "Cache-Control": "no-store",
11334
+ "X-Content-Type-Options": "nosniff"
11335
+ });
11336
+ res.end(body);
11337
+ }
11338
+ function sanitizeSnapshotOptions(input = {}) {
11339
+ const source = input && typeof input === "object" ? input : {};
11340
+ const limit = Number(source.limit);
11341
+ return {
11342
+ streamEntryLimit: Number.isFinite(limit) ? Math.max(10, Math.min(200, Math.floor(limit))) : 80,
11343
+ logLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11344
+ sessionEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11345
+ runtimeEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160
11346
+ };
11347
+ }
11348
+ function summarizeSnapshot(snapshot) {
11349
+ const runningEmitters = Array.isArray(snapshot?.emitters?.running) ? snapshot.emitters.running.length : 0;
11350
+ const configuredEmitters = Array.isArray(snapshot?.emitters?.configured) ? snapshot.emitters.configured.length : 0;
11351
+ const streams = Array.isArray(snapshot?.streams) ? snapshot.streams.length : 0;
11352
+ const providers = Array.isArray(snapshot?.gateway?.providers) ? snapshot.gateway.providers.length : 0;
11353
+ const logs = snapshot?.diagnostics?.stats?.logs?.retained ?? 0;
11354
+ const sessionEvents = snapshot?.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11355
+ return { streams, runningEmitters, configuredEmitters, providers, logs, sessionEvents };
11356
+ }
11357
+ function createHtml() {
11358
+ return `<!doctype html>
11359
+ <html lang="en">
11360
+ <head>
11361
+ <meta charset="utf-8" />
11362
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
11363
+ <title>tap diagnostics</title>
11364
+ <style>
11365
+ :root {
11366
+ color-scheme: light;
11367
+ --paper: oklch(96% 0.018 86);
11368
+ --paper-2: oklch(91% 0.025 82);
11369
+ --ink: oklch(18% 0.026 70);
11370
+ --muted: oklch(43% 0.04 73);
11371
+ --line: oklch(72% 0.052 76);
11372
+ --amber: oklch(70% 0.15 72);
11373
+ --amber-dark: oklch(42% 0.12 67);
11374
+ --red: oklch(55% 0.18 30);
11375
+ --green: oklch(55% 0.13 145);
11376
+ --blue: oklch(48% 0.1 245);
11377
+ --surface: oklch(99% 0.012 88);
11378
+ --shadow: 0 24px 80px oklch(28% 0.04 70 / 0.14);
11379
+ --font-display: "Bahnschrift", "Aptos Display", "Segoe UI Variable Display", sans-serif;
11380
+ --font-body: "Aptos", "Segoe UI Variable Text", sans-serif;
11381
+ --font-mono: "Cascadia Code", "Consolas", monospace;
11382
+ }
11383
+
11384
+ * { box-sizing: border-box; }
11385
+ html { min-height: 100%; background: var(--paper); color: var(--ink); }
11386
+ body {
11387
+ margin: 0;
11388
+ min-height: 100vh;
11389
+ font-family: var(--font-body);
11390
+ background:
11391
+ linear-gradient(90deg, oklch(34% 0.08 68 / 0.08) 1px, transparent 1px) 0 0 / 42px 42px,
11392
+ linear-gradient(0deg, oklch(34% 0.08 68 / 0.055) 1px, transparent 1px) 0 0 / 42px 42px,
11393
+ radial-gradient(circle at 8% 12%, oklch(76% 0.13 74 / 0.35), transparent 28rem),
11394
+ var(--paper);
11395
+ }
11396
+
11397
+ button, input { font: inherit; }
11398
+ button:focus-visible, input:focus-visible {
11399
+ outline: 3px solid var(--amber-dark);
11400
+ outline-offset: 3px;
11401
+ }
11402
+
11403
+ .shell {
11404
+ width: min(1540px, calc(100vw - clamp(18px, 4vw, 64px)));
11405
+ margin: 0 auto;
11406
+ padding: clamp(22px, 4vw, 54px) 0;
11407
+ }
11408
+
11409
+ .masthead {
11410
+ display: grid;
11411
+ grid-template-columns: minmax(0, 1fr);
11412
+ gap: 22px;
11413
+ align-items: end;
11414
+ border-bottom: 3px solid var(--ink);
11415
+ padding-bottom: clamp(18px, 3vw, 34px);
11416
+ }
11417
+
11418
+ .mark {
11419
+ width: 72px;
11420
+ aspect-ratio: 1;
11421
+ display: grid;
11422
+ place-items: center;
11423
+ border: 3px solid var(--ink);
11424
+ background: var(--amber);
11425
+ box-shadow: 8px 8px 0 var(--ink);
11426
+ font-family: var(--font-display);
11427
+ font-size: 44px;
11428
+ line-height: 1;
11429
+ }
11430
+
11431
+ .title-wrap {
11432
+ display: grid;
11433
+ grid-template-columns: auto minmax(0, 1fr);
11434
+ gap: clamp(16px, 3vw, 28px);
11435
+ align-items: center;
11436
+ }
11437
+
11438
+ h1 {
11439
+ margin: 0;
11440
+ font-family: var(--font-display);
11441
+ font-size: clamp(2.4rem, 7vw, 6.8rem);
11442
+ line-height: 0.82;
11443
+ letter-spacing: -0.08em;
11444
+ text-transform: uppercase;
11445
+ max-width: 11ch;
11446
+ }
11447
+
11448
+ .dek {
11449
+ margin: 12px 0 0;
11450
+ max-width: 78ch;
11451
+ color: var(--muted);
11452
+ font-size: clamp(0.98rem, 1.8vw, 1.18rem);
11453
+ line-height: 1.45;
11454
+ }
11455
+
11456
+ .controls {
11457
+ display: flex;
11458
+ flex-wrap: wrap;
11459
+ gap: 10px;
11460
+ align-items: center;
11461
+ justify-content: space-between;
11462
+ }
11463
+
11464
+ .filter-row {
11465
+ display: flex;
11466
+ gap: 8px;
11467
+ flex-wrap: wrap;
11468
+ }
11469
+
11470
+ .chip, .button {
11471
+ border: 2px solid var(--ink);
11472
+ background: var(--surface);
11473
+ color: var(--ink);
11474
+ min-height: 40px;
11475
+ padding: 8px 13px;
11476
+ box-shadow: 3px 3px 0 var(--ink);
11477
+ cursor: pointer;
11478
+ transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease;
11479
+ }
11480
+
11481
+ .chip[aria-pressed="true"], .button.primary {
11482
+ background: var(--ink);
11483
+ color: var(--paper);
11484
+ }
11485
+
11486
+ @media (hover: hover) {
11487
+ .chip:hover, .button:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
11488
+ }
11489
+
11490
+ .search {
11491
+ display: flex;
11492
+ align-items: center;
11493
+ gap: 10px;
11494
+ min-width: min(100%, 380px);
11495
+ border: 2px solid var(--ink);
11496
+ background: var(--surface);
11497
+ box-shadow: 3px 3px 0 var(--ink);
11498
+ padding: 8px 12px;
11499
+ }
11500
+
11501
+ .search input {
11502
+ width: 100%;
11503
+ border: 0;
11504
+ background: transparent;
11505
+ color: var(--ink);
11506
+ outline: 0;
11507
+ }
11508
+
11509
+ .dashboard {
11510
+ display: grid;
11511
+ grid-template-columns: minmax(0, 1fr);
11512
+ gap: clamp(18px, 3vw, 34px);
11513
+ margin-top: clamp(22px, 4vw, 48px);
11514
+ }
11515
+
11516
+ .metrics {
11517
+ display: grid;
11518
+ grid-template-columns: repeat(2, minmax(0, 1fr));
11519
+ gap: 12px;
11520
+ }
11521
+
11522
+ .metric {
11523
+ min-height: 118px;
11524
+ border: 2px solid var(--ink);
11525
+ background: var(--surface);
11526
+ box-shadow: var(--shadow);
11527
+ padding: 16px;
11528
+ display: flex;
11529
+ flex-direction: column;
11530
+ justify-content: space-between;
11531
+ }
11532
+
11533
+ .metric b {
11534
+ font-family: var(--font-display);
11535
+ font-size: clamp(2rem, 5vw, 4.2rem);
11536
+ line-height: 0.9;
11537
+ letter-spacing: -0.05em;
11538
+ }
11539
+
11540
+ .metric span {
11541
+ color: var(--muted);
11542
+ text-transform: uppercase;
11543
+ font-size: 0.72rem;
11544
+ letter-spacing: 0.14em;
11545
+ font-weight: 800;
11546
+ }
11547
+
11548
+ .panel {
11549
+ border: 2px solid var(--ink);
11550
+ background: oklch(98% 0.014 88 / 0.96);
11551
+ box-shadow: 8px 8px 0 oklch(18% 0.026 70 / 0.22);
11552
+ min-width: 0;
11553
+ }
11554
+
11555
+ .panel-head {
11556
+ display: flex;
11557
+ align-items: baseline;
11558
+ justify-content: space-between;
11559
+ gap: 12px;
11560
+ padding: 14px 16px;
11561
+ border-bottom: 2px solid var(--ink);
11562
+ background: var(--paper-2);
11563
+ }
11564
+
11565
+ .panel-head h2 {
11566
+ margin: 0;
11567
+ font-family: var(--font-display);
11568
+ font-size: clamp(1.2rem, 2vw, 1.7rem);
11569
+ letter-spacing: -0.03em;
11570
+ text-transform: uppercase;
11571
+ }
11572
+
11573
+ .panel-head small {
11574
+ color: var(--muted);
11575
+ font-family: var(--font-mono);
11576
+ font-size: 0.75rem;
11577
+ }
11578
+
11579
+ .panel-body {
11580
+ padding: 14px;
11581
+ max-height: min(62vh, 720px);
11582
+ overflow: auto;
11583
+ }
11584
+
11585
+ .stream-grid, .emitter-grid, .provider-grid {
11586
+ display: grid;
11587
+ gap: 12px;
11588
+ }
11589
+
11590
+ .record {
11591
+ border-left: 4px solid var(--line);
11592
+ background: color-mix(in oklch, var(--surface), var(--paper) 24%);
11593
+ padding: 11px 12px;
11594
+ }
11595
+
11596
+ .record strong {
11597
+ display: inline-flex;
11598
+ gap: 8px;
11599
+ align-items: baseline;
11600
+ font-family: var(--font-display);
11601
+ letter-spacing: -0.01em;
11602
+ }
11603
+
11604
+ .meta {
11605
+ color: var(--muted);
11606
+ font-family: var(--font-mono);
11607
+ font-size: 0.72rem;
11608
+ overflow-wrap: anywhere;
11609
+ }
11610
+
11611
+ .entry {
11612
+ margin-top: 8px;
11613
+ padding-top: 8px;
11614
+ border-top: 1px dashed var(--line);
11615
+ font-family: var(--font-mono);
11616
+ font-size: 0.78rem;
11617
+ line-height: 1.45;
11618
+ white-space: pre-wrap;
11619
+ overflow-wrap: anywhere;
11620
+ }
11621
+
11622
+ .timeline {
11623
+ display: grid;
11624
+ gap: 9px;
11625
+ }
11626
+
11627
+ .tick {
11628
+ display: grid;
11629
+ grid-template-columns: 90px minmax(0, 1fr);
11630
+ gap: 12px;
11631
+ border-bottom: 1px dashed var(--line);
11632
+ padding: 9px 0;
11633
+ }
11634
+
11635
+ .tick time {
11636
+ color: var(--muted);
11637
+ font-family: var(--font-mono);
11638
+ font-size: 0.72rem;
11639
+ }
11640
+
11641
+ .badge {
11642
+ display: inline-flex;
11643
+ align-items: center;
11644
+ gap: 6px;
11645
+ padding: 2px 7px;
11646
+ border: 1px solid var(--ink);
11647
+ background: var(--paper);
11648
+ font-family: var(--font-mono);
11649
+ font-size: 0.68rem;
11650
+ text-transform: uppercase;
11651
+ letter-spacing: 0.08em;
11652
+ }
11653
+
11654
+ .badge.error { background: color-mix(in oklch, var(--red), var(--paper) 72%); }
11655
+ .badge.warning { background: color-mix(in oklch, var(--amber), var(--paper) 54%); }
11656
+ .badge.ready, .badge.running, .badge.success { background: color-mix(in oklch, var(--green), var(--paper) 70%); }
11657
+ .badge.info, .badge.debug { background: color-mix(in oklch, var(--blue), var(--paper) 76%); }
11658
+
11659
+ .details {
11660
+ margin-top: 6px;
11661
+ color: var(--ink);
11662
+ font-family: var(--font-mono);
11663
+ font-size: 0.78rem;
11664
+ white-space: pre-wrap;
11665
+ overflow-wrap: anywhere;
11666
+ }
11667
+
11668
+ .empty {
11669
+ border: 2px dashed var(--line);
11670
+ padding: 18px;
11671
+ color: var(--muted);
11672
+ background: color-mix(in oklch, var(--surface), var(--paper) 45%);
11673
+ }
11674
+
11675
+ .footer {
11676
+ margin-top: 24px;
11677
+ color: var(--muted);
11678
+ font-size: 0.86rem;
11679
+ }
11680
+
11681
+ @media (min-width: 780px) {
11682
+ .masthead { grid-template-columns: minmax(0, 1fr) auto; }
11683
+ .dashboard { grid-template-columns: minmax(280px, 0.62fr) minmax(0, 1fr); align-items: start; }
11684
+ .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
11685
+ .timeline-panel { grid-column: 1 / -1; }
11686
+ }
11687
+
11688
+ @media (min-width: 1180px) {
11689
+ .dashboard { grid-template-columns: 360px minmax(0, 1fr) 420px; }
11690
+ .timeline-panel { grid-column: auto; }
11691
+ .metrics { grid-template-columns: 1fr; }
11692
+ }
11693
+
11694
+ @media (prefers-reduced-motion: reduce) {
11695
+ *, *::before, *::after {
11696
+ animation-duration: 0.01ms !important;
11697
+ animation-iteration-count: 1 !important;
11698
+ scroll-behavior: auto !important;
11699
+ transition-duration: 0.01ms !important;
11700
+ }
11701
+ }
11702
+ </style>
11703
+ </head>
11704
+ <body>
11705
+ <main class="shell">
11706
+ <header class="masthead">
11707
+ <div class="title-wrap">
11708
+ <div class="mark" aria-hidden="true">\u203B</div>
11709
+ <div>
11710
+ <h1>Tap flight recorder</h1>
11711
+ <p class="dek">A live canvas for streams, emitters, provider gateway state, injection queues, session events, and tap diagnostics. Bounded, redacted, and built for incident-speed inspection.</p>
11712
+ </div>
11713
+ </div>
11714
+ <div class="controls" aria-label="Diagnostics controls">
11715
+ <div class="filter-row" role="group" aria-label="Timeline filter">
11716
+ <button class="chip" data-filter="all" aria-pressed="true">All</button>
11717
+ <button class="chip" data-filter="streams" aria-pressed="false">Streams</button>
11718
+ <button class="chip" data-filter="logs" aria-pressed="false">Logs</button>
11719
+ <button class="chip" data-filter="session" aria-pressed="false">Session</button>
11720
+ <button class="chip" data-filter="runtime" aria-pressed="false">Runtime</button>
11721
+ </div>
11722
+ <label class="search">
11723
+ <span aria-hidden="true">\u2315</span>
11724
+ <input id="search" type="search" placeholder="Filter evidence..." autocomplete="off" />
11725
+ </label>
11726
+ <button id="pause" class="button" type="button">Pause</button>
11727
+ <button id="refresh" class="button primary" type="button">Refresh</button>
11728
+ </div>
11729
+ </header>
11730
+
11731
+ <section class="dashboard" aria-live="polite">
11732
+ <aside>
11733
+ <div class="metrics" id="metrics"></div>
11734
+ <p class="footer" id="heartbeat">Waiting for first snapshot...</p>
11735
+ </aside>
11736
+
11737
+ <section class="panel">
11738
+ <div class="panel-head">
11739
+ <h2>Streams and emitters</h2>
11740
+ <small id="stream-count">0 streams</small>
11741
+ </div>
11742
+ <div class="panel-body">
11743
+ <div class="stream-grid" id="streams"></div>
11744
+ </div>
11745
+ </section>
11746
+
11747
+ <section class="panel">
11748
+ <div class="panel-head">
11749
+ <h2>Providers</h2>
11750
+ <small id="provider-count">0 providers</small>
11751
+ </div>
11752
+ <div class="panel-body">
11753
+ <div class="provider-grid" id="providers"></div>
11754
+ </div>
11755
+ </section>
11756
+
11757
+ <section class="panel timeline-panel">
11758
+ <div class="panel-head">
11759
+ <h2>Evidence timeline</h2>
11760
+ <small id="timeline-count">0 events</small>
11761
+ </div>
11762
+ <div class="panel-body">
11763
+ <div class="timeline" id="timeline"></div>
11764
+ </div>
11765
+ </section>
11766
+ </section>
11767
+ </main>
11768
+
11769
+ <script>
11770
+ const state = { snapshot: null, paused: false, filter: "all", query: "" };
11771
+ const el = (id) => document.getElementById(id);
11772
+ const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch]));
11773
+ const timeOnly = (value) => {
11774
+ const date = new Date(value);
11775
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour12: false });
11776
+ };
11777
+ const compactJson = (value) => {
11778
+ if (value === null || value === undefined) return "";
11779
+ if (typeof value === "string") return value;
11780
+ try { return JSON.stringify(value, null, 2); } catch { return String(value); }
11781
+ };
11782
+
11783
+ function metric(label, value, tone = "") {
11784
+ return '<article class="metric ' + tone + '"><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value) + '</b></article>';
11785
+ }
11786
+
11787
+ function renderMetrics(snapshot) {
11788
+ const running = snapshot.emitters?.running?.length ?? 0;
11789
+ const configured = snapshot.emitters?.configured?.length ?? 0;
11790
+ const streams = snapshot.streams?.length ?? 0;
11791
+ const providers = snapshot.gateway?.providers?.length ?? 0;
11792
+ const queue = snapshot.notifications?.queueSize ?? 0;
11793
+ const sessionEvents = snapshot.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11794
+ el("metrics").innerHTML = [
11795
+ metric("streams", streams),
11796
+ metric("running emitters", running),
11797
+ metric("providers", providers),
11798
+ metric("queued injections", queue),
11799
+ metric("configured emitters", configured),
11800
+ metric("session events", sessionEvents)
11801
+ ].join("");
11802
+ el("heartbeat").textContent = "Snapshot " + timeOnly(snapshot.generatedAt) + " | pid " + (snapshot.process?.pid ?? "?") + " | gateway " + (snapshot.gateway?.running ? "ready" : "stopped");
11803
+ }
11804
+
11805
+ function renderStreams(snapshot) {
11806
+ const streams = snapshot.streams ?? [];
11807
+ el("stream-count").textContent = streams.length + " streams";
11808
+ if (streams.length === 0) {
11809
+ el("streams").innerHTML = '<div class="empty">No streams are currently retained.</div>';
11810
+ return;
11811
+ }
11812
+ const emitterRows = [...(snapshot.emitters?.running ?? []), ...(snapshot.emitters?.configured ?? [])]
11813
+ .map((emitter) => '<div class="record"><strong>' + escapeHtml(emitter.name) + ' <span class="badge ' + escapeHtml(emitter.status) + '">' + escapeHtml(emitter.status) + '</span></strong><div class="meta">' + escapeHtml(emitter.emitterType) + ' | ' + escapeHtml(emitter.runSchedule) + ' | stream=' + escapeHtml(emitter.stream) + '</div><div class="meta">lines=' + escapeHtml(emitter.lineCount ?? 0) + ' dropped=' + escapeHtml(emitter.droppedLineCount ?? 0) + '</div></div>')
11814
+ .join("");
11815
+ const streamRows = streams.map((stream) => {
11816
+ const latest = (stream.entries ?? []).slice(-3).reverse().map((entry) => '<div class="entry"><span class="meta">' + escapeHtml(timeOnly(entry.timestamp)) + ' ' + escapeHtml(entry.source) + (entry.monitorName ? ' / ' + escapeHtml(entry.monitorName) : '') + '</span>\\n' + escapeHtml(entry.text) + '</div>').join("");
11817
+ return '<div class="record"><strong>' + escapeHtml(stream.name) + ' <span class="badge ' + (stream.sessionInjector?.enabled ? 'ready' : 'info') + '">' + (stream.sessionInjector?.enabled ? 'injector on' : 'kept') + '</span></strong><div class="meta">' + escapeHtml(stream.entries?.length ?? 0) + ' retained entries | delivery=' + escapeHtml(stream.sessionInjector?.delivery ?? 'surface') + '</div>' + (latest || '<div class="entry">No entries retained for this stream.</div>') + '</div>';
11818
+ }).join("");
11819
+ el("streams").innerHTML = streamRows + (emitterRows ? '<div class="panel-head" style="margin:14px -14px 12px"><h2>Emitter roll call</h2><small>' + ((snapshot.emitters?.running?.length ?? 0) + (snapshot.emitters?.configured?.length ?? 0)) + ' emitters</small></div><div class="emitter-grid">' + emitterRows + '</div>' : "");
11820
+ }
11821
+
11822
+ function renderProviders(snapshot) {
11823
+ const gateway = snapshot.gateway ?? {};
11824
+ const providers = gateway.providers ?? [];
11825
+ el("provider-count").textContent = providers.length + " providers";
11826
+ const gatewayRecord = '<div class="record"><strong>gateway <span class="badge ' + (gateway.running ? 'ready' : 'error') + '">' + (gateway.running ? 'running' : 'stopped') + '</span></strong><div class="meta">port=' + escapeHtml(gateway.port ?? "?") + ' connections=' + escapeHtml(gateway.connectionCount ?? 0) + ' reloadPending=' + escapeHtml(gateway.reloadPending ?? false) + ' token=' + (gateway.tokenPresent ? 'present' : 'absent') + '</div></div>';
11827
+ if (providers.length === 0) {
11828
+ el("providers").innerHTML = gatewayRecord + '<div class="empty">No external providers are bound. The canvas still shows tap-native streams and emitters.</div>';
11829
+ return;
11830
+ }
11831
+ const rows = providers.map((provider) => {
11832
+ const tools = (provider.tools ?? []).map((tool) => tool.name).join(", ") || "no tools";
11833
+ return '<div class="record"><strong>' + escapeHtml(provider.name) + ' <span class="badge ready">' + escapeHtml(provider.id) + '</span></strong><div class="meta">session=' + escapeHtml(provider.sessionId ?? "none") + ' | tools=' + escapeHtml(provider.toolCount ?? 0) + '</div><div class="details">' + escapeHtml(tools) + '</div></div>';
11834
+ }).join("");
11835
+ el("providers").innerHTML = gatewayRecord + rows;
11836
+ }
11837
+
11838
+ function collectTimeline(snapshot) {
11839
+ const items = [];
11840
+ for (const stream of snapshot.streams ?? []) {
11841
+ for (const entry of stream.entries ?? []) {
11842
+ items.push({ group: "streams", timestamp: entry.timestamp, source: "stream/" + stream.name, level: "info", message: entry.text, detail: entry.monitorName ? entry.monitorName + " " + (entry.stream ?? "") : entry.source });
11843
+ }
11844
+ }
11845
+ for (const log of snapshot.diagnostics?.logs ?? []) {
11846
+ items.push({ group: "logs", timestamp: log.timestamp, source: log.source, level: log.level, message: log.message, detail: compactJson(log.metadata) });
11847
+ }
11848
+ for (const event of snapshot.diagnostics?.sessionEvents ?? []) {
11849
+ items.push({ group: "session", timestamp: event.timestamp, source: "session", level: "debug", message: event.type, detail: compactJson(event.data) });
11850
+ }
11851
+ for (const event of snapshot.diagnostics?.runtimeEvents ?? []) {
11852
+ items.push({ group: "runtime", timestamp: event.timestamp, source: event.type, level: "info", message: event.message, detail: compactJson(event.metadata) });
11853
+ }
11854
+ return items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
11855
+ }
11856
+
11857
+ function renderTimeline(snapshot) {
11858
+ const query = state.query.trim().toLowerCase();
11859
+ const items = collectTimeline(snapshot).filter((item) => {
11860
+ if (state.filter !== "all" && item.group !== state.filter) return false;
11861
+ if (!query) return true;
11862
+ return [item.source, item.level, item.message, item.detail].join(" ").toLowerCase().includes(query);
11863
+ }).slice(0, 260);
11864
+ el("timeline-count").textContent = items.length + " visible";
11865
+ if (items.length === 0) {
11866
+ el("timeline").innerHTML = '<div class="empty">No evidence matches the current filter.</div>';
11867
+ return;
11868
+ }
11869
+ el("timeline").innerHTML = items.map((item) => '<div class="tick"><time>' + escapeHtml(timeOnly(item.timestamp)) + '</time><div><span class="badge ' + escapeHtml(item.level) + '">' + escapeHtml(item.group) + '</span> <span class="meta">' + escapeHtml(item.source) + '</span><div><strong>' + escapeHtml(item.message) + '</strong></div>' + (item.detail ? '<div class="details">' + escapeHtml(item.detail) + '</div>' : '') + '</div></div>').join("");
11870
+ }
11871
+
11872
+ function render(snapshot) {
11873
+ state.snapshot = snapshot;
11874
+ renderMetrics(snapshot);
11875
+ renderStreams(snapshot);
11876
+ renderProviders(snapshot);
11877
+ renderTimeline(snapshot);
11878
+ }
11879
+
11880
+ async function refresh() {
11881
+ if (state.paused) return;
11882
+ const response = await fetch("/api/snapshot", { cache: "no-store" });
11883
+ if (response.ok) render(await response.json());
11884
+ }
11885
+
11886
+ function connectEvents() {
11887
+ if (!("EventSource" in window)) {
11888
+ setInterval(refresh, 1800);
11889
+ refresh();
11890
+ return;
11891
+ }
11892
+ const source = new EventSource("/events");
11893
+ source.addEventListener("snapshot", (event) => {
11894
+ if (!state.paused) render(JSON.parse(event.data));
11895
+ });
11896
+ source.onerror = () => {
11897
+ setTimeout(refresh, 2000);
11898
+ };
11899
+ }
11900
+
11901
+ document.querySelectorAll("[data-filter]").forEach((button) => {
11902
+ button.addEventListener("click", () => {
11903
+ state.filter = button.dataset.filter;
11904
+ document.querySelectorAll("[data-filter]").forEach((item) => item.setAttribute("aria-pressed", String(item === button)));
11905
+ if (state.snapshot) renderTimeline(state.snapshot);
11906
+ });
11907
+ });
11908
+ el("search").addEventListener("input", (event) => {
11909
+ state.query = event.target.value;
11910
+ if (state.snapshot) renderTimeline(state.snapshot);
11911
+ });
11912
+ el("pause").addEventListener("click", () => {
11913
+ state.paused = !state.paused;
11914
+ el("pause").textContent = state.paused ? "Resume" : "Pause";
11915
+ if (!state.paused) refresh();
11916
+ });
11917
+ el("refresh").addEventListener("click", () => refresh());
11918
+ connectEvents();
11919
+ </script>
11920
+ </body>
11921
+ </html>`;
11922
+ }
11923
+ function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } = {}) {
11924
+ const instances = /* @__PURE__ */ new Map();
11925
+ function snapshot(options = {}) {
11926
+ return typeof getSnapshot === "function" ? getSnapshot(sanitizeSnapshotOptions(options)) : { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "No diagnostics snapshot provider configured." };
11927
+ }
11928
+ function log(message, level = "info", metadata = {}) {
11929
+ diagnostics2?.log?.("canvas", message, { level, metadata });
11930
+ }
11931
+ async function startServer(instanceId) {
11932
+ const server = createServer((req, res) => {
11933
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
11934
+ if (url.pathname === "/") {
11935
+ textResponse(res, 200, createHtml(), "text/html; charset=utf-8");
11936
+ return;
11937
+ }
11938
+ if (url.pathname === "/api/snapshot") {
11939
+ jsonResponse(res, 200, snapshot({ limit: url.searchParams.get("limit") }));
11940
+ return;
11941
+ }
11942
+ if (url.pathname === "/events") {
11943
+ res.writeHead(200, {
11944
+ "Content-Type": "text/event-stream; charset=utf-8",
11945
+ "Cache-Control": "no-store",
11946
+ "Connection": "keep-alive",
11947
+ "X-Accel-Buffering": "no"
11948
+ });
11949
+ const send = () => {
11950
+ res.write(`event: snapshot
11951
+ data: ${JSON.stringify(snapshot())}
11952
+
11953
+ `);
11954
+ };
11955
+ send();
11956
+ const interval = setInterval(send, DEFAULT_REFRESH_MS);
11957
+ req.on("close", () => clearInterval(interval));
11958
+ return;
11959
+ }
11960
+ jsonResponse(res, 404, { error: "not_found" });
11961
+ });
11962
+ await new Promise((resolve, reject) => {
11963
+ server.once("error", reject);
11964
+ server.listen(0, "127.0.0.1", resolve);
11965
+ });
11966
+ const address = server.address();
11967
+ const port = typeof address === "object" && address ? address.port : 0;
11968
+ const entry = { server, url: `http://127.0.0.1:${port}/` };
11969
+ instances.set(instanceId, entry);
11970
+ log(`Diagnostics canvas server started for instance '${instanceId}'.`, "info", { url: entry.url });
11971
+ return entry;
11972
+ }
11973
+ async function stopServer(instanceId) {
11974
+ const entry = instances.get(instanceId);
11975
+ if (!entry) {
11976
+ return;
11977
+ }
11978
+ instances.delete(instanceId);
11979
+ await new Promise((resolve) => entry.server.close(() => resolve()));
11980
+ log(`Diagnostics canvas server stopped for instance '${instanceId}'.`);
11981
+ }
11982
+ return createCanvas({
11983
+ id: TAP_DIAGNOSTICS_CANVAS_ID,
11984
+ displayName: "Tap diagnostics",
11985
+ description: "Live tap flight recorder for streams, emitters, provider gateway state, logs, and session events.",
11986
+ inputSchema: {
11987
+ type: "object",
11988
+ properties: {
11989
+ limit: { type: "integer", minimum: 10, maximum: 300, description: "Maximum retained rows to show per diagnostics section." }
11990
+ }
11991
+ },
11992
+ actions: [
11993
+ {
11994
+ name: "refresh_snapshot",
11995
+ description: "Return a fresh summary of the tap diagnostics canvas data.",
11996
+ inputSchema: {
11997
+ type: "object",
11998
+ properties: {
11999
+ limit: { type: "integer", minimum: 10, maximum: 300 }
12000
+ }
12001
+ },
12002
+ handler: async ({ input }) => ({
12003
+ ok: true,
12004
+ summary: summarizeSnapshot(snapshot(input))
12005
+ })
12006
+ },
12007
+ {
12008
+ name: "export_snapshot",
12009
+ description: "Return the current tap diagnostics snapshot as structured JSON.",
12010
+ inputSchema: {
12011
+ type: "object",
12012
+ properties: {
12013
+ limit: { type: "integer", minimum: 10, maximum: 300 }
12014
+ }
12015
+ },
12016
+ handler: async ({ input }) => snapshot(input)
12017
+ }
12018
+ ],
12019
+ open: async (ctx) => {
12020
+ let entry = instances.get(ctx.instanceId);
12021
+ if (!entry) {
12022
+ entry = await startServer(ctx.instanceId);
12023
+ }
12024
+ return {
12025
+ title: "Tap diagnostics",
12026
+ status: "live flight recorder",
12027
+ url: entry.url
12028
+ };
12029
+ },
12030
+ onClose: async (ctx) => {
12031
+ await stopServer(ctx.instanceId);
12032
+ }
12033
+ });
12034
+ }
12035
+
10895
12036
  // src/tap-runtime/runtime-factory.mjs
10896
12037
  var PROVIDER_SHUTDOWN_DEADLINE_MS = 1e4;
10897
12038
  var EMITTER_SHUTDOWN_WAIT_TIMEOUT_MS = 1e4;
@@ -10917,10 +12058,15 @@ function createCopilotChannelsRuntime(options = {}) {
10917
12058
  ...options.runtimeServiceOptions ?? {},
10918
12059
  cwd: options.cwd,
10919
12060
  session: options.session,
12061
+ diagnostics: options.diagnostics,
10920
12062
  shutdownSession: () => handleSessionShutdown(activeSession)
10921
12063
  });
10922
- process.stderr.write(`[tap-runtime] init \u2014 cwd=${runtimeService.session.getBaseCwd()}
12064
+ function logRuntime(message, options2 = {}) {
12065
+ runtimeService.diagnostics?.log?.("runtime", message, options2);
12066
+ process.stderr.write(`[tap-runtime] ${message}
10923
12067
  `);
12068
+ }
12069
+ logRuntime(`init \u2014 cwd=${runtimeService.session.getBaseCwd()}`);
10924
12070
  const tools = createTools({ tools: runtimeService.tools });
10925
12071
  const hooks = createHooks({ runtime: runtimeService.hooks });
10926
12072
  const tapToolsFn = () => tools;
@@ -10930,8 +12076,19 @@ function createCopilotChannelsRuntime(options = {}) {
10930
12076
  deliverPush: runtimeService.provider.deliverPush,
10931
12077
  log: runtimeService.provider.log
10932
12078
  });
10933
- process.stderr.write(`[tap-runtime] gateway created
10934
- `);
12079
+ logRuntime("gateway created");
12080
+ function getDiagnosticSnapshot(options2 = {}) {
12081
+ return runtimeService.diagnostics.snapshot(options2, {
12082
+ gateway: typeof gateway.getDiagnosticState === "function" ? gateway.getDiagnosticState() : null,
12083
+ tools: gateway.isRunning() ? gateway.getAllTools(tools) : tools
12084
+ });
12085
+ }
12086
+ const canvases = [
12087
+ createTapDiagnosticsCanvas({
12088
+ getSnapshot: getDiagnosticSnapshot,
12089
+ diagnostics: runtimeService.diagnostics
12090
+ })
12091
+ ];
10935
12092
  gateway.onToolsChanged(runtimeService.provider.replaceSessionTools);
10936
12093
  let cleanupShutdownListener = () => {
10937
12094
  };
@@ -10958,7 +12115,7 @@ function createCopilotChannelsRuntime(options = {}) {
10958
12115
  return;
10959
12116
  }
10960
12117
  handled = true;
10961
- process.stderr.write("[tap-runtime] session.shutdown received \u2014 stopping runtime\n");
12118
+ logRuntime("session.shutdown received \u2014 stopping runtime");
10962
12119
  void handleSessionShutdown(session2).catch((error) => {
10963
12120
  logShutdownFailure(session2, error);
10964
12121
  });
@@ -11027,19 +12184,15 @@ function createCopilotChannelsRuntime(options = {}) {
11027
12184
  return;
11028
12185
  }
11029
12186
  try {
11030
- process.stderr.write(`[tap-runtime] starting gateway\u2026
11031
- `);
12187
+ logRuntime("starting gateway...");
11032
12188
  gateway.start();
11033
- process.stderr.write(`[tap-runtime] gateway started
11034
- `);
12189
+ logRuntime("gateway start requested");
11035
12190
  } catch (err) {
11036
- process.stderr.write(`[tap-runtime] gateway start failed: ${err?.message ?? err}
11037
- `);
12191
+ logRuntime(`gateway start request failed: ${err?.message ?? err}`, { level: "warning" });
11038
12192
  }
11039
12193
  }
11040
12194
  function attachSession(nextSession) {
11041
- process.stderr.write(`[tap-runtime] attachSession \u2014 id=${nextSession?.id ?? "(none)"}
11042
- `);
12195
+ logRuntime(`attachSession \u2014 id=${nextSession?.id ?? "(none)"}`);
11043
12196
  activeSession = nextSession ?? null;
11044
12197
  if (shutdownRecord?.settled) {
11045
12198
  shutdownRecord = null;
@@ -11052,18 +12205,22 @@ function createCopilotChannelsRuntime(options = {}) {
11052
12205
  attachSession,
11053
12206
  tools,
11054
12207
  hooks,
12208
+ canvases,
11055
12209
  stopAllEmitters,
11056
12210
  stopAllEmittersAndWait,
11057
12211
  handleSessionShutdown,
11058
12212
  appendStreamMessage: runtimeService.session.appendStreamMessage,
11059
12213
  getTools: () => gateway.isRunning() ? gateway.getAllTools(tools) : tools,
12214
+ getCanvases: () => canvases,
11060
12215
  DEFAULT_STREAM
11061
12216
  };
11062
12217
  }
11063
12218
 
11064
12219
  // src/extension.mjs
12220
+ var diagnostics = globalThis.__tapDiagnostics ??= createDiagnosticsStore();
11065
12221
  function tapLog(msg) {
11066
12222
  const ts = (/* @__PURE__ */ new Date()).toISOString();
12223
+ diagnostics.log("extension", msg);
11067
12224
  process.stderr.write(`[tap ${ts}] ${msg}
11068
12225
  `);
11069
12226
  }
@@ -11071,7 +12228,8 @@ tapLog(`extension.mjs loading \u2014 pid=${process.pid} cwd=${process.cwd()} SES
11071
12228
  var isResume = Boolean(globalThis.__tapRuntime);
11072
12229
  tapLog(`runtime ${isResume ? "resuming (cached)" : "creating (fresh)"}`);
11073
12230
  var runtime = globalThis.__tapRuntime ??= createCopilotChannelsRuntime({
11074
- cwd: process.cwd()
12231
+ cwd: process.cwd(),
12232
+ diagnostics
11075
12233
  });
11076
12234
  tapLog("runtime ready");
11077
12235
  tapLog("calling joinSession\u2026");
@@ -11079,6 +12237,7 @@ var session;
11079
12237
  try {
11080
12238
  session = await joinSession({
11081
12239
  tools: runtime.getTools(),
12240
+ canvases: runtime.getCanvases(),
11082
12241
  hooks: runtime.hooks
11083
12242
  });
11084
12243
  tapLog(`joinSession OK \u2014 session.id=${session.id ?? "(none)"}`);