tracer-sh 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,14 +10,32 @@ During an incident, most time goes to switching between observability tools
10
10
  and gathering context — not fixing the problem. Tracer connects your providers
11
11
  to a single AI chat interface so you find the root cause in one place.
12
12
 
13
+ ## How it works
14
+
15
+ ```
16
+ ┌─────────┐ your API keys ┌──────────────────┐
17
+ │ │ ◄──────────────────────────►│ Observability │
18
+ │ Tracer │ │ Providers │
19
+ │ local │ your API keys ├──────────────────┤
20
+ │ │ ◄──────────────────────────►│ LLM Providers │
21
+ └─────────┘ └──────────────────┘
22
+ ```
23
+
24
+ Everything runs on your machine. Your data stays local in an encrypted SQLite
25
+ database. Tracer talks directly to your provider and LLM APIs using your own
26
+ API keys — no intermediary servers, no telemetry, no data leaves your machine
27
+ except the API calls you control.
28
+
13
29
  ## Debug
14
30
 
15
- Chat with an AI agent that queries your providers in real-time and finds root causes — all from a single conversation.
31
+ Chat with an AI agent that queries your providers in real time and finds root
32
+ causes — all from a single conversation.
16
33
 
17
- - Natural language investigation
34
+ - Natural language investigation across all connected providers
18
35
  - Live query execution with inline charts
36
+ - Attach evidence to any message — screenshots, log files, code, PDFs — via paperclip, drag-and-drop, or paste
19
37
  - Post-mortem reports — download as Markdown to share
20
- - Share investigations as PNG — drop the exported image back into Tracer to re-open the analysis
38
+ - Share investigations as PNG — drop the exported image onto the sidebar to re-open the full analysis
21
39
  - Agent memory across sessions
22
40
  - Session history and cost tracking
23
41
 
@@ -25,65 +43,26 @@ Chat with an AI agent that queries your providers in real-time and finds root ca
25
43
 
26
44
  ## Settings
27
45
 
28
- Configure providers, LLM credentials, agent behavior, and memory. All data is stored locally — nothing leaves your machine except the API calls you configure.
46
+ Configure providers, LLM credentials, integrations, and agent behavior. Each
47
+ provider setup includes connectivity tests and guidance on creating
48
+ least-privilege API keys.
29
49
 
30
- - Anthropic (Claude) and Google (Gemini) API keys
50
+ - LLM backends: Anthropic (Claude), Google (Gemini via AI Studio or Vertex AI)
31
51
  - Data provider setup with connectivity tests
52
+ - Jira integration
32
53
  - Thinking budgets and step limits
33
54
  - Agent memory management
34
55
 
35
56
  ![Settings page](docs/screenshots/settings_page.png)
36
57
 
37
- ## How it works
38
-
39
- ```
40
- ┌─────────┐ your API keys ┌──────────────────┐
41
- │ │ ◄──────────────────────────►│ Observability │
42
- │ Tracer │ │ Providers │
43
- │ local │ your API keys ├──────────────────┤
44
- │ │ ◄──────────────────────────►│ LLM Providers │
45
- └─────────┘ └──────────────────┘
46
- ```
47
-
48
- Everything runs on your machine. Your data stays local in an encrypted SQLite
49
- database. Tracer talks directly to your provider and LLM APIs using your own API
50
- keys — no intermediary servers, no data leaves your machine except API calls you control.
51
-
52
- ## Security
58
+ ## Supported providers
53
59
 
54
- Tracer is built **defense-in-depth**: your secrets provider and LLM API keys,
55
- integration tokens, chat history, and agent memory — sit behind several independent
56
- layers, so no single failure exposes them.
57
-
58
- - **Local-only.** Nothing leaves your machine except the provider and LLM API calls you configure. No Tracer servers, no telemetry, no sync.
59
- - **Encrypted at rest.** The entire SQLite database is encrypted with SQLCipher (AES-256). On disk it is ciphertext — a stolen laptop, a copied `.db` file, or a backup is useless without the key.
60
- - **Machine-bound, user-scoped key.** A random 256-bit key is generated on first run and stored in your OS keychain (macOS Keychain, Windows Credential Manager, or Linux Secret Service). It never leaves the machine and is scoped to your OS user, so another user on the same box can't read the database either.
61
- - **Hardened on disk.** The data directory is created owner-only (`0700`); if no keychain is available, the fallback key file is written `0600`.
62
-
63
- **Automatic and transparent.** New installs are encrypted from the first run. An
64
- existing plaintext database is migrated on the first launch of this version — the
65
- encrypted copy is verified and atomically swapped in, and no plaintext is left behind.
66
- There's nothing to enable.
67
-
68
- **CI / headless.** Where no keychain exists, supply the key yourself with
69
- `TRACER_DB_KEY` — a 64-character hex string (e.g. `openssl rand -hex 32`).
70
-
71
- **What this protects — and what it doesn't.** Encryption at rest defends the file
72
- (theft, backups, copies) and blocks other users on the same machine. It does **not**
73
- defend against you, the logged-in user, running code as yourself: the app must hold the
74
- key at runtime to read its own data, so anyone controlling your user session can too. No
75
- local-first app escapes this — it's the honest boundary of on-device encryption.
60
+ **Data:** New Relic (NRQL), Google Cloud (Logs, Traces, Metrics, Errors), PostHog (HogQL)
76
61
 
77
- **Key loss means data loss.** Because the key lives only in your keychain, losing it (an
78
- OS reinstall or keychain reset) makes the database unrecoverable. For a safety net, back
79
- up the `tracer-sh` / `db-key` keychain value somewhere secure.
62
+ **LLM:** Anthropic (Claude), Google (Gemini AI Studio or Vertex AI)
80
63
 
81
- **Verify it yourself.** The raw file should be unreadable without the key:
82
-
83
- ```bash
84
- sqlite3 ~/.tracer/data/tracer.db '.tables' # → "Error: file is not a database"
85
- head -c 16 ~/.tracer/data/tracer.db | od -c # → random bytes, not "SQLite format 3"
86
- ```
64
+ **Integrations:** Jira the agent reads issue details and comment threads for
65
+ incident context, and posts comments back only when you explicitly ask.
87
66
 
88
67
  ## Install
89
68
 
@@ -95,8 +74,6 @@ Requires [Node.js 20+](https://nodejs.org/).
95
74
  npx tracer-sh@latest
96
75
  ```
97
76
 
98
- `npx` is ephemeral — it runs the newest published version on every launch and isn't a pinned version.
99
-
100
77
  **Install a pinned copy:**
101
78
 
102
79
  ```bash
@@ -104,42 +81,69 @@ npm install -g tracer-sh
104
81
  tracer-sh
105
82
  ```
106
83
 
107
- The first run gets the latest version; it then stays on that version until you explicitly update — either with **Update now** in the app (click the version in the sidebar) or by re-running the install.
108
-
109
- **Which should I pick?** Use the global install if you want a stable version that only changes when you choose; use `npx` to always grab the newest with zero install.
84
+ Either way, Tracer stays on its installed version until you explicitly update:
85
+ click the version in the sidebar and hit **Update now**, or re-run the install
86
+ command. Note that bare `npx tracer-sh` reuses npm's cached copy and does NOT
87
+ check for new releases — use the in-app update or `npx tracer-sh@latest` to get
88
+ the newest version.
110
89
 
111
- Open `http://localhost:3579`, go to **Settings** to add your API keys and choose an LLM — done.
90
+ Open `http://localhost:3579`, go to **Settings** to add your API keys and
91
+ choose an LLM — done.
112
92
 
113
93
  ## Headless / CLI
114
94
 
115
- Run an investigation from the terminal and get back the final analysis — so other tools and agents (including Claude Code) can drive Tracer:
95
+ Run an investigation from the terminal and get back the final analysis — so
96
+ other tools and agents (including Claude Code) can drive Tracer:
116
97
 
117
98
  ```bash
118
99
  tracer-sh analyze "Why did checkout error rate spike after 14:00 UTC?"
119
100
  ```
120
101
 
121
- Continue a prior run with `--session <id>`; pass `--json` for the full response (session id, queries, usage). Requires a running server.
102
+ - `--session <id>` continue a prior run with full context
103
+ - `--provider <name>` — scope the investigation to one provider
104
+ - `--json` — full response envelope (session id, queries, usage)
105
+ - `tracer-sh --help` — usage for every subcommand
122
106
 
123
- ## Supported Providers
107
+ Requires a running server.
124
108
 
125
- **Data:** New Relic (NRQL), Google Cloud (Logs, Traces, Metrics, Errors), PostHog (HogQL)
109
+ ## Security
126
110
 
127
- **LLM:** Anthropic (Claude), Google (Gemini)
111
+ Your secrets — API keys, integration tokens, chat history, agent memory — sit
112
+ behind several independent layers:
128
113
 
129
- ## Uninstall
114
+ - **Local-only.** No Tracer servers, no telemetry, no sync.
115
+ - **Encrypted at rest.** The entire SQLite database is encrypted with SQLCipher (AES-256). A stolen laptop, a copied `.db` file, or a backup is ciphertext without the key.
116
+ - **Machine-bound, user-scoped key.** A random 256-bit key is generated on first run and stored in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service). It never leaves the machine and other OS users can't read it.
117
+ - **Hardened on disk.** The data directory is owner-only (`0700`); a keychain-less fallback key file is `0600`.
118
+
119
+ Encryption is automatic — new installs are encrypted from the first run, and
120
+ an existing plaintext database is migrated in place on first launch. In CI or
121
+ headless environments without a keychain, supply the key yourself via
122
+ `TRACER_DB_KEY` (64-char hex, e.g. `openssl rand -hex 32`).
123
+
124
+ Two honest caveats. Encryption at rest defends the file, not your live
125
+ session: anyone running code as your OS user can read what the app can read —
126
+ that is the boundary of every local-first app. And the key lives only in your
127
+ keychain, so losing it (OS reinstall, keychain reset) makes the database
128
+ unrecoverable; back up the `tracer-sh` / `db-key` keychain value if you want
129
+ a safety net.
130
+
131
+ Verify it yourself:
130
132
 
131
133
  ```bash
132
- npm uninstall -g tracer-sh
134
+ sqlite3 ~/.tracer/data/tracer.db '.tables' # → "Error: file is not a database"
135
+ head -c 16 ~/.tracer/data/tracer.db | od -c # → random bytes, not "SQLite format 3"
133
136
  ```
134
137
 
135
- To also remove your local database (settings, sessions, API keys):
138
+ ## Uninstall
136
139
 
137
140
  ```bash
138
- rm -rf ~/.tracer
141
+ npm uninstall -g tracer-sh
142
+ rm -rf ~/.tracer # also removes settings, sessions, API keys
139
143
  ```
140
144
 
141
- The database encryption key also lives in your OS keychain (service `tracer-sh`,
142
- account `db-key`). Remove it for a clean slate — on macOS:
145
+ The database encryption key lives in your OS keychain (service `tracer-sh`,
146
+ account `db-key`). On macOS:
143
147
 
144
148
  ```bash
145
149
  security delete-generic-password -s tracer-sh -a db-key
@@ -156,19 +160,10 @@ security delete-generic-password -s tracer-sh -a db-key
156
160
 
157
161
  ## Contributing
158
162
 
159
- Contributions are welcome! There are two main ways to help:
160
-
161
- **Report bugs or request features** — [open an issue](https://github.com/sholub-dev/tracer/issues). Include steps to reproduce for bugs, or a clear description for feature requests.
162
-
163
- **Submit a code change:**
164
-
165
- 1. Fork this repo
166
- 2. Create a branch (`git checkout -b fix/my-fix`)
167
- 3. Make your changes and commit
168
- 4. Push to your fork (`git push origin fix/my-fix`)
169
- 5. Open a pull request against `master`
163
+ **Report bugs or request features** [open an issue](https://github.com/sholub-dev/tracer/issues) with steps to reproduce or a clear description.
170
164
 
171
- All PRs require approval before merging.
165
+ **Submit a code change** fork, branch, and open a pull request against
166
+ `master`. All PRs require approval before merging.
172
167
 
173
168
  ## License
174
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracer-sh",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "description": "Local-first debugging & analysis platform",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -154,14 +154,14 @@ _Flag if:_ Count is more than 20% lower than the baseline.
154
154
  ---
155
155
 
156
156
  ### Reporting Format
157
- After completing all applicable checks, report results as a table:
157
+ After completing all applicable checks, report results as a table. (This verdict summary is the ONE permitted markdown table \u2014 each check's underlying data must still be shown via tool calls.)
158
158
 
159
159
  | Check | Current | Baseline (1w ago) | Delta | Status |
160
160
  |-------|---------|-------------------|-------|--------|
161
- | Response Time P99 | ... | ... | +X% | \u2705 Pass / \u26A0\uFE0F Flagged |
162
- | Transaction Error Rate | ... | ... | +X% | \u2705 Pass / \u26A0\uFE0F Flagged |
163
- | External Services Errors | ... | ... | ... | \u2705 Pass / \u26A0\uFE0F Flagged / \u2014 N/A |
164
- | Success Transactions | ... | ... | -X% | \u2705 Pass / \u26A0\uFE0F Flagged |
161
+ | Response Time P99 | ... | ... | +X% | Pass / FLAGGED |
162
+ | Transaction Error Rate | ... | ... | +X% | Pass / FLAGGED |
163
+ | External Services Errors | ... | ... | ... | Pass / FLAGGED / N/A |
164
+ | Success Transactions | ... | ... | -X% | Pass / FLAGGED |
165
165
 
166
166
  End with an overall verdict on its own line:
167
167
  - \`Service appears healthy.\` \u2014 all checks pass
@@ -4,7 +4,7 @@ import {
4
4
  NR_DOMAIN_KNOWLEDGE,
5
5
  NR_INSIDE_OUT_DEBUGGING,
6
6
  NR_SERVICE_HEALTH_RUNBOOK
7
- } from "./chunk-XFJVNMJI.js";
7
+ } from "./chunk-LDYGR5S3.js";
8
8
  import "./chunk-ZAYYPDZP.js";
9
9
  export {
10
10
  NR_AUTH_STOP_RULE,
@@ -13,7 +13,7 @@ import {
13
13
  NR_AUTH_STOP_RULE,
14
14
  NR_DOMAIN_KNOWLEDGE,
15
15
  NR_INSIDE_OUT_DEBUGGING
16
- } from "./chunk-XFJVNMJI.js";
16
+ } from "./chunk-LDYGR5S3.js";
17
17
  import {
18
18
  GCP_AUTH_STOP_RULE,
19
19
  GCP_CROSS_SIGNAL,
@@ -28202,6 +28202,8 @@ var CONFIG = {
28202
28202
  /** Buffer cap for `npm install -g` output; well above the 1MB default so a noisy
28203
28203
  * but successful install (native rebuild logs) isn't misreported as a failure. */
28204
28204
  npmInstallMaxBufferBytes: 16 * 1024 * 1024,
28205
+ /** Re-run the background version check when the cached result is older than this. */
28206
+ updateCheckTtlMs: 6 * 60 * 60 * 1e3,
28205
28207
  // ── Dashboard defaults ──
28206
28208
  widgetDefaultWidth: 6,
28207
28209
  widgetDefaultHeight: 6,
@@ -28233,6 +28235,8 @@ import { join, dirname, sep } from "path";
28233
28235
  import { fileURLToPath } from "url";
28234
28236
  var RESTART_EXIT_CODE = CONFIG.restartExitCode;
28235
28237
  var cachedStatus = null;
28238
+ var lastCheckAtMs = 0;
28239
+ var checkInFlight = false;
28236
28240
  var cachedPackageInfo = null;
28237
28241
  function resolvePackage() {
28238
28242
  if (cachedPackageInfo) return cachedPackageInfo;
@@ -28271,7 +28275,7 @@ function getInstallMethod() {
28271
28275
  return detectInstallMethod(resolvePackage().root);
28272
28276
  }
28273
28277
  function isNewerVersion(latest, current) {
28274
- const parse4 = (v) => v.replace(/^v/, "").split(".").map(Number);
28278
+ const parse4 = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
28275
28279
  const [la, lb, lc] = parse4(latest);
28276
28280
  const [ca, cb, cc] = parse4(current);
28277
28281
  if (la !== ca) return la > ca;
@@ -28291,6 +28295,9 @@ function fetchLatestNpmVersion() {
28291
28295
  });
28292
28296
  }
28293
28297
  function getUpdateStatus() {
28298
+ if (Date.now() - lastCheckAtMs > CONFIG.updateCheckTtlMs) {
28299
+ checkForUpdateBackground();
28300
+ }
28294
28301
  if (cachedStatus) return cachedStatus;
28295
28302
  return {
28296
28303
  available: false,
@@ -28300,11 +28307,15 @@ function getUpdateStatus() {
28300
28307
  };
28301
28308
  }
28302
28309
  function checkForUpdateBackground() {
28310
+ if (checkInFlight) return;
28311
+ checkInFlight = true;
28312
+ lastCheckAtMs = Date.now();
28303
28313
  const current = readCurrentVersion();
28304
28314
  const method = getInstallMethod();
28305
28315
  const unavailable = () => ({ available: false, currentVersion: current, latestVersion: null, method });
28306
28316
  if (current === "unknown") {
28307
28317
  cachedStatus = unavailable();
28318
+ checkInFlight = false;
28308
28319
  return;
28309
28320
  }
28310
28321
  fetchLatestNpmVersion().then((latest) => {
@@ -28315,28 +28326,35 @@ function checkForUpdateBackground() {
28315
28326
  const available = isNewerVersion(latest, current);
28316
28327
  cachedStatus = { available, currentVersion: current, latestVersion: latest, method };
28317
28328
  if (available) {
28318
- const hint = method === "global" ? "click the version in the sidebar to update from the app" : method === "npx" ? "re-run: npx tracer-sh@latest" : "git pull, then restart tracer-sh \u2014 the launcher rebuilds automatically";
28329
+ const hint = method === "dev" ? "git pull, then restart tracer-sh \u2014 the launcher rebuilds automatically" : "click the version in the sidebar to update from the app";
28319
28330
  console.log(`Update available: v${current} \u2192 v${latest} (${hint})`);
28320
28331
  }
28321
28332
  }).catch(() => {
28322
28333
  cachedStatus = unavailable();
28334
+ }).finally(() => {
28335
+ checkInFlight = false;
28323
28336
  });
28324
28337
  }
28338
+ function npxPrefixDir(root) {
28339
+ return dirname(dirname(root));
28340
+ }
28325
28341
  function performSelfUpdate() {
28326
- const method = getInstallMethod();
28327
- if (method !== "global") {
28342
+ const { root } = resolvePackage();
28343
+ const method = detectInstallMethod(root);
28344
+ if (method === "dev" || !root) {
28328
28345
  return Promise.resolve({
28329
28346
  ok: false,
28330
28347
  method,
28331
- error: method === "npx" ? "Running via npx, which can't be updated in place. Re-run `npx tracer-sh@latest` to get the latest version." : "Running from a local source checkout, so in-app update is disabled."
28348
+ error: "Running from a local source checkout, so in-app update is disabled."
28332
28349
  });
28333
28350
  }
28351
+ const install = method === "global" ? "npm install -g tracer-sh@latest" : `npm install --prefix "${npxPrefixDir(root)}" tracer-sh@latest`;
28334
28352
  return new Promise((resolve5) => {
28335
28353
  exec(
28336
28354
  // Quiet flags keep output small (a verbose install can otherwise overflow
28337
28355
  // the stdout buffer and look like a failure); maxBuffer adds headroom for
28338
28356
  // native rebuild logs so a successful install is never misreported.
28339
- "npm install -g tracer-sh@latest --no-fund --no-audit --loglevel=error",
28357
+ `${install} --no-fund --no-audit --loglevel=error`,
28340
28358
  { encoding: "utf-8", timeout: CONFIG.npmInstallTimeoutMs, maxBuffer: CONFIG.npmInstallMaxBufferBytes },
28341
28359
  (err, _stdout, stderr) => {
28342
28360
  if (err) {
@@ -63435,7 +63453,7 @@ function createDeleteMemoryTool(memoryExecute, opts) {
63435
63453
 
63436
63454
  // src/agents/utility/memory-domain-knowledge.ts
63437
63455
  var DOMAIN_LOADERS = {
63438
- newrelic: async () => (await import("./domain-knowledge-6MEHHDWL.js")).NR_DOMAIN_KNOWLEDGE,
63456
+ newrelic: async () => (await import("./domain-knowledge-KF3XTKCK.js")).NR_DOMAIN_KNOWLEDGE,
63439
63457
  gcp: async () => (await import("./domain-knowledge-HC2HKQB3.js")).GCP_DOMAIN_KNOWLEDGE,
63440
63458
  posthog: async () => (await import("./domain-knowledge-RET7XGI5.js")).POSTHOG_DOMAIN_KNOWLEDGE
63441
63459
  };
@@ -63449,7 +63467,7 @@ async function getDomainKnowledge(providerType) {
63449
63467
  var SYSTEM_PROMPT = `You are a Memory Manager. Review completed sessions and extract lessons from FAILURES and STRUGGLE PATTERNS.
63450
63468
 
63451
63469
  ## MANDATORY RULE
63452
- If the session contains ANY failed queries, you MUST address each failure \u2014 either create_memory if no similar memory exists, or update_memory if an existing memory covers the same topic but could be improved. Every failure MUST result in a tool call. Do NOT skip failures.
63470
+ If the session contains ANY failed queries, you MUST address each failure \u2014 either create_memory if no similar memory exists, or update_memory if an existing memory covers the same topic but could be improved. Every failure MUST result in a tool call, with ONE exception: transient infrastructure failures (timeout, rate limit, 5xx/server error, network blip) teach nothing about the query language \u2014 for those, state you are skipping them and why instead of saving a memory. A false lesson is worse than none, because memories override future instructions.
63453
63471
 
63454
63472
  ## Purpose of memories
63455
63473
  Memories capture corrections from real failures and discoveries in the user's specific environment. Each user's system has unique event types, field names, and naming conventions that differ from generic documentation. When the agent tries something and it fails or struggles, the correction is extremely valuable for future sessions.
@@ -63479,6 +63497,7 @@ If the agent failed then corrected itself, capture the MISTAKE \u2192 CORRECTION
63479
63497
  If the agent failed but never found a correction, still save the mistake: "Don't use [wrong syntax/field] in NRQL"
63480
63498
 
63481
63499
  ## When NOT to save
63500
+ - Transient failures \u2014 timeouts, rate limits, 5xx/server errors, network blips. Not query-language lessons; saving them poisons future sessions.
63482
63501
  - General best practices that aren't tied to a specific failure or struggle
63483
63502
  - Successful patterns that didn't involve a prior failure or struggle
63484
63503
  - User-specific data (IDs, account names, endpoints)
@@ -63545,18 +63564,18 @@ ${domainKnowledge}
63545
63564
  ` : "";
63546
63565
  let tailInstruction;
63547
63566
  if (failures.length > 0) {
63548
- tailInstruction = `This session had ${failures.length} failed queries. You MUST address each failure with create_memory or update_memory \u2014 no exceptions.`;
63567
+ tailInstruction = `This session had ${failures.length} failed queries. You MUST address each failure with create_memory or update_memory \u2014 the only exception is transient infrastructure failures, which you skip with a stated reason.`;
63549
63568
  if (emptyCount > 0) tailInstruction += ` Additionally, ${emptyCount} queries returned empty results \u2014 review the session for struggle patterns.`;
63550
63569
  } else if (emptyCount > 0) {
63551
63570
  tailInstruction = `This session had no errors but ${emptyCount} queries returned empty results. Review the full session timeline for struggle patterns \u2014 repeated attempts, name variations, trial-and-error discovery. If there is a generalized learning, save it. If results were legitimately empty, respond with "No changes needed."`;
63552
63571
  } else {
63553
63572
  tailInstruction = `If the session was clean, respond with "No changes needed." and make no tool calls.`;
63554
63573
  }
63555
- const failuresSection = failures.length > 0 ? `## \u26A0 FAILURES DETECTED (${failures.length}) \u2014 each MUST be addressed
63574
+ const failuresSection = failures.length > 0 ? `## FAILURES DETECTED (${failures.length}) \u2014 each MUST be addressed
63556
63575
  ${failures.map((f) => `- Query #${f.idx}: \`${f.query}\`
63557
63576
  Error: ${f.error}`).join("\n")}
63558
63577
 
63559
- For each failure above, call create_memory (or update_memory if a similar memory already exists).
63578
+ For each failure above, call create_memory (or update_memory if a similar memory already exists), unless it is a transient infrastructure failure \u2014 then say so and skip it.
63560
63579
 
63561
63580
  ` : "";
63562
63581
  const prompt = `Review this completed ${providerType} session and decide what to remember.
@@ -63833,6 +63852,8 @@ ${DETECTIVE_MINDSET}
63833
63852
 
63834
63853
  ${EVIDENCE_GROUNDING}
63835
63854
 
63855
+ ${ROOT_CAUSE_DISCIPLINE}
63856
+
63836
63857
  ${EXECUTION_DISCIPLINE}
63837
63858
 
63838
63859
  ${providerFragments.join("\n\n---\n\n")}
@@ -63842,7 +63863,7 @@ ${buildAnalysisSection(maxSteps)}`;
63842
63863
  function buildRules(opts) {
63843
63864
  const rules = [
63844
63865
  `1. **ONE tool call per step.** After each tool result, write a brief summary, then make the next call.`,
63845
- `2. **Empty results = wrong query, not missing data.** Fix the filter, field name, or time range. Do not retry the same approach.`,
63866
+ `2. **Empty results: suspect the query first, then prove absence.** Check field name, case, quoting, and time range; fix and retry differently. If a deliberately broadened probe (wider window, fewer filters) is also empty, the absence IS the finding \u2014 report it. Never keep reshaping the same query hoping data appears.`,
63846
63867
  `3. **NEVER repeat a failed query.** Read the error, fix the cause. Same error twice \u2192 completely different approach.`,
63847
63868
  `4. **Use discovered identifiers exactly.** If the actual name differs from the task, use the exact discovered value.`,
63848
63869
  `5. You MUST write a non-empty text response when done \u2014 the user sees your text as the analysis.`,
@@ -63880,12 +63901,25 @@ Your only sources of truth are the literal text of tool results from this sessio
63880
63901
 
63881
63902
  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.
63882
63903
  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.
63883
- 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.`;
63904
+ 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.
63905
+ 4. **Exact values only.** Every number, identifier, timestamp, and quoted error message in your response must appear literally in a tool result. Values you compute from results must be labeled as computed, with their inputs shown. A name you inferred (service, field, event) must be confirmed by a query before it appears in a finding.
63906
+ 5. **Scope claims to what you queried.** "No errors" means "no errors matching my filter in my window" \u2014 state the window. Never generalize a claim beyond the time range, filter, or service actually queried.
63907
+ 6. **Label confidence.** State each conclusion as confirmed (a result directly shows it), likely (converging evidence, no direct proof), or unverified (plausible, untested). Only a query result upgrades a claim \u2014 more prose does not. Never present likely or unverified as confirmed.`;
63908
+ var ROOT_CAUSE_DISCIPLINE = `## Root-Cause Discipline
63909
+
63910
+ For "why is X happening" investigations; skip for simple lookups. Steps 1-2 are usually a single time-bucketed query; steps 3-6 are reasoning applied to results you already have \u2014 they cost thought, not extra steps.
63911
+
63912
+ 1. **Verify the symptom before explaining it.** The user's description is a claim, not a fact. Your first query confirms the problem actually appears in the data \u2014 right service, right window, roughly the reported magnitude. If it doesn't, report exactly that (with the probe you ran) instead of hunting for causes of something the data does not show.
63913
+ 2. **Anchor the timeline.** Establish when the symptom started with a time-bucketed query. A cause must precede the onset \u2014 anything that began after it is a consequence or a coincidence. Ask what changed at onset: deployment, config, traffic shape, a dependency's errors.
63914
+ 3. **Name the hypothesis each query tests.** Prefer queries that could DISPROVE it \u2014 a query that can only agree with you proves nothing. One matching correlation is never, by itself, a root cause.
63915
+ 4. **Follow the chain to the earliest anomaly.** Timeouts, retries, and 5xx responses are usually symptoms. Keep asking "what made THAT happen" until you reach the earliest anomalous signal visible in the data. If the chain leaves the data you can query (application code, third-party internals), that boundary itself is the finding \u2014 never bridge it with a plausible story.
63916
+ 5. **Rule out the strongest alternative.** Before declaring a root cause, name the best competing explanation and the evidence that eliminates it. If you cannot eliminate it, present both candidates and what distinguishes them.
63917
+ 6. **Check magnitudes against a baseline.** Compare to the same window a day or week earlier before calling anything a spike or drop. Keep your own numbers consistent \u2014 if two of your results disagree (sampling, different windows), reconcile the disagreement before building on either.`;
63884
63918
  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.`;
63885
63919
  var EXECUTION_DISCIPLINE = `## Execution Discipline
63886
63920
 
63887
63921
  For multi-step investigations:
63888
- 1. **Step N: [Goal]** \u2014 state what gap this fills
63922
+ 1. **Step N: [Goal]** \u2014 state the hypothesis this tests or the gap it fills
63889
63923
  2. **Tool call** \u2192 ONE query
63890
63924
  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]
63891
63925
  4. **\u2192 Can I answer now?** \u2014 If YES: respond. If NO: state what's missing.
@@ -63906,7 +63940,7 @@ function analysisBlock() {
63906
63940
  - Do not start writing until you have a clear chain and a concrete list of visuals to run.
63907
63941
  2. ${markerStep}
63908
63942
  3. **Visual-first narrative.** Walk through what happened and back EVERY substantive finding with a tool call that displays the supporting data (chart or table in the UI). Weave tool calls between narrative paragraphs \u2014 do not cluster them all at the top or bottom. Short connecting text explains each visual; the visuals carry the evidence.
63909
- 4. **End with a concise conclusion** \u2014 the root cause, or the specific gap that prevents naming one, phrased as a deduction from the visuals above.
63943
+ 4. **End with a concise conclusion** \u2014 the root cause with its confidence label (confirmed / likely / unverified), or the specific gap that prevents naming one, phrased as a deduction from the visuals above. If the conclusion is not confirmed, name the single piece of evidence that would settle it.
63910
63944
 
63911
63945
  **Rules:**
63912
63946
  - **Tool calls are mandatory, not optional.** Every substantive claim needs a tool call showing the data. Narrative without visuals is not acceptable. Cite investigation steps inline with \`[step N]\` only when it adds auditability \u2014 do not substitute citations for visuals.
@@ -63927,7 +63961,7 @@ You have a maximum of ${maxSteps} steps. Most investigations should finish in 3-
63927
63961
 
63928
63962
  ## Final Reminders
63929
63963
  - **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.
63930
- - **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.`;
63964
+ - **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; claims stay scoped to the window actually queried; conclusions carry confidence labels; gaps are stated as "the data does not show". No fixes.`;
63931
63965
  }
63932
63966
 
63933
63967
  // src/lib/prompt-builder.ts
@@ -63958,6 +63992,8 @@ ${DETECTIVE_MINDSET}
63958
63992
 
63959
63993
  ${EVIDENCE_GROUNDING}
63960
63994
 
63995
+ ${ROOT_CAUSE_DISCIPLINE}
63996
+
63961
63997
  ${config2.insideOutDebugging}
63962
63998
 
63963
63999
  ${EXECUTION_DISCIPLINE}
@@ -73994,7 +74030,7 @@ Only after reviewing all memories, perform any needed updates or deletes.
73994
74030
  - **Default to KEEP.** Only delete when you are 100% certain the memory is harmful or an exact duplicate.
73995
74031
  - Merge duplicates: UPDATE the better one, DELETE the other.
73996
74032
  - Rewrite vague notes to be specific and actionable (max 15 words).
73997
- - Delete memories that teach bad NRQL syntax (e.g. using GROUP BY, DISTINCT, or other SQL that doesn't exist in NRQL).`;
74033
+ - Delete memories that teach syntax invalid for THIS provider's query language \u2014 judge against the provider domain knowledge in the prompt, never against another provider's dialect (e.g. GROUP BY is invalid NRQL but valid HogQL).`;
73998
74034
  async function runMemoryOptimizer(db2, toolName) {
73999
74035
  const memories = db2.select().from(toolMemories).where(eq(toolMemories.toolName, toolName)).all();
74000
74036
  const emptyStats = { kept: 0, updated: 0, deleted: 0 };
@@ -74606,8 +74642,8 @@ var updateRouter = router({
74606
74642
  currentVersion: status.currentVersion,
74607
74643
  latestVersion: status.latestVersion,
74608
74644
  method: status.method,
74609
- // Only a global install can be upgraded in place from the app.
74610
- canSelfUpdate: status.method === "global"
74645
+ // Global and npx installs upgrade in place; only source checkouts can't.
74646
+ canSelfUpdate: status.method !== "dev"
74611
74647
  };
74612
74648
  }),
74613
74649
  // Upgrade a global install in place, then trigger a graceful restart so the
@@ -75190,6 +75226,13 @@ ${formatted}`;
75190
75226
  }
75191
75227
 
75192
75228
  // src/agents/base-agent.ts
75229
+ var IMAGE_ANALYSIS_GUIDANCE = `## Working with attached images and files
75230
+ The user has attached one or more images or files. Treat each as primary evidence, not decoration:
75231
+ - Analyze every attachment carefully and in full \u2014 do not skim. Inspect fine detail, not just the gist.
75232
+ - Extract ALL information that could matter: exact error messages and stack traces, status/error codes, log lines, timestamps, numeric values with their units, axis labels and series values on charts/graphs, configuration keys, IDs, and any text visible in screenshots.
75233
+ - Transcribe values precisely and double-check each reading before relying on it \u2014 re-read the image to confirm digits, spelling, and signs rather than approximating.
75234
+ - Quote what is actually shown rather than paraphrasing loosely, and tie every conclusion back to specific details in the attachment.
75235
+ - If any part is blurry, cropped, truncated, or ambiguous, say so explicitly and ask \u2014 never guess at an unreadable value.`;
75193
75236
  function sanitizeMessages(messages) {
75194
75237
  return messages.map((msg) => {
75195
75238
  if (msg.role !== "assistant") return msg;
@@ -75274,11 +75317,16 @@ When the user's question spans multiple providers, query each relevant provider
75274
75317
  const fragments = collected.promptFragments ?? [];
75275
75318
  systemPrompt = fragments.length > 0 ? `${basePrompt}
75276
75319
 
75320
+ ${EVIDENCE_GROUNDING}
75321
+
75277
75322
  ${fragments.join("\n\n")}` : `${basePrompt}
75278
75323
 
75279
75324
  No observability providers are currently configured. If the user asks about observability data, let them know they can connect providers in the Settings page.`;
75280
75325
  }
75281
75326
  systemPrompt += "\n\n" + getCurrentDateBlock(context2.db);
75327
+ if (modelInput.some((m) => m.parts.some((p) => p.type === "file"))) {
75328
+ systemPrompt += "\n\n" + IMAGE_ANALYSIS_GUIDANCE;
75329
+ }
75282
75330
  if (summaryForPrompt) {
75283
75331
  systemPrompt += `
75284
75332
 
@@ -75725,10 +75773,10 @@ If a tool call fails, retry with a corrected approach. If you fail the same tool
75725
75773
 
75726
75774
  ## Chart Type Selection
75727
75775
  - "auto" \u2014 let the frontend auto-detect based on query shape
75728
- - "timeseries" \u2014 for TIMESERIES queries (line charts)
75729
- - "table" \u2014 for FACET queries without TIMESERIES
75776
+ - "timeseries" \u2014 for time-bucketed queries (NRQL TIMESERIES, HogQL GROUP BY time bucket) \u2014 line charts
75777
+ - "table" \u2014 for grouped/top-N results without a time axis (NRQL FACET, SQL GROUP BY)
75730
75778
  - "scalar" \u2014 for single-value aggregations
75731
- - "histogram" \u2014 for histogram() queries
75779
+ - "histogram" \u2014 for distribution queries (e.g. NRQL histogram())
75732
75780
 
75733
75781
  ## Widget Sizing
75734
75782
  The dashboard uses a ${CONFIG.gridColumns}\xD7${CONFIG.gridColumns} grid (${CONFIG.gridColumns} columns, ${CONFIG.gridColumns} rows fill the viewport). Both axes are responsive to screen size.
@@ -76207,6 +76255,9 @@ The condition is a JS expression evaluated against the query \`result\` array. E
76207
76255
  - \`result[0].average > 2\` \u2014 alert when average exceeds 2 seconds
76208
76256
  - \`result[0].errorRate > 0.05\` \u2014 alert when error rate exceeds 5%
76209
76257
 
76258
+ ## Threshold Grounding
76259
+ Never invent a threshold. Unless the user gave an explicit number, run the monitor's query over recent data FIRST and pick the threshold relative to the observed values. When proposing a condition, state the observed baseline it is based on (e.g. "normal is 5-10/min, alerting above 50"). A threshold chosen without looking at the data either never fires or fires constantly.
76260
+
76210
76261
  ## Frequency Recommendations
76211
76262
  - 30s \u2014 critical real-time checks
76212
76263
  - 60s \u2014 standard monitoring (default)
@@ -1 +1 @@
1
- import{a as q,r as s,j as r}from"./index-L_n_IFmZ.js";var P=q();function $(c){const n=c?`tracer:starred:${c}`:null,[i,S]=s.useState(()=>{if(!n)return new Set;try{const a=localStorage.getItem(n);return a?new Set(JSON.parse(a)):new Set}catch{return new Set}});return[i,a=>{S(u=>{const t=new Set(u);return t.has(a)?t.delete(a):t.add(a),n&&localStorage.setItem(n,JSON.stringify([...t])),t})}]}function I({options:c,value:n,onChange:i,placeholder:S="Select...",storageKey:v,fitContent:a,disabled:u}){const[t,l]=s.useState(!1),[w,y]=s.useState(""),f=s.useRef(null),j=s.useRef(null),N=s.useRef(null),g=s.useRef(null),[x,C]=$(v),[m,L]=s.useState({top:0,left:0,minWidth:0});s.useEffect(()=>{if(!t)return;const e=d=>{f.current?.contains(d.target)||j.current?.contains(d.target)||l(!1)};return document.addEventListener("mousedown",e),()=>document.removeEventListener("mousedown",e)},[t]);const R=s.useCallback(()=>{if(!f.current)return;const e=f.current.getBoundingClientRect();L({top:e.bottom+4,left:e.left,minWidth:e.width})},[]);s.useEffect(()=>{t&&(R(),y(""),requestAnimationFrame(()=>N.current?.focus()))},[t,R]);const h=c.find(e=>e.value===n),p=s.useMemo(()=>{const e=w.toLowerCase();return[...e?c.filter(o=>o.label.toLowerCase().includes(e)||o.value.toLowerCase().includes(e)):c].sort((o,b)=>{const k=x.has(o.value)?0:1,E=x.has(b.value)?0:1;return k!==E?k-E:o.label.localeCompare(b.label)})},[c,w,x]);s.useEffect(()=>{if(t&&n&&g.current){const e=g.current.querySelector(`[data-value="${CSS.escape(n)}"]`);e&&e.scrollIntoView({block:"nearest"})}},[t,n]);const W=t?P.createPortal(r.jsxs("div",{ref:j,className:"fixed z-[100] bg-white border border-[#d4d2cd] rounded shadow-lg",style:{top:m.top,left:m.left,minWidth:m.minWidth,width:a?"max-content":m.minWidth},children:[r.jsx("div",{className:"p-1.5 border-b border-[#e8e6e1]",children:r.jsx("input",{ref:N,type:"text",value:w,onChange:e=>y(e.target.value),placeholder:"Search...",className:"w-full px-2 py-1.5 text-xs text-[#2c2c2c] font-sans bg-[#f5f4f0] border border-[#e8e6e1] rounded focus:outline-none focus:border-[#2b5ea7] placeholder:text-[#9c9890]",onKeyDown:e=>{e.key==="Escape"&&l(!1),e.key==="Enter"&&p.length>0&&(i(p[0].value),l(!1))}})}),r.jsx("div",{ref:g,className:"max-h-[280px] overflow-y-auto",children:p.length===0?r.jsx("div",{className:"px-3 py-3 text-xs text-[#9c9890] text-center",children:"No projects found"}):p.map(e=>{const d=e.value===n,o=x.has(e.value);return r.jsxs("div",{"data-value":e.value,className:`flex items-center gap-1.5 px-2 py-1.5 text-xs font-sans cursor-pointer transition-colors ${d?"bg-[#2b5ea7]/10 text-[#2b5ea7]":"text-[#2c2c2c] hover:bg-[#f5f4f0]"}`,onClick:()=>{i(e.value),l(!1)},children:[r.jsx("button",{type:"button",onClick:b=>{b.stopPropagation(),C(e.value)},className:`shrink-0 w-4 h-4 flex items-center justify-center text-[10px] transition-colors ${o?"text-[#d4a017]":"text-[#d4d2cd] hover:text-[#9c9890]"}`,title:o?"Unstar":"Star to pin to top",children:o?"★":"☆"}),r.jsx("span",{className:a?"whitespace-nowrap":"truncate flex-1",children:e.label})]},e.value)})})]}),document.body):null;return r.jsxs(r.Fragment,{children:[r.jsxs("button",{ref:f,type:"button",onClick:()=>!u&&l(!t),disabled:u,className:"w-full bg-white border border-[#d4d2cd] rounded px-3 py-2 text-xs text-[#2c2c2c] font-sans text-left flex items-center justify-between focus:outline-none focus:border-[#2b5ea7] hover:border-[#b0ada6] transition-colors disabled:opacity-50 disabled:cursor-not-allowed",children:[r.jsx("span",{className:h?"text-[#2c2c2c] truncate":"text-[#9c9890]",children:h?h.displayLabel??h.label:S}),r.jsx("span",{className:"text-[#9c9890] text-[10px] ml-2 shrink-0 transition-transform duration-200",style:{transform:t?"rotate(180deg)":"rotate(0deg)"},children:"▾"})]}),W]})}export{I as S,P as r,$ as u};
1
+ import{a as q,r as s,j as r}from"./index-DWtKdjfG.js";var P=q();function $(c){const n=c?`tracer:starred:${c}`:null,[i,S]=s.useState(()=>{if(!n)return new Set;try{const a=localStorage.getItem(n);return a?new Set(JSON.parse(a)):new Set}catch{return new Set}});return[i,a=>{S(u=>{const t=new Set(u);return t.has(a)?t.delete(a):t.add(a),n&&localStorage.setItem(n,JSON.stringify([...t])),t})}]}function I({options:c,value:n,onChange:i,placeholder:S="Select...",storageKey:v,fitContent:a,disabled:u}){const[t,l]=s.useState(!1),[w,y]=s.useState(""),f=s.useRef(null),j=s.useRef(null),N=s.useRef(null),g=s.useRef(null),[x,C]=$(v),[m,L]=s.useState({top:0,left:0,minWidth:0});s.useEffect(()=>{if(!t)return;const e=d=>{f.current?.contains(d.target)||j.current?.contains(d.target)||l(!1)};return document.addEventListener("mousedown",e),()=>document.removeEventListener("mousedown",e)},[t]);const R=s.useCallback(()=>{if(!f.current)return;const e=f.current.getBoundingClientRect();L({top:e.bottom+4,left:e.left,minWidth:e.width})},[]);s.useEffect(()=>{t&&(R(),y(""),requestAnimationFrame(()=>N.current?.focus()))},[t,R]);const h=c.find(e=>e.value===n),p=s.useMemo(()=>{const e=w.toLowerCase();return[...e?c.filter(o=>o.label.toLowerCase().includes(e)||o.value.toLowerCase().includes(e)):c].sort((o,b)=>{const k=x.has(o.value)?0:1,E=x.has(b.value)?0:1;return k!==E?k-E:o.label.localeCompare(b.label)})},[c,w,x]);s.useEffect(()=>{if(t&&n&&g.current){const e=g.current.querySelector(`[data-value="${CSS.escape(n)}"]`);e&&e.scrollIntoView({block:"nearest"})}},[t,n]);const W=t?P.createPortal(r.jsxs("div",{ref:j,className:"fixed z-[100] bg-white border border-[#d4d2cd] rounded shadow-lg",style:{top:m.top,left:m.left,minWidth:m.minWidth,width:a?"max-content":m.minWidth},children:[r.jsx("div",{className:"p-1.5 border-b border-[#e8e6e1]",children:r.jsx("input",{ref:N,type:"text",value:w,onChange:e=>y(e.target.value),placeholder:"Search...",className:"w-full px-2 py-1.5 text-xs text-[#2c2c2c] font-sans bg-[#f5f4f0] border border-[#e8e6e1] rounded focus:outline-none focus:border-[#2b5ea7] placeholder:text-[#9c9890]",onKeyDown:e=>{e.key==="Escape"&&l(!1),e.key==="Enter"&&p.length>0&&(i(p[0].value),l(!1))}})}),r.jsx("div",{ref:g,className:"max-h-[280px] overflow-y-auto",children:p.length===0?r.jsx("div",{className:"px-3 py-3 text-xs text-[#9c9890] text-center",children:"No projects found"}):p.map(e=>{const d=e.value===n,o=x.has(e.value);return r.jsxs("div",{"data-value":e.value,className:`flex items-center gap-1.5 px-2 py-1.5 text-xs font-sans cursor-pointer transition-colors ${d?"bg-[#2b5ea7]/10 text-[#2b5ea7]":"text-[#2c2c2c] hover:bg-[#f5f4f0]"}`,onClick:()=>{i(e.value),l(!1)},children:[r.jsx("button",{type:"button",onClick:b=>{b.stopPropagation(),C(e.value)},className:`shrink-0 w-4 h-4 flex items-center justify-center text-[10px] transition-colors ${o?"text-[#d4a017]":"text-[#d4d2cd] hover:text-[#9c9890]"}`,title:o?"Unstar":"Star to pin to top",children:o?"★":"☆"}),r.jsx("span",{className:a?"whitespace-nowrap":"truncate flex-1",children:e.label})]},e.value)})})]}),document.body):null;return r.jsxs(r.Fragment,{children:[r.jsxs("button",{ref:f,type:"button",onClick:()=>!u&&l(!t),disabled:u,className:"w-full bg-white border border-[#d4d2cd] rounded px-3 py-2 text-xs text-[#2c2c2c] font-sans text-left flex items-center justify-between focus:outline-none focus:border-[#2b5ea7] hover:border-[#b0ada6] transition-colors disabled:opacity-50 disabled:cursor-not-allowed",children:[r.jsx("span",{className:h?"text-[#2c2c2c] truncate":"text-[#9c9890]",children:h?h.displayLabel??h.label:S}),r.jsx("span",{className:"text-[#9c9890] text-[10px] ml-2 shrink-0 transition-transform duration-200",style:{transform:t?"rotate(180deg)":"rotate(0deg)"},children:"▾"})]}),W]})}export{I as S,P as r,$ as u};