@visorcraft/idlehands 1.1.0 → 1.1.2

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);
@@ -1452,6 +1487,7 @@ export async function createSession(opts) {
1452
1487
  messages = compacted;
1453
1488
  if (dropped.length) {
1454
1489
  messages.push({ role: 'system', content: `[compacted: ${dropped.length} messages archived to Vault - vault_search to recall]` });
1490
+ await injectVaultContext().catch(() => { });
1455
1491
  }
1456
1492
  }
1457
1493
  return {
@@ -1810,7 +1846,6 @@ export async function createSession(opts) {
1810
1846
  // that happen back-to-back with no other tool calls in between.
1811
1847
  let lastTurnSigs = new Set();
1812
1848
  const consecutiveCounts = new Map();
1813
- let lastPassiveVaultQuery = '';
1814
1849
  let malformedCount = 0;
1815
1850
  let noProgressTurns = 0;
1816
1851
  const NO_PROGRESS_TURN_CAP = 3;
@@ -1823,34 +1858,6 @@ export async function createSession(opts) {
1823
1858
  let lastSuccessfulTestRun = null;
1824
1859
  // One-time nudge to prevent post-success churn after green test runs.
1825
1860
  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
1861
  const archiveToolOutputForVault = async (msg) => {
1855
1862
  if (!lens || !vault || msg.role !== 'tool' || typeof msg.content !== 'string')
1856
1863
  return msg;
@@ -1952,8 +1959,9 @@ export async function createSession(opts) {
1952
1959
  }
1953
1960
  }
1954
1961
  messages = compacted;
1955
- if (vaultMode === 'passive' && compactedDropped) {
1956
- await maybeInjectVaultContext().catch(() => { });
1962
+ if (dropped.length) {
1963
+ 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.]` });
1964
+ await injectVaultContext().catch(() => { });
1957
1965
  }
1958
1966
  const ac = makeAbortController();
1959
1967
  inFlight = ac;
@@ -2240,9 +2248,13 @@ export async function createSession(opts) {
2240
2248
  // Update to "now" for next turn.
2241
2249
  mutationVersionBySig.set(sig, mutationVersion);
2242
2250
  if (!hasMutatedSince) {
2243
- // Allow a few more repeats for exec since "run tests" loops are common.
2251
+ const count = sigCounts.get(sig) ?? 0;
2244
2252
  const loopThreshold = harness.quirks.loopsOnToolError ? 3 : 6;
2245
- if ((sigCounts.get(sig) ?? 0) >= loopThreshold) {
2253
+ // At 3x, inject vault context so the model gets the data it needs
2254
+ if (count >= 3 && count < loopThreshold) {
2255
+ await injectVaultContext().catch(() => { });
2256
+ }
2257
+ if (count >= loopThreshold) {
2246
2258
  const args = sig.slice(toolName.length + 1);
2247
2259
  const argsPreview = args.length > 220 ? args.slice(0, 220) + '…' : args;
2248
2260
  throw new Error(`tool ${toolName}: identical call repeated ${loopThreshold}x across turns; breaking loop. ` +
@@ -2264,12 +2276,7 @@ export async function createSession(opts) {
2264
2276
  }
2265
2277
  const consec = consecutiveCounts.get(sig) ?? 1;
2266
2278
  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
- });
2279
+ await injectVaultContext().catch(() => { });
2273
2280
  }
2274
2281
  // Hard-break: after 6 consecutive identical reads, stop the session
2275
2282
  if (consec >= 6) {