vibeostheog 0.22.12 → 0.22.16

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,28 @@
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 codex branding
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
+
1
26
  ## 0.22.12
2
27
  - fix: harden scratchpad cache
3
28
 
@@ -19,7 +44,6 @@
19
44
  ## 0.22.6
20
45
  - feat: wire CostAnomalyDetector into tool-execute hook
21
46
  - feat: replace TokenAnomalyDetector with CostAnomalyDetector
22
- - feat: replace TokenAnomalyDetector with CostAnomalyDetector
23
47
  - fix: bin/setup.js now delegates to deploy.mjs for proper plugin install
24
48
  - fix: restore anomaly detector class in TS source, add mega regression tests
25
49
  - fix: read path prefers global scratchpad over stale session-local copies
@@ -51,7 +75,6 @@ Merge pull request #91 from DrunkkToys/pr/cache-write-savings
51
75
  ## 0.20.15
52
76
  - feat: dashboard blackbox telemetry — bidirectional BE/FE sync
53
77
  - fix: mock auth and clear OPENCODE_MODEL in bootstrap test, commit blackbox .js for CI
54
- - fix: mock auth and clear OPENCODE_MODEL in bootstrap test, commit blackbox .js for CI
55
78
  - docs: fix speed mode quality rating in comparison table (#83)
56
79
  - docs: fix token defaults in env vars table
57
80
  - docs: update README to reflect actual features and fix inaccuracies
@@ -93,10 +116,9 @@ release: v0.20.13 — holistic CLI footer fix + regression tests (#80)
93
116
  ## 0.20.7
94
117
  - fix: ship compiled OpenCode plugin bundle
95
118
  - fix: always show model label in tool.execute.after footer, even with zero savings
96
- - fix: always show model label in tool.execute.after footer, even with zero savings
97
119
  - fix: restore release tarball pack step
98
- Merge pull request #74 from DrunkkToys/codex/release-live-bundle
99
- 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
100
122
 
101
123
 
102
124
  ## 0.20.6
@@ -113,17 +135,17 @@ Merge pull request #72 from DrunkkToys/codex/alpha-token-install-validation
113
135
  - fix: prefer valid api tokens over placeholder env
114
136
  - fix: gate footer stderr by runtime
115
137
  - fix: quiet footer stderr noise
116
- Merge pull request #70 from DrunkkToys/codex/alpha-token-kill-switch
138
+ Merge pull request #70 from DrunkkToys/feature/alpha-token-kill-switch
117
139
 
118
140
 
119
141
  ## 0.20.3
120
142
  - fix: embed valid alpha token fallback
121
- Merge pull request #69 from DrunkkToys/codex/alpha-token-release
143
+ Merge pull request #69 from DrunkkToys/feature/alpha-token-release
122
144
 
123
145
 
124
146
  ## 0.20.2
125
147
  - fix: restore embedded api token fallback
126
- Merge pull request #68 from DrunkkToys/codex/embed-api-token
148
+ Merge pull request #68 from DrunkkToys/feature/embed-api-token
127
149
 
128
150
 
129
151
  ## 0.20.1
@@ -132,8 +154,8 @@ Merge pull request #68 from DrunkkToys/codex/embed-api-token
132
154
 
133
155
  ## 0.20.0
134
156
  - fix: resolve live OpenCode model and refresh README launch copy
135
- Merge pull request #61 from DrunkkToys/codex/release-candidate-blackbox-footer
136
- 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
137
159
 
138
160
 
139
161
  ## 0.19.9
@@ -147,9 +169,9 @@ Fix local blackbox tracker hydration
147
169
  Add model refresh silence regression test
148
170
  Fix blackbox session context
149
171
  Fix thinking directive precedence
150
- Merge pull request #58 from DrunkkToys/codex/fix-home-context
172
+ Merge pull request #58 from DrunkkToys/feature/fix-home-context
151
173
  Fix session state home context
152
- Merge pull request #57 from DrunkkToys/codex/fix-opencode-launch-config
174
+ Merge pull request #57 from DrunkkToys/feature/fix-opencode-launch-config
153
175
  Fix OpenCode launch config
154
176
 
155
177
 
@@ -157,9 +179,9 @@ Fix OpenCode launch config
157
179
  - feat: use native opencode model lists
158
180
  - fix: make OpenCode footer agnostic
159
181
  - fix: make vibeOS compatibility paths dynamic
160
- Merge pull request #56 from DrunkkToys/codex/agnostic-opencode-release
161
- Merge pull request #55 from DrunkkToys/codex/agnostic-opencode-models
162
- 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
163
185
  Bootstrap trinity tiers from OpenCode model
164
186
 
165
187
 
@@ -184,7 +206,7 @@ Bootstrap trinity tiers from OpenCode model
184
206
  - fix: make README and runtime self-contained
185
207
  - test: add 59 integration + e2e tests for cross-module behavior and user workflows
186
208
  Merge pull request #49 from DrunkkToys/oc-desktop-live-savings-refresh
187
- Merge pull request #48 from DrunkkToys/codex/live-savings-refresh
209
+ Merge pull request #48 from DrunkkToys/feature/live-savings-refresh
188
210
  Invalidate savings cache on state writes
189
211
 
190
212
 
@@ -194,7 +216,7 @@ Invalidate savings cache on state writes
194
216
  - fix: stress mitigation directive uses raw stress score, not API-scaled
195
217
  - fix: _refreshModel respects project-local opencode.json over bootstrap default slot
196
218
  - docs: mark v0.19.0 as alpha milestone release
197
- Merge pull request #47 from DrunkkToys/codex/status-lock-backend-fix
219
+ Merge pull request #47 from DrunkkToys/feature/status-lock-backend-fix
198
220
  Expose status lock and backend state
199
221
  Fix stress mitigation and TDD smoke coverage
200
222
  Rebuild bundle after telemetry merge
@@ -223,14 +245,12 @@ Fix budget-first mode and stabilize tests
223
245
  - fix: trinity slots now authoritative over opencode.json model
224
246
 
225
247
  ## 0.18.5
226
- - fix: trinity slots now authoritative over opencode.json model
227
248
 
228
249
 
229
250
  ## 0.18.4
230
251
  - fix: quality tracking now computes avg from lifetime score/count instead of hardcoding 0
231
252
  - fix: savings rate shown with 4 decimal precision (was rounding to $0.00/hr)
232
253
  - fix: cache savings minimum enforced at $0.0001 per scratchpad hit (was rounding to $0)
233
- - fix: ledger reconciliation flushes buffer before reading + uses Math.max() to prevent state drops
234
254
  - fix: model lock no longer overridden by bogus opencode.json model
235
255
 
236
256
  ## 0.18.3
@@ -247,7 +267,6 @@ Fix budget-first mode and stabilize tests
247
267
 
248
268
  ## 0.16.0
249
269
  - feat: dopamine-style footer + natural language system directives
250
- - feat: dopamine-style footer + natural language system directives
251
270
  - feat: turn-aware compaction directive at turn 7+
252
271
  - feat: add forensic/web-research modes + 1084-datapoint benchmark
253
272
  - fix: flash icon only when API connected, unified [VIBE→MODE⚡] format
@@ -289,7 +308,6 @@ footer: TDD tag controlled by blackbox, VIBE replaces AUTO
289
308
 
290
309
  ## 0.15.10
291
310
  - fix: prevent empty footer from message.updated blocking text.complete
292
- - fix: prevent empty footer from message.updated blocking text.complete
293
311
  - fix: deploy copies .env.production alongside plugin
294
312
 
295
313
 
@@ -322,7 +340,6 @@ Build: self-contained bundle (vibeOScore resolved)
322
340
  - fix: remove sticky fallback flag that kills auto mode after single API failure
323
341
  - refactor: architecture simplification and scale readiness
324
342
  - docs: update vibeOS skills to match current plugin behavior
325
- - docs: update vibeOS skills to match current plugin behavior
326
343
  - chore: finalize cleanup
327
344
  - chore: update import paths for vibeOScore monorepo migration
328
345
  Merge pull request #32 from DrunkkToys/refactor/architecture-simplify-scale
@@ -365,15 +382,6 @@ Merge pull request #21 from DrunkkToys/fix/api-token-and-blackbox-control-vector
365
382
 
366
383
 
367
384
  ## 0.14.4
368
- - fix: add contents:write permission to release workflow
369
- - fix: add test:ci script for fast unit tests, separate from integration tests
370
- - fix: configure git identity in release workflow
371
- - fix: exclude slow delegation enforcer test from npm test
372
- - fix: increase test-timeout to 120s for slow delegation enforcer test
373
- - fix: exclude dashboard test from test suite and add --test-timeout=60000
374
- - fix: add --test-timeout=60000 to prevent cancelledByParent test failures in CI
375
- - fix: exclude dashboard from tsconfig to resolve CI build failure
376
- - fix: update API token and add blackboxControlVector client method
377
385
  Merge pull request #24 from DrunkkToys/fix/ci-test-exclude-dashboard
378
386
  Merge pull request #23 from DrunkkToys/fix/ci-test-timeout
379
387
  Merge pull request #22 from DrunkkToys/fix/ci-exclude-dashboard
@@ -533,7 +541,6 @@ Merge pull request #21 from DrunkkToys/fix/api-token-and-blackbox-control-vector
533
541
  - fix: resolution-tracker thresholds - isConverging >=0.5, detectLoop Jaccard 0.6, isRefining >-0.01
534
542
  - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
535
543
  - refactor: merge extracted modules into src/index.ts (6656→1061 lines)
536
- - refactor: extract 16 modules (7207 lines) from src/index.ts into src/lib/
537
544
  - refactor: swap blackbox import to LocalBlackboxStub (forensic)
538
545
  - refactor: blackbox moved to API-server-only — plugin uses local stub
539
546
  - refactor: rename CodeX MCP server to vibeOS MCP server
@@ -561,74 +568,6 @@ test api put
561
568
 
562
569
 
563
570
  ## 0.13.3
564
- - feat: blackbox dynamically controls thinking mode per sub-regime for cost savings
565
- - feat: complete remote API migration — dual-path scoreStress, patternsObserve/Record, TDD exports with local fallback + neutral env test
566
- - feat: complete remote API migration — dual-path scoreStress, patternsObserve/Record, TDD exports with local fallback
567
- - feat: blackbox ML enhancements — real features, loop prevention, pivot detection, outcome tracking, calibration
568
- - feat: v0.10.0 — 6 enhancement phases implemented
569
- - feat: WordPress integration - atomic seat+token creation
570
- - feat: Phase 2 - Integrate remote API client into plugin runtime
571
- - feat: Phase 1 - Remote API server for protected algorithms
572
- - feat: CodeX MCP server and dashboard sidebar plugin integration
573
- - feat: vibeOS TUI dashboard sidebar plugin
574
- - fix: release.mjs — add missing closing brace for deploy else block
575
- - fix: stabilize refactored modules — ES module bindings, setters, missing imports
576
- - fix: flow-enforcer race condition, blackbox default ON, dynamic footer
577
- - fix: lock model name, enforcement logging, TDD framework detection, cache display rounding
578
- - fix: validateState sessions object, remove stale report writes, drop dead code
579
- - fix: state validation, flow TODO dedup, session checkpointing, fetch verification
580
- - fix: _appendFooter full model names, → arrow, inline stress; 361/362 pass
581
- - fix: atomic state writes, safeJsonParse in flow-enforcer, hook error handling (#15)
582
- - fix: model split always shown, stress inline in footer, not separate line
583
- - fix: footer uses slot model name, → arrow, inline stress always, remove session-report writes, disable blackbox default
584
- - fix: sync second footer builder in tool.execute.after with new template
585
- - fix: compact footer with inline stress gauge, full model names, robust test assertions
586
- - fix: footer uses trinity tier model name, all 362 tests pass
587
- - fix: resolve pricing cache corruption, improve TODO extraction, and tune delegation savings
588
- - fix: use dynamic mcp port fallback
589
- - fix: handle mcp server close-reopen race
590
- - fix: await mcp server startup
591
- - fix: harden prompt send and unblock typecheck
592
- - fix: sync opencode.json model with brain tier, restore footer icons (trend arrows, stress gauge)
593
- - fix: deploy script missing vibeOS-api-server/ directory
594
- - fix: footer prepended to output.output, fix tests, remove stale vibeOS/ directory
595
- - fix: migrate footer from context-polluting text.complete to UI-only output.title
596
- - fix: restore experimental.text.complete and message.updated hooks lost during stash
597
- - fix: ensure model-tiers.json is created when no model is detected
598
- - fix: update trinity status test for new dashboard format
599
- - fix: compute cache savings from actual file size, remove /bin/zsh.001 floor, fix state corruption from flow_warns overwrite
600
- - fix: add proper named export for auto-discovery, fix function closure
601
- - fix: add startup toast to verify TUI plugin function execution
602
- - fix: add auto-activation to sync script, add sidebar widget diagnostics
603
- - fix: restore vibeOS sidebar dashboard widget, fix plugin path in opencode config
604
- - fix: add size guard to readJsonOrEmpty to prevent OOM on massive state files
605
- - fix: add generation counter + concurrent-write detection to updateState
606
- - fix: dedup double footer from competing message.updated / text.complete hooks
607
- - fix: append ledger entry in recordSaving() and recordCacheSaving()
608
- - fix: make MCP server close() async, export closeMcpServer for test cleanup
609
- - fix: isolate tests from real config (chdir sandbox, VIBEOS_MCP_PORT=0, HOME cleanup)
610
- - fix: release/deploy synced lib deps - blackbox missing caused footer (and all hooks) to disappear
611
- - fix: resolution-tracker thresholds - isConverging >=0.5, detectLoop Jaccard 0.6, isRefining >-0.01
612
- - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
613
- - refactor: merge extracted modules into src/index.ts (6656→1061 lines)
614
- - refactor: extract 16 modules (7207 lines) from src/index.ts into src/lib/
615
- - refactor: swap blackbox import to LocalBlackboxStub (forensic)
616
- - refactor: blackbox moved to API-server-only — plugin uses local stub
617
- - refactor: rename CodeX MCP server to vibeOS MCP server
618
- - docs: add final stabilization campaign report (#14)
619
- - docs: add stabilization audit reports for sessions 02-06 and 09 (#13)
620
- - docs: add stabilization baseline report (#12)
621
- - docs: update README and AGENTS for remote API protection (Phase 1+2)
622
- - docs: fix brand name, update AGENTS line count, document shell.env hook
623
- - docs: update README and AGENTS for v0.9.1 features
624
- - test: add cross-session restart E2E test (BUG 10)
625
- - chore: remove TDD auto-generated test artifacts
626
- - chore: hardcode public VIBEOS_API_TOKEN as default
627
- - chore: bump to 0.11.0 — blackbox ML engine, loop prevention, pivot detection, API-only architecture
628
- - chore: replace diagnostic log with visible toast
629
- - chore: add secrets to .gitignore (.env.production, PRODUCTION-CREDENTIALS.md)
630
- - ci: add vibeOS test workflow
631
- - chore: v0.9.1
632
571
  bump 0.13.2 — state.ts stub exports, fix ESM import errors
633
572
  bump 0.13.1 — trinity optimize (5 modes + auto), compaction every 10 turns, state.ts stub exports
634
573
  Merge pull request #18 from DrunkkToys/revert/low-value-api-migration
@@ -657,11 +596,7 @@ test api put
657
596
  ## 0.13.0
658
597
 
659
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)
660
- - feat: blackbox dynamically controls thinking mode per sub-regime for cost savings
661
599
  - fix: flow-enforcer race condition, blackbox default ON, dynamic footer improvement
662
- - fix: lock model name, enforcement logging, TDD framework detection, cache display rounding
663
- - perf: conditional directive injection — skip TDD/FLOW/orchestrator when control vector signals relaxed mode
664
- - fix: model split always shown, stress inline in footer, not separate line
665
600
  - fix: atomic state writes, safeJsonParse in flow-enforcer, hook error handling
666
601
  - perf: inline stress in footer, remove session-report writes, disable blackbox default
667
602
  - docs: AGENTS.md updated — 8 hooks (added session.compacting), new src/lib/ architecture
@@ -715,7 +650,6 @@ test api put
715
650
 
716
651
  ## 0.9.1
717
652
  - feat: vibeOS MCP server HTTP API
718
- - feat: vibeOS TUI dashboard sidebar plugin
719
653
  - chore: sync-ts-build and flow-enforcer enhancements
720
654
 
721
655
  ## 0.9.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeostheog",
3
- "version": "0.22.12",
3
+ "version": "0.22.16",
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",
@@ -16,10 +16,10 @@
16
16
  "ts:audit": "node scripts/ts-audit.mjs",
17
17
  "test": "VIBEOS_MCP_PORT=0 node --test --test-timeout=240000 tests/deep_integration.test.mjs tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_api_migration.neutral.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_delegation_enforcer.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_internals_stress_patterns_offtopic.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs tests/test_10fixes_regression.test.mjs tests/test_cross_session_regression.test.mjs tests/test_mega_all_fixes.test.mjs tests/test_smart_cache_regression.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
18
18
  "test:ci": "VIBEOS_MCP_PORT=0 node --test --test-timeout=30000 tests/production_regressions.test.mjs tests/release_hardening_tigerteam.test.mjs tests/test_const_assignment_regression.test.mjs tests/test_diagnose_cmd.test.mjs tests/test_install_and_recovery.test.mjs tests/test_saveos_e2e_cleanup.test.mjs tests/test_tdd_enforcer.test.mjs tests/test_10fixes_regression.test.mjs tests/test_cross_session_regression.test.mjs tests/test_mega_all_fixes.test.mjs tests/test_smart_cache_regression.test.mjs src/tests/*.test.js src/utils/tests/*.test.mjs \"src/vibeOS-lib/tests/auto-select-mode.test.mjs\" \"src/vibeOS-lib/tests/blackbox-regression.test.mjs\" \"src/vibeOS-lib/tests/blackbox-smoke.test.mjs\" \"src/vibeOS-lib/tests/budget-first-mode.test.mjs\" \"src/vibeOS-lib/tests/flow-enforcer.test.mjs\" \"src/vibeOS-lib/tests/flow-secrets.test.mjs\" \"src/vibeOS-lib/tests/session-metrics.test.mjs\" \"src/vibeOS-lib/tests/test_stress.test.mjs\"",
19
- "codex:guard": "bash plugins/vibetheog-codex/scripts/run-guard.sh",
20
- "codex:guard:full": "VIBETHEOG_GUARD_FULL=1 bash plugins/vibetheog-codex/scripts/run-guard.sh",
21
- "codex:hook:precommit": "bash plugins/vibetheog-codex/hooks/pre-commit.sh",
22
- "codex:hook:summary": "bash plugins/vibetheog-codex/hooks/post-command-summary.sh",
19
+ "guard": "bash plugins/vibetheog-guard/scripts/run-guard.sh",
20
+ "guard:full": "VIBETHEOG_GUARD_FULL=1 bash plugins/vibetheog-guard/scripts/run-guard.sh",
21
+ "hook:precommit": "bash plugins/vibetheog-guard/hooks/pre-commit.sh",
22
+ "hook:summary": "bash plugins/vibetheog-guard/hooks/post-command-summary.sh",
23
23
  "precommit": "node scripts/pre-commit.mjs",
24
24
  "audit-state": "node scripts/audit-state.mjs",
25
25
  "migrate-ledger": "node scripts/migrate-ledger.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
  };