@visorcraft/idlehands 2.2.7 → 2.2.9

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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Information density scoring for smart compaction.
3
+ *
4
+ * Scores messages by how valuable they are to keep in context,
5
+ * so compaction can prioritize dropping low-value messages first.
6
+ *
7
+ * Higher score = more valuable = keep longer.
8
+ */
9
+ /**
10
+ * Score a conversation message for compaction priority.
11
+ * Returns 0-100 where higher = more important to keep.
12
+ */
13
+ export function scoreMessage(msg, index, totalMessages, opts) {
14
+ let score = 50; // baseline
15
+ const reasons = [];
16
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content ?? '');
17
+ const contentLen = content.length;
18
+ // ── Role-based scoring ──
19
+ if (msg.role === 'system') {
20
+ return { index, score: 100, reason: 'system prompt (never drop)' };
21
+ }
22
+ if (msg.role === 'user') {
23
+ score += 15; // User messages are important context
24
+ reasons.push('user');
25
+ // Last user message is critical
26
+ if (index >= totalMessages - 3) {
27
+ score += 30;
28
+ reasons.push('recent');
29
+ }
30
+ }
31
+ if (msg.role === 'assistant') {
32
+ // Assistant messages with code are high-value
33
+ if (content.includes('```')) {
34
+ score += 20;
35
+ reasons.push('has_code');
36
+ }
37
+ // Assistant messages with substantive text (not just tool calls)
38
+ if (contentLen > 200 && !msg.tool_calls?.length) {
39
+ score += 10;
40
+ reasons.push('substantive');
41
+ }
42
+ // Planning/thinking text is lower value once executed
43
+ if (msg.tool_calls?.length && contentLen < 100) {
44
+ score -= 15;
45
+ reasons.push('thin_planning');
46
+ }
47
+ }
48
+ if (msg.role === 'tool') {
49
+ // Tool results for read operations are generally low-value (can be re-read)
50
+ if (content.includes('[read_file]') || content.includes('[list_dir]')) {
51
+ score -= 20;
52
+ reasons.push('read_result');
53
+ }
54
+ // Error messages are important (prevent re-attempting)
55
+ if (content.includes('ERROR') || content.includes('error:') || content.includes('failed')) {
56
+ score += 15;
57
+ reasons.push('has_error');
58
+ }
59
+ // Very long tool results are candidates for dropping (bulky)
60
+ if (contentLen > 3000) {
61
+ score -= 10;
62
+ reasons.push('bulky');
63
+ }
64
+ }
65
+ // ── Recency bonus ──
66
+ // Messages near the end of the conversation are more valuable
67
+ const recencyRatio = index / totalMessages;
68
+ if (recencyRatio > 0.8) {
69
+ score += 25;
70
+ reasons.push('very_recent');
71
+ }
72
+ else if (recencyRatio > 0.5) {
73
+ score += 10;
74
+ reasons.push('recent_half');
75
+ }
76
+ // ── Active file relevance ──
77
+ if (opts?.activeFiles?.size) {
78
+ for (const file of opts.activeFiles) {
79
+ const basename = file.split('/').pop() ?? '';
80
+ if (content.includes(basename)) {
81
+ score += 15;
82
+ reasons.push('active_file');
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ // ── Instruction relevance ──
88
+ if (opts?.lastInstruction) {
89
+ const keywords = opts.lastInstruction.toLowerCase().split(/\s+/).filter(w => w.length > 3);
90
+ const contentLower = content.toLowerCase();
91
+ const hits = keywords.filter(k => contentLower.includes(k)).length;
92
+ if (hits > 2) {
93
+ score += 10;
94
+ reasons.push('relevant');
95
+ }
96
+ }
97
+ return {
98
+ index,
99
+ score: Math.max(0, Math.min(100, score)),
100
+ reason: reasons.join(', ') || 'baseline',
101
+ };
102
+ }
103
+ /**
104
+ * Given scored messages, return indices to drop (sorted lowest score first)
105
+ * until the token budget target is met.
106
+ */
107
+ export function selectDropCandidates(scored, opts) {
108
+ const candidates = scored
109
+ .filter(s => s.index >= opts.minIndex && s.index <= opts.maxIndex && s.score < 100)
110
+ .sort((a, b) => a.score - b.score); // lowest score first
111
+ return candidates.slice(0, opts.targetDrop).map(c => c.index);
112
+ }
113
+ //# sourceMappingURL=compaction-scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction-scoring.js","sourceRoot":"","sources":["../../src/agent/compaction-scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAwD,EACxD,KAAa,EACb,aAAqB,EACrB,IAKC;IAED,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC,WAAW;IAC3B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAClG,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAElC,2BAA2B;IAE3B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IACrE,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACxB,KAAK,IAAI,EAAE,CAAC,CAAC,sCAAsC;QACnD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAErB,gCAAgC;QAChC,IAAI,KAAK,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YAC/B,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC7B,8CAA8C;QAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3B,CAAC;QAED,iEAAiE;QACjE,IAAI,UAAU,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;YAChD,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC9B,CAAC;QAED,sDAAsD;QACtD,IAAI,GAAG,CAAC,UAAU,EAAE,MAAM,IAAI,UAAU,GAAG,GAAG,EAAE,CAAC;YAC/C,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACxB,4EAA4E;QAC5E,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACtE,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC9B,CAAC;QAED,uDAAuD;QACvD,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1F,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5B,CAAC;QAED,6DAA6D;QAC7D,IAAI,UAAU,GAAG,IAAI,EAAE,CAAC;YACtB,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,8DAA8D;IAC9D,MAAM,YAAY,GAAG,KAAK,GAAG,aAAa,CAAC;IAC3C,IAAI,YAAY,GAAG,GAAG,EAAE,CAAC;QACvB,KAAK,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9B,CAAC;SAAM,IAAI,YAAY,GAAG,GAAG,EAAE,CAAC;QAC9B,KAAK,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9B,CAAC;IAED,8BAA8B;IAC9B,IAAI,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAC7C,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,KAAK,IAAI,EAAE,CAAC;gBACZ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC5B,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,IAAI,IAAI,EAAE,eAAe,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3F,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACnE,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACb,KAAK,IAAI,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK;QACL,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,UAAU;KACzC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAuB,EACvB,IAOC;IAED,MAAM,UAAU,GAAG,MAAM;SACtB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC;SAClF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB;IAE3D,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
package/dist/agent.js CHANGED
@@ -41,6 +41,7 @@ import { normalizeApprovalMode } from './shared/config-utils.js';
41
41
  import { collectSnapshot } from './sys/context.js';
42
42
  import { ToolError, ValidationError } from './tools/tool-error.js';
43
43
  import * as tools from './tools.js';
44
+ import { EditTransaction } from './tools/transaction.js';
44
45
  import { stateDir, timestampedId } from './utils.js';
45
46
  import { VaultStore } from './vault.js';
46
47
  export { parseToolCallsFromContent };
@@ -489,6 +490,7 @@ export async function createSession(opts) {
489
490
  let inFlight = null;
490
491
  let initialConnectionProbeDone = false;
491
492
  let lastEditedPath;
493
+ let lastTurnTransaction;
492
494
  // Plan mode state (Phase 8)
493
495
  let planSteps = [];
494
496
  // Sub-agent queue state (Phase 18): enforce sequential execution on single-GPU setups.
@@ -3002,6 +3004,8 @@ export async function createSession(opts) {
3002
3004
  const absPath = args.path.startsWith('/')
3003
3005
  ? args.path
3004
3006
  : path.resolve(projectDir, args.path);
3007
+ // Track in turn transaction for potential atomic rollback.
3008
+ turnTransaction.track(absPath);
3005
3009
  // ── Pre-dispatch: block edits to files in a mutation spiral ──
3006
3010
  if (fileMutationBlocked.has(absPath)) {
3007
3011
  const basename = path.basename(absPath);
@@ -3493,6 +3497,7 @@ export async function createSession(opts) {
3493
3497
  return { id: callId, content: truncated.content };
3494
3498
  };
3495
3499
  const results = [];
3500
+ const turnTransaction = new EditTransaction();
3496
3501
  let invalidArgsThisTurn = false;
3497
3502
  // Helper: catch tool errors but re-throw AgentLoopBreak (those must break the outer loop)
3498
3503
  const catchToolError = async (e, tc) => {
@@ -3647,6 +3652,11 @@ export async function createSession(opts) {
3647
3652
  });
3648
3653
  }
3649
3654
  }
3655
+ // Store the turn transaction for potential post-turn rollback.
3656
+ if (turnTransaction.hasChanges) {
3657
+ turnTransaction.commit();
3658
+ lastTurnTransaction = turnTransaction;
3659
+ }
3650
3660
  // Bail immediately if cancelled during tool execution
3651
3661
  if (ac.signal.aborted)
3652
3662
  break;
@@ -3918,6 +3928,16 @@ export async function createSession(opts) {
3918
3928
  return currentContextTokens > 0 ? currentContextTokens : estimateTokensFromMessages(messages);
3919
3929
  },
3920
3930
  ask,
3931
+ rollbackLastTurnEdits: async () => {
3932
+ if (!lastTurnTransaction || !lastTurnTransaction.hasChanges) {
3933
+ return { ok: false, error: 'No file edits to roll back.' };
3934
+ }
3935
+ const tx = lastTurnTransaction;
3936
+ lastTurnTransaction = undefined;
3937
+ const callCtx = { cwd: projectDir, noConfirm: true, dryRun: false };
3938
+ const results = await tx.rollback(callCtx);
3939
+ return { ok: true, results };
3940
+ },
3921
3941
  rollback: () => {
3922
3942
  const cp = conversationBranch.rollback();
3923
3943
  if (!cp)