vibeostheog 0.22.12 → 0.22.15

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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.22.15
2
+
3
+ - fix: prefer session cache over global cache in getScratchpadHit()
4
+ swap lookup order on direct-hash and pointer-resolved paths
5
+ so session-scoped outputs always take priority
6
+ - fix: remove user-wide cache fallback from getScratchpadHit()
7
+ no more SCRATCHPAD_GLOBAL_DIR — cache scope is session only
8
+ (callers may pass project-scoped baseDir for project-level cache)
9
+ - fix: setApiToken() now resets _apiFallbackMode / _apiClient / runtime connection state
10
+ so a token update breaks out of permanent API-fallback deadlock
11
+ - fix: syncApiTokenFromDisk() else branch also clears fallback state
12
+ - test: add regression tests for fallback-mode reset behavior
13
+
14
+ ## 0.22.14
15
+
16
+
17
+ ## 0.22.13
18
+
19
+
1
20
  ## 0.22.12
2
21
  - fix: harden scratchpad cache
3
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeostheog",
3
- "version": "0.22.12",
3
+ "version": "0.22.15",
4
4
  "description": "Cost-aware delegation enforcer for OpenCode. Tracks model usage, routes Task subagents to cheaper tiers, surfaces cumulative savings in chat. Includes research audit, reporting framework, project memory, progressive scratchpad decadence, and trinity CLI for brain/medium/cheap slot switching.",
5
5
  "scripts": {
6
6
  "release": "node scripts/release.mjs",
@@ -535,6 +535,10 @@ export function setApiToken(newToken) {
535
535
  persistPrimaryApiEnvState({ token: VIBEOS_API_TOKEN, disabled: false });
536
536
  if (_anomalyDetector)
537
537
  _anomalyDetector.reset();
538
+ _apiClient = null;
539
+ _apiFallbackMode = false;
540
+ _apiFallbackSince = null;
541
+ resetApiConnection();
538
542
  console.error("[vibeOS] API token updated via setApiToken");
539
543
  }
540
544
  catch (e) {
@@ -671,6 +675,8 @@ function syncApiTokenFromDisk() {
671
675
  VIBEOS_API_DISABLED = false;
672
676
  VIBEOS_API_TOKEN ||= EMBEDDED_API_TOKEN;
673
677
  VIBEOS_API_ENABLED = process.env.VIBEOS_API_ENABLED !== "false" && (!!VIBEOS_API_TOKEN || !!VIBEOS_API_BOOTSTRAP_TOKEN);
678
+ _apiFallbackMode = false;
679
+ _apiFallbackSince = null;
674
680
  }
675
681
  }
676
682
  export function getApiClient() {
@@ -201,7 +201,12 @@ async function _appendFooter(input, output, directory) {
201
201
  liveModel = readConfig(directory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
202
202
  }
203
203
  const displayModel = resolveDisplayModelId(liveModel || brainModel || currentModel || "", directory) || liveModel || brainModel || currentModel;
204
- const execution = resolveExecutionIdentity(input?.args?.model || liveModel || brainModel || currentModel || displayModel || "", directory);
204
+ const resolvedModel = displayModel || liveModel || brainModel || currentModel || "";
205
+ if (resolvedModel && resolvedModel !== currentModel) {
206
+ setCurrentModel(resolvedModel);
207
+ setCurrentTier(classify(resolvedModel));
208
+ }
209
+ const execution = resolveExecutionIdentity(input?.args?.model || resolvedModel || "", directory);
205
210
  let modelTag = `[${shortModelName(displayModel)}]`;
206
211
  const _workerModel = slot === "brain" ? TRINITY_MEDIUM : null;
207
212
  const totalTurns = (sesModelTurns?.brain || 0) + (sesModelTurns?.worker || 0);
@@ -215,21 +220,21 @@ async function _appendFooter(input, output, directory) {
215
220
  saveReport({
216
221
  type: "session",
217
222
  summary: "Session cost: $" + formatUsd(ltCost) + " | cache saved: $" + formatUsd(ltCache) + " | delegation saved: $" + formatUsd(Number(sesTasks || 0)) + " | task delegations: " + Number(sesTaskDelegations || 0),
218
- metrics: {
219
- sessionId: _OC_SID,
220
- projectFingerprint: currentProjectFingerprint || "unknown",
221
- projectName: currentProjectName || "unknown",
222
- sessionCost: ltCost,
223
+ metrics: {
224
+ sessionId: _OC_SID,
225
+ projectFingerprint: currentProjectFingerprint || "unknown",
226
+ projectName: currentProjectName || "unknown",
227
+ sessionCost: ltCost,
223
228
  cacheSavings: ltCache,
224
229
  delegationSavingsUsd: sesTasks,
225
- taskDelegationCount: sesTaskDelegations,
226
- // Backward compatibility (legacy field historically misnamed)
227
- tasksDelegated: sesTaskDelegations,
228
- model: currentModel,
229
- slot: loadSelection().active_slot || "unknown",
230
- editSavings: sesEdit,
231
- creditSavings: sesCredit,
232
- context7Savings: sesC7,
230
+ taskDelegationCount: sesTaskDelegations,
231
+ // Backward compatibility (legacy field historically misnamed)
232
+ tasksDelegated: sesTaskDelegations,
233
+ model: resolvedModel || currentModel,
234
+ slot: loadSelection().active_slot || "unknown",
235
+ editSavings: sesEdit,
236
+ creditSavings: sesCredit,
237
+ context7Savings: sesC7,
233
238
  quotaSavings: sesQuota,
234
239
  },
235
240
  tags: ["auto", "cost"],
@@ -684,7 +684,12 @@ export const onToolExecuteAfter = async (input, output) => {
684
684
  liveModel = readConfig(projectDirectory) || readConfig(join(process.env.HOME || "", ".config", "opencode")) || process?.env?.OPENCODE_MODEL || "";
685
685
  }
686
686
  const displayModel = resolveDisplayModelId(liveModel || currentModel || "", projectDirectory) || liveModel || currentModel;
687
- const execution = resolveExecutionIdentity(input?.args?.model || liveModel || currentModel || displayModel || "", projectDirectory);
687
+ const resolvedModel = displayModel || liveModel || currentModel || "";
688
+ if (resolvedModel && resolvedModel !== currentModel) {
689
+ setCurrentModel(resolvedModel);
690
+ setCurrentTier(classify(resolvedModel));
691
+ }
692
+ const execution = resolveExecutionIdentity(input?.args?.model || resolvedModel || "", projectDirectory);
688
693
  _footerText = `— ${flashIcon ? `${flashIcon} ` : ""}Quality: ${formatQualityName(execution.quality)} | Provider: ${formatProviderName(execution.provider)} | Model: ${execution.model}`;
689
694
  if (ltTotal > 0) {
690
695
  _footerText += ` | $${formatUsd(ltTotal)} saved`;
@@ -705,7 +710,7 @@ export const onToolExecuteAfter = async (input, output) => {
705
710
  if (_autoReportCount % 5 === 0 && ltTotal > 0) {
706
711
  saveReport({
707
712
  type: "session", summary: `Session cost: $${formatUsd(ltCost)} | cache saved: $${formatUsd(ltCache)} | delegation saved: $${formatUsd(ltTasks)}`,
708
- metrics: { sessionId: _OC_SID, sessionCost: ltCost, cacheSavings: ltCache, delegationSavingsUsd: ltTasks, model: currentModel, slot: selNow.active_slot || "unknown" },
713
+ metrics: { sessionId: _OC_SID, sessionCost: ltCost, cacheSavings: ltCache, delegationSavingsUsd: ltTasks, model: resolvedModel || currentModel, slot: selNow.active_slot || "unknown" },
709
714
  tags: ["auto", "cost"],
710
715
  });
711
716
  }
package/src/lib/state.js CHANGED
@@ -831,6 +831,53 @@ function indexAppend(hash, tool, size, extra) {
831
831
  }
832
832
  // ── Scratchpad hit detection ─────────────────────────────────────────
833
833
  const scratchpadHitsSeen = new Set();
834
+ function scanRecentScratchpad(dir, titleCase, maxScan = 2000) {
835
+ try {
836
+ if (!existsSync(dir))
837
+ return null;
838
+ const entries = readdirSync(dir);
839
+ const ptrFiles = entries.filter(e => e.endsWith(".ptr"));
840
+ const ptrCandidates = [];
841
+ for (const pf of ptrFiles) {
842
+ if (ptrCandidates.length >= 50)
843
+ break;
844
+ try {
845
+ const st = statSync(join(dir, pf));
846
+ ptrCandidates.push({ ptrPath: join(dir, pf), mtimeMs: st.mtimeMs });
847
+ }
848
+ catch { }
849
+ }
850
+ ptrCandidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
851
+ let scanned = 0;
852
+ for (const { ptrPath } of ptrCandidates) {
853
+ if (scanned++ >= maxScan)
854
+ break;
855
+ try {
856
+ const ptrData = safeJsonParse(readFileSync(ptrPath, "utf-8"));
857
+ if (!ptrData?.contentHash)
858
+ continue;
859
+ const ptrTool = typeof ptrData.tool === "string" ? (TOOL_NAME_NORMALIZE[ptrData.tool] || ptrData.tool) : null;
860
+ if (titleCase && ptrTool && ptrTool !== titleCase)
861
+ continue;
862
+ const contentHash = String(ptrData.contentHash);
863
+ const f = join(dir, `${contentHash}.txt`);
864
+ if (!existsSync(f))
865
+ continue;
866
+ const st = statSync(f);
867
+ const ageSec = (Date.now() - st.mtimeMs) / 1000;
868
+ if (ageSec > SCRATCHPAD_MAX_AGE_SEC)
869
+ continue;
870
+ const sumPath = join(dir, `${contentHash}.summary.txt`);
871
+ return { hash: contentHash, fullPath: f, sizeBytes: st.size, ageSec: Math.round(ageSec), summaryPath: existsSync(sumPath) ? sumPath : null };
872
+ }
873
+ catch { }
874
+ }
875
+ return null;
876
+ }
877
+ catch {
878
+ return null;
879
+ }
880
+ }
834
881
  function getScratchpadHit(toolLower, args, baseDir = null) {
835
882
  if (!SCRATCHPAD_TOOLS.has(toolLower))
836
883
  return null;
@@ -838,15 +885,12 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
838
885
  const inputJson = stableJson(args ?? {});
839
886
  const hash = createHash("sha256").update(`${titleCase}\n${inputJson}\n`).digest("hex").slice(0, 16);
840
887
  const sessionDir = baseDir || getSessionScratchpadDir();
841
- const globalDir = SCRATCHPAD_GLOBAL_DIR;
842
888
  const sessionPath = join(sessionDir, `${hash}.txt`);
843
- const globalPath = join(globalDir, `${hash}.txt`);
844
- let fullPath = existsSync(globalPath) ? globalPath : (existsSync(sessionPath) ? sessionPath : null);
889
+ let fullPath = existsSync(sessionPath) ? sessionPath : null;
845
890
  if (!fullPath) {
846
891
  // Try pointer files (created by compressToolOutputs mapping input hash -> content hash)
847
892
  const ptrSessionPath = join(sessionDir, `${hash}.ptr`);
848
- const ptrGlobalPath = join(globalDir, `${hash}.ptr`);
849
- const ptrPath = existsSync(ptrSessionPath) ? ptrSessionPath : (existsSync(ptrGlobalPath) ? ptrGlobalPath : null);
893
+ const ptrPath = existsSync(ptrSessionPath) ? ptrSessionPath : null;
850
894
  let resolvedHash = hash;
851
895
  if (ptrPath) {
852
896
  try {
@@ -854,27 +898,28 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
854
898
  if (ptrData?.contentHash) {
855
899
  resolvedHash = ptrData.contentHash;
856
900
  const rSessionPath = join(sessionDir, `${resolvedHash}.txt`);
857
- const rGlobalPath = join(globalDir, `${resolvedHash}.txt`);
858
- fullPath = existsSync(rGlobalPath) ? rGlobalPath : (existsSync(rSessionPath) ? rSessionPath : null);
901
+ fullPath = existsSync(rSessionPath) ? rSessionPath : null;
859
902
  }
860
903
  }
861
904
  catch { }
862
- }
863
- if (!fullPath) {
864
- return null;
865
- }
866
905
  }
906
+ if (!fullPath) {
907
+ const recent = scanRecentScratchpad(sessionDir, titleCase, 2000);
908
+ if (recent)
909
+ return recent;
910
+ return null;
911
+ }
912
+ }
867
913
  try {
868
914
  const st = statSync(fullPath);
869
915
  const ageSec = (Date.now() - st.mtimeMs) / 1000;
870
916
  if (ageSec > SCRATCHPAD_MAX_AGE_SEC)
871
917
  return null;
872
- const sessionSummaryPath = join(sessionDir, `${hash}.summary.txt`);
873
- const globalSummaryPath = join(globalDir, `${hash}.summary.txt`);
874
- const summaryPath = existsSync(sessionSummaryPath) ? sessionSummaryPath : (existsSync(globalSummaryPath) ? globalSummaryPath : null);
918
+ const summaryPath = join(sessionDir, `${hash}.summary.txt`);
919
+ const finalSummary = existsSync(summaryPath) ? summaryPath : null;
875
920
  return {
876
921
  hash, fullPath, sizeBytes: st.size, ageSec: Math.round(ageSec),
877
- summaryPath,
922
+ summaryPath: finalSummary,
878
923
  };
879
924
  }
880
925
  catch {
@@ -1740,7 +1785,7 @@ LEDGER_BUFFER_MAX, LEDGER_BUFFER_FLUSH_MS, _ledgerBuffer, _ledgerBufferTimer, _f
1740
1785
  // Stable JSON
1741
1786
  stableJson, _readHead, indexAppend,
1742
1787
  // Scratchpad hits
1743
- scratchpadHitsSeen, getScratchpadHit, recordScratchpadObservation, _pruneScratchpadDir, runDecadenceCycle, applyDecadence, cleanupStaleSessionScratchpads, pruneScratchpadOnce,
1788
+ scratchpadHitsSeen, scanRecentScratchpad, getScratchpadHit, recordScratchpadObservation, _pruneScratchpadDir, runDecadenceCycle, applyDecadence, cleanupStaleSessionScratchpads, pruneScratchpadOnce,
1744
1789
  // Active jobs
1745
1790
  loadActiveJobs, getActiveJobForProject, saveActiveJobForProject, saveJobRecord, loadJobRecord,
1746
1791
  // Project memory
@@ -10,6 +10,28 @@ class LocalBlackboxStub {
10
10
  this.history = [];
11
11
  this.loopCount = 0;
12
12
  }
13
+ normalizeText(text) {
14
+ return (text || "")
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\s]+/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+ getRepeatStreak() {
21
+ if (this.history.length < 2)
22
+ return 0;
23
+ const normalizedLast = this.normalizeText(this.history[this.history.length - 1].text);
24
+ if (!normalizedLast)
25
+ return 0;
26
+ let streak = 1;
27
+ for (let i = this.history.length - 2; i >= 0; i--) {
28
+ const normalized = this.normalizeText(this.history[i].text);
29
+ if (!normalized || normalized !== normalizedLast)
30
+ break;
31
+ streak++;
32
+ }
33
+ return streak;
34
+ }
13
35
  extractFeatures(text) {
14
36
  if (!text || typeof text !== "string")
15
37
  return {};
@@ -78,10 +100,11 @@ class LocalBlackboxStub {
78
100
  const action = this.classifyAction(text);
79
101
  const entropy = this.computeEntropy(features);
80
102
  const uncertainty = this.computeUncertainty(features);
81
- const isLooping = this.detectBasicLoop(text);
82
103
  this.history.push({ text, timestamp: Date.now() / 1000 });
83
104
  if (this.history.length > 10)
84
105
  this.history.shift();
106
+ const repeatStreak = this.getRepeatStreak();
107
+ const isLooping = repeatStreak >= 2 || this.detectBasicLoop(text);
85
108
  if (isLooping)
86
109
  this.loopCount++;
87
110
  else
@@ -95,7 +118,8 @@ class LocalBlackboxStub {
95
118
  continuity_state: "MEDIUM",
96
119
  is_looping: isLooping,
97
120
  loop_consecutive: this.loopCount,
98
- loop_intervention_level: this.loopCount >= 3 ? "escalated" : this.loopCount >= 2 ? "assertive" : this.loopCount >= 1 ? "gentle" : "none",
121
+ repeat_streak: repeatStreak,
122
+ loop_intervention_level: repeatStreak >= 3 || this.loopCount >= 3 ? "escalated" : repeatStreak >= 2 || this.loopCount >= 2 ? "assertive" : this.loopCount >= 1 ? "gentle" : "none",
99
123
  pivot_detected: false,
100
124
  pivot_score: 0.0,
101
125
  outcome: null,
@@ -109,8 +133,8 @@ class LocalBlackboxStub {
109
133
  detectBasicLoop(text, threshold = 0.5) {
110
134
  if (this.history.length < 3)
111
135
  return false;
112
- const currWords = new Set(text.toLowerCase().split(/\s+/).filter(w => w.length > 3));
113
- const pastWords = new Set(this.history[this.history.length - 3].text.toLowerCase().split(/\s+/).filter(w => w.length > 3));
136
+ const currWords = new Set(this.normalizeText(text).split(/\s+/).filter(w => w.length > 3));
137
+ const pastWords = new Set(this.normalizeText(this.history[this.history.length - 3].text).split(/\s+/).filter(w => w.length > 3));
114
138
  if (currWords.size === 0 || pastWords.size === 0)
115
139
  return false;
116
140
  const intersection = new Set([...currWords].filter(w => pastWords.has(w)));
@@ -121,11 +145,11 @@ class LocalBlackboxStub {
121
145
  if (this.loopCount < 1)
122
146
  return null;
123
147
  const interventions = {
124
- gentle: { level: "gentle", directive: "You may be repeating yourselftry rephrasing the core question.", resetSuggested: false },
125
- assertive: { level: "assertive", directive: "You are stuck in a loop. List 3 alternative approaches.", resetSuggested: false },
126
- escalated: { level: "escalated", directive: "CRITICAL: Loop detected. STOP the current approach and SWITCH topics.", resetSuggested: true },
148
+ gentle: { level: "gentle", directive: "You may be repeating the same answer path stop and restate the core question from a new angle.", resetSuggested: false },
149
+ assertive: { level: "assertive", directive: "You are stuck in a loop. STOP repeating the current answer path and list 3 alternative approaches.", resetSuggested: false },
150
+ escalated: { level: "escalated", directive: "CRITICAL: repeated loop detected. STOP the current approach entirely and SWITCH topics or reset strategy.", resetSuggested: true },
127
151
  };
128
- return this.loopCount >= 3 ? interventions.escalated : this.loopCount >= 2 ? interventions.assertive : interventions.gentle;
152
+ return this.getRepeatStreak() >= 3 || this.loopCount >= 3 ? interventions.escalated : this.getRepeatStreak() >= 2 || this.loopCount >= 2 ? interventions.assertive : interventions.gentle;
129
153
  }
130
154
  getPivotDirective() { return null; }
131
155
  recordOutcome(_outcome) { }
@@ -270,7 +270,8 @@ function buildDirectives(cv, regime, state, action, optimizationMode) {
270
270
  if (state.is_looping && state.loop_intervention_level && state.loop_intervention_level !== "none") {
271
271
  const severity = state.loop_intervention_level === "escalated" ? "CRITICAL"
272
272
  : state.loop_intervention_level === "assertive" ? "WARNING" : "NOTICE";
273
- d.push(`[loop prevention: ${severity}] The conversation may be looping try a different approach. (level: ${state.loop_intervention_level})`);
273
+ const repeatNote = state.repeat_streak >= 2 ? ` Repeated prompt streak: ${state.repeat_streak}.` : "";
274
+ d.push(`[loop prevention: ${severity}] The conversation may be looping — stop repeating the same answer path and try a different approach.${repeatNote} (level: ${state.loop_intervention_level})`);
274
275
  }
275
276
  if (optimizationMode && optimizationMode !== "balanced") {
276
277
  d.push(`[optimization: ${optimizationMode}] Session optimization mode is "${optimizationMode}". This overrides default per-regime behavior.`);
@@ -21,6 +21,28 @@ export class ResolutionTracker {
21
21
  this.outcomeHistory = [];
22
22
  this.calibratedWeights = null;
23
23
  }
24
+ normalizeText(text) {
25
+ return (text || "")
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9\s]+/g, " ")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ }
31
+ getRepeatStreak() {
32
+ if (this.history.length < 2)
33
+ return 0;
34
+ const normalizedLast = this.normalizeText(this.history[this.history.length - 1].text);
35
+ if (!normalizedLast)
36
+ return 0;
37
+ let streak = 1;
38
+ for (let i = this.history.length - 2; i >= 0; i--) {
39
+ const normalized = this.normalizeText(this.history[i].text);
40
+ if (!normalized || normalized !== normalizedLast)
41
+ break;
42
+ streak++;
43
+ }
44
+ return streak;
45
+ }
24
46
  update(userText, features, action, entropy, uncertainty, embedding = null) {
25
47
  const entry = {
26
48
  text: userText,
@@ -90,6 +112,7 @@ export class ResolutionTracker {
90
112
  const entropyTrend = this.calcEntropyTrend();
91
113
  const featureContradiction = this.calcFeatureContradiction();
92
114
  const embeddingDelta = this.calcEmbeddingDelta();
115
+ const repeatStreak = this.getRepeatStreak();
93
116
  const isLooping = this.detectLoop();
94
117
  const intentState = this.computeIntentState();
95
118
  const continuityState = this.continuityState(intentState);
@@ -135,9 +158,9 @@ export class ResolutionTracker {
135
158
  const momentum = this.calcMomentum(entropyTrend, actionConsistency, embeddingDelta, isLooping, lastEntry.action, lastEntry.entropy);
136
159
  let loopLevel = "none";
137
160
  if (isLooping) {
138
- if (this.loopCount >= 4)
161
+ if (repeatStreak >= 3 || this.loopCount >= 4)
139
162
  loopLevel = "escalated";
140
- else if (this.loopCount >= 3)
163
+ else if (repeatStreak >= 2 || this.loopCount >= 3)
141
164
  loopLevel = "assertive";
142
165
  else if (this.loopCount >= 2)
143
166
  loopLevel = "suggestive";
@@ -165,6 +188,7 @@ export class ResolutionTracker {
165
188
  continuity_state: continuityState,
166
189
  is_looping: isLooping,
167
190
  loop_consecutive: this.loopCount,
191
+ repeat_streak: repeatStreak,
168
192
  loop_intervention_level: loopLevel,
169
193
  pivot_detected: pivotDetected,
170
194
  pivot_score: Math.round(pivotScore * 10000) / 10000,
@@ -224,10 +248,13 @@ export class ResolutionTracker {
224
248
  detectLoop(k = 3, threshold = 0.6) {
225
249
  const effectiveThreshold = this.calibratedWeights?.loopJaccard ?? threshold;
226
250
  const effectiveK = this.calibratedWeights?.loopK ?? k;
251
+ const repeatStreak = this.getRepeatStreak();
252
+ if (repeatStreak >= 2)
253
+ return true;
227
254
  if (this.history.length < effectiveK + 1)
228
255
  return false;
229
- const currWords = new Set(this.history[this.history.length - 1].text.toLowerCase().split(/\s+/));
230
- const pastWords = new Set(this.history[this.history.length - (effectiveK + 1)].text.toLowerCase().split(/\s+/));
256
+ const currWords = new Set(this.normalizeText(this.history[this.history.length - 1].text).split(/\s+/).filter(Boolean));
257
+ const pastWords = new Set(this.normalizeText(this.history[this.history.length - (effectiveK + 1)].text).split(/\s+/).filter(Boolean));
231
258
  if (currWords.size === 0 || pastWords.size === 0)
232
259
  return false;
233
260
  const intersection = new Set([...currWords].filter(w => pastWords.has(w)));
@@ -353,19 +380,19 @@ export class ResolutionTracker {
353
380
  return null;
354
381
  const interventions = {
355
382
  gentle: {
356
- directive: "You may be repeating yourselftry rephrasing the core question differently or approaching from a new angle.",
383
+ directive: "You may be repeating the same answer path stop and restate the core question from a new angle before continuing.",
357
384
  resetSuggested: false,
358
385
  },
359
386
  suggestive: {
360
- directive: "The conversation is looping. Step back and identify what new information you need. Consider asking a different question or taking a break from this topic.",
387
+ directive: "The conversation is looping. Do not continue the same answer path. Step back, identify what new information is missing, and ask for a different constraint or approach.",
361
388
  resetSuggested: false,
362
389
  },
363
390
  assertive: {
364
- directive: "You are stuck in a loop. The current approach is not productive. PIVOT: list 3 alternative approaches you haven't tried and pick one.",
391
+ directive: "You are stuck in a loop. STOP repeating the current answer path. PIVOT: list 3 alternative approaches you have not tried and choose one.",
365
392
  resetSuggested: false,
366
393
  },
367
394
  escalated: {
368
- directive: "CRITICAL: You have been looping for several turns. STOP the current approach entirely. Either SWITCH to a completely different topic or reset your strategy. Continued looping wastes time and tokens.",
395
+ directive: "CRITICAL: repeated loop detected. STOP the current approach entirely. Reset the strategy, SWITCH topics or scope, and do not continue the same line of reasoning.",
369
396
  resetSuggested: true,
370
397
  },
371
398
  };