vibeostheog 0.22.6 → 0.22.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.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,49 @@
1
+ ## 0.22.16
2
+
3
+ - test: add 12 cache isolation scenarios (no cross-session/project hallucination)
4
+ - test: add 3 regression tests for setApiToken fallback-mode reset
5
+ - chore: remove old branding from guard plugin
6
+
7
+ ## 0.22.15
8
+
9
+ - fix: remove user-wide cache fallback from getScratchpadHit()
10
+ no more SCRATCHPAD_GLOBAL_DIR — cache scope is session/project only
11
+ - fix: prefer session cache over global cache in getScratchpadHit()
12
+ swap lookup order on direct-hash and pointer-resolved paths
13
+ - fix: setApiToken() now resets _apiFallbackMode so a token update
14
+ breaks out of permanent API-fallback deadlock
15
+ - fix: syncApiTokenFromDisk() else branch also clears fallback state
16
+ - test: add 12 cache isolation scenarios (no cross-session hallucination)
17
+ - test: 3 regression tests for setApiToken fallback-mode reset
18
+ - test: add cache isolation test suite (15 total regression tests)
19
+
20
+ ## 0.22.14
21
+
22
+
23
+ ## 0.22.13
24
+
25
+
26
+ ## 0.22.12
27
+ - fix: harden scratchpad cache
28
+
29
+
30
+ ## 0.22.11
31
+ - fix: harden blackbox pivot detection and add regression coverage
32
+
33
+ ## 0.22.10
34
+ - fix: append enforcement tags (ENF, FLOW, TDD, LOCK) to live footer
35
+ - fix: flow-todo-queue path inconsistency and loadRules loop bug (#105)
36
+ - chore: bump to 0.22.8 (#106)
37
+
38
+
39
+ ## 0.22.8
40
+ - fix: flow-todo-queue path inconsistency (missing dot prefix broke trinity todo visibility)
41
+ - fix: getSessionFlowCounts calling loadRules() inside loop (redundant statSync per entry)
42
+ - chore: bump to 0.22.8
43
+
1
44
  ## 0.22.6
2
45
  - feat: wire CostAnomalyDetector into tool-execute hook
3
46
  - feat: replace TokenAnomalyDetector with CostAnomalyDetector
4
- - feat: replace TokenAnomalyDetector with CostAnomalyDetector
5
47
  - fix: bin/setup.js now delegates to deploy.mjs for proper plugin install
6
48
  - fix: restore anomaly detector class in TS source, add mega regression tests
7
49
  - fix: read path prefers global scratchpad over stale session-local copies
@@ -33,7 +75,6 @@ Merge pull request #91 from DrunkkToys/pr/cache-write-savings
33
75
  ## 0.20.15
34
76
  - feat: dashboard blackbox telemetry — bidirectional BE/FE sync
35
77
  - fix: mock auth and clear OPENCODE_MODEL in bootstrap test, commit blackbox .js for CI
36
- - fix: mock auth and clear OPENCODE_MODEL in bootstrap test, commit blackbox .js for CI
37
78
  - docs: fix speed mode quality rating in comparison table (#83)
38
79
  - docs: fix token defaults in env vars table
39
80
  - docs: update README to reflect actual features and fix inaccuracies
@@ -75,10 +116,9 @@ release: v0.20.13 — holistic CLI footer fix + regression tests (#80)
75
116
  ## 0.20.7
76
117
  - fix: ship compiled OpenCode plugin bundle
77
118
  - fix: always show model label in tool.execute.after footer, even with zero savings
78
- - fix: always show model label in tool.execute.after footer, even with zero savings
79
119
  - fix: restore release tarball pack step
80
- Merge pull request #74 from DrunkkToys/codex/release-live-bundle
81
- Merge pull request #72 from DrunkkToys/codex/alpha-token-install-validation
120
+ Merge pull request #74 from DrunkkToys/feature/release-live-bundle
121
+ Merge pull request #72 from DrunkkToys/feature/alpha-token-install-validation
82
122
 
83
123
 
84
124
  ## 0.20.6
@@ -95,17 +135,17 @@ Merge pull request #72 from DrunkkToys/codex/alpha-token-install-validation
95
135
  - fix: prefer valid api tokens over placeholder env
96
136
  - fix: gate footer stderr by runtime
97
137
  - fix: quiet footer stderr noise
98
- Merge pull request #70 from DrunkkToys/codex/alpha-token-kill-switch
138
+ Merge pull request #70 from DrunkkToys/feature/alpha-token-kill-switch
99
139
 
100
140
 
101
141
  ## 0.20.3
102
142
  - fix: embed valid alpha token fallback
103
- Merge pull request #69 from DrunkkToys/codex/alpha-token-release
143
+ Merge pull request #69 from DrunkkToys/feature/alpha-token-release
104
144
 
105
145
 
106
146
  ## 0.20.2
107
147
  - fix: restore embedded api token fallback
108
- Merge pull request #68 from DrunkkToys/codex/embed-api-token
148
+ Merge pull request #68 from DrunkkToys/feature/embed-api-token
109
149
 
110
150
 
111
151
  ## 0.20.1
@@ -114,8 +154,8 @@ Merge pull request #68 from DrunkkToys/codex/embed-api-token
114
154
 
115
155
  ## 0.20.0
116
156
  - fix: resolve live OpenCode model and refresh README launch copy
117
- Merge pull request #61 from DrunkkToys/codex/release-candidate-blackbox-footer
118
- Merge pull request #59 from DrunkkToys/codex/fix-thinking-directive-precedence
157
+ Merge pull request #61 from DrunkkToys/feature/release-candidate-blackbox-footer
158
+ Merge pull request #59 from DrunkkToys/feature/fix-thinking-directive-precedence
119
159
 
120
160
 
121
161
  ## 0.19.9
@@ -129,9 +169,9 @@ Fix local blackbox tracker hydration
129
169
  Add model refresh silence regression test
130
170
  Fix blackbox session context
131
171
  Fix thinking directive precedence
132
- Merge pull request #58 from DrunkkToys/codex/fix-home-context
172
+ Merge pull request #58 from DrunkkToys/feature/fix-home-context
133
173
  Fix session state home context
134
- Merge pull request #57 from DrunkkToys/codex/fix-opencode-launch-config
174
+ Merge pull request #57 from DrunkkToys/feature/fix-opencode-launch-config
135
175
  Fix OpenCode launch config
136
176
 
137
177
 
@@ -139,9 +179,9 @@ Fix OpenCode launch config
139
179
  - feat: use native opencode model lists
140
180
  - fix: make OpenCode footer agnostic
141
181
  - fix: make vibeOS compatibility paths dynamic
142
- Merge pull request #56 from DrunkkToys/codex/agnostic-opencode-release
143
- Merge pull request #55 from DrunkkToys/codex/agnostic-opencode-models
144
- merge: origin/master into codex/agnostic-opencode-models
182
+ Merge pull request #56 from DrunkkToys/feature/agnostic-opencode-release
183
+ Merge pull request #55 from DrunkkToys/feature/agnostic-opencode-models
184
+ merge: origin/master into feature/agnostic-opencode-models
145
185
  Bootstrap trinity tiers from OpenCode model
146
186
 
147
187
 
@@ -166,7 +206,7 @@ Bootstrap trinity tiers from OpenCode model
166
206
  - fix: make README and runtime self-contained
167
207
  - test: add 59 integration + e2e tests for cross-module behavior and user workflows
168
208
  Merge pull request #49 from DrunkkToys/oc-desktop-live-savings-refresh
169
- Merge pull request #48 from DrunkkToys/codex/live-savings-refresh
209
+ Merge pull request #48 from DrunkkToys/feature/live-savings-refresh
170
210
  Invalidate savings cache on state writes
171
211
 
172
212
 
@@ -176,7 +216,7 @@ Invalidate savings cache on state writes
176
216
  - fix: stress mitigation directive uses raw stress score, not API-scaled
177
217
  - fix: _refreshModel respects project-local opencode.json over bootstrap default slot
178
218
  - docs: mark v0.19.0 as alpha milestone release
179
- Merge pull request #47 from DrunkkToys/codex/status-lock-backend-fix
219
+ Merge pull request #47 from DrunkkToys/feature/status-lock-backend-fix
180
220
  Expose status lock and backend state
181
221
  Fix stress mitigation and TDD smoke coverage
182
222
  Rebuild bundle after telemetry merge
@@ -205,14 +245,12 @@ Fix budget-first mode and stabilize tests
205
245
  - fix: trinity slots now authoritative over opencode.json model
206
246
 
207
247
  ## 0.18.5
208
- - fix: trinity slots now authoritative over opencode.json model
209
248
 
210
249
 
211
250
  ## 0.18.4
212
251
  - fix: quality tracking now computes avg from lifetime score/count instead of hardcoding 0
213
252
  - fix: savings rate shown with 4 decimal precision (was rounding to $0.00/hr)
214
253
  - fix: cache savings minimum enforced at $0.0001 per scratchpad hit (was rounding to $0)
215
- - fix: ledger reconciliation flushes buffer before reading + uses Math.max() to prevent state drops
216
254
  - fix: model lock no longer overridden by bogus opencode.json model
217
255
 
218
256
  ## 0.18.3
@@ -229,7 +267,6 @@ Fix budget-first mode and stabilize tests
229
267
 
230
268
  ## 0.16.0
231
269
  - feat: dopamine-style footer + natural language system directives
232
- - feat: dopamine-style footer + natural language system directives
233
270
  - feat: turn-aware compaction directive at turn 7+
234
271
  - feat: add forensic/web-research modes + 1084-datapoint benchmark
235
272
  - fix: flash icon only when API connected, unified [VIBE→MODE⚡] format
@@ -271,7 +308,6 @@ footer: TDD tag controlled by blackbox, VIBE replaces AUTO
271
308
 
272
309
  ## 0.15.10
273
310
  - fix: prevent empty footer from message.updated blocking text.complete
274
- - fix: prevent empty footer from message.updated blocking text.complete
275
311
  - fix: deploy copies .env.production alongside plugin
276
312
 
277
313
 
@@ -304,7 +340,6 @@ Build: self-contained bundle (vibeOScore resolved)
304
340
  - fix: remove sticky fallback flag that kills auto mode after single API failure
305
341
  - refactor: architecture simplification and scale readiness
306
342
  - docs: update vibeOS skills to match current plugin behavior
307
- - docs: update vibeOS skills to match current plugin behavior
308
343
  - chore: finalize cleanup
309
344
  - chore: update import paths for vibeOScore monorepo migration
310
345
  Merge pull request #32 from DrunkkToys/refactor/architecture-simplify-scale
@@ -347,15 +382,6 @@ Merge pull request #21 from DrunkkToys/fix/api-token-and-blackbox-control-vector
347
382
 
348
383
 
349
384
  ## 0.14.4
350
- - fix: add contents:write permission to release workflow
351
- - fix: add test:ci script for fast unit tests, separate from integration tests
352
- - fix: configure git identity in release workflow
353
- - fix: exclude slow delegation enforcer test from npm test
354
- - fix: increase test-timeout to 120s for slow delegation enforcer test
355
- - fix: exclude dashboard test from test suite and add --test-timeout=60000
356
- - fix: add --test-timeout=60000 to prevent cancelledByParent test failures in CI
357
- - fix: exclude dashboard from tsconfig to resolve CI build failure
358
- - fix: update API token and add blackboxControlVector client method
359
385
  Merge pull request #24 from DrunkkToys/fix/ci-test-exclude-dashboard
360
386
  Merge pull request #23 from DrunkkToys/fix/ci-test-timeout
361
387
  Merge pull request #22 from DrunkkToys/fix/ci-exclude-dashboard
@@ -515,7 +541,6 @@ Merge pull request #21 from DrunkkToys/fix/api-token-and-blackbox-control-vector
515
541
  - fix: resolution-tracker thresholds - isConverging >=0.5, detectLoop Jaccard 0.6, isRefining >-0.01
516
542
  - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
517
543
  - refactor: merge extracted modules into src/index.ts (6656→1061 lines)
518
- - refactor: extract 16 modules (7207 lines) from src/index.ts into src/lib/
519
544
  - refactor: swap blackbox import to LocalBlackboxStub (forensic)
520
545
  - refactor: blackbox moved to API-server-only — plugin uses local stub
521
546
  - refactor: rename CodeX MCP server to vibeOS MCP server
@@ -543,74 +568,6 @@ test api put
543
568
 
544
569
 
545
570
  ## 0.13.3
546
- - feat: blackbox dynamically controls thinking mode per sub-regime for cost savings
547
- - feat: complete remote API migration — dual-path scoreStress, patternsObserve/Record, TDD exports with local fallback + neutral env test
548
- - feat: complete remote API migration — dual-path scoreStress, patternsObserve/Record, TDD exports with local fallback
549
- - feat: blackbox ML enhancements — real features, loop prevention, pivot detection, outcome tracking, calibration
550
- - feat: v0.10.0 — 6 enhancement phases implemented
551
- - feat: WordPress integration - atomic seat+token creation
552
- - feat: Phase 2 - Integrate remote API client into plugin runtime
553
- - feat: Phase 1 - Remote API server for protected algorithms
554
- - feat: CodeX MCP server and dashboard sidebar plugin integration
555
- - feat: vibeOS TUI dashboard sidebar plugin
556
- - fix: release.mjs — add missing closing brace for deploy else block
557
- - fix: stabilize refactored modules — ES module bindings, setters, missing imports
558
- - fix: flow-enforcer race condition, blackbox default ON, dynamic footer
559
- - fix: lock model name, enforcement logging, TDD framework detection, cache display rounding
560
- - fix: validateState sessions object, remove stale report writes, drop dead code
561
- - fix: state validation, flow TODO dedup, session checkpointing, fetch verification
562
- - fix: _appendFooter full model names, → arrow, inline stress; 361/362 pass
563
- - fix: atomic state writes, safeJsonParse in flow-enforcer, hook error handling (#15)
564
- - fix: model split always shown, stress inline in footer, not separate line
565
- - fix: footer uses slot model name, → arrow, inline stress always, remove session-report writes, disable blackbox default
566
- - fix: sync second footer builder in tool.execute.after with new template
567
- - fix: compact footer with inline stress gauge, full model names, robust test assertions
568
- - fix: footer uses trinity tier model name, all 362 tests pass
569
- - fix: resolve pricing cache corruption, improve TODO extraction, and tune delegation savings
570
- - fix: use dynamic mcp port fallback
571
- - fix: handle mcp server close-reopen race
572
- - fix: await mcp server startup
573
- - fix: harden prompt send and unblock typecheck
574
- - fix: sync opencode.json model with brain tier, restore footer icons (trend arrows, stress gauge)
575
- - fix: deploy script missing vibeOS-api-server/ directory
576
- - fix: footer prepended to output.output, fix tests, remove stale vibeOS/ directory
577
- - fix: migrate footer from context-polluting text.complete to UI-only output.title
578
- - fix: restore experimental.text.complete and message.updated hooks lost during stash
579
- - fix: ensure model-tiers.json is created when no model is detected
580
- - fix: update trinity status test for new dashboard format
581
- - fix: compute cache savings from actual file size, remove /bin/zsh.001 floor, fix state corruption from flow_warns overwrite
582
- - fix: add proper named export for auto-discovery, fix function closure
583
- - fix: add startup toast to verify TUI plugin function execution
584
- - fix: add auto-activation to sync script, add sidebar widget diagnostics
585
- - fix: restore vibeOS sidebar dashboard widget, fix plugin path in opencode config
586
- - fix: add size guard to readJsonOrEmpty to prevent OOM on massive state files
587
- - fix: add generation counter + concurrent-write detection to updateState
588
- - fix: dedup double footer from competing message.updated / text.complete hooks
589
- - fix: append ledger entry in recordSaving() and recordCacheSaving()
590
- - fix: make MCP server close() async, export closeMcpServer for test cleanup
591
- - fix: isolate tests from real config (chdir sandbox, VIBEOS_MCP_PORT=0, HOME cleanup)
592
- - fix: release/deploy synced lib deps - blackbox missing caused footer (and all hooks) to disappear
593
- - fix: resolution-tracker thresholds - isConverging >=0.5, detectLoop Jaccard 0.6, isRefining >-0.01
594
- - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
595
- - refactor: merge extracted modules into src/index.ts (6656→1061 lines)
596
- - refactor: extract 16 modules (7207 lines) from src/index.ts into src/lib/
597
- - refactor: swap blackbox import to LocalBlackboxStub (forensic)
598
- - refactor: blackbox moved to API-server-only — plugin uses local stub
599
- - refactor: rename CodeX MCP server to vibeOS MCP server
600
- - docs: add final stabilization campaign report (#14)
601
- - docs: add stabilization audit reports for sessions 02-06 and 09 (#13)
602
- - docs: add stabilization baseline report (#12)
603
- - docs: update README and AGENTS for remote API protection (Phase 1+2)
604
- - docs: fix brand name, update AGENTS line count, document shell.env hook
605
- - docs: update README and AGENTS for v0.9.1 features
606
- - test: add cross-session restart E2E test (BUG 10)
607
- - chore: remove TDD auto-generated test artifacts
608
- - chore: hardcode public VIBEOS_API_TOKEN as default
609
- - chore: bump to 0.11.0 — blackbox ML engine, loop prevention, pivot detection, API-only architecture
610
- - chore: replace diagnostic log with visible toast
611
- - chore: add secrets to .gitignore (.env.production, PRODUCTION-CREDENTIALS.md)
612
- - ci: add vibeOS test workflow
613
- - chore: v0.9.1
614
571
  bump 0.13.2 — state.ts stub exports, fix ESM import errors
615
572
  bump 0.13.1 — trinity optimize (5 modes + auto), compaction every 10 turns, state.ts stub exports
616
573
  Merge pull request #18 from DrunkkToys/revert/low-value-api-migration
@@ -639,11 +596,7 @@ test api put
639
596
  ## 0.13.0
640
597
 
641
598
  - refactor: extract 16 modules from src/index.ts into src/lib/ (state, pricing, trinity, TDD, hooks, reporting, research-audit, api-client, credit-api, turn-classify, index-helpers)
642
- - feat: blackbox dynamically controls thinking mode per sub-regime for cost savings
643
599
  - fix: flow-enforcer race condition, blackbox default ON, dynamic footer improvement
644
- - fix: lock model name, enforcement logging, TDD framework detection, cache display rounding
645
- - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
646
- - fix: model split always shown, stress inline in footer, not separate line
647
600
  - fix: atomic state writes, safeJsonParse in flow-enforcer, hook error handling
648
601
  - perf: inline stress in footer, remove session-report writes, disable blackbox default
649
602
  - docs: AGENTS.md updated — 8 hooks (added session.compacting), new src/lib/ architecture
@@ -697,7 +650,6 @@ test api put
697
650
 
698
651
  ## 0.9.1
699
652
  - feat: vibeOS MCP server HTTP API
700
- - feat: vibeOS TUI dashboard sidebar plugin
701
653
  - chore: sync-ts-build and flow-enforcer enhancements
702
654
 
703
655
  ## 0.9.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeostheog",
3
- "version": "0.22.6",
3
+ "version": "0.22.9",
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"],
@@ -291,6 +296,7 @@ async function _appendFooter(input, output, directory) {
291
296
  }
292
297
  if (modeLabel)
293
298
  vibeLine += ` | ${formatQualityName(modeLabel)}`;
299
+ vibeLine += enfSuffixFooter;
294
300
  vibeLine += ` | VIBE${flashIcon ? " ⚡" : ""}`;
295
301
  if (_footerStress > 0.4) {
296
302
  const stressLabel = _footerStress > 0.7 ? "high" : "elevated";
@@ -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
@@ -837,7 +837,6 @@ function scanRecentScratchpad(dir, titleCase, maxScan = 2000) {
837
837
  return null;
838
838
  const entries = readdirSync(dir);
839
839
  const ptrFiles = entries.filter(e => e.endsWith(".ptr"));
840
- // Try .ptr files first (created by compressToolOutputs mapping input hash -> content hash)
841
840
  const ptrCandidates = [];
842
841
  for (const pf of ptrFiles) {
843
842
  if (ptrCandidates.length >= 50)
@@ -849,14 +848,18 @@ function scanRecentScratchpad(dir, titleCase, maxScan = 2000) {
849
848
  catch { }
850
849
  }
851
850
  ptrCandidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
851
+ let scanned = 0;
852
852
  for (const { ptrPath } of ptrCandidates) {
853
+ if (scanned++ >= maxScan)
854
+ break;
853
855
  try {
854
856
  const ptrData = safeJsonParse(readFileSync(ptrPath, "utf-8"));
855
857
  if (!ptrData?.contentHash)
856
858
  continue;
857
- if (titleCase && ptrData.tool && TOOL_NAME_NORMALIZE[ptrData.tool] !== titleCase)
859
+ const ptrTool = typeof ptrData.tool === "string" ? (TOOL_NAME_NORMALIZE[ptrData.tool] || ptrData.tool) : null;
860
+ if (titleCase && ptrTool && ptrTool !== titleCase)
858
861
  continue;
859
- const contentHash = ptrData.contentHash;
862
+ const contentHash = String(ptrData.contentHash);
860
863
  const f = join(dir, `${contentHash}.txt`);
861
864
  if (!existsSync(f))
862
865
  continue;
@@ -869,28 +872,6 @@ function scanRecentScratchpad(dir, titleCase, maxScan = 2000) {
869
872
  }
870
873
  catch { }
871
874
  }
872
- // Fallback: scan .txt files
873
- const txtFiles = entries.filter(e => e.endsWith(".txt") && !e.endsWith(".summary.txt"));
874
- if (txtFiles.length === 0)
875
- return null;
876
- const candidateHashes = [];
877
- for (let i = txtFiles.length - 1; i >= 0; i--) {
878
- const f = txtFiles[i];
879
- if (candidateHashes.length > 50)
880
- break;
881
- candidateHashes.push(f.replace(/\.txt$/, ""));
882
- }
883
- for (const hash of candidateHashes) {
884
- const f = join(dir, `${hash}.txt`);
885
- if (!existsSync(f))
886
- continue;
887
- const st = statSync(f);
888
- const ageSec = (Date.now() - st.mtimeMs) / 1000;
889
- if (ageSec > SCRATCHPAD_MAX_AGE_SEC)
890
- continue;
891
- const sumPath = join(dir, `${hash}.summary.txt`);
892
- return { hash, fullPath: f, sizeBytes: st.size, ageSec: Math.round(ageSec), summaryPath: existsSync(sumPath) ? sumPath : null };
893
- }
894
875
  return null;
895
876
  }
896
877
  catch {
@@ -904,15 +885,12 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
904
885
  const inputJson = stableJson(args ?? {});
905
886
  const hash = createHash("sha256").update(`${titleCase}\n${inputJson}\n`).digest("hex").slice(0, 16);
906
887
  const sessionDir = baseDir || getSessionScratchpadDir();
907
- const globalDir = SCRATCHPAD_GLOBAL_DIR;
908
888
  const sessionPath = join(sessionDir, `${hash}.txt`);
909
- const globalPath = join(globalDir, `${hash}.txt`);
910
- let fullPath = existsSync(globalPath) ? globalPath : (existsSync(sessionPath) ? sessionPath : null);
889
+ let fullPath = existsSync(sessionPath) ? sessionPath : null;
911
890
  if (!fullPath) {
912
891
  // Try pointer files (created by compressToolOutputs mapping input hash -> content hash)
913
892
  const ptrSessionPath = join(sessionDir, `${hash}.ptr`);
914
- const ptrGlobalPath = join(globalDir, `${hash}.ptr`);
915
- const ptrPath = existsSync(ptrSessionPath) ? ptrSessionPath : (existsSync(ptrGlobalPath) ? ptrGlobalPath : null);
893
+ const ptrPath = existsSync(ptrSessionPath) ? ptrSessionPath : null;
916
894
  let resolvedHash = hash;
917
895
  if (ptrPath) {
918
896
  try {
@@ -920,30 +898,28 @@ function getScratchpadHit(toolLower, args, baseDir = null) {
920
898
  if (ptrData?.contentHash) {
921
899
  resolvedHash = ptrData.contentHash;
922
900
  const rSessionPath = join(sessionDir, `${resolvedHash}.txt`);
923
- const rGlobalPath = join(globalDir, `${resolvedHash}.txt`);
924
- fullPath = existsSync(rGlobalPath) ? rGlobalPath : (existsSync(rSessionPath) ? rSessionPath : null);
901
+ fullPath = existsSync(rSessionPath) ? rSessionPath : null;
925
902
  }
926
903
  }
927
904
  catch { }
928
- }
929
- if (!fullPath) {
930
- const recent = scanRecentScratchpad(sessionDir, titleCase, 2000) || scanRecentScratchpad(globalDir, titleCase, 2000);
931
- if (recent)
932
- return recent;
933
- return null;
934
- }
935
905
  }
906
+ if (!fullPath) {
907
+ const recent = scanRecentScratchpad(sessionDir, titleCase, 2000);
908
+ if (recent)
909
+ return recent;
910
+ return null;
911
+ }
912
+ }
936
913
  try {
937
914
  const st = statSync(fullPath);
938
915
  const ageSec = (Date.now() - st.mtimeMs) / 1000;
939
916
  if (ageSec > SCRATCHPAD_MAX_AGE_SEC)
940
917
  return null;
941
- const sessionSummaryPath = join(sessionDir, `${hash}.summary.txt`);
942
- const globalSummaryPath = join(globalDir, `${hash}.summary.txt`);
943
- const summaryPath = existsSync(sessionSummaryPath) ? sessionSummaryPath : (existsSync(globalSummaryPath) ? globalSummaryPath : null);
918
+ const summaryPath = join(sessionDir, `${hash}.summary.txt`);
919
+ const finalSummary = existsSync(summaryPath) ? summaryPath : null;
944
920
  return {
945
921
  hash, fullPath, sizeBytes: st.size, ageSec: Math.round(ageSec),
946
- summaryPath,
922
+ summaryPath: finalSummary,
947
923
  };
948
924
  }
949
925
  catch {
@@ -195,7 +195,8 @@ export function createTrinityTool(deps) {
195
195
  const allEntries = [...BRANDED_MODES, ...RUNTIME_MODES];
196
196
  const modeEntry = allEntries.find(e => e.id === slot);
197
197
  if (modeEntry) {
198
- const tierSlot = modeEntry.pipeline[0] || "cheap";
198
+ const rawTier = modeEntry.pipeline[0] || "cheap";
199
+ const tierSlot = new Set(["brain", "medium", "cheap"]).has(rawTier) ? rawTier : "cheap";
199
200
  deps.writeSelection("active_slot", tierSlot);
200
201
  deps.writeSelection("onboarding_mode", modeEntry.tdd === "quality" || modeEntry.enforcement === "strict" ? "strict" : "assist");
201
202
  deps.writeSelection("delegation_enforce", modeEntry.enforcement === "strict" || modeEntry.enforcement === "on");
@@ -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,
@@ -52,6 +74,10 @@ export class ResolutionTracker {
52
74
  return state;
53
75
  }
54
76
  detectPivotSignal(current, previous) {
77
+ if (!current.embedding || !previous.embedding) {
78
+ return false;
79
+ }
80
+ const embeddingDelta = 1.0 - cosineSimilarity(current.embedding, previous.embedding);
55
81
  const drift = this.history.length >= 4
56
82
  ? this.computeIntentState().drift_rate
57
83
  : 0;
@@ -60,8 +86,8 @@ export class ResolutionTracker {
60
86
  const lengthRatio = previous.text.length > 0
61
87
  ? Math.abs(current.text.length - previous.text.length) / previous.text.length
62
88
  : 0;
63
- const pivotScore = drift * 0.35 + repeatRatio * 0.15 + instructionChange * 0.25 + lengthRatio * 0.25;
64
- return pivotScore > 0.45;
89
+ const pivotScore = drift * 0.2 + embeddingDelta * 0.35 + repeatRatio * 0.1 + instructionChange * 0.15 + lengthRatio * 0.2;
90
+ return pivotScore > 0.4;
65
91
  }
66
92
  computeState() {
67
93
  const n = this.history.length;
@@ -86,6 +112,7 @@ export class ResolutionTracker {
86
112
  const entropyTrend = this.calcEntropyTrend();
87
113
  const featureContradiction = this.calcFeatureContradiction();
88
114
  const embeddingDelta = this.calcEmbeddingDelta();
115
+ const repeatStreak = this.getRepeatStreak();
89
116
  const isLooping = this.detectLoop();
90
117
  const intentState = this.computeIntentState();
91
118
  const continuityState = this.continuityState(intentState);
@@ -131,9 +158,9 @@ export class ResolutionTracker {
131
158
  const momentum = this.calcMomentum(entropyTrend, actionConsistency, embeddingDelta, isLooping, lastEntry.action, lastEntry.entropy);
132
159
  let loopLevel = "none";
133
160
  if (isLooping) {
134
- if (this.loopCount >= 4)
161
+ if (repeatStreak >= 3 || this.loopCount >= 4)
135
162
  loopLevel = "escalated";
136
- else if (this.loopCount >= 3)
163
+ else if (repeatStreak >= 2 || this.loopCount >= 3)
137
164
  loopLevel = "assertive";
138
165
  else if (this.loopCount >= 2)
139
166
  loopLevel = "suggestive";
@@ -161,6 +188,7 @@ export class ResolutionTracker {
161
188
  continuity_state: continuityState,
162
189
  is_looping: isLooping,
163
190
  loop_consecutive: this.loopCount,
191
+ repeat_streak: repeatStreak,
164
192
  loop_intervention_level: loopLevel,
165
193
  pivot_detected: pivotDetected,
166
194
  pivot_score: Math.round(pivotScore * 10000) / 10000,
@@ -220,10 +248,13 @@ export class ResolutionTracker {
220
248
  detectLoop(k = 3, threshold = 0.6) {
221
249
  const effectiveThreshold = this.calibratedWeights?.loopJaccard ?? threshold;
222
250
  const effectiveK = this.calibratedWeights?.loopK ?? k;
251
+ const repeatStreak = this.getRepeatStreak();
252
+ if (repeatStreak >= 2)
253
+ return true;
223
254
  if (this.history.length < effectiveK + 1)
224
255
  return false;
225
- const currWords = new Set(this.history[this.history.length - 1].text.toLowerCase().split(/\s+/));
226
- 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));
227
258
  if (currWords.size === 0 || pastWords.size === 0)
228
259
  return false;
229
260
  const intersection = new Set([...currWords].filter(w => pastWords.has(w)));
@@ -349,19 +380,19 @@ export class ResolutionTracker {
349
380
  return null;
350
381
  const interventions = {
351
382
  gentle: {
352
- 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.",
353
384
  resetSuggested: false,
354
385
  },
355
386
  suggestive: {
356
- 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.",
357
388
  resetSuggested: false,
358
389
  },
359
390
  assertive: {
360
- 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.",
361
392
  resetSuggested: false,
362
393
  },
363
394
  escalated: {
364
- 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.",
365
396
  resetSuggested: true,
366
397
  },
367
398
  };
@@ -144,7 +144,7 @@ function getStateFile() {
144
144
  return join(getVibeOSHome(), "delegation-state.json");
145
145
  }
146
146
  function getFlowTodoFile() {
147
- return join(getVibeOSHome(), "flow-todo-queue.jsonl");
147
+ return join(getVibeOSHome(), ".flow-todo-queue.jsonl");
148
148
  }
149
149
  const FLOW_DEDUP_FILE = join(getVibeOSHome(), ".flow-dedup-keys.json");
150
150
  const MAX_FLOW_TODOS = 200;
@@ -295,8 +295,8 @@ export function getFlowWarns() {
295
295
  }
296
296
  export function getSessionFlowCounts() {
297
297
  const counts = { warn: 0, hint: 0, flag: 0 };
298
+ const rules = loadRules();
298
299
  for (const key of _flowWarnsSeen) {
299
- const rules = loadRules();
300
300
  const [ruleId] = key.split("::");
301
301
  const rule = rules.find((r) => r.id === ruleId);
302
302
  if (rule && counts[rule.severity] !== undefined)
@@ -369,7 +369,7 @@ export function recordFlowTodo({ filePath, content }) {
369
369
  }
370
370
  }
371
371
  catch { }
372
- console.error(`[flow-enforcer] 📋 Extracted ${todos.length} TODO(s) from ${filePath} → flow-todo-queue.jsonl`);
372
+ console.error(`[flow-enforcer] 📋 Extracted ${todos.length} TODO(s) from ${filePath} → .flow-todo-queue.jsonl`);
373
373
  return todos.length;
374
374
  }
375
375
  catch {