@visorcraft/idlehands 1.1.0 → 1.1.3

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
@@ -1408,6 +1408,41 @@ export async function createSession(opts) {
1408
1408
  const clearPlan = () => {
1409
1409
  planSteps = [];
1410
1410
  };
1411
+ // Session-level vault context injection: search vault for entries relevant to
1412
+ // the last user message and inject them into the conversation. Used after any
1413
+ // compaction to restore context the model lost when messages were dropped.
1414
+ let lastVaultInjectionQuery = '';
1415
+ const injectVaultContext = async () => {
1416
+ if (!vault)
1417
+ return;
1418
+ let lastUser = null;
1419
+ for (let j = messages.length - 1; j >= 0; j--) {
1420
+ if (messages[j].role === 'user') {
1421
+ lastUser = messages[j];
1422
+ break;
1423
+ }
1424
+ }
1425
+ const userText = userContentToText((lastUser?.content ?? '')).trim();
1426
+ if (!userText)
1427
+ return;
1428
+ const query = userText.slice(0, 200);
1429
+ if (query === lastVaultInjectionQuery)
1430
+ return;
1431
+ const hits = await vault.search(query, 4);
1432
+ if (!hits.length)
1433
+ return;
1434
+ const lines = hits.map((r) => `${r.updatedAt} ${r.kind} ${r.key ?? r.tool ?? r.id} ${String(r.value ?? r.snippet ?? '').replace(/\s+/g, ' ').slice(0, 180)}`);
1435
+ if (!lines.length)
1436
+ return;
1437
+ lastVaultInjectionQuery = query;
1438
+ const vaultContextHeader = vaultMode === 'passive'
1439
+ ? '[Trifecta Vault (passive)]'
1440
+ : '[Vault context after compaction]';
1441
+ messages.push({
1442
+ role: 'user',
1443
+ content: `${vaultContextHeader} Relevant entries for "${query}":\n${lines.join('\n')}`
1444
+ });
1445
+ };
1411
1446
  const compactHistory = async (opts) => {
1412
1447
  const beforeMessages = messages.length;
1413
1448
  const beforeTokens = estimateTokensFromMessages(messages);
@@ -1422,9 +1457,10 @@ export async function createSession(opts) {
1422
1457
  messages,
1423
1458
  contextWindow,
1424
1459
  maxTokens,
1425
- minTailMessages: 12,
1426
- compactAt: cfg.compact_at ?? 0.8,
1460
+ minTailMessages: opts?.force ? 2 : 12,
1461
+ compactAt: opts?.force ? 0.5 : (cfg.compact_at ?? 0.8),
1427
1462
  toolSchemaTokens: estimateToolSchemaTokens(getToolsSchema()),
1463
+ force: opts?.force,
1428
1464
  });
1429
1465
  }
1430
1466
  const compactedByRefs = new Set(compacted);
@@ -1452,6 +1488,7 @@ export async function createSession(opts) {
1452
1488
  messages = compacted;
1453
1489
  if (dropped.length) {
1454
1490
  messages.push({ role: 'system', content: `[compacted: ${dropped.length} messages archived to Vault - vault_search to recall]` });
1491
+ await injectVaultContext().catch(() => { });
1455
1492
  }
1456
1493
  }
1457
1494
  return {
@@ -1810,7 +1847,6 @@ export async function createSession(opts) {
1810
1847
  // that happen back-to-back with no other tool calls in between.
1811
1848
  let lastTurnSigs = new Set();
1812
1849
  const consecutiveCounts = new Map();
1813
- let lastPassiveVaultQuery = '';
1814
1850
  let malformedCount = 0;
1815
1851
  let noProgressTurns = 0;
1816
1852
  const NO_PROGRESS_TURN_CAP = 3;
@@ -1823,34 +1859,6 @@ export async function createSession(opts) {
1823
1859
  let lastSuccessfulTestRun = null;
1824
1860
  // One-time nudge to prevent post-success churn after green test runs.
1825
1861
  let finalizeAfterTestsNudgeUsed = false;
1826
- const maybeInjectVaultContext = async () => {
1827
- if (!vault || vaultMode !== 'passive')
1828
- return;
1829
- let lastUser = null;
1830
- for (let j = messages.length - 1; j >= 0; j--) {
1831
- if (messages[j].role === 'user') {
1832
- lastUser = messages[j];
1833
- break;
1834
- }
1835
- }
1836
- const userText = userContentToText((lastUser?.content ?? '')).trim();
1837
- if (!userText)
1838
- return;
1839
- const query = userText.slice(0, 200);
1840
- if (query === lastPassiveVaultQuery)
1841
- return;
1842
- const hits = await vault.search(query, 4);
1843
- if (!hits.length)
1844
- return;
1845
- const lines = hits.map((r) => `${r.updatedAt} ${r.kind} ${r.key ?? r.tool ?? r.id} ${String(r.value ?? r.snippet ?? '').replace(/\s+/g, ' ').slice(0, 180)}`);
1846
- if (!lines.length)
1847
- return;
1848
- lastPassiveVaultQuery = query;
1849
- messages.push({
1850
- role: 'user',
1851
- content: `[Trifecta Vault (passive)] Relevant entries for "${query}":\n${lines.join('\n')}`
1852
- });
1853
- };
1854
1862
  const archiveToolOutputForVault = async (msg) => {
1855
1863
  if (!lens || !vault || msg.role !== 'tool' || typeof msg.content !== 'string')
1856
1864
  return msg;
@@ -1952,8 +1960,9 @@ export async function createSession(opts) {
1952
1960
  }
1953
1961
  }
1954
1962
  messages = compacted;
1955
- if (vaultMode === 'passive' && compactedDropped) {
1956
- await maybeInjectVaultContext().catch(() => { });
1963
+ if (dropped.length) {
1964
+ messages.push({ role: 'system', content: `[auto-compacted: ${dropped.length} old messages dropped to stay within context budget. Do NOT re-read files or re-run commands you have already seen — use vault_search to recall prior results if needed.]` });
1965
+ await injectVaultContext().catch(() => { });
1957
1966
  }
1958
1967
  const ac = makeAbortController();
1959
1968
  inFlight = ac;
@@ -1961,10 +1970,11 @@ export async function createSession(opts) {
1961
1970
  const callerSignal = hookObj.signal;
1962
1971
  const onCallerAbort = () => ac.abort();
1963
1972
  callerSignal?.addEventListener('abort', onCallerAbort, { once: true });
1964
- // Per-request timeout: the lesser of 120s or the remaining session wall time.
1973
+ // Per-request timeout: the lesser of response_timeout (default 300s) or the remaining session wall time.
1965
1974
  // This prevents a single slow request from consuming the entire session budget.
1975
+ const perReqCap = cfg.response_timeout && cfg.response_timeout > 0 ? cfg.response_timeout : 300;
1966
1976
  const wallRemaining = Math.max(0, cfg.timeout - (Date.now() - wallStart) / 1000);
1967
- const reqTimeout = Math.min(120, Math.max(10, wallRemaining));
1977
+ const reqTimeout = Math.min(perReqCap, Math.max(10, wallRemaining));
1968
1978
  const timer = setTimeout(() => ac.abort(), reqTimeout * 1000);
1969
1979
  reqCounter++;
1970
1980
  const turnStartMs = Date.now();
@@ -2240,9 +2250,13 @@ export async function createSession(opts) {
2240
2250
  // Update to "now" for next turn.
2241
2251
  mutationVersionBySig.set(sig, mutationVersion);
2242
2252
  if (!hasMutatedSince) {
2243
- // Allow a few more repeats for exec since "run tests" loops are common.
2253
+ const count = sigCounts.get(sig) ?? 0;
2244
2254
  const loopThreshold = harness.quirks.loopsOnToolError ? 3 : 6;
2245
- if ((sigCounts.get(sig) ?? 0) >= loopThreshold) {
2255
+ // At 3x, inject vault context so the model gets the data it needs
2256
+ if (count >= 3 && count < loopThreshold) {
2257
+ await injectVaultContext().catch(() => { });
2258
+ }
2259
+ if (count >= loopThreshold) {
2246
2260
  const args = sig.slice(toolName.length + 1);
2247
2261
  const argsPreview = args.length > 220 ? args.slice(0, 220) + '…' : args;
2248
2262
  throw new Error(`tool ${toolName}: identical call repeated ${loopThreshold}x across turns; breaking loop. ` +
@@ -2264,12 +2278,7 @@ export async function createSession(opts) {
2264
2278
  }
2265
2279
  const consec = consecutiveCounts.get(sig) ?? 1;
2266
2280
  if (consec >= 3) {
2267
- const args = sig.slice(toolName.length + 1);
2268
- const argsPreview = args.length > 220 ? args.slice(0, 220) + '…' : args;
2269
- messages.push({
2270
- role: 'user',
2271
- content: `[System] STOP READING: You have read the same resource ${consec} consecutive times (${toolName} ${argsPreview}). The content has NOT changed. You already have this data. Proceed immediately with your next action (write_file, edit_file, exec, etc.) — do NOT read this resource again.`,
2272
- });
2281
+ await injectVaultContext().catch(() => { });
2273
2282
  }
2274
2283
  // Hard-break: after 6 consecutive identical reads, stop the session
2275
2284
  if (consec >= 6) {