@pencil-agent/nano-pencil 1.13.4 → 1.13.6

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 (27) hide show
  1. package/dist/build-meta.json +6 -0
  2. package/dist/extensions/defaults/CLAUDE.md +5 -5
  3. package/dist/extensions/defaults/debug/collectors.d.ts +11 -0
  4. package/dist/extensions/defaults/debug/collectors.js +67 -0
  5. package/dist/extensions/defaults/debug/index.d.ts +2 -2
  6. package/dist/extensions/defaults/debug/index.js +90 -36
  7. package/dist/extensions/defaults/presence/index.d.ts +47 -0
  8. package/dist/extensions/defaults/presence/index.js +44 -19
  9. package/dist/extensions/defaults/sal/README.md +4 -0
  10. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +2 -1
  11. package/dist/extensions/defaults/sal/eval/insforge-sink.js +30 -1
  12. package/dist/extensions/defaults/sal/eval/types.d.ts +1 -1
  13. package/dist/extensions/defaults/sal/index.d.ts +38 -4
  14. package/dist/extensions/defaults/sal/index.js +315 -15
  15. package/dist/modes/interactive/interactive-mode.js +29 -6
  16. package/dist/modes/interactive/services/tips.js +30 -11
  17. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +230 -57
  18. package/dist/node_modules/@pencil-agent/ai/models.generated.js +270 -99
  19. package/package.json +3 -2
  20. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +0 -251
  21. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +0 -123
  22. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +0 -1222
  23. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +0 -158
  24. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -128
  25. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +0 -321
  26. package/docs/loop-usage-examples.md +0 -215
  27. package/docs/planmode.md +0 -1987
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "1.13.6",
3
+ "commitHash": "d5ae966",
4
+ "branch": "main",
5
+ "builtAt": "2026-04-23T04:35:24.822Z"
6
+ }
@@ -30,7 +30,7 @@ loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSche
30
30
  loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
31
31
  loop/README.md: Loop extension documentation - recurring scheduler usage and flags
32
32
  btw/index.ts: BTW extension entry - /btw command for quick side questions without interrupting main task, uses completeSimple() for lightweight response, BTW_MESSAGE_TYPE renderer
33
- debug/index.ts: Debug extension entry - /debug command for system diagnostics with three-layer analysis (Phenomenon/Essence/Philosophy), supports /debug env|session|model subcommands, uses completeSimple() for LLM analysis, DEBUG_MESSAGE_TYPE renderer
33
+ debug/index.ts: Debug extension entry - /debug command dispatches diagnostics through full agent loop (sendUserMessage + before_agent_start hook), three-layer analysis (Phenomenon/Essence/Philosophy), supports /debug env|session|model quick subcommands, DEBUG_MESSAGE_TYPE renderer
34
34
  debug/collectors.ts: Diagnostic data collectors for /debug command, collectSystemInfo/collectModelInfo/collectSessionInfo/collectConfigInfo/collectGitInfo/collectAgentState, sanitizeForLLM, formatDiagnosticData
35
35
  plan/index.ts: Plan Mode extension entry - registers /plan command, EnterPlanMode/ExitPlanMode tools, permission gating, workflow prompt injection
36
36
  plan/types.ts: PlanModeState, PlanModeAttachment types, PlanModeConfig, PlanApprovalRequest/Response
@@ -42,14 +42,14 @@ plan/exit-plan-mode-tool.ts: createExitPlanModeTool() - ExitPlanMode tool with p
42
42
  plan/plan-agents.ts: Explore/Plan subagent definitions with read-only tools for plan mode workflow
43
43
  plan/plan-validation.ts: validatePlan() - validates plan has required sections (Context, Approach, Files, Verification)
44
44
  plan/teammate-approval.ts: isInTeammateContext(), submitPlanToLeader(), formatPlanSubmittedMessage() - teammate plan approval integration
45
- sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set
46
- sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, buildTerrainIndex(), checkDipCoverage(), isSnapshotStale(), moduleIdForPath(), parses P2 CLAUDE.md and P3 file headers
45
+ sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/tool_trace/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set; auto-injects pencil_version from build-meta.json into run_start; emergency flush on beforeExit/SIGHUP/SIGTERM; stale run cleanup is opt-in via NANOPENCIL_EVAL_CLEANUP_STALE_RUNS / credentials cleanup_stale_runs; tool_trace is a bounded per-turn summary and includes no-tool turns
46
+ sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, async buildTerrainIndex()/isSnapshotStale() (fs/promises + periodic yields so TUI can flush under block terminals like Warp), checkDipCoverage(), moduleIdForPath(), parses P2 CLAUDE.md and P3 file headers
47
47
  sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
48
48
  sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
49
49
  sal/eval/index.ts: createEvalSink() factory + barrel re-exports; adapter selection via options.adapter or endpoint scheme inference (http(s)→insforge, file://|/|./|../→jsonl, missing→noop); ONLY entry point SAL imports from
50
50
  sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor/memory_recalls), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
51
51
  sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
52
- sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
52
+ sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces bounded per-turn summaries (including no-tool turns and truncation counters), memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
53
53
  sal/eval/jsonl-sink.ts: JsonlEvalSink — append-only filesystem adapter, one JSON object per line, accepts file:// URLs or plain paths, auto-creates parent dir, batched writes
54
54
  sal/README.md: SAL extension usage, sidecar output layout, weights override, pluggability contract
55
55
  team/index.ts: AgentTeam extension entry, /team:/team:spawn/:send/:status/:stop/:terminate/:approve/:mode commands, TEAM_MESSAGE_TYPE renderer
@@ -64,4 +64,4 @@ team/TESTING.md: Manual & smoke-test guide for Phase B AgentTeam
64
64
 
65
65
  Rule: Members complete, one item per line, parent links valid, precise terms first
66
66
 
67
- [COVENANT]: Update this file header on changes and verify against parent CLAUDE.md
67
+ [COVENANT]: Update this file header on changes and verify against parent CLAUDE.md
@@ -82,4 +82,15 @@ export declare function collectGitInfo(cwd: string): Promise<CollectorResult<Git
82
82
  export declare function collectAgentState(ctx: ExtensionContext): Promise<CollectorResult<AgentState>>;
83
83
  export declare function sanitizeForLLM(data: DiagnosticData): DiagnosticData;
84
84
  export declare function formatDiagnosticData(data: DiagnosticData): string;
85
+ export interface PreferencesInfo {
86
+ locale: string;
87
+ localeSource: "memory" | "settings" | "system";
88
+ memoryDir: string;
89
+ languagePreference: {
90
+ found: boolean;
91
+ name?: string;
92
+ summary?: string;
93
+ }[];
94
+ }
95
+ export declare function collectPreferencesInfo(ctx: ExtensionContext): Promise<CollectorResult<PreferencesInfo>>;
85
96
  export {};
@@ -246,3 +246,70 @@ export function formatDiagnosticData(data) {
246
246
  }
247
247
  return parts.join("\n\n");
248
248
  }
249
+ export async function collectPreferencesInfo(ctx) {
250
+ try {
251
+ const os = await import("node:os");
252
+ const fs = await import("node:fs");
253
+ const path = await import("node:path");
254
+ // Check memory directory for language preferences
255
+ const memoryDir = process.env.NANOMEM_MEMORY_DIR || path.join(os.homedir(), ".nanopencil", "agent", "memory");
256
+ let locale = "en";
257
+ let localeSource = "system";
258
+ const languagePreference = [];
259
+ // Try to read from preferences.json
260
+ const prefsPath = path.join(memoryDir, "preferences.json");
261
+ if (fs.existsSync(prefsPath)) {
262
+ try {
263
+ const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
264
+ // Find language-related preferences
265
+ const langPrefs = prefs.filter((p) => {
266
+ const text = (p.name || "") + (p.summary || "") + (p.detail || "");
267
+ return /中文|chinese|语言|locale|zh/i.test(text);
268
+ });
269
+ if (langPrefs.length > 0) {
270
+ locale = "zh";
271
+ localeSource = "memory";
272
+ for (const p of langPrefs.slice(0, 3)) {
273
+ languagePreference.push({
274
+ found: true,
275
+ name: p.name,
276
+ summary: (p.summary || "").slice(0, 80),
277
+ });
278
+ }
279
+ }
280
+ }
281
+ catch {
282
+ // Ignore read errors
283
+ }
284
+ }
285
+ // Check settings.json for locale
286
+ const settingsPath = path.join(os.homedir(), ".nanopencil", "agent", "settings.json");
287
+ if (localeSource === "system" && fs.existsSync(settingsPath)) {
288
+ try {
289
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
290
+ if (settings.locale) {
291
+ locale = settings.locale;
292
+ localeSource = "settings";
293
+ }
294
+ }
295
+ catch {
296
+ // Ignore read errors
297
+ }
298
+ }
299
+ return {
300
+ data: {
301
+ locale,
302
+ localeSource,
303
+ memoryDir,
304
+ languagePreference,
305
+ },
306
+ error: null,
307
+ };
308
+ }
309
+ catch (error) {
310
+ return {
311
+ data: null,
312
+ error: String(error),
313
+ };
314
+ }
315
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: debugExtension - registers /debug command and DEBUG_MESSAGE_TYPE renderer
2
+ * [WHO]: debugExtension - /debug command, before_agent_start hook injects diagnostic system prompt, agent_end cleanup, dispatched via sendUserMessage for streaming UX
3
3
  * [FROM]: Depends on core/extensions/types, @pencil-agent/tui, ./collectors
4
4
  * [TO]: Auto-loaded by builtin-extensions.ts as a default extension
5
- * [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis
5
+ * [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis through full agent loop
6
6
  */
7
7
  import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
8
  export default function debugExtension(api: ExtensionAPI): Promise<void>;
@@ -1,13 +1,14 @@
1
1
  /**
2
- * [WHO]: debugExtension - registers /debug command and DEBUG_MESSAGE_TYPE renderer
2
+ * [WHO]: debugExtension - /debug command, before_agent_start hook injects diagnostic system prompt, agent_end cleanup, dispatched via sendUserMessage for streaming UX
3
3
  * [FROM]: Depends on core/extensions/types, @pencil-agent/tui, ./collectors
4
4
  * [TO]: Auto-loaded by builtin-extensions.ts as a default extension
5
- * [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis
5
+ * [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis through full agent loop
6
6
  */
7
7
  import { Box, Container, Spacer, Text } from "@pencil-agent/tui";
8
8
  import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
9
9
  const DEBUG_MESSAGE_TYPE = "debug";
10
- const DEBUG_TIMEOUT_MS = 45_000;
10
+ const DEBUG_PROMPT_PREFIX = "[DEBUG:";
11
+ const DEBUG_TAG = "[DEBUG]";
11
12
  const DEBUG_SYSTEM_PROMPT = `You are a diagnostic analyst for nanoPencil (a terminal-native AI coding agent).
12
13
  Analyze the provided system state and produce a structured three-layer diagnostic report.
13
14
 
@@ -37,7 +38,15 @@ Rules:
37
38
  - If the user provided an issue description, focus analysis on that issue
38
39
  - If no specific issue, perform a general health assessment
39
40
  - Use concise language; prefer tables and bullet lists over prose
40
- - If a diagnostic collection failed, treat that failure itself as a diagnostic signal`;
41
+ - If a diagnostic collection failed, treat that failure itself as a diagnostic signal
42
+ - Do NOT use any tools — this is a pure analysis task`;
43
+ // ============================================================================
44
+ // Pending diagnostic state (set by command handler, consumed by hooks)
45
+ // ============================================================================
46
+ let pendingDiagnosticPrompt;
47
+ function isDebugPrompt(text) {
48
+ return text.startsWith(DEBUG_PROMPT_PREFIX);
49
+ }
41
50
  function parseDebugArgs(args) {
42
51
  const trimmed = args.trim().toLowerCase();
43
52
  if (trimmed === "env")
@@ -49,14 +58,12 @@ function parseDebugArgs(args) {
49
58
  return { subcommand: "full", issueDescription: args.trim() || undefined };
50
59
  }
51
60
  // ============================================================================
52
- // Full diagnostic flow
61
+ // Full diagnostic flow — collect then dispatch through agent loop
53
62
  // ============================================================================
54
63
  async function handleFullDiagnostic(args, ctx, api) {
55
64
  const parsed = parseDebugArgs(args);
56
- // Show status indicator
57
65
  ctx.ui.setStatus("debug", "Collecting diagnostics...");
58
66
  try {
59
- // Collect all categories in parallel
60
67
  const [system, model, session, config, git, agent] = await Promise.allSettled([
61
68
  collectSystemInfo(),
62
69
  collectModelInfo(ctx),
@@ -74,35 +81,17 @@ async function handleFullDiagnostic(args, ctx, api) {
74
81
  agent: agent.status === "fulfilled" ? agent.value : { data: null, error: String(agent.reason) },
75
82
  };
76
83
  const data = sanitizeForLLM(raw);
77
- // Build user message for LLM
84
+ ctx.ui.setStatus("debug", undefined);
78
85
  const parts = [];
86
+ parts.push(`${DEBUG_TAG} Perform a three-layer diagnostic analysis.`);
79
87
  if (parsed.issueDescription) {
80
- parts.push(`## User-Reported Issue\n${parsed.issueDescription}\n`);
88
+ parts.push(`\nUser-Reported Issue: ${parsed.issueDescription}`);
81
89
  }
82
- parts.push("## Collected Diagnostics\n");
90
+ parts.push(`\nCollected Diagnostics:\n`);
83
91
  parts.push(formatDiagnosticData(data));
84
- const userMessage = parts.join("\n");
85
- // Call LLM with timeout
86
- const response = await Promise.race([
87
- ctx.completeSimple(DEBUG_SYSTEM_PROMPT, userMessage),
88
- new Promise((resolve) => setTimeout(() => resolve(undefined), DEBUG_TIMEOUT_MS)),
89
- ]);
90
- ctx.ui.setStatus("debug", undefined);
91
- if (response) {
92
- api.sendMessage({
93
- customType: DEBUG_MESSAGE_TYPE,
94
- content: response,
95
- display: true,
96
- });
97
- }
98
- else {
99
- // LLM unavailable — show raw data as fallback
100
- api.sendMessage({
101
- customType: DEBUG_MESSAGE_TYPE,
102
- content: `**LLM analysis unavailable** (timeout or no API key). Raw diagnostics:\n\n${formatDiagnosticData(data)}`,
103
- display: true,
104
- });
105
- }
92
+ const prompt = `${DEBUG_PROMPT_PREFIX}${Date.now()}]\n${parts.join("\n")}`;
93
+ pendingDiagnosticPrompt = prompt;
94
+ api.sendUserMessage(prompt, { deliverAs: "followUp" });
106
95
  }
107
96
  catch (error) {
108
97
  ctx.ui.setStatus("debug", undefined);
@@ -111,7 +100,7 @@ async function handleFullDiagnostic(args, ctx, api) {
111
100
  }
112
101
  }
113
102
  // ============================================================================
114
- // Quick subcommand — show raw data without LLM
103
+ // Quick subcommand — show raw data without agent loop
115
104
  // ============================================================================
116
105
  async function handleQuickSub(subcommand, ctx, api) {
117
106
  let result;
@@ -166,7 +155,6 @@ async function handleDebugCommand(args, ctx, api) {
166
155
  // Extension entry
167
156
  // ============================================================================
168
157
  export default async function debugExtension(api) {
169
- // Register debug message renderer (same pattern as btw)
170
158
  api.registerMessageRenderer(DEBUG_MESSAGE_TYPE, (message, _options, theme) => {
171
159
  const text = typeof message.content === "string"
172
160
  ? message.content
@@ -181,9 +169,75 @@ export default async function debugExtension(api) {
181
169
  container.addChild(box);
182
170
  return container;
183
171
  });
184
- // Register /debug command
172
+ api.on("before_agent_start", (event) => {
173
+ if (!isDebugPrompt(event.prompt))
174
+ return;
175
+ return { appendSystemPrompt: DEBUG_SYSTEM_PROMPT };
176
+ });
177
+ api.on("agent_end", () => {
178
+ if (pendingDiagnosticPrompt) {
179
+ pendingDiagnosticPrompt = undefined;
180
+ }
181
+ });
185
182
  api.registerCommand("debug", {
186
- description: "Run system diagnostics with three-layer analysis (/debug [env|session|model|<issue>])",
183
+ description: "Run system diagnostics (/debug [env|session|model|preferences|<issue>])",
187
184
  handler: (args, ctx) => handleDebugCommand(args, ctx, api),
188
185
  });
186
+ // Register /set-locale command
187
+ api.registerCommand("set-locale", {
188
+ description: "Set language preference (/set-locale zh|en)",
189
+ handler: async (args, ctx) => {
190
+ const trimmed = args.trim().toLowerCase();
191
+ if (trimmed !== "zh" && trimmed !== "en") {
192
+ ctx.ui.notify("Usage: /set-locale zh or /set-locale en", "info");
193
+ return;
194
+ }
195
+ // Get memory directory
196
+ const os = await import("node:os");
197
+ const fs = await import("node:fs");
198
+ const path = await import("node:path");
199
+ const memoryDir = process.env.NANOMEM_MEMORY_DIR || path.join(os.homedir(), ".nanopencil", "agent", "memory");
200
+ const prefsPath = path.join(memoryDir, "preferences.json");
201
+ try {
202
+ let prefs = [];
203
+ if (fs.existsSync(prefsPath)) {
204
+ prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
205
+ }
206
+ // Check if language preference already exists
207
+ const existingIndex = prefs.findIndex((p) => {
208
+ const name = p.name || "";
209
+ return name.includes("用户偏好") || name.includes("language preference") || name.includes("locale");
210
+ });
211
+ const newPref = {
212
+ id: `set-locale-${Date.now()}`,
213
+ type: "preference",
214
+ name: trimmed === "zh" ? "用户偏好中文" : "Language Preference (English)",
215
+ summary: trimmed === "zh" ? "用户希望我用中文回复" : "User prefers English",
216
+ detail: trimmed === "zh" ? "用户通过 /set-locale 命令明确设置语言为中文" : "User explicitly set language to English via /set-locale command",
217
+ content: trimmed === "zh" ? "用户希望用中文回复" : "User prefers English responses",
218
+ tags: ["locale", "language", trimmed === "zh" ? "中文" : "english"],
219
+ importance: 10,
220
+ strength: 1000,
221
+ created: new Date().toISOString(),
222
+ eventTime: new Date().toISOString(),
223
+ accessCount: 0,
224
+ retention: "core",
225
+ salience: 10,
226
+ stability: "stable",
227
+ relations: [],
228
+ };
229
+ if (existingIndex >= 0) {
230
+ prefs[existingIndex] = newPref;
231
+ }
232
+ else {
233
+ prefs.push(newPref);
234
+ }
235
+ fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2));
236
+ ctx.ui.notify(`Locale set to ${trimmed === "zh" ? "中文" : "English"}. Restart or run /debug preferences to verify.`, "info");
237
+ }
238
+ catch (error) {
239
+ ctx.ui.notify(`Failed to set locale: ${error}`, "error");
240
+ }
241
+ },
242
+ });
189
243
  }
@@ -7,13 +7,60 @@
7
7
  import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
8
  declare function getFallbackOpeningLines(locale?: "en" | "zh"): string[];
9
9
  declare function getFallbackIdleLines(locale?: "en" | "zh"): string[];
10
+ type PresenceState = {
11
+ lastActivityAt: number;
12
+ idleReminderSent: boolean;
13
+ openingStartedAt?: number;
14
+ openingSent: boolean;
15
+ openingTimer?: ReturnType<typeof setTimeout>;
16
+ idleTimer?: ReturnType<typeof setInterval>;
17
+ unsubscribeInput?: () => void;
18
+ memEngine?: {
19
+ getAllEntries(): Promise<{
20
+ knowledge: Array<{
21
+ type?: string;
22
+ tags: string[];
23
+ name?: string;
24
+ summary?: string;
25
+ detail?: string;
26
+ content?: string;
27
+ }>;
28
+ lessons: Array<{
29
+ type?: string;
30
+ tags: string[];
31
+ name?: string;
32
+ summary?: string;
33
+ detail?: string;
34
+ content?: string;
35
+ importance?: number;
36
+ }>;
37
+ }>;
38
+ getAllEpisodes(): Promise<Array<{
39
+ date?: string;
40
+ consolidated?: boolean;
41
+ endedAt?: string;
42
+ startedAt?: string;
43
+ summary?: string;
44
+ userGoal?: string;
45
+ }>>;
46
+ searchEntries(query: string): Promise<Array<{
47
+ type?: string;
48
+ tags: string[];
49
+ }>>;
50
+ };
51
+ recentPresenceLines: string[];
52
+ lastPresenceAt?: number;
53
+ idleGenerating?: boolean;
54
+ };
10
55
  declare function resolveBundledPackageEntry(packageName: "mem-core" | "soul-core"): string | undefined;
11
56
  declare function importRuntimeModule<T>(moduleNames: string[], bundledPackageName?: "mem-core" | "soul-core"): Promise<T | undefined>;
57
+ declare function detectLanguageFromMemory(state: PresenceState): Promise<"en" | "zh" | undefined>;
12
58
  export default function presenceExtension(api: ExtensionAPI): Promise<void>;
13
59
  export declare const __testUtils: {
14
60
  getFallbackOpeningLines: typeof getFallbackOpeningLines;
15
61
  getFallbackIdleLines: typeof getFallbackIdleLines;
16
62
  resolveBundledPackageEntry: typeof resolveBundledPackageEntry;
17
63
  importRuntimeModule: typeof importRuntimeModule;
64
+ detectLanguageFromMemory: typeof detectLanguageFromMemory;
18
65
  };
19
66
  export {};
@@ -153,10 +153,6 @@ function clearTimers(state) {
153
153
  state.unsubscribeInput?.();
154
154
  state.unsubscribeInput = undefined;
155
155
  }
156
- function getMemoryDir() {
157
- // Use the same memory directory as the main app
158
- return process.env.NANOMEM_MEMORY_DIR || join(homedir(), ".nanomem", "memory");
159
- }
160
156
  async function initMemEngine(state) {
161
157
  if (state.memEngine)
162
158
  return;
@@ -176,6 +172,18 @@ async function initMemEngine(state) {
176
172
  state.memEngine = undefined;
177
173
  }
178
174
  }
175
+ function getMemoryDir() {
176
+ // Use the same memory directory as the main app
177
+ // Priority: env var > nanopencil default > legacy nanomem path
178
+ if (process.env.NANOMEM_MEMORY_DIR)
179
+ return process.env.NANOMEM_MEMORY_DIR;
180
+ // Check if nanopencil's memory directory exists
181
+ const nanopencilMemory = join(homedir(), ".nanopencil", "agent", "memory");
182
+ if (existsSync(nanopencilMemory))
183
+ return nanopencilMemory;
184
+ // Fallback to legacy path
185
+ return join(homedir(), ".nanomem", "memory");
186
+ }
179
187
  function getProject() {
180
188
  const parts = process.cwd().split("/").filter(Boolean);
181
189
  return parts.length >= 2
@@ -203,21 +211,32 @@ async function detectLanguageFromMemory(state) {
203
211
  }
204
212
  }
205
213
  catch { /* ignore */ }
214
+ let zhScore = 0;
215
+ let enScore = 0;
216
+ const zhTerms = "(中文|chinese|zh-hans|mandarin|普通话)";
217
+ const enTerms = "(英文|english|en-us)";
218
+ const negPrefix = "(?:don't|do not|no|not|不用|不要|别|不想用)";
219
+ const useWords = "(?:\\s+use|\\s+using|\\s+说|\\s+讲|\\s+用)?";
220
+ const zhNegative = new RegExp(`${negPrefix}${useWords}\\s*${zhTerms}`);
221
+ const enNegative = new RegExp(`${negPrefix}${useWords}\\s*${enTerms}`);
222
+ const zhPositive = new RegExp(zhTerms);
223
+ const enPositive = new RegExp(enTerms);
206
224
  // Check preference content for language indicators
207
225
  for (const pref of preferences) {
208
226
  const text = `${pref.name || ""} ${pref.summary || ""} ${pref.detail || ""} ${pref.content || ""}`.toLowerCase();
209
- // Check for Chinese preference
210
- if (/中文|chinese|zh-hans|mandarin|普通话/.test(text)) {
211
- if (!text.includes("don't") && !text.includes("no chinese") && !text.includes("不用中文")) {
212
- return "zh";
213
- }
214
- }
215
- // Check for explicit English preference
216
- if (/英文|english|en-us/.test(text)) {
217
- if (!text.includes("don't") && !text.includes("no english") && !text.includes("不用英文")) {
218
- return "en";
219
- }
220
- }
227
+ const hasZh = zhPositive.test(text);
228
+ const hasEn = enPositive.test(text);
229
+ const noZh = zhNegative.test(text);
230
+ const noEn = enNegative.test(text);
231
+ if (hasZh && !noZh)
232
+ zhScore += 2;
233
+ if (hasEn && !noEn)
234
+ enScore += 2;
235
+ // Cross-language hints: "don't use Chinese" slightly supports English, and vice versa.
236
+ if (noZh)
237
+ enScore += 1;
238
+ if (noEn)
239
+ zhScore += 1;
221
240
  }
222
241
  // Check recent episodes for language patterns
223
242
  const episodes = await state.memEngine.getAllEpisodes();
@@ -233,8 +252,12 @@ async function detectLanguageFromMemory(state) {
233
252
  englishContent++;
234
253
  }
235
254
  if (chineseContent > englishContent)
236
- return "zh";
255
+ zhScore += 1;
237
256
  if (englishContent > chineseContent && englishContent > 2)
257
+ enScore += 1;
258
+ if (zhScore > enScore && zhScore > 0)
259
+ return "zh";
260
+ if (enScore > zhScore && enScore > 0)
238
261
  return "en";
239
262
  return undefined;
240
263
  }
@@ -437,9 +460,10 @@ function getLastUserMessage(ctx) {
437
460
  const entry = entries[i];
438
461
  if (entry.type !== "message")
439
462
  continue;
440
- if (entry.role !== "user")
463
+ const message = entry.message;
464
+ if (!message || message.role !== "user")
441
465
  continue;
442
- const c = entry.content;
466
+ const c = message.content;
443
467
  if (typeof c === "string")
444
468
  return c;
445
469
  if (Array.isArray(c)) {
@@ -705,4 +729,5 @@ export const __testUtils = {
705
729
  getFallbackIdleLines,
706
730
  resolveBundledPackageEntry,
707
731
  importRuntimeModule,
732
+ detectLanguageFromMemory,
708
733
  };
@@ -20,6 +20,10 @@ pencil --nosal -p "your prompt"
20
20
 
21
21
  When `--nosal` is set, all hooks return early and zero work is performed.
22
22
 
23
+ ## Terminal compatibility (Warp, block UIs)
24
+
25
+ SAL builds a **terrain snapshot** of the workspace (walk + read DIP headers). That work is **asynchronous and periodically yields to the event loop** so the TUI can flush user input and status lines to the terminal while indexing runs. If you still see UI glitches in a specific terminal, use `--nosal` to confirm whether SAL is involved, then file an issue with `TERM_PROGRAM`, Warp version, and repro steps.
26
+
23
27
  ## Slash commands
24
28
 
25
29
  | Command | Purpose |
@@ -2,7 +2,7 @@
2
2
  * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
3
3
  * [FROM]: Depends on node:https, node:http, node:url; ./types.js for 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), turn_anchor→eval_turns + eval_sal_anchors×2, run_end→eval_runs PATCH
5
+ * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
6
6
  *
7
7
  * Pluggable: nothing in this file may be imported from outside the eval/ directory.
8
8
  * To add a new backend, write a sibling file with the same EvalSink interface.
@@ -29,6 +29,7 @@ export declare class InsForgeEvalSink implements EvalSink {
29
29
  private handleTurnAnchor;
30
30
  private handleRunEnd;
31
31
  private handleMemoryRecalls;
32
+ private handleToolTrace;
32
33
  private postJson;
33
34
  private patchJson;
34
35
  private httpJson;
@@ -2,7 +2,7 @@
2
2
  * [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
3
3
  * [FROM]: Depends on node:https, node:http, node:url; ./types.js for 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), turn_anchor→eval_turns + eval_sal_anchors×2, run_end→eval_runs PATCH
5
+ * [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
6
6
  *
7
7
  * Pluggable: nothing in this file may be imported from outside the eval/ directory.
8
8
  * To add a new backend, write a sibling file with the same EvalSink interface.
@@ -103,6 +103,9 @@ export class InsForgeEvalSink {
103
103
  case "memory_recalls":
104
104
  await this.handleMemoryRecalls(event);
105
105
  break;
106
+ case "tool_trace":
107
+ await this.handleToolTrace(event);
108
+ break;
106
109
  case "run_end":
107
110
  await this.handleRunEnd(event);
108
111
  break;
@@ -123,6 +126,7 @@ export class InsForgeEvalSink {
123
126
  task_file: strOrNull(p.task_file),
124
127
  model: strOrNull(p.model),
125
128
  thinking: p.thinking === true,
129
+ pencil_version: strOrNull(p.pencil_version),
126
130
  commit_hash: strOrNull(p.commit, "unknown"),
127
131
  branch_name: strOrNull(p.branch, "unknown"),
128
132
  workspace_root: strOrNull(p.workspace_root),
@@ -213,6 +217,31 @@ export class InsForgeEvalSink {
213
217
  }));
214
218
  await this.postJson(`${this.base}/api/database/records/eval_memory_recalls`, rows, { prefer: "resolution=ignore-duplicates" });
215
219
  }
220
+ // INSERT into eval_tool_traces — one row per turn with tool usage summary
221
+ // InsForge columns are all TEXT; JSONB fields must be serialized to strings.
222
+ async handleToolTrace(ev) {
223
+ const p = ev.payload;
224
+ const taskSignals = p.task_signals;
225
+ await this.postJson(`${this.base}/api/database/records/eval_tool_traces`, [{
226
+ run_id: ev.run_id,
227
+ turn_id: String(p.turn_id ?? 0),
228
+ event_id: ev.event_id,
229
+ tool_calls: p.tool_calls ? JSON.stringify(p.tool_calls) : null,
230
+ tool_sequence: p.tool_sequence ? JSON.stringify(p.tool_sequence) : null,
231
+ intent: strOrNull(taskSignals?.intent),
232
+ prompt_length: String(taskSignals?.prompt_length ?? 0),
233
+ has_error_trace: String(taskSignals?.has_error_trace === true),
234
+ has_file_reference: String(taskSignals?.has_file_reference === true),
235
+ has_tool_usage: String(p.has_tool_usage === true),
236
+ total_tool_calls: String(p.total_tool_calls ?? 0),
237
+ total_errors: String(p.total_errors ?? 0),
238
+ completed_tool_calls: String(p.completed_tool_calls ?? 0),
239
+ truncated_tool_calls: String(p.truncated_tool_calls ?? 0),
240
+ truncated_tool_summary: String(p.truncated_tool_summary ?? 0),
241
+ duration_ms: String(p.duration_ms ?? 0),
242
+ recorded_at: ev.ts,
243
+ }], { prefer: "resolution=ignore-duplicates" });
244
+ }
216
245
  // ------------------------------------------------------------------
217
246
  // HTTP helpers
218
247
  // ------------------------------------------------------------------
@@ -5,7 +5,7 @@
5
5
  * [HERE]: extensions/defaults/sal/eval/types.ts - transport-agnostic event types and the EvalSink contract; concrete adapters live in sibling files
6
6
  */
7
7
  export type EvalVariant = "sal" | "control" | "baseline";
8
- export type EvalEventType = "run_start" | "run_end" | "turn_anchor" | "memory_recalls";
8
+ export type EvalEventType = "run_start" | "run_end" | "turn_anchor" | "memory_recalls" | "tool_trace";
9
9
  /** Wire format for eval events. Adapter implementations decide how to materialize. */
10
10
  export interface EvalEventEnvelope {
11
11
  run_id: string;