@visorcraft/idlehands 1.1.11 → 1.1.14

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.
package/dist/agent.js CHANGED
@@ -1068,6 +1068,10 @@ export async function createSession(opts) {
1068
1068
  noConfirm: cfg.no_confirm || cfg.approval_mode === 'yolo',
1069
1069
  dryRun: cfg.dry_run,
1070
1070
  mode: cfg.mode ?? 'code',
1071
+ allowedWriteRoots: cfg.allowed_write_roots,
1072
+ requireDirPinForMutations: cfg.require_dir_pin_for_mutations,
1073
+ dirPinned: cfg.dir_pinned,
1074
+ repoCandidates: cfg.repo_candidates,
1071
1075
  confirm: overrides?.confirmBridge ?? defaultConfirmBridge,
1072
1076
  replay,
1073
1077
  vault,
@@ -1213,6 +1217,21 @@ export async function createSession(opts) {
1213
1217
  if (!opts?.dry) {
1214
1218
  if (dropped.length && vault) {
1215
1219
  try {
1220
+ // Store the original/current user prompt before compaction so it survives context loss.
1221
+ let userPromptToPreserve = null;
1222
+ for (let i = messages.length - 1; i >= 0; i--) {
1223
+ const m = messages[i];
1224
+ if (m.role === 'user') {
1225
+ const text = userContentToText((m.content ?? '')).trim();
1226
+ if (text && !text.startsWith('[Trifecta Vault') && !text.startsWith('[Vault context') && text.length > 20) {
1227
+ userPromptToPreserve = text;
1228
+ break;
1229
+ }
1230
+ }
1231
+ }
1232
+ if (userPromptToPreserve) {
1233
+ await vault.upsertNote('current_task', userPromptToPreserve.slice(0, 2000), 'system');
1234
+ }
1216
1235
  await vault.archiveToolMessages(dropped, new Map());
1217
1236
  await vault.note('compaction_summary', `Dropped ${dropped.length} messages (${freedTokens} tokens).`);
1218
1237
  }
@@ -1855,6 +1874,23 @@ export async function createSession(opts) {
1855
1874
  const dropped = beforeMsgs.filter((m) => !compactedByRefs.has(m));
1856
1875
  if (dropped.length && vault) {
1857
1876
  try {
1877
+ // Store the original/current user prompt before compaction so it survives context loss.
1878
+ // Find the last substantive user message that looks like a task/instruction.
1879
+ let userPromptToPreserve = null;
1880
+ for (let i = beforeMsgs.length - 1; i >= 0; i--) {
1881
+ const m = beforeMsgs[i];
1882
+ if (m.role === 'user') {
1883
+ const text = userContentToText((m.content ?? '')).trim();
1884
+ // Skip vault injection messages and short prompts
1885
+ if (text && !text.startsWith('[Trifecta Vault') && !text.startsWith('[Vault context') && text.length > 20) {
1886
+ userPromptToPreserve = text;
1887
+ break;
1888
+ }
1889
+ }
1890
+ }
1891
+ if (userPromptToPreserve) {
1892
+ await vault.upsertNote('current_task', userPromptToPreserve.slice(0, 2000), 'system');
1893
+ }
1858
1894
  const toArchive = lens
1859
1895
  ? await Promise.all(dropped.map((m) => archiveToolOutputForVault(m)))
1860
1896
  : dropped;
@@ -2163,7 +2199,13 @@ export async function createSession(opts) {
2163
2199
  mutationVersionBySig.set(sig, mutationVersion);
2164
2200
  if (!hasMutatedSince) {
2165
2201
  const count = sigCounts.get(sig) ?? 0;
2166
- const loopThreshold = harness.quirks.loopsOnToolError ? 3 : 6;
2202
+ let loopThreshold = harness.quirks.loopsOnToolError ? 3 : 6;
2203
+ // If the cached observation already tells the model "no matches found",
2204
+ // break much earlier — the model is ignoring the hint.
2205
+ const cachedObs = execObservationCacheBySig.get(sig) ?? '';
2206
+ if (cachedObs.includes('Do NOT retry')) {
2207
+ loopThreshold = Math.min(loopThreshold, 3);
2208
+ }
2167
2209
  // At 3x, inject vault context so the model gets the data it needs
2168
2210
  if (count >= 3 && count < loopThreshold) {
2169
2211
  await injectVaultContext().catch(() => { });
@@ -2190,7 +2232,7 @@ export async function createSession(opts) {
2190
2232
  }
2191
2233
  // Read-only tools: only count consecutive identical calls (back-to-back turns
2192
2234
  // with no other tool calls in between). A read → edit → read cycle is normal
2193
- // and resets the counter. After 4 consecutive identical reads, inject a hint.
2235
+ // and resets the counter.
2194
2236
  if (isReadOnlyTool(toolName)) {
2195
2237
  // Check if this sig was also in the previous turn's set
2196
2238
  if (lastTurnSigs.has(sig)) {
@@ -2203,8 +2245,8 @@ export async function createSession(opts) {
2203
2245
  if (consec >= 3) {
2204
2246
  await injectVaultContext().catch(() => { });
2205
2247
  }
2206
- // Hard-break: after 6 consecutive identical reads, stop the session
2207
- if (consec >= 6) {
2248
+ // Hard-break: after 4 consecutive identical reads, stop the session
2249
+ if (consec >= 4) {
2208
2250
  throw new Error(`tool ${toolName}: identical read repeated ${consec}x consecutively; breaking loop. ` +
2209
2251
  `The resource content has not changed between reads.`);
2210
2252
  }
@@ -2442,6 +2484,14 @@ export async function createSession(opts) {
2442
2484
  content = await mcpManager.callTool(name, callArgs);
2443
2485
  }
2444
2486
  }
2487
+ // Append a hint when a read-only tool is called consecutively with
2488
+ // identical arguments — the model may not realize the content hasn't changed.
2489
+ if (isReadOnlyToolDynamic(name)) {
2490
+ const consec = consecutiveCounts.get(sig) ?? 0;
2491
+ if (consec >= 2) {
2492
+ 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.]`;
2493
+ }
2494
+ }
2445
2495
  // Hook: onToolResult (Phase 8.5 + Phase 7 rich display)
2446
2496
  let toolSuccess = true;
2447
2497
  let summary = reusedCachedReadOnlyExec