@kinetica/admin-agent 0.2.2 → 0.2.4

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
@@ -332,14 +332,14 @@ The `--bundle` flag points the agent at an **extracted** support-bundle director
332
332
 
333
333
  Available against an extracted `gpudb_sysinfo` support bundle (see [Offline Bundle Mode](#offline-bundle-mode)). All read-only; the search/timeline tools stream and bound their output so a large rank log (tens of MB, hundreds of thousands of lines) never blows up the context.
334
334
 
335
- | Tool | Description |
336
- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
- | `kinetica_load_bundle` | Attach an extracted bundle directory; without a path it opens a directory picker (a model-supplied path needs operator confirmation) |
338
- | `kinetica_bundle_list_files` | Inventory: detected version, ranks + services present, file counts/sizes by kind, plus a layout-match verdict + per-file confidence for off-shape bundles — call this first |
339
- | `kinetica_bundle_log_timeline` | Per-time-bucket severity counts across ranks (the incident shape) — call before searching |
340
- | `kinetica_bundle_search_logs` | Bounded log search by regex, min-severity, time window, and rank / host-manager / component (reads both rolling and Loki-export logs) |
341
- | `kinetica_bundle_read_config` | Read the bundle's real on-disk `gpudb.conf`, with optional section/key filter |
342
- | `kinetica_bundle_read_sysinfo` | OS/process/version diagnostic files (memory, CPU, disk, GPU, network, process args) |
335
+ | Tool | Description |
336
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | `kinetica_load_bundle` | Attach an extracted bundle directory; without a path it opens a directory picker (a model-supplied path needs operator confirmation) |
338
+ | `kinetica_bundle_list_files` | Inventory: detected version, ranks + services present, file counts/sizes by kind, plus a layout-match verdict + per-file confidence for off-shape bundles — call this first |
339
+ | `kinetica_bundle_log_timeline` | Per-time-bucket severity counts across ranks (the incident shape) — call before searching |
340
+ | `kinetica_bundle_search_logs` | Bounded log search by regex, min-severity, time window, and rank / host-manager / component (reads both rolling and Loki-export logs); `include_multiline` stitches a multi-line record — e.g. a full `Executing SQL:` query whose embedded newlines span many lines — back onto each match |
341
+ | `kinetica_bundle_read_config` | Read the bundle's real on-disk `gpudb.conf`, with optional section/key filter |
342
+ | `kinetica_bundle_read_sysinfo` | OS/process/version diagnostic files (memory, CPU, disk, GPU, network, process args) |
343
343
 
344
344
  ### Reporting
345
345
 
@@ -417,7 +417,7 @@ References provide domain knowledge (not diagnostic runbooks). Create a `.md` fi
417
417
 
418
418
  **Playbooks** (6): memory-pressure, gpu-out-of-memory, query-contention, resource-group-exhaustion, stale-rank, config-drift
419
419
 
420
- **References** (9):
420
+ **References** (10):
421
421
 
422
422
  - `gpudb-conf` — master config file structure, section index, tiered storage semantics
423
423
  - `tiered-objects` — `ki_tiered_objects` schema, ID format, diagnostic queries
@@ -427,11 +427,12 @@ References provide domain knowledge (not diagnostic runbooks). Create a `.md` fi
427
427
  - `mutation-safety` — pre-execution checklist for rebalance, alter-config, and DDL paths
428
428
  - `sql-alter-table` — Kinetica 7.2 ALTER TABLE grammar, column property flags, shard-key immutability
429
429
  - `sql-create-index` — column index syntax, chunk skip index, when to use which
430
+ - `sql-dialect` — PostgreSQL-baseline mental model + a "false friends" table of cross-dialect SQL that looks valid but fails in Kinetica (e.g. `TRY_CAST`/`SAFE_CAST`, backtick quoting, `NUMERIC` vs `DECIMAL`); steers remediation SQL away from SQL Server/Snowflake/Oracle idioms
430
431
  - `version-quirks-7.2` — endpoint/property differences between 7.2.x and earlier releases
431
432
 
432
433
  Plus a **bundle-scoped reference** (`support-bundle` — bundle layout, the two per-rank log families, raw + Loki-JSONL log-line formats, severity ordering, file parsing, crash-SQL forensics, and how to work an off-shape bundle via the `layout_match`/confidence signals) that lives in `knowledge/references/bundle/`. It loads in **every** session — even a pure live one — so that a bundle attached mid-session via `kinetica_load_bundle` has its parsing knowledge ready in the (build-once) prompt; the corpus is cached, so the cost to a session that never attaches a bundle is negligible.
433
434
 
434
- > **Heads up — prompt budget:** all playbooks and references are front-loaded into a single system prompt at startup, so its token cost grows with the knowledge corpus. A startup tripwire (`agent/prompt-budget.ts`) prints the assembled prompt size under `DEBUG` and warns on stderr once it exceeds ~20,000 estimated tokens. Current baseline is ~13.4k tokens (6 playbooks + 9 references). If you add substantial knowledge and trip that warning, treat it as the cue to switch from "load everything" to keyword-based playbook selection.
435
+ > **Heads up — prompt budget:** all playbooks and references are front-loaded into a single system prompt at startup, so its token cost grows with the knowledge corpus. A startup tripwire (`agent/prompt-budget.ts`) prints the assembled prompt size under `DEBUG` and warns on stderr once it exceeds ~20,000 estimated tokens. Current baseline is ~15.5k tokens (6 playbooks + 10 references). If you add substantial knowledge and trip that warning, treat it as the cue to switch from "load everything" to keyword-based playbook selection.
435
436
 
436
437
  ## Development
437
438
 
@@ -4034,6 +4034,8 @@ function parseLogLine(line) {
4034
4034
 
4035
4035
  // src/bundle/log-search.ts
4036
4036
  var DEFAULT_MAX_MATCHES = 200;
4037
+ var MULTILINE_MAX_LINES = 300;
4038
+ var MULTILINE_MAX_CHARS = 2e4;
4037
4039
  var REGEX_SCAN_MAX = 8192;
4038
4040
  var GRANULARITY_LEN = {
4039
4041
  day: 10,
@@ -4075,6 +4077,23 @@ function matchesFilters(parsed, query3, regex, minRank) {
4075
4077
  return false;
4076
4078
  return true;
4077
4079
  }
4080
+ function buildMatch(lineNumber, parsed) {
4081
+ return {
4082
+ lineNumber,
4083
+ ...parsed.timestamp !== void 0 ? { timestamp: parsed.timestamp } : {},
4084
+ ...parsed.severity !== void 0 ? { severity: parsed.severity } : {},
4085
+ ...parsed.rank !== void 0 ? { rank: parsed.rank } : {},
4086
+ message: parsed.message,
4087
+ raw: parsed.raw
4088
+ };
4089
+ }
4090
+ function finalizeMultiline(pending) {
4091
+ if (pending.extra.length === 0) return pending.base;
4092
+ const joined = pending.extra.join("\n");
4093
+ const suffix = pending.truncated ? "\n\u2026 [continuation truncated]" : "";
4094
+ return { ...pending.base, message: `${pending.base.message}
4095
+ ${joined}${suffix}` };
4096
+ }
4078
4097
  async function searchLogFile(filePath, query3) {
4079
4098
  const maxMatches = query3.maxMatches ?? DEFAULT_MAX_MATCHES;
4080
4099
  const minRank = query3.minSeverity !== void 0 ? severityRank(query3.minSeverity) : -Infinity;
@@ -4096,9 +4115,17 @@ async function searchLogFile(filePath, query3) {
4096
4115
  ...query3.fromTs !== void 0 ? { fromTs: floorTimestamp(query3.fromTs) } : {},
4097
4116
  ...query3.toTs !== void 0 ? { toTs: ceilTimestamp(query3.toTs) } : {}
4098
4117
  };
4118
+ const coalesce = query3.coalesceMultiline === true;
4099
4119
  const matches = [];
4100
4120
  let totalMatched = 0;
4101
4121
  let linesScanned = 0;
4122
+ let pending;
4123
+ const flushPending = () => {
4124
+ if (pending) {
4125
+ matches.push(finalizeMultiline(pending));
4126
+ pending = void 0;
4127
+ }
4128
+ };
4102
4129
  try {
4103
4130
  const rl = (0, import_node_readline.createInterface)({
4104
4131
  input: (0, import_node_fs4.createReadStream)(filePath, { encoding: "utf-8" }),
@@ -4107,20 +4134,29 @@ async function searchLogFile(filePath, query3) {
4107
4134
  for await (const line of rl) {
4108
4135
  linesScanned++;
4109
4136
  const parsed = parseLogLine(line);
4137
+ if (pending) {
4138
+ if (parsed.timestamp === void 0) {
4139
+ if (!pending.truncated && pending.extra.length < MULTILINE_MAX_LINES && pending.chars + line.length + 1 <= MULTILINE_MAX_CHARS) {
4140
+ pending.extra.push(line);
4141
+ pending.chars += line.length + 1;
4142
+ } else {
4143
+ pending.truncated = true;
4144
+ }
4145
+ continue;
4146
+ }
4147
+ flushPending();
4148
+ }
4110
4149
  if (!matchesFilters(parsed, boundedQuery, regex, minRank)) continue;
4111
4150
  totalMatched++;
4112
4151
  if (matches.length < maxMatches) {
4113
- matches.push({
4114
- lineNumber: linesScanned,
4115
- ...parsed.timestamp !== void 0 ? { timestamp: parsed.timestamp } : {},
4116
- ...parsed.severity !== void 0 ? { severity: parsed.severity } : {},
4117
- ...parsed.rank !== void 0 ? { rank: parsed.rank } : {},
4118
- message: parsed.message,
4119
- raw: parsed.raw
4120
- });
4152
+ const base = buildMatch(linesScanned, parsed);
4153
+ if (coalesce) pending = { base, extra: [], chars: 0, truncated: false };
4154
+ else matches.push(base);
4121
4155
  }
4122
4156
  }
4157
+ flushPending();
4123
4158
  } catch (err) {
4159
+ flushPending();
4124
4160
  const message = err instanceof Error ? err.message : String(err);
4125
4161
  return {
4126
4162
  matches,
@@ -4508,7 +4544,8 @@ function toLineQuery(q) {
4508
4544
  ...q.minSeverity !== void 0 ? { minSeverity: q.minSeverity } : {},
4509
4545
  ...q.fromTs !== void 0 ? { fromTs: q.fromTs } : {},
4510
4546
  ...q.toTs !== void 0 ? { toTs: q.toTs } : {},
4511
- ...q.maxMatches !== void 0 ? { maxMatches: q.maxMatches } : {}
4547
+ ...q.maxMatches !== void 0 ? { maxMatches: q.maxMatches } : {},
4548
+ ...q.coalesceMultiline !== void 0 ? { coalesceMultiline: q.coalesceMultiline } : {}
4512
4549
  };
4513
4550
  }
4514
4551
  function toTimelineLineQuery(q) {
@@ -4847,6 +4884,9 @@ var BundleSearchLogsSchema = import_zod20.z.object({
4847
4884
  host_manager: import_zod20.z.boolean().describe("Search the host-manager (hm) log \u2014 a singleton service, not a rank.").optional(),
4848
4885
  component: import_zod20.z.string().optional(),
4849
4886
  include_components: import_zod20.z.boolean().optional(),
4887
+ include_multiline: import_zod20.z.boolean().describe(
4888
+ "Reconstruct multi-line log records: append continuation lines (those with no timestamp) to each match. Use this to capture a full SQL statement on an 'Executing SQL:' line \u2014 the query often spans many lines because the SQL has embedded newlines, and a plain match shows only its first line. Works on the rolling core logs (logs-local/); Loki per-rank tails (logs/rankN.log) keep only the statement's first line, so there are no continuation lines to stitch there."
4889
+ ).optional(),
4850
4890
  max_matches: import_zod20.z.number().int().min(1).max(1e3).optional()
4851
4891
  });
4852
4892
  async function bundleSearchLogs(source, args = {}) {
@@ -4859,6 +4899,7 @@ async function bundleSearchLogs(source, args = {}) {
4859
4899
  ...args.host_manager !== void 0 ? { hostManager: args.host_manager } : {},
4860
4900
  ...args.component !== void 0 ? { component: args.component } : {},
4861
4901
  ...args.include_components !== void 0 ? { includeComponents: args.include_components } : {},
4902
+ ...args.include_multiline !== void 0 ? { coalesceMultiline: args.include_multiline } : {},
4862
4903
  ...args.max_matches !== void 0 ? { maxMatches: args.max_matches } : {}
4863
4904
  };
4864
4905
  const result = await source.searchLogs(query3);
@@ -20,6 +20,7 @@ Severity order for filtering is `WARN < UERR < ERROR < FATAL`, so `min_severity=
20
20
 
21
21
  - The logs are large (a rank log can exceed 100k lines). NEVER ask for a whole file. Use `kinetica_bundle_log_timeline` to localize, then `kinetica_bundle_search_logs` with a tight time window + severity to extract only relevant lines. The match cap is shared across files — if you see "capped", narrow the query rather than asking for more.
22
22
  - You can pass a timeline bucket label straight into `from_ts`/`to_ts` (e.g. `2026-06-11 15` searches that whole hour) — partial timestamps are widened to cover the full period.
23
+ - A single log record can span multiple physical lines when a logged value (notably a SQL statement) contains embedded newlines — the continuation lines have no timestamp. A plain search returns only the first line. Pass `include_multiline: true` to stitch the continuation lines back onto each match and recover the whole record. See "Finding a crash's triggering SQL".
23
24
  - Timestamps are plain local strings without a timezone; compare them lexically and treat cross-rank timing cautiously.
24
25
  - **Ranks vs. the host manager:** `rank` selects a numeric rank (`r0`, `r1`, …) only. The host manager (`core-gpudb-rolling-hm.log`) is a singleton service, NOT a rank — search or timeline it with `host_manager: true`, never `rank: "hm"`. By default both `log_timeline` and `search_logs` already cover the host manager along with the numeric ranks; `kinetica_bundle_list_files` lists it under `services_present`.
25
26
 
@@ -29,11 +30,13 @@ When a worker rank segfaults mid-query, that rank's log holds the **backtrace**
29
30
 
30
31
  Workflow, given a `JobId` from a worker's crash stack:
31
32
 
32
- 1. `kinetica_bundle_search_logs` with `rank: "r0"` and `regex` = the JobId. r0 logs the `/execute/sql` receipt (submitting user), the `Sql/SqlDriver.cpp … Executing SQL:` line, and per-operation endpoint lines.
33
- 2. The per-operation lines (`Endpoint_aggregate_group_by.cpp`, filter/join endpoints) carry `table:`, `column_names:`/`aliases:` (the SELECT list), and `expr:` (the full WHERE predicate) reconstruct the query from these.
34
- 3. **Quirk:** if `Found plan for the SQL in cache` precedes it, the `Executing SQL:` line is truncated to just `SELECT`. Use the per-operation endpoint lines (step 2) their predicate survives a cache hit. A `datetime()`/timestamp filter showing up here often _is_ the input that triggered a parser segfault.
33
+ 1. `kinetica_bundle_search_logs` with `rank: "r0"`, `regex` = the JobId, **and `include_multiline: true`**. r0 logs the `/execute/sql` receipt (submitting user), the `Sql/SqlDriver.cpp … Executing SQL:` line, and per-operation endpoint lines.
34
+ 2. **Read the full statement straight from the `Executing SQL:` line — do not reconstruct it.** Kinetica logs the SQL verbatim, so a real query spans MANY physical lines: `FROM …`, `JOIN …`, `WHERE …` each land on their own line with no timestamp prefix. Those continuation lines belong to the same log record; `include_multiline: true` stitches them back onto the match so you see the WHOLE query. WITHOUT it, a match is only the first physical line (e.g. `… Executing SQL: SELECT c."circuitId", c."circuitRef"`) and you would wrongly report the query as "truncated." Quote the statement verbatim from this single (now multi-line) match.
35
+ 3. **Cache-hit fallback only:** if `Found plan for the SQL in cache` precedes the job, Kinetica logs `Executing SQL:` as just the statement keyword (e.g. a bare `SELECT` or `EXECUTE PROCEDURE …`) with no continuation lines to stitch. ONLY then fall back to the per-operation endpoint lines (`Endpoint_aggregate_group_by.cpp`, filter/join endpoints): they carry `table:`, `column_names:`/`aliases:` (the SELECT list), and `expr:` (the full WHERE predicate), whose values survive a cache hit. A `datetime()`/timestamp filter showing up here often _is_ the input that triggered a parser segfault.
35
36
 
36
- See `rank-architecture.md` (Where queries are logged) for why this locality holds.
37
+ **Where the full multi-line query actually lives:** `include_multiline` recovers the whole statement only from the **rolling core logs** (`logs-local/core-gpudb-rolling-r0.log`), where the SQL's embedded newlines are preserved as continuation lines. The **Loki per-rank tail** (`logs/rank0.log`) keeps only the statement's first physical line — promtail captures each line as its own record, so the `FROM`/`JOIN`/`WHERE` lines are simply not in that export, and nothing can stitch them. So this workflow depends on r0 being present under `logs-local/`. If r0 exists only as a Loki tail (rare for the coordinator, but possible for a Loki-only bundle), the complete query may not be in the bundle at all — say so rather than reporting the first line as the whole query, and fall back to step 3's endpoint lines.
38
+
39
+ See `rank-architecture.md` (Where queries are logged) for why this locality holds, and "Two log families" below for rolling-vs-Loki precedence.
37
40
 
38
41
  ### Files of interest
39
42
 
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Kinetica SQL Dialect — PostgreSQL Baseline & False Friends
3
+ category: sql-syntax
4
+ keywords:
5
+ [
6
+ sql-dialect,
7
+ postgresql,
8
+ false-friends,
9
+ try-cast,
10
+ safe-cast,
11
+ cast,
12
+ convert,
13
+ remediation-sql,
14
+ datediff,
15
+ timestamp,
16
+ nested-aggregate,
17
+ identifiers,
18
+ backticks,
19
+ decimal,
20
+ numeric,
21
+ ]
22
+ ---
23
+
24
+ ## Mental Model — Start from PostgreSQL
25
+
26
+ Kinetica SQL is **PostgreSQL-compatible**: treat standard PostgreSQL syntax,
27
+ functions, and behavior as the baseline. The deviations documented here (and in
28
+ `version-quirks-7.2.md`) **override** that baseline. When no Kinetica-specific
29
+ rule applies, the PostgreSQL form is the safe default.
30
+
31
+ **The common failure when recommending remediation SQL is importing idioms from
32
+ OTHER dialects** — SQL Server, Snowflake, Oracle, MySQL. Those are not the
33
+ baseline; PostgreSQL is. The table below lists imports that look valid but fail
34
+ in Kinetica.
35
+
36
+ > Dialect facts adapted from the official `kineticadb/agent-skills` knowledge
37
+ > base (Apache-2.0).
38
+
39
+ ## False Friends — Looks Valid, FAILS in Kinetica
40
+
41
+ Do NOT put any of these in a remediation suggestion. Use the Kinetica form.
42
+
43
+ | Looks valid (other dialect) | Why it fails | Use instead |
44
+ | -------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
45
+ | `TRY_CAST(x AS t)` / `SAFE_CAST(x, t)` | No error-tolerant cast exists in Kinetica | `CAST(x AS t)` or `CONVERT(x, t)`; shorthand `INT(x)`, `DOUBLE(x)`, `STRING(x)` |
46
+ | `` `ident` `` (backtick quoting) | Backticks are not a valid identifier quote | ANSI double quotes: `"ident"` |
47
+ | `ts1 - ts2` (timestamp subtraction) | Timestamp arithmetic with `-` is not supported | `DATEDIFF('unit', ts1, ts2)` |
48
+ | `NUMERIC(p, s)` | The type is named `DECIMAL`, not `NUMERIC` | `DECIMAL(p, s)` (max precision 27, max scale 18) |
49
+ | `SUM(COUNT(*))` (nested aggregates) | Fails with "Aggregate expressions cannot be nested" | Separate into CTEs — window/aggregate in different stages |
50
+ | `ANALYZE TABLE t` | No cost-based optimizer stats (see `version-quirks-7.2.md`) | No equivalent — do NOT suggest a "refresh table stats" step |
51
+ | `SELECT ... ;` (trailing semicolon) | A trailing `;` is rejected | Omit the trailing semicolon |
52
+ | `ORDER BY <array_col>` | Cannot sort by an `array<...>` column | Index an element (`ORDER BY "col"[1]`) or sort by a scalar column |
53
+
54
+ `TRY_CAST` / `SAFE_CAST` warrant special note: they come from SQL Server,
55
+ Snowflake, and BigQuery, and Kinetica has no cast variant that returns NULL on
56
+ conversion failure. If a value might not convert cleanly, filter the source rows
57
+ (`WHERE` / `CASE`) before casting rather than reaching for a non-existent
58
+ `TRY_*` function.
59
+
60
+ ## Type Conversion — the Valid Forms
61
+
62
+ - Standard `CAST(expr AS type)` and `CONVERT(expr, type)` both work.
63
+ - Shorthand cast functions: `INT(expr)`, `LONG(expr)`, `DOUBLE(expr)`,
64
+ `FLOAT(expr)`, `DECIMAL(expr)`, `STRING(expr)`, `ULONG(expr)`.
65
+ - `JSON_EXTRACT_VALUE` always returns TEXT — you MUST cast for numeric use:
66
+ `CAST(JSON_EXTRACT_VALUE("payload", '$.count') AS INTEGER) > 100`.
67
+
68
+ ## Date / Time — Use Functions, Not Arithmetic
69
+
70
+ | Kinetica form | Replaces (PostgreSQL / other) |
71
+ | ------------------------------------ | --------------------------------- |
72
+ | `DATEDIFF('unit', start, end)` | `EXTRACT(EPOCH FROM end - start)` |
73
+ | `DATEADD('unit', amount, ts)` | `ts + INTERVAL '...'` |
74
+ | `TIME_BUCKET(INTERVAL 'n' UNIT, ts)` | `date_bin()` |
75
+
76
+ Units: `SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR` (also
77
+ `MICROSECOND` / `MILLISECOND`). INTERVAL syntax: `INTERVAL '30' MINUTE`.
78
+
79
+ ## Identifier & Statement Hygiene
80
+
81
+ - **Double quotes only** for identifiers (`"my_col"`) — never backticks.
82
+ - **Identifiers are case-sensitive** — `"UserID"` ≠ `"userid"`. Verify column
83
+ names against the discovered schema before recommending SQL.
84
+ - **Fully-qualify table names** — `"schema"."table"`.
85
+ - **No trailing semicolons.**
86
+
87
+ ## Kinetica Conveniences (valid, non-obvious)
88
+
89
+ - `SELECT * EXCLUDE (col1, col2)` — wildcard minus specific columns.
90
+ - `IF(cond, a, b)` — ternary (PostgreSQL has only `CASE`).
91
+ - `NVL(x, default)` / `NVL2(x, not_null, null_val)` — null handling.
92
+ - `DECODE(expr, m1, v1, ..., default)` — pattern matching.
93
+
94
+ ## When Unsure — Verify Empirically
95
+
96
+ The live database is the source of truth. Before recommending any remediation
97
+ SQL whose syntax you are not certain Kinetica supports, validate it with
98
+ `kinetica_explain_query` against the live instance. If it cannot be validated
99
+ (or there is no live connection), label the suggestion as unverified rather than
100
+ asserting it is correct.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetica/admin-agent",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Autonomous diagnostic agent for Kinetica databases",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kinetica",