@pentoshi/clai 2.0.18 → 2.0.20

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 (58) hide show
  1. package/dist/agent/context-manager.js +24 -9
  2. package/dist/agent/context-manager.js.map +1 -1
  3. package/dist/agent/events.d.ts +1 -1
  4. package/dist/agent/loop-guard.d.ts +9 -0
  5. package/dist/agent/loop-guard.js +45 -3
  6. package/dist/agent/loop-guard.js.map +1 -1
  7. package/dist/agent/runner.d.ts +7 -0
  8. package/dist/agent/runner.js +182 -28
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/task-analyzer.d.ts +3 -3
  11. package/dist/agent/task-analyzer.js +16 -4
  12. package/dist/agent/task-analyzer.js.map +1 -1
  13. package/dist/commands/update.js +1 -1
  14. package/dist/os/permissions.d.ts +14 -0
  15. package/dist/os/permissions.js +76 -0
  16. package/dist/os/permissions.js.map +1 -0
  17. package/dist/prompts/index.d.ts +1 -1
  18. package/dist/prompts/index.js +8 -1
  19. package/dist/prompts/index.js.map +1 -1
  20. package/dist/repl.js +17 -0
  21. package/dist/repl.js.map +1 -1
  22. package/dist/safety/classifier.js.map +1 -1
  23. package/dist/store/config.js +25 -6
  24. package/dist/store/config.js.map +1 -1
  25. package/dist/store/history.js +80 -46
  26. package/dist/store/history.js.map +1 -1
  27. package/dist/store/keys.js +20 -6
  28. package/dist/store/keys.js.map +1 -1
  29. package/dist/store/logs.js +16 -6
  30. package/dist/store/logs.js.map +1 -1
  31. package/dist/store/plan.js +44 -21
  32. package/dist/store/plan.js.map +1 -1
  33. package/dist/store/scope.js +17 -5
  34. package/dist/store/scope.js.map +1 -1
  35. package/dist/tools/fs.d.ts +33 -5
  36. package/dist/tools/fs.js +22 -19
  37. package/dist/tools/fs.js.map +1 -1
  38. package/dist/tools/registry.d.ts +1 -0
  39. package/dist/tools/registry.js +14 -12
  40. package/dist/tools/registry.js.map +1 -1
  41. package/dist/tui/App.js +3 -2
  42. package/dist/tui/App.js.map +1 -1
  43. package/dist/tui/components/ConfirmModal.js +9 -2
  44. package/dist/tui/components/ConfirmModal.js.map +1 -1
  45. package/dist/tui/components/Pager.js +14 -1
  46. package/dist/tui/components/Pager.js.map +1 -1
  47. package/dist/tui/components/PickerPanel.d.ts +2 -1
  48. package/dist/tui/components/PickerPanel.js +4 -2
  49. package/dist/tui/components/PickerPanel.js.map +1 -1
  50. package/dist/tui/confirm.d.ts +1 -1
  51. package/dist/tui/confirm.js +6 -0
  52. package/dist/tui/confirm.js.map +1 -1
  53. package/dist/tui/render-lines.js +23 -9
  54. package/dist/tui/render-lines.js.map +1 -1
  55. package/dist/tui/state.d.ts +1 -1
  56. package/dist/tui/state.js +4 -1
  57. package/dist/tui/state.js.map +1 -1
  58. package/package.json +1 -1
@@ -16,8 +16,8 @@ export function estimateMessagesTokens(messages) {
16
16
  }
17
17
  return sum;
18
18
  }
19
- const DEFAULT_BUDGET_TOKENS = 24_000;
20
- const DEFAULT_KEEP_RECENT = 8;
19
+ const DEFAULT_BUDGET_TOKENS = 32_000;
20
+ const DEFAULT_KEEP_RECENT = 12;
21
21
  /**
22
22
  * Replace older messages with a single condensed "memory" message while
23
23
  * preserving the system prompt and the most recent N messages.
@@ -75,12 +75,27 @@ export function compactMessages(messages, options = {}) {
75
75
  export async function compactMessagesWithSummary(messages, summarize, options = {}, sessionTranscript) {
76
76
  const before = messages.length;
77
77
  const beforeTokens = estimateMessagesTokens(messages);
78
- const keepRecent = Math.max(2, options.keepRecent ?? DEFAULT_KEEP_RECENT);
78
+ const isForced = options.budgetTokens === 0;
79
+ let keepRecent = Math.max(2, options.keepRecent ?? DEFAULT_KEEP_RECENT);
79
80
  const start = messages[0]?.role === "system" ? 1 : 0;
80
- const tailStart = Math.max(start, messages.length - keepRecent);
81
- const older = messages.slice(start, tailStart);
82
- if (older.length === 0) {
83
- return { messages: [...messages], before, after: before, beforeTokens, afterTokens: beforeTokens, summarized: false };
81
+ let tailStart = Math.max(start, messages.length - keepRecent);
82
+ let older = messages.slice(start, tailStart);
83
+ // If forced and the older slice would be empty, try keeping fewer recent
84
+ // messages (minimum 1) so we have something to compact (e.g. the first user prompt).
85
+ if (older.length === 0 && isForced && messages.length >= start + 2) {
86
+ keepRecent = 1;
87
+ tailStart = messages.length - 1;
88
+ older = messages.slice(start, tailStart);
89
+ }
90
+ if (older.length === 0 && !sessionTranscript?.trim()) {
91
+ return {
92
+ messages: [...messages],
93
+ before,
94
+ after: before,
95
+ beforeTokens,
96
+ afterTokens: beforeTokens,
97
+ summarized: false,
98
+ };
84
99
  }
85
100
  const messageTranscript = older
86
101
  .map((message) => `${message.role.toUpperCase()}: ${redactSecrets(message.content)}`)
@@ -107,7 +122,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
107
122
  ...messages.slice(tailStart),
108
123
  ];
109
124
  const afterTokens = estimateMessagesTokens(compacted);
110
- if (afterTokens >= beforeTokens) {
125
+ if (afterTokens >= beforeTokens && !isForced) {
111
126
  const deterministicCompacted = compactMessages(messages, { ...options, budgetTokens: 0 });
112
127
  const detTokens = estimateMessagesTokens(deterministicCompacted);
113
128
  if (detTokens < beforeTokens) {
@@ -144,7 +159,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
144
159
  }
145
160
  const compacted = compactMessages(messages, { ...options, budgetTokens: 0 });
146
161
  const afterTokens = estimateMessagesTokens(compacted);
147
- if (afterTokens >= beforeTokens) {
162
+ if (afterTokens >= beforeTokens && !isForced) {
148
163
  return {
149
164
  messages: [...messages],
150
165
  before,
@@ -1 +1 @@
1
- {"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAuB;IAC5D,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB;IAC9D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAkBD,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAuB,EACvB,UAA0B,EAAE;IAE5B,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC,CAAC;IAC1E,IAAI,QAAQ,CAAC,MAAM,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACvD,IAAI,sBAAsB,CAAC,QAAQ,CAAC,IAAI,MAAM;QAAE,OAAO,QAAQ,CAAC;IAEhE,oEAAoE;IACpE,MAAM,IAAI,GAAkB,EAAE,CAAC;IAC/B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACvB,KAAK,GAAG,CAAC,CAAC;IACZ,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACpE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEzC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,iBAAiB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACvC,IAAI,IAAI;gBAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,kBAAkB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAgB;QACxB,IAAI,EAAE,QAAQ;QACd,OAAO,EACL,6JAA6J;YAC7J,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;KACrB,CAAC;IAEF,OAAO,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,QAAuB,EACvB,SAA8C,EAC9C,UAA0B,EAAE,EAC5B,iBAAsC;IAEtC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,MAAM,YAAY,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAC;IAE5C,IAAI,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;IAC9D,IAAI,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAE7C,yEAAyE;IACzE,qFAAqF;IACrF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACnE,UAAU,GAAG,CAAC,CAAC;QACf,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAChC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,EAAE,EAAE,CAAC;QACrD,OAAO;YACL,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC;YACvB,MAAM;YACN,KAAK,EAAE,MAAM;YACb,YAAY;YACZ,WAAW,EAAE,YAAY;YACzB,UAAU,EAAE,KAAK;SAClB,CAAC;IACJ,CAAC;IAED,MAAM,iBAAiB,GAAG,KAAK;SAC5B,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;SACpF,IAAI,CAAC,MAAM,CAAC,CAAC;IAChB,MAAM,UAAU,GAAG,iBAAiB,EAAE,IAAI,EAAE;QAC1C,CAAC,CAAC,aAAa,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;QACzC,CAAC,CAAC,iBAAiB,CAAC;IACtB,MAAM,MAAM,GAAG;QACb,4HAA4H;QAC5H,sNAAsN;QACtN,+JAA+J;QAC/J,mHAAmH;QACnH,EAAE;QACF,UAAU;KACX,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,MAAM,SAAS,GAAkB;YAC/B,GAAG,IAAI;YACP,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,mDAAmD,OAAO,EAAE,EAAE;YACzF,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC;SAC7B,CAAC;QACF,MAAM,WAAW,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;QACtD,IAAI,WAAW,IAAI,YAAY,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7C,MAAM,sBAAsB,GAAG,eAAe,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;YAC1F,MAAM,SAAS,GAAG,sBAAsB,CAAC,sBAAsB,CAAC,CAAC;YACjE,IAAI,SAAS,GAAG,YAAY,EAAE,CAAC;gBAC7B,OAAO;oBACL,QAAQ,EAAE,sBAAsB;oBAChC,MAAM;oBACN,KAAK,EAAE,sBAAsB,CAAC,MAAM;oBACpC,YAAY;oBACZ,WAAW,EAAE,SAAS;oBACtB,UAAU,EAAE,KAAK;iBAClB,CAAC;YACJ,CAAC;YACD,OAAO;gBACL,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC;gBACvB,MAAM;gBACN,KAAK,EAAE,MAAM;gBACb,YAAY;gBACZ,WAAW,EAAE,YAAY;gBACzB,UAAU,EAAE,KAAK;aAClB,CAAC;QACJ,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,MAAM;YACvB,YAAY;YACZ,WAAW;YACX,UAAU,EAAE,IAAI;SACjB,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;YAClG,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,SAAS,GAAG,eAAe,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7E,MAAM,WAAW,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;QACtD,IAAI,WAAW,IAAI,YAAY,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7C,OAAO;gBACL,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC;gBACvB,MAAM;gBACN,KAAK,EAAE,MAAM;gBACb,YAAY;gBACZ,WAAW,EAAE,YAAY;gBACzB,UAAU,EAAE,KAAK;aAClB,CAAC;QACJ,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,MAAM;YACvB,YAAY;YACZ,WAAW;YACX,UAAU,EAAE,KAAK;SAClB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgB;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,IAAI,OAAO,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,OAAO,CAAC;IAC/C,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC"}
@@ -48,7 +48,7 @@ export type AgentEvent = {
48
48
  } | {
49
49
  type: "confirm-request";
50
50
  id: string;
51
- kind: "tool" | "pentest" | "reset";
51
+ kind: "tool" | "pentest" | "reset" | "continue";
52
52
  prompt: string;
53
53
  } | {
54
54
  type: "turn-end";
@@ -31,6 +31,10 @@ export declare class LoopGuard {
31
31
  * expected to fix the cause (install a tool, create a dir) and retry. Only
32
32
  * calls that already SUCCEEDED are deduped, since re-running them wastes a
33
33
  * step and risks an infinite summarize loop.
34
+ *
35
+ * Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
36
+ * because they may legitimately need re-calling after context compaction
37
+ * removes their results.
34
38
  */
35
39
  shouldBlock(name: string, args: Record<string, unknown>): {
36
40
  block: boolean;
@@ -42,6 +46,11 @@ export declare class LoopGuard {
42
46
  * (e.g., command not found → retry → not found → ...).
43
47
  */
44
48
  hasRepeatedFailures(threshold?: number): boolean;
49
+ /**
50
+ * Reset counters for read-only tools. Called after context compaction
51
+ * so the model can re-fetch data whose results were compacted away.
52
+ */
53
+ resetReadOnly(): void;
45
54
  /**
46
55
  * Get the total number of recorded attempts.
47
56
  */
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Non-mutating tools that may legitimately need re-calling after context
3
+ * compaction removes their earlier results. These get a higher dedup
4
+ * threshold (3 vs 2 for write tools) and their counters can be reset when
5
+ * context is compacted.
6
+ */
7
+ const READ_ONLY_TOOLS = new Set([
8
+ "web.fetch",
9
+ "http.fetch",
10
+ "web.search",
11
+ "dns.lookup",
12
+ "whois.lookup",
13
+ "fs.read",
14
+ "fs.list",
15
+ "fs.search",
16
+ "sysinfo",
17
+ "net.context",
18
+ "tool.check",
19
+ "image.ocr",
20
+ "pdf.read",
21
+ ]);
1
22
  /**
2
23
  * Track and detect tool-call repetition patterns so the agent doesn't
3
24
  * waste steps in loops.
@@ -47,6 +68,10 @@ export class LoopGuard {
47
68
  * expected to fix the cause (install a tool, create a dir) and retry. Only
48
69
  * calls that already SUCCEEDED are deduped, since re-running them wastes a
49
70
  * step and risks an infinite summarize loop.
71
+ *
72
+ * Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
73
+ * because they may legitimately need re-calling after context compaction
74
+ * removes their results.
50
75
  */
51
76
  shouldBlock(name, args) {
52
77
  const sig = this.canonicalize(name, args);
@@ -60,7 +85,10 @@ export class LoopGuard {
60
85
  // that just wrote a file to "use the results you already have" is
61
86
  // nonsensical and has caused models to assume the whole task is done.
62
87
  const isWrite = name === "fs.write" || name === "fs.writeMany" || name === "fs.edit";
63
- if (count === 1) {
88
+ // Read-only tools get a higher threshold — they may need re-calling
89
+ // after context compaction removes their earlier results.
90
+ const threshold = READ_ONLY_TOOLS.has(name) ? 3 : 2;
91
+ if (count < threshold) {
64
92
  return {
65
93
  block: false,
66
94
  reason: isWrite
@@ -68,12 +96,12 @@ export class LoopGuard {
68
96
  : `${name} has already been called with these arguments once and succeeded. Consider using the results you already have.`,
69
97
  };
70
98
  }
71
- // count >= 2 and at least one success: block
99
+ // count >= threshold and at least one success: block
72
100
  return {
73
101
  block: true,
74
102
  reason: isWrite
75
103
  ? `${name} was already called ${count} time(s) with the identical path and content. That file is already written. Continue with the remaining files/steps or give your final answer.`
76
- : `${name} was already called ${count} time(s) with the same arguments. Summarize existing results instead.`,
104
+ : `${name} was already called ${count} time(s) with the same arguments. The data is already in your context — analyze what you have and move to the next step.`,
77
105
  };
78
106
  }
79
107
  getAttemptCount(name, args) {
@@ -90,6 +118,20 @@ export class LoopGuard {
90
118
  const recent = this.attempts.slice(-threshold);
91
119
  return recent.every((a) => !a.ok);
92
120
  }
121
+ /**
122
+ * Reset counters for read-only tools. Called after context compaction
123
+ * so the model can re-fetch data whose results were compacted away.
124
+ */
125
+ resetReadOnly() {
126
+ for (const sig of [...this.signatureCount.keys()]) {
127
+ const name = sig.split("::")[0] ?? "";
128
+ if (READ_ONLY_TOOLS.has(name)) {
129
+ this.signatureCount.delete(sig);
130
+ this.signatureSuccess.delete(sig);
131
+ }
132
+ }
133
+ this.attempts = this.attempts.filter((a) => !READ_ONLY_TOOLS.has(a.callName));
134
+ }
93
135
  /**
94
136
  * Get the total number of recorded attempts.
95
137
  */
@@ -1 +1 @@
1
- {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,8 @@ export declare function createSessionPolicy(): SessionPolicy;
18
18
  export interface ConfirmPort {
19
19
  confirmTool(call: ToolCall): Promise<boolean>;
20
20
  confirmPentest(): Promise<boolean>;
21
+ /** Ask the user whether to continue after hitting the step budget. */
22
+ confirmContinue?(steps: number): Promise<boolean>;
21
23
  }
22
24
  export interface AgentRunOptions {
23
25
  provider?: ProviderId | undefined;
@@ -120,6 +122,11 @@ export declare function sameToolCall(a: ToolCall, b: ToolCall): boolean;
120
122
  * copies and note the collapse so the meaning is preserved without the bulk.
121
123
  */
122
124
  export declare function collapseRepeatedText(text: string): string;
125
+ /**
126
+ * Detect pentest/security tasks that need the full step budget.
127
+ * Mirrors looksLikeBuildTask but for security work.
128
+ */
129
+ export declare function looksLikePentestTask(prompt: string, history?: ChatMessage[] | undefined): boolean;
123
130
  /**
124
131
  * Decide whether this turn should get the build workflow (explore → plan →
125
132
  * implement) and a generous step budget. Looks at the current prompt first,
@@ -2,7 +2,8 @@ import { confirm } from "@inquirer/prompts";
2
2
  import chalk from "chalk";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { join } from "node:path";
5
+ import { fixOwner, handlePermissionError } from "../os/permissions.js";
6
+ import { join, resolve } from "node:path";
6
7
  import { streamWithProvider } from "../llm/router.js";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { jobManager } from "../tools/jobs.js";
@@ -26,6 +27,7 @@ import { analyzeTask } from "./task-analyzer.js";
26
27
  import { LoopGuard } from "./loop-guard.js";
27
28
  import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
28
29
  import { renderPlanChecklist, renderPlanSidePane } from "../ui/plan-pane.js";
30
+ import { pathInsideSandbox } from "../tools/fs.js";
29
31
  /** Render the plan as a right-side pane on wide terminals, else inline. */
30
32
  function renderPlanForTerminal(plan) {
31
33
  const cols = process.stdout.columns ?? 0;
@@ -547,6 +549,25 @@ const LOCAL_RUNTIME_RE = /\b(?:current\s+(?:directory|dir|cwd|working\s+director
547
549
  // under-budgeting silently truncates a half-built project.
548
550
  const BUILD_TASK_RE = /\b(?:build|create|scaffold|generate|make|set\s*up|setup|bootstrap|init(?:ialize)?|implement|add|write|develop|code|refactor|migrate|convert|wire\s*up|integrate)\b[\s\S]{0,80}\b(?:app|application|project|site|website|web\s*app|server|api|service|component|page|module|feature|cli|script|library|package|frontend|backend|fullstack|game|bot|dashboard|form|endpoint|database|schema|test|tests|suite)\b/i;
549
551
  const BUILD_STACK_RE = /\b(?:react|next(?:\.?js)?|vue|svelte|angular|vite|webpack|express|fastify|nest(?:js)?|django|flask|fastapi|rails|laravel|spring|node(?:\.?js)?|typescript|tailwind|redux|prisma|mongoose|graphql|docker|kubernetes)\b/i;
552
+ // Pentest / security keywords — these tasks are inherently multi-step and
553
+ // always deserve the full step budget, just like build tasks.
554
+ const PENTEST_TASK_RE = /\b(?:pentest|pen[\s-]?test|penetration|security\s*(?:test|audit|scan|assess)|csrf|xss|sqli|sql[\s-]?inject|rce|lfi|rfi|ssrf|idor|xxe|brute[\s-]?force|enumerat|exploit|vulnerabilit|recon|bug[\s-]?bounty|ctf|capture[\s-]?the[\s-]?flag|red[\s-]?team|offensive|nmap|nikto|nuclei|ffuf|gobuster|sqlmap|hydra|metasploit)\b/i;
555
+ /**
556
+ * Detect pentest/security tasks that need the full step budget.
557
+ * Mirrors looksLikeBuildTask but for security work.
558
+ */
559
+ export function looksLikePentestTask(prompt, history) {
560
+ if (PENTEST_TASK_RE.test(prompt))
561
+ return true;
562
+ if (history && history.length > 0) {
563
+ const recent = history.slice(-6);
564
+ for (const msg of recent) {
565
+ if (msg.role === "user" && PENTEST_TASK_RE.test(msg.content))
566
+ return true;
567
+ }
568
+ }
569
+ return false;
570
+ }
550
571
  // Short continuation prompts that, on their own, carry no build signal but
551
572
  // clearly mean "keep going with what we were doing".
552
573
  const CONTINUATION_RE = /^(?:do\s+it|build\s+it|build\s+fully|build\s+it\s+fully|go\s+ahead|continue|proceed|keep\s+going|finish(?:\s+it)?|complete(?:\s+it)?|yes|ok(?:ay)?|make\s+it|run\s+it|next|on\s+your\s+own|build\s+(?:fully\s+)?on\s+your\s+own)\b/i;
@@ -752,6 +773,58 @@ function restoreInteractiveStdin() {
752
773
  /* ignore */
753
774
  }
754
775
  }
776
+ /**
777
+ * Build a rich summary when the agent stops (user declined to continue or
778
+ * maxIterations ceiling hit). Includes plan state, key findings, and clear
779
+ * resume instructions so a later "continue" can pick up exactly here.
780
+ */
781
+ async function buildRichStopSummary(messages, session, steps) {
782
+ const plan = await loadPlan(session.sessionId).catch(() => undefined);
783
+ const parts = [];
784
+ parts.push(`Session paused after ${steps} steps.\n`);
785
+ // Plan state
786
+ if (plan) {
787
+ parts.push("## Plan Status");
788
+ parts.push(`Goal: ${plan.goal}`);
789
+ for (const task of plan.tasks) {
790
+ const icon = task.state === "done"
791
+ ? "✓"
792
+ : task.state === "in_progress"
793
+ ? "▶"
794
+ : task.state === "failed"
795
+ ? "✗"
796
+ : task.state === "skipped"
797
+ ? "↷"
798
+ : "·";
799
+ parts.push(` ${icon} [${task.id}] (${task.state}) ${task.title}${task.note ? ` — ${task.note}` : ""}`);
800
+ }
801
+ const next = plan.tasks.find((t) => t.state === "pending" || t.state === "in_progress");
802
+ if (next) {
803
+ parts.push(`\nNext task to resume: ${next.id} — "${next.title}"`);
804
+ }
805
+ const doneCount = plan.tasks.filter((t) => t.state === "done").length;
806
+ parts.push(`\nProgress: ${doneCount}/${plan.tasks.length} tasks done.`);
807
+ }
808
+ // Key findings from tool results (last 20 tool messages)
809
+ const toolMsgs = messages.filter((m) => m.role === "tool").slice(-20);
810
+ if (toolMsgs.length > 0) {
811
+ parts.push("\n## Key Findings So Far");
812
+ for (const msg of toolMsgs) {
813
+ const firstLine = msg.content.split("\n")[0] ?? "";
814
+ // Extract tool name from the structured format "Tool <name> result ..."
815
+ const toolMatch = firstLine.match(/^Tool (\S+) result/);
816
+ if (toolMatch) {
817
+ parts.push(`- ${toolMatch[1]}: ${firstLine.slice(0, 150)}`);
818
+ }
819
+ else {
820
+ parts.push(`- ${firstLine.slice(0, 150)}`);
821
+ }
822
+ }
823
+ }
824
+ parts.push("\n## To Resume");
825
+ parts.push('Type "continue" to pick up from where this session left off.');
826
+ return parts.join("\n");
827
+ }
755
828
  /**
756
829
  * Tools allowed while an UN-approved plan is active. Before the user runs
757
830
  * /implement, the agent may only (re)create the plan and do read-only
@@ -793,11 +866,18 @@ async function saveToolOutput(call, output) {
793
866
  if (!output.trim())
794
867
  return undefined;
795
868
  const dir = join(homedir(), ".clai", "outputs");
796
- await mkdir(dir, { recursive: true });
797
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
798
- const path = join(dir, `${stamp}-${safeArtifactName(call.name)}.txt`);
799
- await writeFile(path, `${output}\n`, "utf8");
800
- return path;
869
+ try {
870
+ await mkdir(dir, { recursive: true });
871
+ await fixOwner(dir);
872
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
873
+ const path = join(dir, `${stamp}-${safeArtifactName(call.name)}.txt`);
874
+ await writeFile(path, `${output}\n`, "utf8");
875
+ await fixOwner(path);
876
+ return path;
877
+ }
878
+ catch (err) {
879
+ handlePermissionError(err);
880
+ }
801
881
  }
802
882
  function summarizeOutput(output, maxChars = 8_000) {
803
883
  if (output.length <= maxChars)
@@ -876,6 +956,12 @@ const inquirerConfirmPort = {
876
956
  default: false,
877
957
  });
878
958
  },
959
+ async confirmContinue(steps) {
960
+ return confirm({
961
+ message: chalk.yellow(` ${steps} steps reached — continue?`),
962
+ default: true,
963
+ });
964
+ },
879
965
  };
880
966
  async function ensurePentestAuthorization(call, autoConfirm, session, confirmPort) {
881
967
  if (!isPentestToolCall(call))
@@ -1166,7 +1252,9 @@ export async function runAgentLoop(prompt, options = {}) {
1166
1252
  // "now"/"latest" that trip the volatile-info regex; without this guard the
1167
1253
  // agent burns its turn searching the date instead of writing files.
1168
1254
  const buildLikeTurn = looksLikeBuildTask(prompt, options.history);
1255
+ const pentestLikeTurn = looksLikePentestTask(prompt, options.history);
1169
1256
  const freshWebSearchRequired = !buildLikeTurn &&
1257
+ !pentestLikeTurn &&
1170
1258
  toolNames.includes("web.search") &&
1171
1259
  requiresFreshWebSearch(prompt);
1172
1260
  const systemSections = [renderAgentSystemPrompt(toolNames.join(", "))];
@@ -1278,14 +1366,15 @@ export async function runAgentLoop(prompt, options = {}) {
1278
1366
  const analysis = analyzeTask(prompt);
1279
1367
  const hasHistory = (options.history?.length ?? 0) > 0;
1280
1368
  const buildLike = buildLikeTurn;
1369
+ const pentestLike = looksLikePentestTask(prompt, options.history);
1281
1370
  let stepBudget = analysis.complexity === "simple"
1282
1371
  ? 20
1283
1372
  : analysis.complexity === "standard"
1284
1373
  ? 40
1285
1374
  : maxSteps;
1286
- if (buildLike) {
1287
- // Scaffolding / multi-file work needs room: many file writes plus a
1288
- // verify/build step. Continuation prompts ("do it") inherit this too.
1375
+ if (buildLike || pentestLike) {
1376
+ // Scaffolding / multi-file work / pentest tasks need room.
1377
+ // Continuation prompts ("do it") inherit this too.
1289
1378
  stepBudget = Math.max(stepBudget, maxSteps);
1290
1379
  }
1291
1380
  else if (hasHistory) {
@@ -1295,7 +1384,7 @@ export async function runAgentLoop(prompt, options = {}) {
1295
1384
  }
1296
1385
  // Hard ceiling on total loop iterations (productive + recovery) so a model
1297
1386
  // stuck emitting only thinking or malformed calls can't loop indefinitely.
1298
- const maxIterations = stepBudget * 3;
1387
+ let maxIterations = stepBudget * 3;
1299
1388
  let productiveSteps = 0;
1300
1389
  let step = -1;
1301
1390
  let nextToolEventId = 0;
@@ -1393,7 +1482,19 @@ export async function runAgentLoop(prompt, options = {}) {
1393
1482
  if (needsPentestAuth) {
1394
1483
  pentestJustConfirmed = true;
1395
1484
  }
1396
- const forceManualConfirm = call.name === "fs.delete";
1485
+ let forceManualConfirm = call.name === "fs.delete";
1486
+ if (call.name.startsWith("fs.") &&
1487
+ !isPreApprovalAllowedTool(call.name)) {
1488
+ const pathArg = typeof call.args.path === "string" ? call.args.path : undefined;
1489
+ if (pathArg) {
1490
+ const expandHomeLocal = (p) => p.startsWith("~/") || p.startsWith("~\\") ? join(homedir(), p.slice(2)) : p === "~" ? homedir() : p;
1491
+ const resolved = resolve(expandHomeLocal(pathArg));
1492
+ const mode = (call.name === "fs.read" || call.name === "fs.list" || call.name === "fs.search") ? "read" : "write";
1493
+ if (!pathInsideSandbox(resolved, mode)) {
1494
+ forceManualConfirm = true;
1495
+ }
1496
+ }
1497
+ }
1397
1498
  if (decision.level === "confirm" && !pentestJustConfirmed) {
1398
1499
  const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
1399
1500
  restoreInteractiveStdin();
@@ -1475,6 +1576,7 @@ export async function runAgentLoop(prompt, options = {}) {
1475
1576
  return;
1476
1577
  printLive(chunk);
1477
1578
  },
1579
+ confirmed: true,
1478
1580
  });
1479
1581
  if (liveBytes > 0 || liveTruncatedNotified) {
1480
1582
  writeToolOutput(toolEventId, "\n", "\n");
@@ -1555,8 +1657,59 @@ export async function runAgentLoop(prompt, options = {}) {
1555
1657
  // `step` is the productive-step index (used for display + audit). It only
1556
1658
  // advances when the previous iteration actually executed a tool.
1557
1659
  step = productiveSteps;
1558
- if (productiveSteps >= stepBudget)
1559
- break;
1660
+ // ── Step budget gate: ask the user instead of hard-stopping ────────
1661
+ if (productiveSteps >= stepBudget) {
1662
+ const askContinue = confirmPort.confirmContinue ?? inquirerConfirmPort.confirmContinue;
1663
+ let shouldContinue = false;
1664
+ try {
1665
+ shouldContinue = await askContinue(productiveSteps);
1666
+ restoreInteractiveStdin();
1667
+ }
1668
+ catch {
1669
+ // Abort / non-interactive — treat as decline.
1670
+ shouldContinue = false;
1671
+ }
1672
+ if (shouldContinue) {
1673
+ // Extend the budget for another chunk of work.
1674
+ const extension = Math.max(40, maxSteps);
1675
+ stepBudget += extension;
1676
+ maxIterations = stepBudget * 3;
1677
+ // Compact older messages to free context space.
1678
+ const compacted = compactMessages(messages, { budgetTokens: 0, keepRecent: 12 });
1679
+ if (compacted.length < messages.length) {
1680
+ messages.splice(0, messages.length, ...compacted);
1681
+ loopGuard.resetReadOnly();
1682
+ await auditLog("agent.compact", {
1683
+ newLength: messages.length,
1684
+ estimatedTokens: estimateMessagesTokens(messages),
1685
+ reason: "step-budget-continue",
1686
+ });
1687
+ }
1688
+ // Inject a progress summary so the model stays focused.
1689
+ const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
1690
+ let progressNote = "The step limit was reached and the user chose to continue. ";
1691
+ progressNote += "Review what you have accomplished so far and continue with the NEXT unfinished step. ";
1692
+ progressNote += "Do NOT repeat work already done. Do NOT re-fetch pages or re-run scans whose results you already have.";
1693
+ if (livePlan) {
1694
+ const doneTasks = livePlan.tasks.filter(t => t.state === "done");
1695
+ const pendingTasks = livePlan.tasks.filter(t => t.state === "pending" || t.state === "in_progress");
1696
+ progressNote += `\n\nPlan progress: ${doneTasks.length}/${livePlan.tasks.length} tasks done.`;
1697
+ if (pendingTasks.length > 0) {
1698
+ progressNote += ` Next: ${pendingTasks[0].id} — "${pendingTasks[0].title}".`;
1699
+ }
1700
+ }
1701
+ messages.push({ role: "user", content: progressNote });
1702
+ writeNotice("info", `continuing — budget extended to ${stepBudget} steps`, chalk.dim(` ℹ continuing — budget extended to ${stepBudget} steps\n`));
1703
+ // Continue the loop — model doesn't know it paused.
1704
+ }
1705
+ else {
1706
+ // User declined — build a rich summary and return.
1707
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
1708
+ writeAssistantMessage(richSummary);
1709
+ lastAnswer = richSummary;
1710
+ return finishTurn(lastAnswer, productiveSteps);
1711
+ }
1712
+ }
1560
1713
  options.signal?.throwIfAborted();
1561
1714
  // `call` and `assistantText` are shared by both paths below: a fresh
1562
1715
  // model round-trip, or draining a previously-queued tool call.
@@ -1678,24 +1831,19 @@ export async function runAgentLoop(prompt, options = {}) {
1678
1831
  // answer and the user has to re-submit the same prompt.
1679
1832
  if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
1680
1833
  emptyVisibleRetries += 1;
1681
- if (emptyVisibleRetries <= 2) {
1834
+ if (emptyVisibleRetries <= 3) {
1682
1835
  writeThinkingBlock(assistantText.thinkContent);
1683
1836
  writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
1684
1837
  messages.push({
1685
1838
  role: "assistant",
1686
1839
  content: collapseRepeatedText(completion.text),
1687
1840
  });
1841
+ // Keep nudges SHORT — cheap models lose the key instruction in long text.
1688
1842
  const buildNudge = buildLikeTurn && !activePlan
1689
- ? "You only produced internal reasoning with no visible answer or tool call. " +
1690
- "This is a BUILD/SCAFFOLD task with NO plan yet. " +
1691
- "You MUST call plan.create using the ```tool format to create a comprehensive plan BEFORE writing any files or running any commands. " +
1692
- "Do NOT use fs.write, fs.writeMany, fs.edit, shell.exec, shell.start, or pkg.install yet. " +
1693
- "Your ONLY allowed action right now is plan.create (or read/list for exploration)."
1694
- : "You only produced internal reasoning with no visible answer or tool call. " +
1695
- "You MUST either call a tool using the ```tool format or provide your final answer. " +
1696
- "Do NOT wrap your tool call inside considering or reasoning tags — put it in the VISIBLE response, not hidden. " +
1697
- "If images are attached, inspect them directly for visual details (text, colors, layout, spacing, style) instead of using OCR unless explicitly needed. " +
1698
- "Do NOT just think — take action NOW.";
1843
+ ? "No visible output. Emit a ```tool block to call plan.create now. " +
1844
+ "Do NOT hide tool calls in <think> tags — put them in the visible response."
1845
+ : "No visible output. Emit a ```tool block or give your final answer. " +
1846
+ "Do NOT hide tool calls in <think> tags put them in the visible response.";
1699
1847
  messages.push(recoveryUserMessage(buildNudge));
1700
1848
  continue;
1701
1849
  }
@@ -2003,10 +2151,13 @@ export async function runAgentLoop(prompt, options = {}) {
2003
2151
  return finishTurn(lastAnswer, productiveSteps);
2004
2152
  }
2005
2153
  // Compact older messages when the running estimate exceeds budget
2006
- if (estimateMessagesTokens(messages) > 24_000) {
2007
- const compacted = compactMessages(messages);
2154
+ if (estimateMessagesTokens(messages) > 32_000) {
2155
+ const compacted = compactMessages(messages, { keepRecent: 12 });
2008
2156
  if (compacted.length < messages.length) {
2009
2157
  messages.splice(0, messages.length, ...compacted);
2158
+ // Reset read-only tool counters so the model can re-fetch data
2159
+ // whose results were compacted away.
2160
+ loopGuard.resetReadOnly();
2010
2161
  await auditLog("agent.compact", {
2011
2162
  newLength: messages.length,
2012
2163
  estimatedTokens: estimateMessagesTokens(messages),
@@ -2015,8 +2166,11 @@ export async function runAgentLoop(prompt, options = {}) {
2015
2166
  }
2016
2167
  }
2017
2168
  }
2018
- lastAnswer = `Stopped after ${productiveSteps} steps.`;
2019
- writeNotice("warn", lastAnswer, " " + chalk.yellow(lastAnswer) + "\n");
2169
+ // maxIterations ceiling reached (safety net — normally the step budget
2170
+ // gate with user confirmation handles stopping gracefully).
2171
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2172
+ writeAssistantMessage(richSummary);
2173
+ lastAnswer = richSummary;
2020
2174
  return finishTurn(lastAnswer, productiveSteps);
2021
2175
  }
2022
2176
  catch (error) {