tracer-sh 0.2.4 → 0.2.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.
@@ -16548,6 +16548,49 @@ var TOOL_NAMES = {
16548
16548
  var CLIENT_TOOL_NAMES = Object.fromEntries(
16549
16549
  Object.entries(TOOL_NAMES).map(([k, v]) => [k, `tool-${v}`])
16550
16550
  );
16551
+ var ANALYSIS_MARKER = "<analysis>";
16552
+ function isAnalysisMessage(msg) {
16553
+ if (msg.role !== "assistant" || !msg.parts) return false;
16554
+ return msg.parts.some((p) => {
16555
+ if (p.type === CLIENT_TOOL_NAMES.BEGIN_ANALYSIS) return true;
16556
+ if (p.type !== "text") return false;
16557
+ const text3 = p.text;
16558
+ return typeof text3 === "string" && text3.includes(ANALYSIS_MARKER);
16559
+ });
16560
+ }
16561
+ function compactionUpTo(messages, boundaryIdx) {
16562
+ const boundary = messages[boundaryIdx];
16563
+ if (!boundary || boundary.role !== "assistant") return null;
16564
+ if (!isAnalysisMessage(boundary)) return boundaryIdx + 1;
16565
+ return boundaryIdx >= 1 ? boundaryIdx : null;
16566
+ }
16567
+ function findAnalysisMarker(parts) {
16568
+ const toolIdx = parts.findIndex((p) => p.type === CLIENT_TOOL_NAMES.BEGIN_ANALYSIS);
16569
+ if (toolIdx !== -1) return { kind: "tool", partIdx: toolIdx };
16570
+ for (let i = 0; i < parts.length; i++) {
16571
+ const p = parts[i];
16572
+ if (p.type !== "text") continue;
16573
+ const text3 = p.text;
16574
+ if (typeof text3 !== "string") continue;
16575
+ const idx = text3.indexOf(ANALYSIS_MARKER);
16576
+ if (idx !== -1) return { kind: "text", partIdx: i, charIdx: idx };
16577
+ }
16578
+ return null;
16579
+ }
16580
+ function splitAtAnalysis(parts) {
16581
+ const marker26 = findAnalysisMarker(parts);
16582
+ if (!marker26) return null;
16583
+ if (marker26.kind === "tool") {
16584
+ return { before: parts.slice(0, marker26.partIdx), analysis: parts.slice(marker26.partIdx + 1) };
16585
+ }
16586
+ const p = parts[marker26.partIdx];
16587
+ const beforeText = p.text.slice(0, marker26.charIdx);
16588
+ const afterText = p.text.slice(marker26.charIdx + ANALYSIS_MARKER.length);
16589
+ return {
16590
+ before: [...parts.slice(0, marker26.partIdx), ...beforeText.trim() ? [{ ...p, text: beforeText }] : []],
16591
+ analysis: [...afterText.trim() ? [{ ...p, text: afterText }] : [], ...parts.slice(marker26.partIdx + 1)]
16592
+ };
16593
+ }
16551
16594
  var ImportedAnalysisSchema = external_exports.object({
16552
16595
  v: external_exports.literal(1),
16553
16596
  kind: external_exports.literal("analysis"),
@@ -16556,6 +16599,14 @@ var ImportedAnalysisSchema = external_exports.object({
16556
16599
  parts: external_exports.array(external_exports.looseObject({ type: external_exports.string() })).max(200)
16557
16600
  });
16558
16601
 
16602
+ // ../shared/src/feature-flags.ts
16603
+ var FEATURES = {
16604
+ /** Dashboard page with grid widgets. */
16605
+ dashboards: false,
16606
+ /** Monitor page with alerting. */
16607
+ monitors: false
16608
+ };
16609
+
16559
16610
  // src/config.ts
16560
16611
  var CONFIG = {
16561
16612
  /** HTTP server port. Override with TRACER_PORT env var. */
@@ -20363,6 +20414,13 @@ var chatSessions = sqliteTable("chat_sessions", {
20363
20414
  status: text("status").notNull().default("idle"),
20364
20415
  /** Null for normal sessions. "imported" for sessions re-hydrated from a dropped analysis PNG. */
20365
20416
  kind: text("kind"),
20417
+ /** Compaction: LLM-generated (possibly user-edited) summary of the first summaryUpTo messages. */
20418
+ summary: text("summary"),
20419
+ /** Count of leading messages covered by the summary. Index-based because
20420
+ * assistant messages can carry empty ids; prefixes are stable here (messages
20421
+ * are only appended or suffix-truncated, never reordered). */
20422
+ summaryUpTo: integer2("summary_up_to"),
20423
+ summaryCreatedAt: integer2("summary_created_at"),
20366
20424
  createdAt: integer2("created_at").notNull().$defaultFn(() => unixNow()),
20367
20425
  updatedAt: integer2("updated_at").notNull().$defaultFn(() => unixNow())
20368
20426
  }, (t2) => [
@@ -20472,6 +20530,8 @@ chmodSync(TRACER_HOME, 448);
20472
20530
  chmodSync(dataDir, 448);
20473
20531
  var sqlite = new Database(join2(dataDir, "tracer.db"));
20474
20532
  sqlite.pragma("journal_mode = WAL");
20533
+ sqlite.pragma("synchronous = NORMAL");
20534
+ sqlite.pragma("busy_timeout = 5000");
20475
20535
  sqlite.pragma("foreign_keys = ON");
20476
20536
  var db = drizzle(sqlite, { schema: schema_exports });
20477
20537
 
@@ -20504,6 +20564,9 @@ function runSetup() {
20504
20564
  messages TEXT NOT NULL,
20505
20565
  status TEXT NOT NULL DEFAULT 'idle',
20506
20566
  kind TEXT,
20567
+ summary TEXT,
20568
+ summary_up_to INTEGER,
20569
+ summary_created_at INTEGER,
20507
20570
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
20508
20571
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
20509
20572
  );
@@ -20607,7 +20670,12 @@ function runSetup() {
20607
20670
  for (const ddl of [
20608
20671
  `ALTER TABLE sub_agent_runs ADD COLUMN session_id TEXT`,
20609
20672
  `ALTER TABLE tool_memories ADD COLUMN review_note TEXT`,
20610
- `ALTER TABLE chat_sessions ADD COLUMN kind TEXT`
20673
+ `ALTER TABLE chat_sessions ADD COLUMN kind TEXT`,
20674
+ `ALTER TABLE chat_sessions ADD COLUMN summary TEXT`,
20675
+ `ALTER TABLE chat_sessions ADD COLUMN summary_up_to INTEGER`,
20676
+ `ALTER TABLE chat_sessions ADD COLUMN summary_created_at INTEGER`,
20677
+ // Drops the short-lived id-based boundary column (never shipped in a release).
20678
+ `ALTER TABLE chat_sessions DROP COLUMN summary_up_to_id`
20611
20679
  ]) {
20612
20680
  try {
20613
20681
  sqlite.exec(ddl);
@@ -37930,7 +37998,7 @@ function injectMemories(prompt, memoryContext) {
37930
37998
  const memoryBlock = `
37931
37999
 
37932
38000
  ## ${MEMORY_SECTION_NAME}
37933
- These OVERRIDE any conflicting instructions above \u2014 they are verified fixes from past errors:
38001
+ These OVERRIDE any conflicting instructions above \u2014 they are lessons recorded from real failures in past sessions:
37934
38002
  ${lines.join("\n")}
37935
38003
  `;
37936
38004
  const firstBreak = prompt.indexOf("\n\n");
@@ -46223,6 +46291,8 @@ You MUST save a memory for every query failure:
46223
46291
  - Wrong field names \u2192 "Don't use X, use Y for [purpose]"
46224
46292
  - Wrong event types \u2192 "Don't query X for [goal], use Y instead"
46225
46293
 
46294
+ **Corrections must be demonstrated, not guessed.** Only write "use Y instead" if Y was actually run in this session and succeeded. If no working alternative was demonstrated, save only the mistake ("Don't use X in [context]") \u2014 a wrong correction is worse than none, because memories override future instructions.
46295
+
46226
46296
  ### From struggle patterns (evaluated)
46227
46297
  Review the full session timeline for patterns where the agent struggled \u2014 multiple attempts with variations before finding the correct approach. Look for:
46228
46298
  - Repeated EMPTY results with name/field/value variations followed by eventual success
@@ -46581,7 +46651,7 @@ function sanitizeNrqlRows(rows) {
46581
46651
  }
46582
46652
 
46583
46653
  // src/lib/shared-prompts.ts
46584
- var UNIFIED_ROLE_INTRO = `You are Tracer, an observability expert in a direct conversation with a developer. You have DIRECT access to the query tools of multiple providers at once \u2014 each provider's syntax, fields, and debugging guidance are documented below. Pick the right provider(s) for each question; when a question spans providers, query them and correlate across the results in one investigation. You have full conversation history and can reference previous messages. You run as an AUTONOMOUS MULTI-STEP AGENT \u2014 after each tool call you automatically receive results and CAN (and often SHOULD) make additional tool calls before finishing.`;
46654
+ var UNIFIED_ROLE_INTRO = `You are Tracer, an observability expert in a direct conversation with a developer. You have DIRECT access to the query tools of multiple providers at once \u2014 each provider's syntax, common fields, and debugging guidance are documented below. Pick the right provider(s) for each question; when a question spans providers, query them and correlate across the results in one investigation. You have full conversation history and can reference previous messages. You run as an AUTONOMOUS MULTI-STEP AGENT \u2014 after each tool call you automatically receive results and CAN (and often SHOULD) make additional tool calls before finishing.`;
46585
46655
  function buildUnifiedModePrompt(providerFragments, maxSteps) {
46586
46656
  return `${UNIFIED_ROLE_INTRO}
46587
46657
 
@@ -46590,6 +46660,8 @@ ${buildRules({ investigation: true })}
46590
46660
 
46591
46661
  ${DETECTIVE_MINDSET}
46592
46662
 
46663
+ ${EVIDENCE_GROUNDING}
46664
+
46593
46665
  ${EXECUTION_DISCIPLINE}
46594
46666
 
46595
46667
  ${providerFragments.join("\n\n---\n\n")}
@@ -46631,13 +46703,20 @@ You have limited steps. Every query must earn its place. Your goal is the **fast
46631
46703
  3. **"Is there a single query that could answer multiple questions at once?"** \u2014 Combine work. Pack information density per query.
46632
46704
 
46633
46705
  **"Good enough" beats "complete."** The user can always ask follow-up questions. Don't anticipate them \u2014 answer what was asked.`;
46706
+ var EVIDENCE_GROUNDING = `## Grounded in Evidence
46707
+
46708
+ Your only sources of truth are the literal text of tool results from this session, what the user has stated in the conversation, and what this prompt documents. Anything else is unknown \u2014 including the meaning of the data you retrieve.
46709
+
46710
+ 1. **Field names and values are opaque labels.** Never translate or assign meaning to a field name, enum value, code, or flag beyond its literal text \u2014 systems attach internal meanings you cannot know. Report the raw value; if its meaning matters and is undocumented, say so.
46711
+ 2. **Absence requires an empty probe.** Only claim something is missing, absent, or "not on file" if a query that would have returned it came back empty. Not having looked is not evidence of absence.
46712
+ 3. **Separate facts, deductions, and gaps.** Facts restate query results. Deductions must follow from stated facts alone \u2014 present them as deductions and name the supporting results; correlation across results is not causation. Gaps are reported as "the data does not show X" \u2014 never filled with a plausible story.`;
46634
46713
  var NO_FIXES_RULE = `**NEVER suggest fixes, remediation, next steps, or actions.** Forbidden phrasings include: "consider," "you should," "try," "might want to," "recommend," "could help," "suggests [action]," "would resolve," "to fix this." Any sentence about what to DO about the problem is forbidden, regardless of phrasing. Your job ends at "here is what happened and the evidence." The developer decides what to do.`;
46635
46714
  var EXECUTION_DISCIPLINE = `## Execution Discipline
46636
46715
 
46637
46716
  For multi-step investigations:
46638
46717
  1. **Step N: [Goal]** \u2014 state what gap this fills
46639
46718
  2. **Tool call** \u2192 ONE query
46640
- 3. **\u2192 Found:** [data] **\u2192 So what:** [inference]
46719
+ 3. **\u2192 Found:** [data] **\u2192 So what:** [only what this data supports \u2014 if it needs an assumption, it's a gap, not a finding]
46641
46720
  4. **\u2192 Can I answer now?** \u2014 If YES: respond. If NO: state what's missing.
46642
46721
 
46643
46722
  For simple questions (counts, lookups), skip this \u2014 just answer directly.`;
@@ -46651,6 +46730,7 @@ function analysisBlock() {
46651
46730
 
46652
46731
  1. **Think first** \u2014 before writing anything, plan the evidence chain in your head:
46653
46732
  - Known facts from query results, inferences that follow from them, and remaining gaps.
46733
+ - Self-audit each claim against the tool results already in this session: if no specific result backs it, drop it or present it explicitly as unverified. This is an in-head check \u2014 never run extra investigation queries for it.
46654
46734
  - Which queries best VISUALIZE each finding \u2014 these become the tool calls you will run in this section.
46655
46735
  - Do not start writing until you have a clear chain and a concrete list of visuals to run.
46656
46736
  2. ${markerStep}
@@ -46676,7 +46756,7 @@ You have a maximum of ${maxSteps} steps. Most investigations should finish in 3-
46676
46756
 
46677
46757
  ## Final Reminders
46678
46758
  - **Tool calls are the evidence.** Every substantive claim in your response needs a visual \u2014 even if the same query already ran during investigation, re-run it here. The analysis section must be self-contained.
46679
- - **Follow the Detective mindset:** correlation \u2260 causation, no gap-filling, no fixes. Every claim traces to a specific query result. Say "insufficient data" when data is missing.`;
46759
+ - **Stay Grounded in Evidence:** every claim maps to a specific tool result; values mean only what their literal text says; absence claims need an empty probe; gaps are stated as "the data does not show". No fixes.`;
46680
46760
  }
46681
46761
 
46682
46762
  // src/lib/prompt-builder.ts
@@ -46705,6 +46785,8 @@ ${buildRules({ investigation: true, extraRules: config2.extraRules })}
46705
46785
 
46706
46786
  ${DETECTIVE_MINDSET}
46707
46787
 
46788
+ ${EVIDENCE_GROUNDING}
46789
+
46708
46790
  ${config2.insideOutDebugging}
46709
46791
 
46710
46792
  ${EXECUTION_DISCIPLINE}
@@ -56759,6 +56841,137 @@ var memoryRouter = router({
56759
56841
  })
56760
56842
  });
56761
56843
 
56844
+ // src/agents/utility/summary.ts
56845
+ var SUMMARY_SYSTEM_PROMPT = `You are compacting an AI debugging-assistant conversation into a detailed summary. Your summary will permanently REPLACE the original messages as the assistant's only memory of them, so anything you omit is lost forever. The assistant must be able to continue the investigation from your summary alone without redoing any completed work.
56846
+
56847
+ Write the summary as markdown with exactly these sections:
56848
+
56849
+ ## Original intent
56850
+ What the user set out to do, in their own framing. Include later refinements or pivots of the goal.
56851
+
56852
+ ## User requests & decisions
56853
+ Chronological list of every instruction, question, constraint, correction, and approval or rejection the user gave, and whether each was fulfilled. The assistant must be able to tell from this section alone what the user has and has not asked for.
56854
+
56855
+ ## Investigation log
56856
+ Numbered, chronological. Each entry pairs an action with its result:
56857
+ 1. One line on what was done and why, then the exact tool call, query, or API call (with its parameters and the time range it covered) in a fenced code block.
56858
+
56859
+ \u2192 Result: what it returned, with the key values verbatim. Render a result with more than one row (facets, top-N lists, table rows) as a markdown table; copy up to roughly 20 rows verbatim and note what was cut.
56860
+
56861
+ Never record an action without its result \u2014 an unpaired action forces the assistant to re-run it.
56862
+
56863
+ ## Key findings & data (verbatim)
56864
+ The distilled facts the investigation established, with exact values copied verbatim \u2014 never paraphrase these:
56865
+ - IDs of any kind (trace IDs, entity GUIDs, account/project IDs, session IDs)
56866
+ - Exact error messages and stack-trace lines
56867
+ - File paths, service names, host names, URLs
56868
+ - Numbers: counts, rates, percentages, latencies, timestamps, time ranges
56869
+ Do not re-copy queries or result tables that already appear in the Investigation log \u2014 state the facts they established and name the log entry they came from.
56870
+
56871
+ ## What did NOT work (dead ends)
56872
+ Approaches tried and abandoned, queries that errored or returned empty, hypotheses ruled out \u2014 and WHY each failed. This prevents the assistant from repeating them. If nothing failed, write "None."
56873
+
56874
+ ## Conclusions & current state
56875
+ Each conclusion the assistant reached, stated together with the evidence supporting it, so it is never re-derived. What was communicated or delivered to the user (answers, recommendations, reports), and any artifacts produced.
56876
+
56877
+ ## Open items
56878
+ Unresolved questions, pending next steps, anything the user asked for that has not been delivered yet. If none, write "None."
56879
+
56880
+ Rules:
56881
+ - Be detailed. Length is not a concern; losing information is. A long, precise summary is always better than a short, vague one.
56882
+ - Copy identifiers, queries, errors, and numbers character-for-character from the conversation.
56883
+ - Format for scanning: queries and commands go in fenced code blocks, multi-row results in markdown tables, and inline identifiers (service names, error classes, IDs, paths) in backticks.
56884
+ - State each piece of data in full exactly once, in the section where it belongs; later mentions reference it instead of repeating it.
56885
+ - Always pair what was run with what it returned.
56886
+ - If the conversation contains an analysis or post-mortem report (the begin_analysis tool marks where one starts), carry its content through verbatim in the relevant sections instead of re-summarizing it.
56887
+ - If an existing summary of older messages is provided, merge it with the new segment into ONE self-contained summary covering everything. Preserve all verbatim data from the existing summary unless the new segment explicitly supersedes it.
56888
+ - Do not add commentary, advice, or information that is not in the conversation.
56889
+ - Output only the summary markdown, nothing else.`;
56890
+ var TOOL_INPUT_CHAR_LIMIT = 2e3;
56891
+ var TOOL_OUTPUT_CHAR_LIMIT = 6e3;
56892
+ var GENERATION_TIMEOUT_MS = 5 * 6e4;
56893
+ function truncate(value, limit) {
56894
+ if (value === void 0) return "(none)";
56895
+ let text3;
56896
+ try {
56897
+ text3 = typeof value === "string" ? value : JSON.stringify(
56898
+ value,
56899
+ (_key, v) => typeof v === "string" && v.length > limit ? v.slice(0, limit) : v
56900
+ );
56901
+ } catch {
56902
+ text3 = String(value);
56903
+ }
56904
+ return text3.length > limit ? `${text3.slice(0, limit)}\u2026 (truncated)` : text3;
56905
+ }
56906
+ function serializeMessagesForSummary(messages) {
56907
+ const blocks = [];
56908
+ messages.forEach((msg, i) => {
56909
+ const lines = [`### Message ${i + 1} \u2014 ${msg.role}`];
56910
+ for (const part of msg.parts) {
56911
+ if (part.type === "text") {
56912
+ lines.push(part.text);
56913
+ continue;
56914
+ }
56915
+ if (part.type === "reasoning" || part.type === "step-start" || part.type.startsWith("data-") || part.type === "file") {
56916
+ continue;
56917
+ }
56918
+ const p = part;
56919
+ if (p.toolCallId) {
56920
+ const name26 = p.type.startsWith("tool-") ? p.type.slice(5) : p.type;
56921
+ const output = p.output !== void 0 ? p.output : p.errorText !== void 0 ? `error: ${p.errorText}` : void 0;
56922
+ lines.push(
56923
+ `[tool: ${name26}]`,
56924
+ `input: ${truncate(p.input, TOOL_INPUT_CHAR_LIMIT)}`,
56925
+ `output: ${truncate(output, TOOL_OUTPUT_CHAR_LIMIT)}`
56926
+ );
56927
+ }
56928
+ }
56929
+ blocks.push(lines.join("\n"));
56930
+ });
56931
+ return blocks.join("\n\n");
56932
+ }
56933
+ async function generateSessionSummary(db2, opts) {
56934
+ const resolved = resolveModel(db2);
56935
+ if ("error" in resolved) {
56936
+ console.warn("[summary] Cannot generate summary:", resolved.error);
56937
+ return { error: resolved.error, config: true };
56938
+ }
56939
+ const serialized = serializeMessagesForSummary(opts.messages);
56940
+ let userContent = opts.priorSummary ? `## Existing summary of older messages (merge into your output)
56941
+ ${opts.priorSummary}
56942
+
56943
+ ## New conversation segment to incorporate
56944
+ ${serialized}` : `## Conversation to summarize
56945
+ ${serialized}`;
56946
+ if (opts.keptAnalysis) {
56947
+ userContent += `
56948
+
56949
+ Note: the assistant's final analysis of the last exchange is preserved verbatim in the conversation right after your summary \u2014 record the work and results above without inventing or restating its conclusions.`;
56950
+ }
56951
+ try {
56952
+ const { text: text3, usage } = await generateText({
56953
+ model: resolved.model,
56954
+ temperature: 0,
56955
+ system: SUMMARY_SYSTEM_PROMPT,
56956
+ messages: [{ role: "user", content: userContent }],
56957
+ providerOptions: resolved.providerOptions,
56958
+ abortSignal: AbortSignal.timeout(GENERATION_TIMEOUT_MS)
56959
+ });
56960
+ const summary = text3.trim();
56961
+ if (!summary) return { error: "Summary generation returned no content" };
56962
+ recordAgentRun(db2, {
56963
+ sessionId: opts.sessionId,
56964
+ agentType: "summary",
56965
+ model: resolved.modelId,
56966
+ usage: extractUsage(usage, resolved.modelId)
56967
+ });
56968
+ return { summary };
56969
+ } catch (err) {
56970
+ console.warn("[summary] Failed to generate summary:", err);
56971
+ return { error: "Summary generation failed" };
56972
+ }
56973
+ }
56974
+
56762
56975
  // src/trpc/routers/sessions.router.ts
56763
56976
  var AGENT_TYPE_LABELS = {
56764
56977
  chat: "Chat",
@@ -56766,7 +56979,8 @@ var AGENT_TYPE_LABELS = {
56766
56979
  gcp: "GCP sub-agent",
56767
56980
  posthog: "PostHog sub-agent",
56768
56981
  title: "Title gen",
56769
- memory: "Memory"
56982
+ memory: "Memory",
56983
+ summary: "Compaction"
56770
56984
  };
56771
56985
  var sessionsRouter = router({
56772
56986
  list: publicProcedure.query(({ ctx }) => {
@@ -56801,7 +57015,10 @@ var sessionsRouter = router({
56801
57015
  status: row.status,
56802
57016
  kind: row.kind,
56803
57017
  messages,
56804
- updatedAt: row.updatedAt
57018
+ updatedAt: row.updatedAt,
57019
+ summary: row.summary,
57020
+ summaryUpTo: row.summaryUpTo,
57021
+ summaryCreatedAt: row.summaryCreatedAt
56805
57022
  };
56806
57023
  }),
56807
57024
  getCost: publicProcedure.input(external_exports.object({ id: external_exports.string() })).query(({ ctx, input }) => {
@@ -56894,19 +57111,104 @@ var sessionsRouter = router({
56894
57111
  return { id };
56895
57112
  }),
56896
57113
  truncateMessages: publicProcedure.input(external_exports.object({ id: external_exports.string(), keepCount: external_exports.number().int().min(0) })).mutation(({ ctx, input }) => {
56897
- const row = ctx.db.select({ messages: chatSessions.messages }).from(chatSessions).where(eq(chatSessions.id, input.id)).get();
56898
- if (!row) return { success: false };
57114
+ const row = ctx.db.select({ messages: chatSessions.messages, summaryUpTo: chatSessions.summaryUpTo }).from(chatSessions).where(eq(chatSessions.id, input.id)).get();
57115
+ if (!row) return { success: false, summaryCleared: false };
56899
57116
  let messages = [];
56900
57117
  try {
56901
57118
  messages = JSON.parse(row.messages);
56902
57119
  } catch {
56903
- return { success: false };
57120
+ return { success: false, summaryCleared: false };
56904
57121
  }
56905
57122
  const truncated = messages.slice(0, input.keepCount);
57123
+ const keptAnalysisBoundary = row.summaryUpTo != null && isAnalysisMessage(messages[row.summaryUpTo] ?? { role: "" });
57124
+ const sourceUpTo = row.summaryUpTo != null ? row.summaryUpTo + (keptAnalysisBoundary ? 1 : 0) : 0;
57125
+ const summaryStale = row.summaryUpTo != null && input.keepCount < sourceUpTo;
56906
57126
  ctx.db.update(chatSessions).set({
56907
57127
  messages: JSON.stringify(truncated),
56908
- updatedAt: unixNow()
57128
+ updatedAt: unixNow(),
57129
+ ...summaryStale ? { summary: null, summaryUpTo: null, summaryCreatedAt: null } : {}
56909
57130
  }).where(eq(chatSessions.id, input.id)).run();
57131
+ return { success: true, summaryCleared: summaryStale };
57132
+ }),
57133
+ compact: publicProcedure.input(external_exports.object({ id: external_exports.string(), upToIndex: external_exports.number().int().min(0) })).mutation(async ({ ctx, input }) => {
57134
+ const row = ctx.db.select().from(chatSessions).where(eq(chatSessions.id, input.id)).get();
57135
+ if (!row) throw new TRPCError({ code: "NOT_FOUND", message: "Session not found" });
57136
+ if (row.status === "streaming" || ctx.activeStreams.has(input.id)) {
57137
+ throw new TRPCError({ code: "CONFLICT", message: "Cannot compact while a response is in progress" });
57138
+ }
57139
+ let messages;
57140
+ try {
57141
+ messages = JSON.parse(row.messages);
57142
+ } catch {
57143
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Session messages are corrupted" });
57144
+ }
57145
+ const boundaryIdx = input.upToIndex;
57146
+ if (boundaryIdx >= messages.length) {
57147
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Message not found in session" });
57148
+ }
57149
+ if (messages[boundaryIdx].role !== "assistant") {
57150
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Can only compact up to an assistant message" });
57151
+ }
57152
+ const summaryUpTo = compactionUpTo(messages, boundaryIdx);
57153
+ if (summaryUpTo == null) {
57154
+ throw new TRPCError({ code: "BAD_REQUEST", message: "There are no messages to summarize before the analysis" });
57155
+ }
57156
+ const keptAnalysis = isAnalysisMessage(messages[boundaryIdx]);
57157
+ const incremental = !!row.summary && row.summaryUpTo != null && row.summaryUpTo < summaryUpTo;
57158
+ const segment = messages.slice(incremental ? row.summaryUpTo : 0, boundaryIdx + 1);
57159
+ if (incremental) {
57160
+ const head = segment[0];
57161
+ if (isAnalysisMessage(head)) {
57162
+ const split = splitAtAnalysis(head.parts);
57163
+ if (split) segment[0] = { ...head, parts: split.analysis };
57164
+ }
57165
+ }
57166
+ if (keptAnalysis) {
57167
+ const last = segment[segment.length - 1];
57168
+ const split = splitAtAnalysis(last.parts);
57169
+ if (split) segment[segment.length - 1] = { ...last, parts: split.before };
57170
+ }
57171
+ const result = await generateSessionSummary(ctx.db, {
57172
+ sessionId: input.id,
57173
+ priorSummary: incremental ? row.summary : void 0,
57174
+ messages: segment,
57175
+ keptAnalysis
57176
+ });
57177
+ if ("error" in result) {
57178
+ throw new TRPCError({
57179
+ code: result.config ? "PRECONDITION_FAILED" : "INTERNAL_SERVER_ERROR",
57180
+ message: result.error
57181
+ });
57182
+ }
57183
+ const fresh = ctx.db.select({ messages: chatSessions.messages, status: chatSessions.status }).from(chatSessions).where(eq(chatSessions.id, input.id)).get();
57184
+ const sourceUpTo = boundaryIdx + 1;
57185
+ let prefixUnchanged = false;
57186
+ if (fresh && fresh.status !== "streaming" && !ctx.activeStreams.has(input.id)) {
57187
+ try {
57188
+ const freshMessages = JSON.parse(fresh.messages);
57189
+ prefixUnchanged = JSON.stringify(freshMessages.slice(0, sourceUpTo)) === JSON.stringify(messages.slice(0, sourceUpTo));
57190
+ } catch {
57191
+ }
57192
+ }
57193
+ if (!prefixUnchanged) {
57194
+ throw new TRPCError({ code: "CONFLICT", message: "The conversation changed while the summary was being generated \u2014 try again" });
57195
+ }
57196
+ const summary = result.summary;
57197
+ const summaryCreatedAt = unixNow();
57198
+ ctx.db.update(chatSessions).set({ summary, summaryUpTo, summaryCreatedAt }).where(eq(chatSessions.id, input.id)).run();
57199
+ return { summary, summaryUpTo, summaryCreatedAt };
57200
+ }),
57201
+ updateSummary: publicProcedure.input(external_exports.object({ id: external_exports.string(), summary: external_exports.string().min(1) })).mutation(({ ctx, input }) => {
57202
+ const row = ctx.db.select({ summaryUpTo: chatSessions.summaryUpTo }).from(chatSessions).where(eq(chatSessions.id, input.id)).get();
57203
+ if (!row) throw new TRPCError({ code: "NOT_FOUND", message: "Session not found" });
57204
+ if (row.summaryUpTo == null) {
57205
+ throw new TRPCError({ code: "CONFLICT", message: "The summary no longer exists" });
57206
+ }
57207
+ ctx.db.update(chatSessions).set({ summary: input.summary }).where(eq(chatSessions.id, input.id)).run();
57208
+ return { success: true };
57209
+ }),
57210
+ clearSummary: publicProcedure.input(external_exports.object({ id: external_exports.string() })).mutation(({ ctx, input }) => {
57211
+ ctx.db.update(chatSessions).set({ summary: null, summaryUpTo: null, summaryCreatedAt: null }).where(eq(chatSessions.id, input.id)).run();
56910
57212
  return { success: true };
56911
57213
  })
56912
57214
  });
@@ -57485,7 +57787,11 @@ function loadSessionMessages(db2, sessionId, newMessage) {
57485
57787
  console.warn(`[chat] Corrupted session ${sessionId}, starting fresh`);
57486
57788
  }
57487
57789
  }
57488
- return [...sanitizeMessages(previous), newMessage];
57790
+ return {
57791
+ messages: [...sanitizeMessages(previous), newMessage],
57792
+ summary: existing?.summary ?? null,
57793
+ summaryUpTo: existing?.summaryUpTo ?? null
57794
+ };
57489
57795
  }
57490
57796
  function finalizeSession(sessionId, context2, broadcaster) {
57491
57797
  if (!context2.activeStreams.has(sessionId)) return;
@@ -57493,7 +57799,7 @@ function finalizeSession(sessionId, context2, broadcaster) {
57493
57799
  broadcaster.finish();
57494
57800
  context2.activeStreams.delete(sessionId);
57495
57801
  }
57496
- async function processLLMStream(sessionId, messages, context2, broadcaster, serverAbort, collectTools, sessionTitle, model, modelId, providerOptions) {
57802
+ async function processLLMStream(sessionId, messages, context2, broadcaster, serverAbort, collectTools, sessionTitle, model, modelId, providerOptions, compaction) {
57497
57803
  const writer = {
57498
57804
  write: (part) => {
57499
57805
  const p = part;
@@ -57504,7 +57810,29 @@ async function processLLMStream(sessionId, messages, context2, broadcaster, serv
57504
57810
  };
57505
57811
  const collected = collectTools(writer);
57506
57812
  const tools = collected.tools;
57507
- const modelMessages = await convertToModelMessages(messages, {
57813
+ let modelInput = messages;
57814
+ let summaryForPrompt = null;
57815
+ if (compaction.summary && compaction.summaryUpTo && compaction.summaryUpTo < messages.length) {
57816
+ const tail = messages.slice(compaction.summaryUpTo);
57817
+ if (tail[0].role === "user") {
57818
+ modelInput = tail;
57819
+ summaryForPrompt = compaction.summary;
57820
+ } else {
57821
+ const split = splitAtAnalysis(tail[0].parts);
57822
+ if (split && split.analysis.length > 0) {
57823
+ tail[0] = { ...tail[0], parts: split.analysis };
57824
+ modelInput = [
57825
+ { id: "", role: "user", parts: [{ type: "text", text: "(The conversation up to this point was compacted into the summary in your instructions.)" }] },
57826
+ ...tail
57827
+ ];
57828
+ summaryForPrompt = compaction.summary;
57829
+ }
57830
+ }
57831
+ }
57832
+ if (compaction.summary && !summaryForPrompt) {
57833
+ console.warn(`[chat] Ignoring stale compaction boundary for ${sessionId} (summaryUpTo=${compaction.summaryUpTo}, messages=${messages.length})`);
57834
+ }
57835
+ const modelMessages = await convertToModelMessages(modelInput, {
57508
57836
  tools,
57509
57837
  convertDataPart: () => void 0
57510
57838
  });
@@ -57525,6 +57853,16 @@ ${fragments.join("\n\n")}` : `${basePrompt}
57525
57853
  No observability providers are currently configured. If the user asks about observability data, let them know they can connect providers in the Settings page.`;
57526
57854
  }
57527
57855
  systemPrompt += "\n\n" + getCurrentDateBlock(context2.db);
57856
+ if (summaryForPrompt) {
57857
+ systemPrompt += `
57858
+
57859
+ ## Earlier conversation summary
57860
+ The earlier part of this conversation was compacted to save context. The summary below replaces those messages and is authoritative: the work it describes is already done \u2014 do NOT redo it. Reuse its recorded results, identifiers, queries, and conclusions.
57861
+
57862
+ <conversation_summary>
57863
+ ${summaryForPrompt}
57864
+ </conversation_summary>`;
57865
+ }
57528
57866
  const result = streamText({
57529
57867
  model,
57530
57868
  temperature: 0,
@@ -57559,6 +57897,7 @@ No observability providers are currently configured. If the user asks about obse
57559
57897
  });
57560
57898
  const title = sessionTitle(enrichedMessages);
57561
57899
  const now2 = unixNow();
57900
+ const messagesJson = JSON.stringify(enrichedMessages);
57562
57901
  recordAgentRun(context2.db, {
57563
57902
  sessionId,
57564
57903
  agentType: "chat",
@@ -57568,7 +57907,7 @@ No observability providers are currently configured. If the user asks about obse
57568
57907
  context2.db.insert(chatSessions).values({
57569
57908
  id: sessionId,
57570
57909
  title,
57571
- messages: JSON.stringify(enrichedMessages),
57910
+ messages: messagesJson,
57572
57911
  status: "done",
57573
57912
  createdAt: now2,
57574
57913
  updatedAt: now2
@@ -57576,7 +57915,7 @@ No observability providers are currently configured. If the user asks about obse
57576
57915
  target: chatSessions.id,
57577
57916
  set: {
57578
57917
  title: sql`CASE WHEN ${chatSessions.title} = ${DEFAULT_SESSION_TITLE} THEN ${title} ELSE ${chatSessions.title} END`,
57579
- messages: JSON.stringify(enrichedMessages),
57918
+ messages: messagesJson,
57580
57919
  status: sql`CASE WHEN ${chatSessions.status} = 'idle' THEN 'idle' ELSE 'done' END`,
57581
57920
  updatedAt: now2
57582
57921
  }
@@ -57637,7 +57976,7 @@ No observability providers are currently configured. If the user asks about obse
57637
57976
  clearTimeout(timeoutId);
57638
57977
  finalizeSession(sessionId, context2, broadcaster);
57639
57978
  }
57640
- async function runChatAgent({ sessionId, messages, context: context2, collectTools, sessionTitle, modelOverride }) {
57979
+ async function runChatAgent({ sessionId, messages, summary, summaryUpTo, context: context2, collectTools, sessionTitle, modelOverride }) {
57641
57980
  const resolved = modelOverride ?? resolveModel(context2.db);
57642
57981
  if ("error" in resolved) return { error: resolved.error };
57643
57982
  const { model, modelId, providerOptions } = resolved;
@@ -57669,7 +58008,8 @@ async function runChatAgent({ sessionId, messages, context: context2, collectToo
57669
58008
  sessionTitle,
57670
58009
  model,
57671
58010
  modelId,
57672
- providerOptions
58011
+ providerOptions,
58012
+ { summary, summaryUpTo }
57673
58013
  ).catch((err) => {
57674
58014
  console.error(`[chat] Unhandled error in LLM processing for ${sessionId}:`, err);
57675
58015
  finalizeSession(sessionId, context2, broadcaster);
@@ -57793,7 +58133,7 @@ function nextYPosition(db2, dashboardId) {
57793
58133
  }
57794
58134
  function collectDashboardTools(registry2, db2, writer, dashboardId) {
57795
58135
  const dbId = dashboardId ?? "";
57796
- const { tools, promptFragments, connectedProviders } = collectBaseTools(registry2, db2, writer);
58136
+ const { tools, promptFragments, connectedProviders } = collectBaseTools(registry2, db2, writer, "unified");
57797
58137
  const defaultProvider = connectedProviders[0];
57798
58138
  tools.create_widget = tool({
57799
58139
  description: "Create a new dashboard widget. The query will be validated by executing it first. The widget auto-positions below existing widgets.",
@@ -57932,7 +58272,7 @@ The global date picker already shows the active time range, so titles should des
57932
58272
 
57933
58273
  ## Scope
57934
58274
  You are managing widgets for the current dashboard only. The widget list above shows only this dashboard's widgets.`;
57935
- const systemPrompt = [basePrompt, providerContext, widgetContext, ...promptFragments].join("\n\n");
58275
+ const systemPrompt = [basePrompt, EVIDENCE_GROUNDING, providerContext, widgetContext, ...promptFragments].join("\n\n");
57936
58276
  return { tools, systemPrompt };
57937
58277
  }
57938
58278
 
@@ -58239,7 +58579,7 @@ function getMonitorContext(db2) {
58239
58579
  ${lines.join("\n")}`;
58240
58580
  }
58241
58581
  function collectMonitorTools(registry2, db2, writer) {
58242
- const { tools, promptFragments, connectedProviders } = collectBaseTools(registry2, db2, writer);
58582
+ const { tools, promptFragments, connectedProviders } = collectBaseTools(registry2, db2, writer, "unified");
58243
58583
  const defaultProvider = connectedProviders[0];
58244
58584
  tools.create_monitor = tool({
58245
58585
  description: "Create a new monitor that periodically checks a query and alerts when a condition is met.",
@@ -58397,7 +58737,7 @@ The condition is a JS expression evaluated against the query \`result\` array. E
58397
58737
 
58398
58738
  ## Scope
58399
58739
  You are managing all monitors. The monitor list above shows all existing monitors.`;
58400
- const systemPrompt = [basePrompt, providerContext, monitorContext, ...promptFragments].join("\n\n");
58740
+ const systemPrompt = [basePrompt, EVIDENCE_GROUNDING, providerContext, monitorContext, ...promptFragments].join("\n\n");
58401
58741
  return { tools, systemPrompt };
58402
58742
  }
58403
58743
 
@@ -58437,7 +58777,7 @@ function generateSessionTitle(db2, sessionId, userMessage) {
58437
58777
  function registerChatRoutes(app, context2) {
58438
58778
  app.post("/api/chat", async (c) => {
58439
58779
  const { id, message, activeProvider } = await c.req.json();
58440
- const messages = loadSessionMessages(context2.db, id, message);
58780
+ const { messages, summary, summaryUpTo } = loadSessionMessages(context2.db, id, message);
58441
58781
  if (messages.length === 1) {
58442
58782
  const textPart = message.parts?.find((p) => p.type === "text");
58443
58783
  if (textPart) {
@@ -58451,6 +58791,8 @@ function registerChatRoutes(app, context2) {
58451
58791
  const result = await runChatAgent({
58452
58792
  sessionId: id,
58453
58793
  messages,
58794
+ summary,
58795
+ summaryUpTo,
58454
58796
  context: context2,
58455
58797
  collectTools: (writer) => collectChatTools(context2.providers, context2.db, writer, scopedProvider, mode),
58456
58798
  sessionTitle: (updatedMessages) => {
@@ -58466,10 +58808,12 @@ function registerChatRoutes(app, context2) {
58466
58808
  app.post("/api/dashboard-chat", async (c) => {
58467
58809
  const { id, message, dashboardId } = await c.req.json();
58468
58810
  const sessionId = dashboardSessionId(dashboardId);
58469
- const messages = loadSessionMessages(context2.db, sessionId, message);
58811
+ const { messages, summary, summaryUpTo } = loadSessionMessages(context2.db, sessionId, message);
58470
58812
  const result = await runChatAgent({
58471
58813
  sessionId,
58472
58814
  messages,
58815
+ summary,
58816
+ summaryUpTo,
58473
58817
  context: context2,
58474
58818
  collectTools: (writer) => collectDashboardTools(context2.providers, context2.db, writer, dashboardId),
58475
58819
  sessionTitle: () => "Dashboard Builder"
@@ -58480,10 +58824,12 @@ function registerChatRoutes(app, context2) {
58480
58824
  app.post("/api/monitor-chat", async (c) => {
58481
58825
  const { message } = await c.req.json();
58482
58826
  const sessionId = SESSION_PREFIX.MONITORS;
58483
- const messages = loadSessionMessages(context2.db, sessionId, message);
58827
+ const { messages, summary, summaryUpTo } = loadSessionMessages(context2.db, sessionId, message);
58484
58828
  const result = await runChatAgent({
58485
58829
  sessionId,
58486
58830
  messages,
58831
+ summary,
58832
+ summaryUpTo,
58487
58833
  context: context2,
58488
58834
  collectTools: (writer) => collectMonitorTools(context2.providers, context2.db, writer),
58489
58835
  sessionTitle: () => "Monitor Builder"
@@ -58634,7 +58980,7 @@ function registerApiRoutes(app, context2) {
58634
58980
  role: "user",
58635
58981
  parts: [{ type: "text", text: message }]
58636
58982
  };
58637
- const messages = loadSessionMessages(context2.db, sessionId, userMessage);
58983
+ const { messages, summary, summaryUpTo } = loadSessionMessages(context2.db, sessionId, userMessage);
58638
58984
  if (messages.length === 1) {
58639
58985
  generateSessionTitle(context2.db, sessionId, message);
58640
58986
  }
@@ -58647,6 +58993,8 @@ function registerApiRoutes(app, context2) {
58647
58993
  const result = await runChatAgent({
58648
58994
  sessionId,
58649
58995
  messages,
58996
+ summary,
58997
+ summaryUpTo,
58650
58998
  context: context2,
58651
58999
  collectTools: (writer) => {
58652
59000
  const collected = collectChatTools(context2.providers, context2.db, writer, scopedProvider, mode);
@@ -58731,24 +59079,31 @@ function mountStaticFiles(app) {
58731
59079
  const webRoot = webCandidates.find((d) => existsSync(resolve3(d, "index.html")));
58732
59080
  if (!webRoot) return;
58733
59081
  const indexHtml = readFileSync4(resolve3(webRoot, "index.html"), "utf-8");
59082
+ const fileCache = /* @__PURE__ */ new Map();
58734
59083
  app.use("*", async (c, next) => {
58735
59084
  const reqPath = c.req.path.slice(1);
58736
59085
  if (!reqPath) {
58737
59086
  await next();
58738
59087
  return;
58739
59088
  }
58740
- const filePath = resolve3(webRoot, reqPath);
58741
- if (filePath.startsWith(webRoot) && existsSync(filePath) && !statSync(filePath).isDirectory()) {
59089
+ let file2 = fileCache.get(reqPath);
59090
+ if (!file2) {
59091
+ const filePath = resolve3(webRoot, reqPath);
59092
+ if (!filePath.startsWith(webRoot) || !existsSync(filePath) || statSync(filePath).isDirectory()) {
59093
+ await next();
59094
+ return;
59095
+ }
58742
59096
  const mime = MIME_TYPES[extname(filePath)] || "application/octet-stream";
58743
59097
  const headers = { "Content-Type": mime };
58744
59098
  if (reqPath.startsWith("assets/")) {
58745
59099
  headers["Cache-Control"] = "public, max-age=31536000, immutable";
58746
59100
  }
58747
- return c.body(readFileSync4(filePath), { headers });
59101
+ file2 = { body: readFileSync4(filePath), headers };
59102
+ fileCache.set(reqPath, file2);
58748
59103
  }
58749
- await next();
59104
+ return c.body(file2.body, { headers: file2.headers });
58750
59105
  });
58751
- app.get("*", (c) => c.html(indexHtml));
59106
+ app.get("*", (c) => c.html(indexHtml, 200, { "Cache-Control": "no-cache" }));
58752
59107
  }
58753
59108
 
58754
59109
  // src/http/app.ts
@@ -58892,8 +59247,8 @@ async function main() {
58892
59247
  });
58893
59248
  const context2 = createContext({ db, providers });
58894
59249
  const app = createApp(context2);
58895
- const scheduler = new MonitorScheduler(db, providers);
58896
- scheduler.start();
59250
+ const scheduler = FEATURES.monitors ? new MonitorScheduler(db, providers) : null;
59251
+ scheduler?.start();
58897
59252
  const server = serve({ fetch: app.fetch, port: CONFIG.port, hostname: CONFIG.host }, (info) => {
58898
59253
  console.log(`Tracer server running on http://localhost:${info.port}`);
58899
59254
  });
@@ -58907,7 +59262,7 @@ Port ${CONFIG.port} is already in use. Run: lsof -ti :${CONFIG.port} | xargs kil
58907
59262
  });
58908
59263
  const shutdown = async (code = 0) => {
58909
59264
  const timeout = setTimeout(() => process.exit(code === 0 ? 1 : code), CONFIG.shutdownGracePeriodMs);
58910
- await scheduler.stop();
59265
+ await scheduler?.stop();
58911
59266
  for (const p of providers.getAllProviders()) {
58912
59267
  await p.dispose().catch(() => {
58913
59268
  });