@pencil-agent/nano-pencil 1.14.2 → 1.14.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.
Files changed (104) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.d.ts +4 -0
  3. package/dist/builtin-extensions.js +16 -16
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/runner.d.ts +70 -4
  7. package/dist/core/extensions/runner.js +188 -8
  8. package/dist/core/extensions/types.d.ts +8 -1
  9. package/dist/core/i18n/slash-commands.d.ts +12 -0
  10. package/dist/core/i18n/slash-commands.js +16 -4
  11. package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
  12. package/dist/core/i18n/slash-commands.zh.js +16 -4
  13. package/dist/core/runtime/agent-session.d.ts +5 -0
  14. package/dist/core/runtime/agent-session.js +85 -27
  15. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  16. package/dist/core/runtime/extension-core-bindings.js +73 -20
  17. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  18. package/dist/core/runtime/retry-coordinator.js +20 -4
  19. package/dist/core/runtime/sdk.js +2 -1
  20. package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
  21. package/dist/core/runtime/slash-command-catalog.js +9 -1
  22. package/dist/core/slash-commands.d.ts +12 -1
  23. package/dist/core/slash-commands.js +81 -32
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/browser/index.js +14 -6
  41. package/dist/extensions/defaults/btw/index.js +2 -2
  42. package/dist/extensions/defaults/debug/index.js +38 -3
  43. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  44. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  45. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  46. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  47. package/dist/extensions/defaults/grub/index.js +4 -3
  48. package/dist/extensions/defaults/interview/index.js +2 -2
  49. package/dist/extensions/defaults/link-world/index.js +14 -6
  50. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
  51. package/dist/extensions/defaults/loop/index.js +35 -0
  52. package/dist/extensions/defaults/mcp/index.js +18 -0
  53. package/dist/extensions/defaults/plan/index.js +29 -11
  54. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  55. package/dist/extensions/defaults/presence/index.js +77 -23
  56. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  57. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  58. package/dist/extensions/defaults/recap/index.js +12 -0
  59. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  61. package/dist/extensions/defaults/sal/index.js +31 -8
  62. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  63. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  64. package/dist/extensions/defaults/security-audit/index.js +141 -83
  65. package/dist/extensions/defaults/subagent/index.js +29 -5
  66. package/dist/extensions/defaults/team/index.js +10 -9
  67. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  68. package/dist/extensions/defaults/team/team-parser.js +91 -3
  69. package/dist/extensions/defaults/team/team-ui.js +4 -14
  70. package/dist/extensions/defaults/token-save/index.js +14 -1
  71. package/dist/extensions/optional/export-html/index.js +19 -5
  72. package/dist/extensions/optional/simplify/index.js +11 -5
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  74. package/dist/modes/interactive/interactive-mode.js +68 -19
  75. package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
  76. package/dist/modes/interactive/slash-command-arguments.js +97 -0
  77. package/dist/modes/rpc/rpc-mode.d.ts +3 -0
  78. package/dist/modes/rpc/rpc-mode.js +40 -31
  79. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  80. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
  81. package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
  82. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  83. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  84. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
  85. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
  86. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  87. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  88. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  89. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  90. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
  91. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  92. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  93. package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
  94. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  95. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
  96. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  97. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
  98. package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
  99. package/dist/packages/mem-core/extension.js +154 -83
  100. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  101. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  102. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  103. package/dist/packages/mem-core/full-insights.js +102 -42
  104. package/package.json +2 -2
@@ -1,32 +1,24 @@
1
1
  /**
2
- * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
3
- * [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
2
+ * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter for SAL)
3
+ * [FROM]: Depends on core/telemetry/* for InsforgeHttpClient + BatchingDispatcher + types; ./types.js for SAL event shape (EvalSink/EvalEventEnvelope/CreateEvalSinkOptions)
4
4
  * [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
5
- * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version with legacy fallback), turn_anchor→eval_turns + eval_sal_anchors×2 with on_conflict-based upsert (suppresses benign 23505 retries) only after parent run confirmation, tool_trace→eval_tool_traces with legacy-schema fallback, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
5
+ * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - SAL-specific event routing only: run_start→eval_runs, turn_anchor→eval_turns+eval_sal_anchors×2, tool_trace→eval_tool_traces (legacy fallback), memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH. HTTP transport + batching come from core/telemetry/.
6
6
  *
7
- * Pluggable: nothing in this file may be imported from outside the eval/ directory.
7
+ * Pluggable: nothing in this file may be imported from outside the eval/ directory other than core/telemetry (the shared base layer).
8
8
  * To add a new backend, write a sibling file with the same EvalSink interface.
9
9
  */
10
10
  import type { CreateEvalSinkOptions, EvalEventEnvelope, EvalSink } from "./types.js";
11
11
  export declare class InsForgeEvalSink implements EvalSink {
12
12
  readonly enabled = true;
13
- private base;
14
- private headers;
15
- private allowSelfSigned;
16
- private batchIntervalMs;
13
+ private http;
14
+ private dispatcher;
17
15
  private onDiagnostic;
18
- private pending;
19
- private flushTimer;
20
- private flushInFlight;
21
- private closed;
22
16
  private confirmedRuns;
23
17
  private failedRuns;
24
18
  constructor(options: CreateEvalSinkOptions);
25
19
  sendEvent(event: EvalEventEnvelope): Promise<void>;
26
20
  flush(): Promise<void>;
27
- private doFlush;
28
21
  close(): Promise<void>;
29
- private scheduleFlush;
30
22
  private routeEvent;
31
23
  private handleRunStart;
32
24
  private ensureRunExists;
@@ -34,8 +26,5 @@ export declare class InsForgeEvalSink implements EvalSink {
34
26
  private handleRunEnd;
35
27
  private handleMemoryRecalls;
36
28
  private handleToolTrace;
37
- private postJson;
38
- private patchJson;
39
- private httpJson;
40
29
  private reportDiagnostic;
41
30
  }
@@ -1,105 +1,50 @@
1
1
  /**
2
- * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
3
- * [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
2
+ * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter for SAL)
3
+ * [FROM]: Depends on core/telemetry/* for InsforgeHttpClient + BatchingDispatcher + types; ./types.js for SAL event shape (EvalSink/EvalEventEnvelope/CreateEvalSinkOptions)
4
4
  * [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
5
- * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version with legacy fallback), turn_anchor→eval_turns + eval_sal_anchors×2 with on_conflict-based upsert (suppresses benign 23505 retries) only after parent run confirmation, tool_trace→eval_tool_traces with legacy-schema fallback, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
5
+ * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - SAL-specific event routing only: run_start→eval_runs, turn_anchor→eval_turns+eval_sal_anchors×2, tool_trace→eval_tool_traces (legacy fallback), memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH. HTTP transport + batching come from core/telemetry/.
6
6
  *
7
- * Pluggable: nothing in this file may be imported from outside the eval/ directory.
7
+ * Pluggable: nothing in this file may be imported from outside the eval/ directory other than core/telemetry (the shared base layer).
8
8
  * To add a new backend, write a sibling file with the same EvalSink interface.
9
9
  */
10
- import { request as httpRequest } from "node:http";
11
- import { request } from "node:https";
12
- import { URL } from "node:url";
13
- import { isDevRuntime } from "../../../../utils/diagnostics.js";
10
+ import { BatchingDispatcher, InsforgeHttpClient, safeHost } from "../../../../core/telemetry/index.js";
14
11
  export class InsForgeEvalSink {
15
12
  enabled = true;
16
- base;
17
- headers;
18
- allowSelfSigned;
19
- batchIntervalMs;
13
+ http;
14
+ dispatcher;
20
15
  onDiagnostic;
21
- pending = [];
22
- flushTimer;
23
- flushInFlight;
24
- closed = false;
25
16
  confirmedRuns = new Set();
26
17
  failedRuns = new Set();
27
18
  constructor(options) {
28
- this.base = options.endpoint.replace(/\/+$/, "");
29
- this.batchIntervalMs = options.batchIntervalMs ?? 2000;
30
19
  this.onDiagnostic = options.onDiagnostic;
31
- this.allowSelfSigned = options.allowSelfSigned ?? false;
32
- if (this.allowSelfSigned && isDevRuntime()) {
33
- console.warn("[sal][eval] TLS certificate verification disabled (allowSelfSigned=true)");
34
- }
35
- const h = {
36
- "Content-Type": "application/json",
37
- ...(options.headers ?? {}),
38
- };
39
- if (options.anonKey) {
40
- h["apikey"] = options.anonKey;
41
- h["Authorization"] = `Bearer ${options.anonKey}`;
42
- }
43
- if (options.apiKey) {
44
- h[options.apiKeyHeader ?? "x-api-key"] = options.apiKey;
45
- if (!options.anonKey) {
46
- h["Authorization"] = `Bearer ${options.apiKey}`;
47
- }
48
- }
49
- this.headers = h;
20
+ this.http = new InsforgeHttpClient({
21
+ endpoint: options.endpoint,
22
+ apiKey: options.apiKey,
23
+ anonKey: options.anonKey,
24
+ apiKeyHeader: options.apiKeyHeader,
25
+ extraHeaders: options.headers,
26
+ allowSelfSigned: options.allowSelfSigned,
27
+ source: "sal.eval",
28
+ onDiagnostic: this.onDiagnostic,
29
+ });
30
+ this.dispatcher = new BatchingDispatcher({
31
+ handler: (event) => this.routeEvent(event),
32
+ intervalMs: options.batchIntervalMs ?? 2000,
33
+ source: "sal.eval",
34
+ onDiagnostic: this.onDiagnostic,
35
+ });
50
36
  }
51
37
  async sendEvent(event) {
52
- if (this.closed)
53
- return;
54
- this.pending.push(event);
55
- this.scheduleFlush();
38
+ this.dispatcher.enqueue(event);
56
39
  }
57
40
  async flush() {
58
- if (this.flushInFlight) {
59
- await this.flushInFlight.catch(() => { });
60
- return;
61
- }
62
- this.flushInFlight = this.doFlush();
63
- try {
64
- await this.flushInFlight;
65
- }
66
- catch (err) {
67
- this.reportDiagnostic("persistence", "SAL eval flush failed.", err, "flush");
68
- }
69
- finally {
70
- this.flushInFlight = undefined;
71
- }
72
- }
73
- async doFlush() {
74
- if (this.flushTimer) {
75
- clearTimeout(this.flushTimer);
76
- this.flushTimer = undefined;
77
- }
78
- while (true) {
79
- const toFlush = this.pending.splice(0);
80
- if (toFlush.length === 0)
81
- break;
82
- for (const event of toFlush) {
83
- await this.routeEvent(event);
84
- }
85
- }
41
+ await this.dispatcher.flush();
86
42
  }
87
43
  async close() {
88
- this.closed = true;
89
- await this.flush().catch((err) => {
90
- this.reportDiagnostic("persistence", "SAL eval close flush failed.", err, "close-flush");
91
- });
92
- }
93
- scheduleFlush() {
94
- if (this.flushTimer)
95
- return;
96
- this.flushTimer = setTimeout(() => {
97
- this.flushTimer = undefined;
98
- void this.flush().catch(() => { });
99
- }, this.batchIntervalMs);
44
+ await this.dispatcher.close();
100
45
  }
101
46
  // ------------------------------------------------------------------
102
- // Routing
47
+ // SAL-specific routing
103
48
  // ------------------------------------------------------------------
104
49
  async routeEvent(event) {
105
50
  try {
@@ -146,8 +91,8 @@ export class InsForgeEvalSink {
146
91
  workspace_root: strOrNull(p.workspace_root),
147
92
  started_at: ev.ts,
148
93
  };
149
- const url = `${this.base}/api/database/records/eval_runs`;
150
- const result = await this.postJson(url, [row], {
94
+ const url = `${this.http.base}/api/database/records/eval_runs`;
95
+ const result = await this.http.postJson(url, [row], {
151
96
  prefer: "resolution=merge-duplicates",
152
97
  quietErrorCodes: ["PGRST204"],
153
98
  });
@@ -156,7 +101,7 @@ export class InsForgeEvalSink {
156
101
  this.failedRuns.delete(ev.run_id);
157
102
  return;
158
103
  }
159
- const fallback = await this.postJson(url, [toLegacyRunStartRow(row)], {
104
+ const fallback = await this.http.postJson(url, [toLegacyRunStartRow(row)], {
160
105
  prefer: "resolution=merge-duplicates",
161
106
  });
162
107
  if (fallback.ok) {
@@ -201,7 +146,7 @@ export class InsForgeEvalSink {
201
146
  // UNIQUE(run_id, turn_id) collision via ON CONFLICT DO UPDATE
202
147
  // instead of returning 409/23505. ignore-duplicates only checks the
203
148
  // primary key, which is auto-uuid here, so it never sees the conflict.
204
- await this.postJson(`${this.base}/api/database/records/eval_turns?on_conflict=run_id,turn_id`, [{
149
+ await this.http.postJson(`${this.http.base}/api/database/records/eval_turns?on_conflict=run_id,turn_id`, [{
205
150
  run_id: ev.run_id,
206
151
  turn_id: turnId,
207
152
  event_id: ev.event_id,
@@ -212,7 +157,7 @@ export class InsForgeEvalSink {
212
157
  }], { prefer: "resolution=merge-duplicates" });
213
158
  const taskAnchor = p.task_anchor;
214
159
  if (taskAnchor) {
215
- await this.postJson(`${this.base}/api/database/records/eval_sal_anchors?on_conflict=run_id,turn_id,anchor_type`, [{
160
+ await this.http.postJson(`${this.http.base}/api/database/records/eval_sal_anchors?on_conflict=run_id,turn_id,anchor_type`, [{
216
161
  run_id: ev.run_id,
217
162
  turn_id: turnId,
218
163
  event_id: `${ev.event_id}-task`,
@@ -225,7 +170,7 @@ export class InsForgeEvalSink {
225
170
  }], { prefer: "resolution=merge-duplicates" });
226
171
  }
227
172
  const actionAnchor = p.action_anchor;
228
- await this.postJson(`${this.base}/api/database/records/eval_sal_anchors?on_conflict=run_id,turn_id,anchor_type`, [{
173
+ await this.http.postJson(`${this.http.base}/api/database/records/eval_sal_anchors?on_conflict=run_id,turn_id,anchor_type`, [{
229
174
  run_id: ev.run_id,
230
175
  turn_id: turnId,
231
176
  event_id: `${ev.event_id}-action`,
@@ -244,7 +189,7 @@ export class InsForgeEvalSink {
244
189
  const normalizedStatus = rawStatus === "success" ? "completed"
245
190
  : rawStatus === "error" ? "failed"
246
191
  : rawStatus ?? "completed";
247
- await this.patchJson(`${this.base}/api/database/records/eval_runs?run_id=eq.${ev.run_id}`, {
192
+ await this.http.patchJson(`${this.http.base}/api/database/records/eval_runs?run_id=eq.${ev.run_id}`, {
248
193
  status: normalizedStatus,
249
194
  turn_count: numOrNull(p.turn_count),
250
195
  total_duration_ms: numOrNull(p.total_duration_ms),
@@ -274,7 +219,7 @@ export class InsForgeEvalSink {
274
219
  inject_rank: numOrNull(r.injectRank),
275
220
  recorded_at: ev.ts,
276
221
  }));
277
- await this.postJson(`${this.base}/api/database/records/eval_memory_recalls`, rows, { prefer: "resolution=ignore-duplicates" });
222
+ await this.http.postJson(`${this.http.base}/api/database/records/eval_memory_recalls`, rows, { prefer: "resolution=ignore-duplicates" });
278
223
  }
279
224
  // INSERT into eval_tool_traces — one row per turn with tool usage summary
280
225
  // InsForge columns are all TEXT; JSONB fields must be serialized to strings.
@@ -300,86 +245,15 @@ export class InsForgeEvalSink {
300
245
  duration_ms: String(p.duration_ms ?? 0),
301
246
  recorded_at: ev.ts,
302
247
  };
303
- const url = `${this.base}/api/database/records/eval_tool_traces`;
304
- const result = await this.postJson(url, [row], {
248
+ const url = `${this.http.base}/api/database/records/eval_tool_traces`;
249
+ const result = await this.http.postJson(url, [row], {
305
250
  prefer: "resolution=ignore-duplicates",
306
251
  quietErrorCodes: ["PGRST204"],
307
252
  });
308
253
  if (!result.ok && result.errorCode === "PGRST204") {
309
- await this.postJson(url, [toLegacyToolTraceRow(row)], { prefer: "resolution=ignore-duplicates" });
254
+ await this.http.postJson(url, [toLegacyToolTraceRow(row)], { prefer: "resolution=ignore-duplicates" });
310
255
  }
311
256
  }
312
- // ------------------------------------------------------------------
313
- // HTTP helpers
314
- // ------------------------------------------------------------------
315
- postJson(url, body, extra) {
316
- const extraHeaders = {};
317
- if (extra?.prefer)
318
- extraHeaders["Prefer"] = extra.prefer;
319
- return this.httpJson("POST", url, body, extraHeaders, extra?.quietErrorCodes);
320
- }
321
- patchJson(url, body) {
322
- return this.httpJson("PATCH", url, body, {});
323
- }
324
- httpJson(method, url, body, extraHeaders, quietErrorCodes = []) {
325
- return new Promise((resolve) => {
326
- const payload = JSON.stringify(body);
327
- let parsed;
328
- try {
329
- parsed = new URL(url);
330
- }
331
- catch {
332
- this.reportDiagnostic("config", "SAL eval endpoint URL is invalid.", { url }, "invalid-url");
333
- resolve({ ok: false });
334
- return;
335
- }
336
- const isHttps = parsed.protocol === "https:";
337
- const requestFn = isHttps ? request : httpRequest;
338
- const port = parsed.port ? Number(parsed.port) : (isHttps ? 443 : 80);
339
- const req = requestFn({
340
- hostname: parsed.hostname,
341
- port,
342
- path: parsed.pathname + parsed.search,
343
- method,
344
- headers: {
345
- ...this.headers,
346
- ...extraHeaders,
347
- "Content-Length": Buffer.byteLength(payload),
348
- },
349
- timeout: 5000,
350
- ...(isHttps && this.allowSelfSigned ? { rejectUnauthorized: false } : {}),
351
- }, (res) => {
352
- let rawBody = "";
353
- res.setEncoding("utf-8");
354
- res.on("data", (chunk) => { rawBody += chunk; });
355
- res.on("end", () => {
356
- const ok = res.statusCode !== undefined && res.statusCode < 300;
357
- const errorCode = parsePostgrestErrorCode(rawBody);
358
- if (!ok && !quietErrorCodes.includes(errorCode ?? "")) {
359
- this.reportDiagnostic(errorCode === "PGRST204" ? "schema" : "network", `SAL eval upload failed with HTTP ${res.statusCode}.`, { method, path: parsed.pathname, statusCode: res.statusCode, body: rawBody.slice(0, 300), errorCode }, `http-${res.statusCode ?? "unknown"}-${errorCode ?? "none"}`);
360
- }
361
- resolve({ ok, statusCode: res.statusCode, body: rawBody, errorCode });
362
- });
363
- });
364
- req.on("error", (err) => {
365
- this.reportDiagnostic("network", "SAL eval upload is failing due to a network connection error.", { host: parsed.hostname, error: err.message }, "network-error");
366
- if (isDevRuntime()) {
367
- console.error(`[sal][eval] network error → ${parsed.hostname}: ${err.message}`);
368
- }
369
- resolve({ ok: false });
370
- });
371
- req.on("timeout", () => {
372
- this.reportDiagnostic("network", "SAL eval upload timed out.", { method, path: parsed.pathname, host: parsed.hostname }, "timeout");
373
- if (isDevRuntime()) {
374
- console.error(`[sal][eval] timeout ${method} ${parsed.pathname}`);
375
- }
376
- req.destroy();
377
- resolve({ ok: false });
378
- });
379
- req.write(payload);
380
- req.end();
381
- });
382
- }
383
257
  reportDiagnostic(category, message, detail, fingerprintSuffix) {
384
258
  this.onDiagnostic?.({
385
259
  source: "sal.eval",
@@ -390,13 +264,13 @@ export class InsForgeEvalSink {
390
264
  fingerprint: `sal.eval:${category}:${fingerprintSuffix}`,
391
265
  context: {
392
266
  adapter: "insforge",
393
- endpoint_host: safeHost(this.base),
267
+ endpoint_host: safeHost(this.http.base),
394
268
  },
395
269
  });
396
270
  }
397
271
  }
398
272
  // ----------------------------------------------------------------------------
399
- // Local helpers
273
+ // SAL-specific row helpers
400
274
  // ----------------------------------------------------------------------------
401
275
  function strOrNull(v, skipValue) {
402
276
  if (v == null || v === "" || v === skipValue)
@@ -409,23 +283,6 @@ function numOrNull(v) {
409
283
  const n = Number(v);
410
284
  return isNaN(n) ? null : n;
411
285
  }
412
- function parsePostgrestErrorCode(rawBody) {
413
- try {
414
- const parsed = JSON.parse(rawBody);
415
- return typeof parsed?.code === "string" ? parsed.code : undefined;
416
- }
417
- catch {
418
- return undefined;
419
- }
420
- }
421
- function safeHost(value) {
422
- try {
423
- return new URL(value).hostname;
424
- }
425
- catch {
426
- return undefined;
427
- }
428
- }
429
286
  function toLegacyRunStartRow(row) {
430
287
  const { pencil_version: _pencilVersion, commit_hash: _commitHash, branch_name: _branchName, workspace_root: _workspaceRoot, ...legacyRow } = row;
431
288
  return legacyRow;
@@ -19,6 +19,17 @@ const NOSAL_FLAG = "nosal";
19
19
  const SAL_AB_FLAG = "sal-ab";
20
20
  const SAL_REBUILD_FLAG = "sal-rebuild-terrain";
21
21
  const DIAGNOSTIC_EVENT_CHANNEL = "diagnostic:event";
22
+ const SAL_COVERAGE_COMPLETIONS = [
23
+ { value: "core/", label: "core/", description: "Check the core runtime files" },
24
+ { value: "extensions/", label: "extensions/", description: "Check extension files" },
25
+ { value: "modes/", label: "modes/", description: "Check interactive, print, and RPC mode files" },
26
+ { value: "packages/", label: "packages/", description: "Check bundled package files" },
27
+ ];
28
+ const SAL_SETUP_ENDPOINT_COMPLETIONS = [
29
+ { value: "https://", label: "https://", description: "Send evaluation records to a hosted endpoint" },
30
+ { value: "file://", label: "file://", description: "Write evaluation records to a local JSONL file" },
31
+ { value: "./", label: "./", description: "Write evaluation records to a file in this workspace" },
32
+ ];
22
33
  async function ensureSnapshot(runtime, forceRebuild) {
23
34
  if (runtime.snapshotErrored)
24
35
  return runtime.snapshot;
@@ -181,17 +192,17 @@ async function cleanupStaleRuns(runtime) {
181
192
  export default async function salExtension(api) {
182
193
  api.registerFlag(NOSAL_FLAG, {
183
194
  type: "boolean",
184
- description: "Disable Structural Anchor Localization (SAL) - fall back to baseline memory mode",
195
+ description: "Turn off SAL workspace guidance for this run",
185
196
  default: false,
186
197
  });
187
198
  api.registerFlag(SAL_AB_FLAG, {
188
199
  type: "boolean",
189
- description: "Enable SAL A/B experiment sidecar files under .memory-experiments",
200
+ description: "Save local SAL comparison records under .memory-experiments",
190
201
  default: false,
191
202
  });
192
203
  api.registerFlag(SAL_REBUILD_FLAG, {
193
204
  type: "boolean",
194
- description: "Force SAL terrain index rebuild on next localization pass",
205
+ description: "Refresh SAL's workspace map before the next turn",
195
206
  default: false,
196
207
  });
197
208
  const workspaceRoot = api.cwd;
@@ -299,7 +310,13 @@ export default async function salExtension(api) {
299
310
  const isEnabled = () => !api.getFlag(NOSAL_FLAG);
300
311
  const isSalAbEnabled = () => resolveSalAbEnabled(api.getFlag(SAL_AB_FLAG));
301
312
  api.registerCommand("sal:coverage", {
302
- description: "Report DIP P3 coverage for SAL prerequisite gating. Usage: /sal:coverage [module1 module2 ...]",
313
+ description: "Check whether folders have the file map headers SAL needs. Usage: /sal:coverage [folder ...]",
314
+ getArgumentCompletions: (argumentPrefix, context) => {
315
+ const used = new Set(context?.previousTokens ?? []);
316
+ const prefix = argumentPrefix.trim().toLowerCase();
317
+ const values = SAL_COVERAGE_COMPLETIONS.filter((item) => !used.has(item.value) && item.value.startsWith(prefix));
318
+ return values.length > 0 ? values : null;
319
+ },
303
320
  handler: async (args, ctx) => {
304
321
  const modules = (args ?? "")
305
322
  .trim()
@@ -316,9 +333,15 @@ export default async function salExtension(api) {
316
333
  },
317
334
  });
318
335
  api.registerCommand("sal:setup", {
319
- description: "Configure SAL eval credentials. " +
320
- "Usage: /sal:setup <endpoint> [api_key] [anon_key] — adapter inferred from endpoint scheme " +
321
- "(http/https InsForge backend; file path or file:// → local JSONL log).",
336
+ description: "Connect evaluation records to a hosted endpoint or local JSONL file. " +
337
+ "Usage: /sal:setup <endpoint> [api_key] [anon_key].",
338
+ getArgumentCompletions: (argumentPrefix, context) => {
339
+ if (context && context.tokenIndex > 0)
340
+ return null;
341
+ const prefix = argumentPrefix.trim().toLowerCase();
342
+ const values = SAL_SETUP_ENDPOINT_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
343
+ return values.length > 0 ? values : null;
344
+ },
322
345
  handler: async (args, ctx) => {
323
346
  const tokens = (args ?? "").trim().split(/\s+/).filter((t) => t.length > 0);
324
347
  const endpoint = tokens[0];
@@ -399,7 +422,7 @@ export default async function salExtension(api) {
399
422
  },
400
423
  });
401
424
  api.registerCommand("sal:status", {
402
- description: "Show current SAL configuration and snapshot status",
425
+ description: "Show whether SAL is active and where its records are going",
403
426
  handler: async (_args, ctx) => {
404
427
  const flagOn = isEnabled();
405
428
  const snapshot = runtime.snapshot;
@@ -30,6 +30,11 @@ export interface EvalCredentials {
30
30
  /** Adapter selector. When omitted, inferred from endpoint scheme (http -> insforge, file/path -> jsonl). */
31
31
  adapter?: EvalAdapterId;
32
32
  }
33
+ /**
34
+ * Delegate to the shared core/telemetry/build-meta loader so SAL eval, the
35
+ * ext-events sink, and any future telemetry consumer all stamp identical
36
+ * pencil_version / commit_hash on emitted rows.
37
+ */
33
38
  export declare function loadBuildMeta(): BuildMeta;
34
39
  export declare const BUILD_META: BuildMeta;
35
40
  export declare function resolveStaleCleanupEnabled(envValue: string | undefined, credentials: EvalCredentials | undefined): boolean;
@@ -4,10 +4,8 @@
4
4
  * [TO]: Consumed by extensions/defaults/sal/index.ts and tests that validate SAL experiment/config behavior
5
5
  * [HERE]: extensions/defaults/sal/sal-config.ts - configuration and path boundary for Structural Anchor Localization
6
6
  */
7
- import { existsSync, readFileSync } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
7
+ import { join } from "node:path";
8
+ import { loadBuildMeta as loadBuildMetaShared, loadInsforgeCredentials } from "../../../core/telemetry/index.js";
11
9
  export const SAL_AB_ENV = "NANOPENCIL_SAL_AB";
12
10
  export const EVAL_ENABLED_ENV = "NANOPENCIL_EVAL_ENABLED";
13
11
  export const EVAL_ENDPOINT_ENV = "NANOPENCIL_EVAL_ENDPOINT";
@@ -19,37 +17,13 @@ export const EVAL_API_KEY_HEADER_ENV = "NANOPENCIL_EVAL_API_KEY_HEADER";
19
17
  export const EVAL_HEADERS_JSON_ENV = "NANOPENCIL_EVAL_HEADERS_JSON";
20
18
  export const EVAL_CREDENTIALS_FILE_ENV = "NANOPENCIL_EVAL_CREDENTIALS_FILE";
21
19
  export const EVAL_STALE_CLEANUP_ENV = "NANOPENCIL_EVAL_CLEANUP_STALE_RUNS";
20
+ /**
21
+ * Delegate to the shared core/telemetry/build-meta loader so SAL eval, the
22
+ * ext-events sink, and any future telemetry consumer all stamp identical
23
+ * pencil_version / commit_hash on emitted rows.
24
+ */
22
25
  export function loadBuildMeta() {
23
- const fallback = { version: "dev" };
24
- try {
25
- const thisFile = fileURLToPath(import.meta.url);
26
- const thisDir = dirname(thisFile);
27
- const distMeta = join(thisDir, "..", "..", "..", "build-meta.json");
28
- if (existsSync(distMeta)) {
29
- const parsed = JSON.parse(readFileSync(distMeta, "utf-8"));
30
- return {
31
- version: parsed.version ?? fallback.version,
32
- commitHash: parsed.commitHash,
33
- branch: parsed.branch,
34
- };
35
- }
36
- const pkgCandidates = [
37
- join(thisDir, "..", "..", "..", "package.json"),
38
- join(thisDir, "..", "..", "..", "..", "package.json"),
39
- ];
40
- for (const p of pkgCandidates) {
41
- if (existsSync(p)) {
42
- const pkg = JSON.parse(readFileSync(p, "utf-8"));
43
- if (pkg.name === "@pencil-agent/nano-pencil") {
44
- return { version: pkg.version ?? fallback.version };
45
- }
46
- }
47
- }
48
- }
49
- catch {
50
- // Non-fatal: dev and tests can run without build metadata.
51
- }
52
- return fallback;
26
+ return loadBuildMetaShared();
53
27
  }
54
28
  export const BUILD_META = loadBuildMeta();
55
29
  export function resolveStaleCleanupEnabled(envValue, credentials) {
@@ -92,55 +66,14 @@ export function parseHeadersJson(raw, reportDiagnostic) {
92
66
  return {};
93
67
  }
94
68
  }
95
- function resolveCredentialsFileCandidates(workspaceRoot) {
96
- const envPath = process.env[EVAL_CREDENTIALS_FILE_ENV];
97
- const workspacePath = join(workspaceRoot, ".memory-experiments", "credentials.json");
98
- const userPath = join(homedir(), ".memory-experiments", "credentials.json");
99
- return [envPath, workspacePath, userPath].filter((path) => Boolean(path));
100
- }
101
- function readCredentialsFromFile(filePath, reportDiagnostic) {
102
- try {
103
- if (!existsSync(filePath))
104
- return undefined;
105
- const raw = readFileSync(filePath, "utf-8");
106
- const parsed = JSON.parse(raw);
107
- if (!parsed || typeof parsed !== "object")
108
- return undefined;
109
- if (Array.isArray(parsed.credentials)) {
110
- const entry = parsed.credentials.find((e) => e.id === "insforge" && e.enabled !== false);
111
- if (!entry)
112
- return undefined;
113
- return {
114
- endpoint: entry.endpoint ?? entry.insforge_url,
115
- insforge_url: entry.insforge_url ?? entry.endpoint,
116
- api_key: entry.api_key ?? entry.apiKey,
117
- anon_key: entry.anon_key,
118
- api_key_header: entry.api_key_header,
119
- headers: entry.headers,
120
- enabled: entry.enabled,
121
- };
122
- }
123
- return parsed;
124
- }
125
- catch (err) {
126
- reportDiagnostic?.({
127
- source: "sal.eval",
128
- severity: "warning",
129
- category: "config",
130
- message: "SAL eval credentials file could not be read.",
131
- detail: { filePath, error: err.message },
132
- fingerprint: "sal.eval:config:credentials-read-failed",
133
- });
134
- return undefined;
135
- }
136
- }
137
69
  export function resolveEvalCredentials(workspaceRoot, reportDiagnostic) {
138
- for (const candidate of resolveCredentialsFileCandidates(workspaceRoot)) {
139
- const creds = readCredentialsFromFile(candidate, reportDiagnostic);
140
- if (creds)
141
- return creds;
142
- }
143
- return undefined;
70
+ // Delegate file discovery + JSON parsing + format normalization to the
71
+ // shared telemetry base. SAL only needs to type-assert the result back to
72
+ // its own extended shape (EvalCredentials adds cleanup_stale_runs +
73
+ // adapter on top of InsforgeCredentialsBase).
74
+ const envPath = process.env[EVAL_CREDENTIALS_FILE_ENV];
75
+ const creds = loadInsforgeCredentials(workspaceRoot, "sal.eval", reportDiagnostic, envPath);
76
+ return creds ?? undefined;
144
77
  }
145
78
  export function normalizeExperimentId(experimentId) {
146
79
  const raw = (experimentId ?? "").trim();