@visorcraft/idlehands 2.0.1 → 2.1.0

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 (59) hide show
  1. package/dist/agent/prompt-builder.js +188 -0
  2. package/dist/agent/prompt-builder.js.map +1 -0
  3. package/dist/agent/query-classifier.js +72 -0
  4. package/dist/agent/query-classifier.js.map +1 -0
  5. package/dist/agent/resilient-provider.js +170 -0
  6. package/dist/agent/resilient-provider.js.map +1 -0
  7. package/dist/agent/response-cache.js +124 -0
  8. package/dist/agent/response-cache.js.map +1 -0
  9. package/dist/agent/semantic-search.js +138 -0
  10. package/dist/agent/semantic-search.js.map +1 -0
  11. package/dist/agent/tool-calls.js +261 -1
  12. package/dist/agent/tool-calls.js.map +1 -1
  13. package/dist/agent/tool-name-alias.js +140 -0
  14. package/dist/agent/tool-name-alias.js.map +1 -0
  15. package/dist/agent.js +146 -43
  16. package/dist/agent.js.map +1 -1
  17. package/dist/anton/controller.js +442 -186
  18. package/dist/anton/controller.js.map +1 -1
  19. package/dist/anton/preflight.js +89 -28
  20. package/dist/anton/preflight.js.map +1 -1
  21. package/dist/anton/prompt.js +20 -0
  22. package/dist/anton/prompt.js.map +1 -1
  23. package/dist/anton/reporter.js +6 -1
  24. package/dist/anton/reporter.js.map +1 -1
  25. package/dist/bot/discord-commands.js +25 -0
  26. package/dist/bot/discord-commands.js.map +1 -1
  27. package/dist/bot/discord.js +15 -0
  28. package/dist/bot/discord.js.map +1 -1
  29. package/dist/bot/telegram-commands.js +21 -0
  30. package/dist/bot/telegram-commands.js.map +1 -1
  31. package/dist/bot/telegram.js +1 -0
  32. package/dist/bot/telegram.js.map +1 -1
  33. package/dist/bot/upgrade-command.js +398 -0
  34. package/dist/bot/upgrade-command.js.map +1 -0
  35. package/dist/bot/ux/discord-renderer.js +5 -21
  36. package/dist/bot/ux/discord-renderer.js.map +1 -1
  37. package/dist/bot/ux/emitter.js +104 -0
  38. package/dist/bot/ux/emitter.js.map +1 -0
  39. package/dist/bot/ux/shared-formatter.js +43 -0
  40. package/dist/bot/ux/shared-formatter.js.map +1 -0
  41. package/dist/bot/ux/telegram-renderer.js +5 -21
  42. package/dist/bot/ux/telegram-renderer.js.map +1 -1
  43. package/dist/cli/commands/upgrade.js +27 -0
  44. package/dist/cli/commands/upgrade.js.map +1 -0
  45. package/dist/client.js +51 -7
  46. package/dist/client.js.map +1 -1
  47. package/dist/harnesses.js +2 -0
  48. package/dist/harnesses.js.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/model-customization.js +3 -1
  52. package/dist/model-customization.js.map +1 -1
  53. package/dist/security/leak-detector.js +109 -0
  54. package/dist/security/leak-detector.js.map +1 -0
  55. package/dist/security/prompt-guard.js +120 -0
  56. package/dist/security/prompt-guard.js.map +1 -0
  57. package/dist/tui/command-handler.js +2 -0
  58. package/dist/tui/command-handler.js.map +1 -1
  59. package/package.json +1 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tool Name Aliasing
3
+ *
4
+ * Local and third-party models frequently hallucinate tool names — calling
5
+ * `bash` instead of `exec`, `file_read` instead of `read_file`, etc.
6
+ * This module maps common aliases to the canonical Idle Hands tool names.
7
+ *
8
+ * Inspired by ZeroClaw's `map_tool_name_alias()`.
9
+ */
10
+ const ALIAS_MAP = {
11
+ // ── exec ──────────────────────────────────────────────────────────────
12
+ shell: 'exec',
13
+ bash: 'exec',
14
+ sh: 'exec',
15
+ command: 'exec',
16
+ cmd: 'exec',
17
+ run: 'exec',
18
+ execute: 'exec',
19
+ terminal: 'exec',
20
+ run_command: 'exec',
21
+ run_shell: 'exec',
22
+ execute_command: 'exec',
23
+ // ── read_file ─────────────────────────────────────────────────────────
24
+ file_read: 'read_file',
25
+ fileread: 'read_file',
26
+ readfile: 'read_file',
27
+ cat: 'read_file',
28
+ view_file: 'read_file',
29
+ open_file: 'read_file',
30
+ get_file: 'read_file',
31
+ show_file: 'read_file',
32
+ // ── read_files ────────────────────────────────────────────────────────
33
+ file_reads: 'read_files',
34
+ batch_read: 'read_files',
35
+ // ── write_file ────────────────────────────────────────────────────────
36
+ file_write: 'write_file',
37
+ filewrite: 'write_file',
38
+ writefile: 'write_file',
39
+ create_file: 'write_file',
40
+ save_file: 'write_file',
41
+ // ── edit_file ─────────────────────────────────────────────────────────
42
+ file_edit: 'edit_file',
43
+ fileedit: 'edit_file',
44
+ editfile: 'edit_file',
45
+ replace: 'edit_file',
46
+ str_replace: 'edit_file',
47
+ str_replace_editor: 'edit_file',
48
+ search_replace: 'edit_file',
49
+ // ── edit_range ────────────────────────────────────────────────────────
50
+ range_edit: 'edit_range',
51
+ replace_range: 'edit_range',
52
+ replace_lines: 'edit_range',
53
+ edit_lines: 'edit_range',
54
+ // ── insert_file ───────────────────────────────────────────────────────
55
+ file_insert: 'insert_file',
56
+ insert: 'insert_file',
57
+ append_file: 'insert_file',
58
+ prepend_file: 'insert_file',
59
+ // ── list_dir ──────────────────────────────────────────────────────────
60
+ file_list: 'list_dir',
61
+ filelist: 'list_dir',
62
+ listfiles: 'list_dir',
63
+ list_files: 'list_dir',
64
+ ls: 'list_dir',
65
+ listdir: 'list_dir',
66
+ directory_list: 'list_dir',
67
+ list_directory: 'list_dir',
68
+ // ── search_files ──────────────────────────────────────────────────────
69
+ search: 'search_files',
70
+ grep: 'search_files',
71
+ find_files: 'search_files',
72
+ file_search: 'search_files',
73
+ ripgrep: 'search_files',
74
+ rg: 'search_files',
75
+ // ── apply_patch ───────────────────────────────────────────────────────
76
+ patch: 'apply_patch',
77
+ diff: 'apply_patch',
78
+ apply_diff: 'apply_patch',
79
+ // ── spawn_task ────────────────────────────────────────────────────────
80
+ delegate: 'spawn_task',
81
+ sub_agent: 'spawn_task',
82
+ subagent: 'spawn_task',
83
+ // ── vault_search ──────────────────────────────────────────────────────
84
+ memory_recall: 'vault_search',
85
+ recall: 'vault_search',
86
+ // ── vault_note ────────────────────────────────────────────────────────
87
+ memory_store: 'vault_note',
88
+ store: 'vault_note',
89
+ };
90
+ /**
91
+ * Resolve a tool name alias to the canonical Idle Hands tool name.
92
+ * Returns the canonical name if an alias is found, or the original name
93
+ * if no alias matches (case-insensitive lookup).
94
+ */
95
+ export function resolveToolAlias(name) {
96
+ const normalized = name.trim().toLowerCase();
97
+ const canonical = ALIAS_MAP[normalized];
98
+ if (canonical) {
99
+ return { resolved: canonical, wasAliased: true };
100
+ }
101
+ // Also check with underscores/hyphens normalized
102
+ const dehyphenated = normalized.replace(/-/g, '_');
103
+ const canonical2 = ALIAS_MAP[dehyphenated];
104
+ if (canonical2) {
105
+ return { resolved: canonical2, wasAliased: true };
106
+ }
107
+ return { resolved: name, wasAliased: false };
108
+ }
109
+ /**
110
+ * Default parameter name for a given tool, used when parsing shortened
111
+ * tool call formats (e.g., `shell>ls` → `{command: "ls"}`).
112
+ */
113
+ export function defaultParamForTool(toolName) {
114
+ const resolved = resolveToolAlias(toolName).resolved;
115
+ switch (resolved) {
116
+ case 'exec':
117
+ return 'command';
118
+ case 'read_file':
119
+ case 'read_files':
120
+ case 'write_file':
121
+ case 'edit_file':
122
+ case 'edit_range':
123
+ case 'insert_file':
124
+ case 'list_dir':
125
+ return 'path';
126
+ case 'search_files':
127
+ return 'pattern';
128
+ case 'apply_patch':
129
+ return 'patch';
130
+ case 'vault_search':
131
+ return 'query';
132
+ case 'vault_note':
133
+ return 'key';
134
+ case 'spawn_task':
135
+ return 'task';
136
+ default:
137
+ return 'input';
138
+ }
139
+ }
140
+ //# sourceMappingURL=tool-name-alias.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-name-alias.js","sourceRoot":"","sources":["../../src/agent/tool-name-alias.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,SAAS,GAA2B;IACxC,yEAAyE;IACzE,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,MAAM;IACZ,EAAE,EAAE,MAAM;IACV,OAAO,EAAE,MAAM;IACf,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,OAAO,EAAE,MAAM;IACf,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,MAAM;IACnB,SAAS,EAAE,MAAM;IACjB,eAAe,EAAE,MAAM;IAEvB,yEAAyE;IACzE,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,WAAW;IACrB,QAAQ,EAAE,WAAW;IACrB,GAAG,EAAE,WAAW;IAChB,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,WAAW;IACrB,SAAS,EAAE,WAAW;IAEtB,yEAAyE;IACzE,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IAExB,yEAAyE;IACzE,UAAU,EAAE,YAAY;IACxB,SAAS,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;IACvB,WAAW,EAAE,YAAY;IACzB,SAAS,EAAE,YAAY;IAEvB,yEAAyE;IACzE,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,WAAW;IACrB,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,WAAW;IACpB,WAAW,EAAE,WAAW;IACxB,kBAAkB,EAAE,WAAW;IAC/B,cAAc,EAAE,WAAW;IAE3B,yEAAyE;IACzE,UAAU,EAAE,YAAY;IACxB,aAAa,EAAE,YAAY;IAC3B,aAAa,EAAE,YAAY;IAC3B,UAAU,EAAE,YAAY;IAExB,yEAAyE;IACzE,WAAW,EAAE,aAAa;IAC1B,MAAM,EAAE,aAAa;IACrB,WAAW,EAAE,aAAa;IAC1B,YAAY,EAAE,aAAa;IAE3B,yEAAyE;IACzE,SAAS,EAAE,UAAU;IACrB,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,UAAU;IACrB,UAAU,EAAE,UAAU;IACtB,EAAE,EAAE,UAAU;IACd,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,UAAU;IAC1B,cAAc,EAAE,UAAU;IAE1B,yEAAyE;IACzE,MAAM,EAAE,cAAc;IACtB,IAAI,EAAE,cAAc;IACpB,UAAU,EAAE,cAAc;IAC1B,WAAW,EAAE,cAAc;IAC3B,OAAO,EAAE,cAAc;IACvB,EAAE,EAAE,cAAc;IAElB,yEAAyE;IACzE,KAAK,EAAE,aAAa;IACpB,IAAI,EAAE,aAAa;IACnB,UAAU,EAAE,aAAa;IAEzB,yEAAyE;IACzE,QAAQ,EAAE,YAAY;IACtB,SAAS,EAAE,YAAY;IACvB,QAAQ,EAAE,YAAY;IAEtB,yEAAyE;IACzE,aAAa,EAAE,cAAc;IAC7B,MAAM,EAAE,cAAc;IAEtB,yEAAyE;IACzE,YAAY,EAAE,YAAY;IAC1B,KAAK,EAAE,YAAY;CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACnD,CAAC;IACD,iDAAiD;IACjD,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACpD,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,SAAS,CAAC;QACnB,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,aAAa,CAAC;QACnB,KAAK,UAAU;YACb,OAAO,MAAM,CAAC;QAChB,KAAK,cAAc;YACjB,OAAO,SAAS,CAAC;QACnB,KAAK,aAAa;YAChB,OAAO,OAAO,CAAC;QACjB,KAAK,cAAc;YACjB,OAAO,OAAO,CAAC;QACjB,KAAK,YAAY;YACf,OAAO,KAAK,CAAC;QACf,KAAK,YAAY;YACf,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC"}
package/dist/agent.js CHANGED
@@ -9,6 +9,11 @@ import { reviewArtifactKeys, looksLikeCodeReviewRequest, looksLikeReviewRetrieva
9
9
  import { capApprovalMode, ensureInformativeAssistantText, isContextWindowExceededError, makeAbortController, userContentToText, userDisallowsDelegation, } from './agent/session-utils.js';
10
10
  import { buildSubAgentContextBlock, extractLensBody } from './agent/subagent-context.js';
11
11
  import { parseToolCallsFromContent, getMissingRequiredParams, getArgValidationIssues, stripMarkdownFences, parseJsonArgs, } from './agent/tool-calls.js';
12
+ import { resolveToolAlias } from './agent/tool-name-alias.js';
13
+ import { buildDefaultSystemPrompt } from './agent/prompt-builder.js';
14
+ import { LeakDetector } from './security/leak-detector.js';
15
+ import { PromptGuard } from './security/prompt-guard.js';
16
+ import { ResponseCache } from './agent/response-cache.js';
12
17
  import { ToolLoopGuard } from './agent/tool-loop-guard.js';
13
18
  import { isLspTool, isMutationTool, isReadOnlyTool, planModeSummary } from './agent/tool-policy.js';
14
19
  import { buildToolsSchema } from './agent/tools-schema.js';
@@ -33,32 +38,10 @@ import * as tools from './tools.js';
33
38
  import { stateDir, timestampedId } from './utils.js';
34
39
  import { VaultStore } from './vault.js';
35
40
  export { parseToolCallsFromContent };
36
- const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
37
-
38
- Rules:
39
- - Work in the current directory. Use relative paths for all file operations.
40
- - Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
41
- - Never use spawn_task to bypass confirmation/safety restrictions (for example blocked package installs). If a command is blocked, adapt the plan or ask the user for approval mode changes.
42
- - Read the target file before editing. You need the exact text for search/replace.
43
- - Use read_file with search=... to jump to relevant code; avoid reading whole files.
44
- - Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
45
- - Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
46
- - Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
47
- - edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\nline B"}
48
- - apply_patch example: {"patch":"--- a/src/foo.ts\n+++ b/src/foo.ts\n@@ -10,2 +10,2 @@\n-old\n+new","files":["src/foo.ts"]}
49
- - write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
50
- - Use insert_file for insertions (prepend/append/line).
51
- - Use exec to run commands, tests, builds; check results before reporting success.
52
- - When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd". Each exec call is a fresh shell; cd does not persist.
53
- - Batch work: read all files you need, then apply all edits, then verify.
54
- - Be concise. Report what you changed and why.
55
- - Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
56
- - If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
57
- - Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.
58
-
59
- Tool call format:
60
- - Use tool_calls. Do not write JSON tool invocations in your message text.
61
- `;
41
+ // System prompt is now built dynamically by the modular prompt builder.
42
+ // See src/agent/prompt-builder.ts for section definitions.
43
+ // The old monolithic SYSTEM_PROMPT is replaced by buildDefaultSystemPrompt().
44
+ const SYSTEM_PROMPT = buildDefaultSystemPrompt();
62
45
  export async function createSession(opts) {
63
46
  const cfg = opts.config;
64
47
  const projectDir = cfg.dir ?? process.cwd();
@@ -145,11 +128,13 @@ export async function createSession(opts) {
145
128
  // whether the harness wants a higher value — harness.defaults.max_tokens wins
146
129
  // when it's larger than the base default (16384), unless the user explicitly
147
130
  // configured a value in their config file or CLI.
148
- let { maxTokens, temperature, topP } = deriveGenerationParams({
131
+ let { maxTokens, temperature, topP, frequencyPenalty, presencePenalty } = deriveGenerationParams({
149
132
  harness,
150
133
  configuredMaxTokens: cfg.max_tokens,
151
134
  configuredTemperature: cfg.temperature,
152
135
  configuredTopP: cfg.top_p,
136
+ configuredFrequencyPenalty: cfg.frequency_penalty,
137
+ configuredPresencePenalty: cfg.presence_penalty,
153
138
  baseMaxTokens: BASE_MAX_TOKENS,
154
139
  });
155
140
  const harnessVaultMode = harness.defaults?.trifecta?.vaultMode || 'off';
@@ -1214,11 +1199,13 @@ export async function createSession(opts) {
1214
1199
  previousContextWindow: contextWindow,
1215
1200
  modelMeta: nextMeta,
1216
1201
  });
1217
- ({ maxTokens, temperature, topP } = deriveGenerationParams({
1202
+ ({ maxTokens, temperature, topP, frequencyPenalty, presencePenalty } = deriveGenerationParams({
1218
1203
  harness,
1219
1204
  configuredMaxTokens: cfg.max_tokens,
1220
1205
  configuredTemperature: cfg.temperature,
1221
1206
  configuredTopP: cfg.top_p,
1207
+ configuredFrequencyPenalty: cfg.frequency_penalty,
1208
+ configuredPresencePenalty: cfg.presence_penalty,
1222
1209
  baseMaxTokens: BASE_MAX_TOKENS,
1223
1210
  }));
1224
1211
  // Update system prompt for the new model/harness
@@ -1414,6 +1401,41 @@ export async function createSession(opts) {
1414
1401
  }
1415
1402
  sessionMetaPending = null;
1416
1403
  }
1404
+ // ── Auto vault context injection ─────────────────────────────────
1405
+ // Search the vault for entries relevant to the user's instruction and
1406
+ // prepend them to the user message so the model has context without
1407
+ // needing to call vault_search. Inspired by ZeroClaw's build_context().
1408
+ if (vault && vaultEnabled) {
1409
+ try {
1410
+ const queryText = typeof instruction === 'string'
1411
+ ? instruction
1412
+ : instruction
1413
+ .filter((p) => p.type === 'text')
1414
+ .map((p) => p.text)
1415
+ .join(' ');
1416
+ const vaultQuery = queryText.trim().slice(0, 200);
1417
+ if (vaultQuery.length >= 10) {
1418
+ const vaultHits = await vault.search(vaultQuery, 4);
1419
+ if (vaultHits.length > 0) {
1420
+ const vaultLines = vaultHits.map((r) => {
1421
+ const title = r.kind === 'note' ? `note:${r.key}` : `tool:${r.tool || r.key || 'unknown'}`;
1422
+ const body = (r.value ?? r.snippet ?? r.content ?? '').replace(/\s+/g, ' ').slice(0, 160);
1423
+ return `- ${title}: ${body}`;
1424
+ });
1425
+ const vaultBlock = `[Vault context]\n${vaultLines.join('\n')}\n`;
1426
+ if (typeof userContent === 'string') {
1427
+ userContent = `${vaultBlock}\n${userContent}`;
1428
+ }
1429
+ else {
1430
+ userContent = [{ type: 'text', text: vaultBlock }, ...userContent];
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ catch {
1436
+ // Vault search is best-effort; don't fail the turn
1437
+ }
1438
+ }
1417
1439
  messages.push({ role: 'user', content: userContent });
1418
1440
  const hookObj = typeof hooks === 'function' ? { onToken: hooks } : (hooks ?? {});
1419
1441
  let turns = 0;
@@ -1709,6 +1731,21 @@ export async function createSession(opts) {
1709
1731
  const toolLoopWarningKeys = new Set();
1710
1732
  let forceToollessRecoveryTurn = false;
1711
1733
  let toollessRecoveryUsed = false;
1734
+ // ── Security: credential leak detection + prompt injection guard ──
1735
+ const leakDetector = new LeakDetector();
1736
+ const promptGuard = new PromptGuard('warn');
1737
+ // ── Performance: response cache for repeated identical prompts ──
1738
+ let responseCache;
1739
+ try {
1740
+ responseCache = new ResponseCache({
1741
+ cacheDir: path.join(projectDir, '.idlehands', 'cache'),
1742
+ ttlMinutes: 60,
1743
+ maxEntries: 200,
1744
+ });
1745
+ }
1746
+ catch {
1747
+ // Cache init failure is non-fatal — proceed without caching
1748
+ }
1712
1749
  // Prevent repeating the same "stop rerunning" reminder every turn.
1713
1750
  const readOnlyExecHintedSigs = new Set();
1714
1751
  // Tool loop recovery: poisoned results and selective tool suppression.
@@ -1985,22 +2022,65 @@ export async function createSession(opts) {
1985
2022
  ? []
1986
2023
  : getToolsSchema().filter((t) => !suppressedTools.has(t.function.name));
1987
2024
  const toolChoiceForTurn = cfg.no_tools || forceToollessRecoveryTurn ? 'none' : 'auto';
1988
- resp = await client.chatStream({
1989
- model,
1990
- messages,
1991
- tools: toolsForTurn,
1992
- tool_choice: toolChoiceForTurn,
1993
- temperature,
1994
- top_p: topP,
1995
- max_tokens: maxTokens,
1996
- extra: { cache_prompt: cfg.cache_prompt ?? true },
1997
- signal: ac.signal,
1998
- requestId: `r${reqCounter}`,
1999
- onToken: hookObj.onToken,
2000
- onFirstDelta,
2001
- });
2025
+ // ── Response cache: check for cached response ──────────────
2026
+ // Only cache tool-less turns (final answers, explanations) since
2027
+ // tool-calling turns have side effects that shouldn't be replayed.
2028
+ const cacheableRequest = toolsForTurn.length === 0 && !!responseCache;
2029
+ const lastUserMsg = messages.filter((m) => m.role === 'user').pop();
2030
+ const userPromptForCache = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
2031
+ const systemPromptForCache = messages.find((m) => m.role === 'system')?.content ?? '';
2032
+ if (cacheableRequest && userPromptForCache.length >= 10) {
2033
+ const cached = responseCache.get(model, systemPromptForCache, userPromptForCache);
2034
+ if (cached) {
2035
+ resp = {
2036
+ id: 'cache-hit',
2037
+ choices: [{
2038
+ index: 0,
2039
+ message: { role: 'assistant', content: cached },
2040
+ finish_reason: 'stop',
2041
+ }],
2042
+ };
2043
+ if (cfg.verbose)
2044
+ console.log('[response-cache] cache hit, skipping API call');
2045
+ }
2046
+ }
2047
+ if (!resp) {
2048
+ resp = await client.chatStream({
2049
+ model,
2050
+ messages,
2051
+ tools: toolsForTurn,
2052
+ tool_choice: toolChoiceForTurn,
2053
+ temperature,
2054
+ top_p: topP,
2055
+ max_tokens: maxTokens,
2056
+ extra: {
2057
+ cache_prompt: cfg.cache_prompt ?? true,
2058
+ // Speculative decoding: draft model params for llama-server
2059
+ ...(cfg.draft_model ? { draft_model: cfg.draft_model } : {}),
2060
+ ...(cfg.draft_n ? { speculative: { n: cfg.draft_n, p_min: cfg.draft_p_min ?? 0.5 } } : {}),
2061
+ ...(frequencyPenalty && { frequency_penalty: frequencyPenalty }),
2062
+ ...(presencePenalty && { presence_penalty: presencePenalty }),
2063
+ },
2064
+ signal: ac.signal,
2065
+ requestId: `r${reqCounter}`,
2066
+ onToken: hookObj.onToken,
2067
+ onFirstDelta,
2068
+ });
2069
+ } // end if (!resp) — cache miss path
2002
2070
  // Successful response resets overflow recovery budget.
2003
2071
  overflowCompactionAttempts = 0;
2072
+ // ── Response cache: store cacheable responses ─────────────
2073
+ if (cacheableRequest && userPromptForCache.length >= 10 && resp.id !== 'cache-hit') {
2074
+ const respContent = resp.choices?.[0]?.message?.content;
2075
+ if (respContent && typeof respContent === 'string') {
2076
+ try {
2077
+ responseCache.set(model, systemPromptForCache, userPromptForCache, respContent, resp.usage?.completion_tokens ?? 0);
2078
+ }
2079
+ catch {
2080
+ // Cache write failure is non-fatal
2081
+ }
2082
+ }
2083
+ }
2004
2084
  }
2005
2085
  catch (e) {
2006
2086
  if (isContextWindowExceededError(e) &&
@@ -2535,7 +2615,13 @@ export async function createSession(opts) {
2535
2615
  throw new AgentLoopBreak('critical tool-loop persisted after one tools-disabled recovery turn. Stopping to avoid infinite loop.');
2536
2616
  }
2537
2617
  const runOne = async (tc) => {
2538
- const name = tc.function.name;
2618
+ // Resolve tool name aliases (bash→exec, file_read→read_file, etc.)
2619
+ const rawName = tc.function.name;
2620
+ const { resolved: name, wasAliased } = resolveToolAlias(rawName);
2621
+ if (wasAliased) {
2622
+ // Patch the tool call in-place so downstream code (loop guard, etc.) sees the canonical name
2623
+ tc.function.name = name;
2624
+ }
2539
2625
  const rawArgs = tc.function.arguments ?? '{}';
2540
2626
  const callId = resolveCallId(tc);
2541
2627
  toolNameByCallId.set(callId, name);
@@ -2850,6 +2936,19 @@ export async function createSession(opts) {
2850
2936
  content += `\n\n[WARNING: You have read this exact same resource ${consec}x consecutively with identical arguments. The content has NOT changed. Do NOT read it again. Use the information above and move on to the next step.]`;
2851
2937
  }
2852
2938
  }
2939
+ // ── Early truncation pass ──────────────────────────────────
2940
+ // Cap extremely large tool output (>50KB) early to avoid
2941
+ // running leak detection, loop guard, and other processing
2942
+ // on megabytes of npm install / build output. The final
2943
+ // precise truncation still happens before return.
2944
+ const EARLY_TRUNCATION_LIMIT = 50_000;
2945
+ if (content.length > EARLY_TRUNCATION_LIMIT) {
2946
+ const headLen = Math.floor(EARLY_TRUNCATION_LIMIT * 0.8);
2947
+ const tailLen = EARLY_TRUNCATION_LIMIT - headLen - 100;
2948
+ content = content.slice(0, headLen) +
2949
+ `\n\n[...${content.length - headLen - tailLen} chars truncated for processing efficiency...]\n\n` +
2950
+ content.slice(-tailLen);
2951
+ }
2853
2952
  // Hook: onToolResult (Phase 8.5 + Phase 7 rich display)
2854
2953
  let toolSuccess = true;
2855
2954
  let summary = reusedCachedReadOnlyExec
@@ -2990,6 +3089,10 @@ export async function createSession(opts) {
2990
3089
  }
2991
3090
  }
2992
3091
  }
3092
+ // ── Credential leak scrubbing ─────────────────────────────
3093
+ // Scan tool output for credential leaks before passing back
3094
+ // to the model (and potentially to a chat channel).
3095
+ content = leakDetector.redactIfNeeded(content);
2993
3096
  // Context-aware truncation: cap oversized tool results before returning
2994
3097
  // to prevent blowing out the context window on subsequent LLM calls.
2995
3098
  const truncated = truncateToolResultContent(content, contextWindow);