@unerr-ai/unerr 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -45
- package/dist/cli.js +37443 -36022
- package/package.json +2 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Intelligence Response Enrichment — populates the internal `meta` / `context`
|
|
3
|
-
* carrier with risk, confidence, and convention data. These fields never reach
|
|
4
|
-
* the wire — `buildSignalPrefix()` drains them into `ur|<tag>` prefix lines at
|
|
5
|
-
* the wire boundary (MCP clients strip `_meta`/`_context` envelopes).
|
|
6
|
-
*
|
|
7
|
-
* P.9: Every intelligence query response carries (on the internal envelope):
|
|
8
|
-
* - meta.confidence: propagated confidence level
|
|
9
|
-
* - meta.risk_level: entity risk classification → `ur|rsk` when high
|
|
10
|
-
* - meta.resolution_ms: query execution time
|
|
11
|
-
* - context.conventions: applicable conventions → `ur|hnt` / `ur|fct` lines
|
|
12
|
-
*/
|
|
13
|
-
import { confidenceToScore } from "../intelligence/confidence-propagation.js";
|
|
14
|
-
/**
|
|
15
|
-
* Enrich a query response with intelligence metadata.
|
|
16
|
-
*/
|
|
17
|
-
export function enrichResponse(content, meta, context) {
|
|
18
|
-
const result = {
|
|
19
|
-
content,
|
|
20
|
-
_meta: {
|
|
21
|
-
confidence: meta.confidence,
|
|
22
|
-
confidence_score: confidenceToScore(meta.confidence),
|
|
23
|
-
risk_level: meta.riskLevel,
|
|
24
|
-
resolution_ms: Math.round(meta.resolutionMs * 100) / 100,
|
|
25
|
-
edge_sources: meta.edgeSources,
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
if (context && Object.keys(context).length > 0) {
|
|
29
|
-
result._context = context;
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Response Envelope — standardized wrapper for every tool response.
|
|
3
|
-
*
|
|
4
|
-
* Every MCP tool response is enriched with:
|
|
5
|
-
* _meta: { "dev.unerr/tokens_saved", "dev.unerr/latency_ms", "dev.unerr/version" }
|
|
6
|
-
* _context: { optional intelligence injections from blast-radius, conventions, etc. }
|
|
7
|
-
*
|
|
8
|
-
* The envelope pipeline is composable: injectors register themselves and
|
|
9
|
-
* are invoked in order for each response. The session dedup layer sits
|
|
10
|
-
* on top, filtering context that has already been delivered.
|
|
11
|
-
*
|
|
12
|
-
* Token estimation uses char/4 as a fast approximation.
|
|
13
|
-
* Sprint G will replace this with a proper tiktoken-based engine.
|
|
14
|
-
*/
|
|
15
|
-
import { estimateTokens } from "../intelligence/token-estimator.js";
|
|
16
|
-
export { estimateTokens };
|
|
17
|
-
const VERSION = "0.1.0";
|
|
18
|
-
/**
|
|
19
|
-
* Update notification state — set by the daemon's version checker.
|
|
20
|
-
* When set, tool responses include `_meta["dev.unerr/update_available"]`.
|
|
21
|
-
* Only injected when >2 minor versions behind (high signal, low noise).
|
|
22
|
-
*/
|
|
23
|
-
let updateNotification = null;
|
|
24
|
-
export function setUpdateNotification(latest, current) {
|
|
25
|
-
updateNotification = { latest, current };
|
|
26
|
-
}
|
|
27
|
-
export function clearUpdateNotification() {
|
|
28
|
-
updateNotification = null;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create a response envelope pipeline with registered context injectors.
|
|
32
|
-
*/
|
|
33
|
-
export function createEnvelopePipeline(injectors = []) {
|
|
34
|
-
/**
|
|
35
|
-
* Wrap a raw tool response in the standard envelope.
|
|
36
|
-
*
|
|
37
|
-
* @param content - The original tool response content
|
|
38
|
-
* @param latencyMs - Time taken to execute the tool (ms)
|
|
39
|
-
* @param toolName - Name of the MCP tool that was called
|
|
40
|
-
* @param toolArgs - Arguments passed to the tool
|
|
41
|
-
* @param originalTokens - Token count of the raw input (for savings calculation)
|
|
42
|
-
*/
|
|
43
|
-
async function wrapResponse(content, latencyMs, toolName = "", toolArgs = {}, originalTokens = 0) {
|
|
44
|
-
const responseTokens = estimateTokens(content);
|
|
45
|
-
const tokensSaved = Math.max(0, originalTokens - responseTokens);
|
|
46
|
-
const _meta = {
|
|
47
|
-
"dev.unerr/tokens_saved": tokensSaved,
|
|
48
|
-
"dev.unerr/latency_ms": Math.round(latencyMs * 100) / 100,
|
|
49
|
-
"dev.unerr/version": VERSION,
|
|
50
|
-
};
|
|
51
|
-
if (updateNotification) {
|
|
52
|
-
_meta["dev.unerr/update_available"] =
|
|
53
|
-
`${updateNotification.current} → ${updateNotification.latest}`;
|
|
54
|
-
}
|
|
55
|
-
const _context = {};
|
|
56
|
-
for (const injector of injectors) {
|
|
57
|
-
try {
|
|
58
|
-
const injected = await injector.inject({
|
|
59
|
-
toolName,
|
|
60
|
-
toolArgs,
|
|
61
|
-
content,
|
|
62
|
-
latencyMs,
|
|
63
|
-
});
|
|
64
|
-
if (injected) {
|
|
65
|
-
for (const [k, v] of Object.entries(injected)) {
|
|
66
|
-
_context[k] = v;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
/* injector failures are silently swallowed — never break the response */
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const envelope = { content, _meta };
|
|
75
|
-
if (Object.keys(_context).length > 0) {
|
|
76
|
-
envelope._context = _context;
|
|
77
|
-
}
|
|
78
|
-
return envelope;
|
|
79
|
-
}
|
|
80
|
-
return { wrapResponse };
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Tier-2 strip: trim wire-only fields from `_meta` before serialization.
|
|
84
|
-
*
|
|
85
|
-
* Internal callers (proxy stats, eventBus, dashboard SSE) read meta BEFORE this
|
|
86
|
-
* runs; the wire envelope drops fields the agent never acts on.
|
|
87
|
-
*
|
|
88
|
-
* Rules — silent by default, vocal only when actionable:
|
|
89
|
-
* - source / mode / mode_reason: always "local" in OSS → strip (mode kept when degraded)
|
|
90
|
-
* - latency_ms: stderr telemetry only → strip
|
|
91
|
-
* - format / columns / columnar_legend / output_format_legend: in-band
|
|
92
|
-
* `_fmt:columnar` body header is canonical; _meta duplicates → strip
|
|
93
|
-
* - tokens_budget: caller already knows what they asked for → strip
|
|
94
|
-
* - tokens_used / truncated: meaningful only when truncated:true → conditional
|
|
95
|
-
*/
|
|
96
|
-
const WIRE_STRIP_ALWAYS = new Set([
|
|
97
|
-
"source",
|
|
98
|
-
"latency_ms",
|
|
99
|
-
"format",
|
|
100
|
-
"columns",
|
|
101
|
-
"columnar_legend",
|
|
102
|
-
"output_format_legend",
|
|
103
|
-
"tokens_budget",
|
|
104
|
-
]);
|
|
105
|
-
export function wireifyMeta(meta) {
|
|
106
|
-
if (!meta)
|
|
107
|
-
return meta;
|
|
108
|
-
const out = {};
|
|
109
|
-
const isLocalMode = meta.mode === "local";
|
|
110
|
-
const isTruncated = meta.truncated === true;
|
|
111
|
-
for (const [k, v] of Object.entries(meta)) {
|
|
112
|
-
if (WIRE_STRIP_ALWAYS.has(k))
|
|
113
|
-
continue;
|
|
114
|
-
if ((k === "mode" || k === "mode_reason") && isLocalMode)
|
|
115
|
-
continue;
|
|
116
|
-
if (k === "tokens_used" && !isTruncated)
|
|
117
|
-
continue;
|
|
118
|
-
if (k === "truncated" && !isTruncated)
|
|
119
|
-
continue;
|
|
120
|
-
if (v === undefined)
|
|
121
|
-
continue;
|
|
122
|
-
out[k] = v;
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Tier-3 strip: build a leading "ur|<tag>" signal block that goes INTO the
|
|
128
|
-
* tool response body (`content[].text`).
|
|
129
|
-
*
|
|
130
|
-
* Why: per the MCP 2025-06-18 spec and confirmed in Claude Code / OpenAI Agents
|
|
131
|
-
* SDK, `_meta` and custom envelope fields like `_context` are filtered by the
|
|
132
|
-
* client and never reach the LLM context window. To keep anti-drift signals
|
|
133
|
-
* actually visible to the model, we prepend them into the body.
|
|
134
|
-
*
|
|
135
|
-
* Token-optimized prefix format (no brackets/colons — minimal token cost):
|
|
136
|
-
* "ur|<tag> <message>"
|
|
137
|
-
*
|
|
138
|
-
* `ur|` is typically a single BPE token in modern tokenizers. The 3-char tag
|
|
139
|
-
* is also one token. So the entire signal prefix costs ~2-3 tokens of overhead
|
|
140
|
-
* vs ~5 with bracketed notation. Legend lives in agent instruction files
|
|
141
|
-
* (CLAUDE.md, .cursor/rules/*, etc.) via instruction-writer.ts so the LLM
|
|
142
|
-
* knows what each one means without paying for the full word every call.
|
|
143
|
-
*
|
|
144
|
-
* hlt halt loop/circuit break — stop retrying
|
|
145
|
-
* dft drift file/entity changed — re-read before edit
|
|
146
|
-
* rsk risk high blast radius — verify callers
|
|
147
|
-
* wrn warn anti-pattern / negative fact
|
|
148
|
-
* hnt hint guidance / co-change suggestion
|
|
149
|
-
* fct fact surfaced project fact (procedural / convention / semantic)
|
|
150
|
-
* ctx context already delivered — don't re-query
|
|
151
|
-
* hth health session degraded — consider new session
|
|
152
|
-
* hst hist prior failures on this entity
|
|
153
|
-
* ur| generic nudge (no tag)
|
|
154
|
-
*
|
|
155
|
-
* Only emitted when actionable. Marketing/metrics/"survived" causal histories
|
|
156
|
-
* are dropped.
|
|
157
|
-
*/
|
|
158
|
-
export const SIGNAL_PREFIX_LEGEND = `ur|<tag> is an unerr signal appended to MCP responses (as a footer). Tags:
|
|
159
|
-
hlt halt — loop/circuit break: stop retrying this entity, mark_blocker and switch approach
|
|
160
|
-
dft drift — file/entity changed: re-read with file_read/get_entity before edit
|
|
161
|
-
rsk risk — high blast radius: call get_references({direction:'callers'}) before edit
|
|
162
|
-
wrn warn — anti-pattern / negative fact: do not reintroduce the listed pattern
|
|
163
|
-
hnt hint — guidance: apply the named pattern or co-modify the listed files
|
|
164
|
-
fct fact — surfaced project fact (procedural / convention / semantic). Fact subtype is in [brackets] in the message.
|
|
165
|
-
hth health — session degraded: start a new session before the next non-trivial task
|
|
166
|
-
hst hist — prior failure modes: read each mode before retrying the same approach
|
|
167
|
-
pg page — pagination/wire-cap: response was capped. Format: "ur|pg <tool> +<remaining> — <cursorArg>:<nextValue>". Paste <nextValue> back into the same arg to fetch the next slice; the values are concrete numbers, not placeholders.
|
|
168
|
-
ur| generic nudge (no tag)`;
|
|
169
|
-
/** Map signal-scorer types → 3-char wire tag. The scorer emits exactly 4
|
|
170
|
-
* types (see SignalType in signal-scorer.ts): warning, guidance, context,
|
|
171
|
-
* history. The fact_type ([procedural], [convention], etc.) is already
|
|
172
|
-
* embedded in the message content by the scorer. */
|
|
173
|
-
export function signalTag(type) {
|
|
174
|
-
switch (type) {
|
|
175
|
-
case "warning":
|
|
176
|
-
return "wrn";
|
|
177
|
-
case "guidance":
|
|
178
|
-
return "hnt";
|
|
179
|
-
case "context":
|
|
180
|
-
return "fct"; // surfaced fact (procedural / convention / semantic)
|
|
181
|
-
case "history":
|
|
182
|
-
return "hst";
|
|
183
|
-
// Direct fact-type passthrough (not currently emitted but reserved):
|
|
184
|
-
case "convention":
|
|
185
|
-
return "cnv";
|
|
186
|
-
case "procedural":
|
|
187
|
-
return "pro";
|
|
188
|
-
case "semantic":
|
|
189
|
-
return "sem";
|
|
190
|
-
case "episodic":
|
|
191
|
-
return "epi";
|
|
192
|
-
case "negative":
|
|
193
|
-
return "wrn";
|
|
194
|
-
default:
|
|
195
|
-
return (type ?? "inf").slice(0, 3);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Hard caps applied AFTER per-tag dedup. The goal is "high-signal, low-noise":
|
|
200
|
-
* - MAX_LINES keeps the model from being drowned in stacked hints.
|
|
201
|
-
* - MAX_BYTES is a final safety net for pathological message payloads.
|
|
202
|
-
*/
|
|
203
|
-
const MAX_SIGNAL_LINES = 2;
|
|
204
|
-
const MAX_SIGNAL_BYTES = 240;
|
|
205
|
-
import { getSignalDedup } from "./signal-dedup.js";
|
|
206
|
-
export function buildSignalPrefix(meta, context, entityKey = null) {
|
|
207
|
-
const dedup = getSignalDedup();
|
|
208
|
-
const lines = [];
|
|
209
|
-
function tryPush(tag, scopeKey, body) {
|
|
210
|
-
if (lines.length >= MAX_SIGNAL_LINES)
|
|
211
|
-
return;
|
|
212
|
-
if (!dedup.shouldEmit(tag, scopeKey, body))
|
|
213
|
-
return;
|
|
214
|
-
lines.push(`ur|${tag} ${body}`);
|
|
215
|
-
}
|
|
216
|
-
if (meta?.circuit_breaker) {
|
|
217
|
-
const cb = meta.circuit_breaker;
|
|
218
|
-
// Build the message from whatever is concrete; if we have neither attempts
|
|
219
|
-
// nor entity AND no explicit message, the line would be "? failed attempts
|
|
220
|
-
// on entity" — useless. Suppress in that case rather than emit a sentinel.
|
|
221
|
-
let msg = null;
|
|
222
|
-
if (cb.message) {
|
|
223
|
-
msg = cb.message;
|
|
224
|
-
}
|
|
225
|
-
else if (typeof cb.attempts === "number" && cb.entity) {
|
|
226
|
-
msg = `${cb.attempts} failed attempts on ${cb.entity} — stop retrying; mark_blocker and switch approach`;
|
|
227
|
-
}
|
|
228
|
-
else if (cb.entity) {
|
|
229
|
-
msg = `repeated failures on ${cb.entity} — stop retrying; mark_blocker and switch approach`;
|
|
230
|
-
}
|
|
231
|
-
if (msg)
|
|
232
|
-
tryPush("hlt", cb.entity ?? entityKey, msg);
|
|
233
|
-
}
|
|
234
|
-
if (meta?.drift) {
|
|
235
|
-
const d = meta.drift;
|
|
236
|
-
if (d.entityStatus) {
|
|
237
|
-
const by = d.lastModifiedBy ? ` by ${d.lastModifiedBy}` : "";
|
|
238
|
-
const where = d.branch ? ` on ${d.branch}` : "";
|
|
239
|
-
tryPush("dft", entityKey, `${d.entityStatus}${where}${by} — re-read before edit`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (meta?.entity_risk) {
|
|
243
|
-
const r = meta.entity_risk;
|
|
244
|
-
if (r.risk_level === "high") {
|
|
245
|
-
// For array/envelope results (e.g. get_references), `r.entity_key` is the
|
|
246
|
-
// max-risk reference's key, NOT the queried entity. Scoping dedup on that
|
|
247
|
-
// key means each new high-risk neighbor re-fires `ur|rsk` instead of
|
|
248
|
-
// being suppressed by `on_change` against the queried entity.
|
|
249
|
-
const refKey = r.entity_key
|
|
250
|
-
? `${entityKey ?? "?"}:ref:${r.entity_key}`
|
|
251
|
-
: entityKey;
|
|
252
|
-
// Table row #15 TRIM — replace vague "verify blast radius" imperative
|
|
253
|
-
// with a concrete next-call the agent can paste.
|
|
254
|
-
tryPush("rsk", refKey, `fan_in=${r.fan_in ?? 0} fan_out=${r.fan_out ?? 0} (high blast radius — get_references first)`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (meta?.causal_history) {
|
|
258
|
-
const ch = meta.causal_history;
|
|
259
|
-
const realFailures = (ch.failure_modes ?? []).filter((m) => m !== "survived");
|
|
260
|
-
if (realFailures.length > 0) {
|
|
261
|
-
tryPush("hst", entityKey, `${ch.interactions ?? 0} interactions, prior: ${realFailures.join(",")}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Dropped: untagged value_guard (no tag → can't be deduped per-tag, low signal).
|
|
265
|
-
// Dropped: meta.context_complete ur|ctx — session-dedup already gates the
|
|
266
|
-
// underlying context keys; "ctx" line was pure noise.
|
|
267
|
-
if (meta?.session_health) {
|
|
268
|
-
const h = meta.session_health;
|
|
269
|
-
if (typeof h.health === "number" && h.health < 0.6) {
|
|
270
|
-
// Table row #20 TRIM — replace "consider new session" hedge with a
|
|
271
|
-
// concrete action the agent / user can execute.
|
|
272
|
-
tryPush("hth", null, `${(h.health * 100).toFixed(0)}% — ${h.recommendation ?? "start a new session before next task"}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (context?.signals && Array.isArray(context.signals)) {
|
|
276
|
-
const signals = context.signals;
|
|
277
|
-
for (const s of signals) {
|
|
278
|
-
if (lines.length >= MAX_SIGNAL_LINES)
|
|
279
|
-
break;
|
|
280
|
-
if (!s.content)
|
|
281
|
-
continue;
|
|
282
|
-
const tag = signalTag(s.type);
|
|
283
|
-
const action = s.action ? ` — ${s.action}` : "";
|
|
284
|
-
tryPush(tag, s.entity ?? entityKey, `${s.content}${action}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// Dropped: context.tool_adoption.hint — appeared on every call regardless of
|
|
288
|
-
// whether tool was already adopted; the instruction-writer surfaces this at
|
|
289
|
-
// session boot.
|
|
290
|
-
if (lines.length === 0)
|
|
291
|
-
return "";
|
|
292
|
-
let block = `${lines.join("\n")}\n\n`;
|
|
293
|
-
if (block.length > MAX_SIGNAL_BYTES) {
|
|
294
|
-
// Trim from the tail; first signals are typically the most critical
|
|
295
|
-
// (hlt > dft > rsk > hst > hth > scorer signals).
|
|
296
|
-
const truncated = [];
|
|
297
|
-
let used = 0;
|
|
298
|
-
for (const line of lines) {
|
|
299
|
-
if (used + line.length + 1 > MAX_SIGNAL_BYTES - 2)
|
|
300
|
-
break;
|
|
301
|
-
truncated.push(line);
|
|
302
|
-
used += line.length + 1;
|
|
303
|
-
}
|
|
304
|
-
block = truncated.length > 0 ? `${truncated.join("\n")}\n\n` : "";
|
|
305
|
-
}
|
|
306
|
-
return block;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Convenience: wrap a single response without a pipeline.
|
|
310
|
-
*/
|
|
311
|
-
export async function wrapResponse(content, latencyMs, originalTokens = 0) {
|
|
312
|
-
return await createEnvelopePipeline().wrapResponse(content, latencyMs, "", {}, originalTokens);
|
|
313
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session-Aware Context Deduplication — tracks which _context keys have been
|
|
3
|
-
* delivered in this session and filters out repeats.
|
|
4
|
-
*
|
|
5
|
-
* Design: in-memory only (no persistence across sessions — that's Sprint H).
|
|
6
|
-
* Bounded to MAX_TRACKED_KEYS to prevent memory leaks in long sessions.
|
|
7
|
-
*
|
|
8
|
-
* Temporal intelligence note: the dedup window corresponds to the episodic
|
|
9
|
-
* memory tier (Section 13.2). Within a session, repeated context has near-zero
|
|
10
|
-
* value. Cross-session dedup requires the warm/cold tier architecture (Sprint H).
|
|
11
|
-
*/
|
|
12
|
-
const MAX_TRACKED_KEYS = 10_000;
|
|
13
|
-
/**
|
|
14
|
-
* Create a session dedup tracker.
|
|
15
|
-
* Filters context keys that have already been delivered for a given entity.
|
|
16
|
-
*/
|
|
17
|
-
export function createSessionDedup() {
|
|
18
|
-
const delivered = new Map();
|
|
19
|
-
let totalKeys = 0;
|
|
20
|
-
function compoundKey(entityKey, contextKey) {
|
|
21
|
-
return `${entityKey}::${contextKey}`;
|
|
22
|
-
}
|
|
23
|
-
function hasDelivered(entityKey, contextKey) {
|
|
24
|
-
const entitySet = delivered.get(entityKey);
|
|
25
|
-
return entitySet?.has(contextKey) ?? false;
|
|
26
|
-
}
|
|
27
|
-
function markDelivered(entityKey, contextKeys) {
|
|
28
|
-
let entitySet = delivered.get(entityKey);
|
|
29
|
-
if (!entitySet) {
|
|
30
|
-
entitySet = new Set();
|
|
31
|
-
delivered.set(entityKey, entitySet);
|
|
32
|
-
}
|
|
33
|
-
for (const key of contextKeys) {
|
|
34
|
-
if (!entitySet.has(key)) {
|
|
35
|
-
entitySet.add(key);
|
|
36
|
-
totalKeys++;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (totalKeys > MAX_TRACKED_KEYS) {
|
|
40
|
-
evictOldest();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function evictOldest() {
|
|
44
|
-
const iterator = delivered.keys();
|
|
45
|
-
let evicted = 0;
|
|
46
|
-
const target = Math.floor(MAX_TRACKED_KEYS * 0.2);
|
|
47
|
-
while (evicted < target) {
|
|
48
|
-
const result = iterator.next();
|
|
49
|
-
if (result.done)
|
|
50
|
-
break;
|
|
51
|
-
const entityKey = result.value;
|
|
52
|
-
const entitySet = delivered.get(entityKey);
|
|
53
|
-
if (entitySet) {
|
|
54
|
-
evicted += entitySet.size;
|
|
55
|
-
totalKeys -= entitySet.size;
|
|
56
|
-
delivered.delete(entityKey);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function filter(entityKey, context) {
|
|
61
|
-
const filtered = {};
|
|
62
|
-
const newKeys = [];
|
|
63
|
-
for (const [key, value] of Object.entries(context)) {
|
|
64
|
-
if (!hasDelivered(entityKey, key)) {
|
|
65
|
-
filtered[key] = value;
|
|
66
|
-
newKeys.push(key);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (newKeys.length > 0) {
|
|
70
|
-
markDelivered(entityKey, newKeys);
|
|
71
|
-
}
|
|
72
|
-
return filtered;
|
|
73
|
-
}
|
|
74
|
-
function getDeliveredCount() {
|
|
75
|
-
return totalKeys;
|
|
76
|
-
}
|
|
77
|
-
function reset() {
|
|
78
|
-
delivered.clear();
|
|
79
|
-
totalKeys = 0;
|
|
80
|
-
}
|
|
81
|
-
return { filter, hasDelivered, markDelivered, getDeliveredCount, reset };
|
|
82
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 6 Sprint FE-E — session-scoped legend delivery (columnar protocol + output format).
|
|
3
|
-
* Legends attach once per session until invalidated (e.g. retry detected).
|
|
4
|
-
*/
|
|
5
|
-
/** One-time output format legend text (~30 tokens). */
|
|
6
|
-
export const OUTPUT_FORMAT_LEGEND = "Output conventions: use unified diff (---/+++/@@ hunks) for edits. " +
|
|
7
|
-
"When a response begins with `ur|ctx`, all context for this entity was already delivered — skip restatement. " +
|
|
8
|
-
"Prefer file path references over content repetition.";
|
|
9
|
-
export function createSessionLegendTracker() {
|
|
10
|
-
let columnarSent = false;
|
|
11
|
-
let outputFormatSent = false;
|
|
12
|
-
return {
|
|
13
|
-
consumeColumnarLegend() {
|
|
14
|
-
if (columnarSent)
|
|
15
|
-
return false;
|
|
16
|
-
columnarSent = true;
|
|
17
|
-
return true;
|
|
18
|
-
},
|
|
19
|
-
consumeOutputFormatLegend() {
|
|
20
|
-
if (outputFormatSent)
|
|
21
|
-
return false;
|
|
22
|
-
outputFormatSent = true;
|
|
23
|
-
return true;
|
|
24
|
-
},
|
|
25
|
-
invalidateAll() {
|
|
26
|
-
columnarSent = false;
|
|
27
|
-
outputFormatSent = false;
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Persistence — cross-session continuity for --mcp mode.
|
|
3
|
-
*
|
|
4
|
-
* Layer 9 PI-4: On reconnect, loads the previous session's summary and
|
|
5
|
-
* generates a targeted resume context that includes:
|
|
6
|
-
* - What was in progress (hot files, incomplete entities)
|
|
7
|
-
* - Relevant facts from facts.db for those hot files
|
|
8
|
-
* - Session metrics (duration, tools used, revert count)
|
|
9
|
-
*
|
|
10
|
-
* Staleness rule: sessions older than 24h are considered too stale.
|
|
11
|
-
* The agent gets fresh fact recall but no session continuity context.
|
|
12
|
-
*
|
|
13
|
-
* Graceful degradation: if any step fails, returns null (no crash).
|
|
14
|
-
*/
|
|
15
|
-
// ── Constants ────────────────────────────────────────────────────────
|
|
16
|
-
const STALENESS_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
17
|
-
const WARM_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
18
|
-
// ── Public API ───────────────────────────────────────────────────────
|
|
19
|
-
/**
|
|
20
|
-
* Generate a session resume payload from the last session's summary.
|
|
21
|
-
* Integrates with facts.db if a fact store is available.
|
|
22
|
-
*
|
|
23
|
-
* Returns null if:
|
|
24
|
-
* - No prior session exists
|
|
25
|
-
* - Last session is older than 24h (too stale)
|
|
26
|
-
* - Any error occurs (graceful degradation)
|
|
27
|
-
*/
|
|
28
|
-
export async function generateSessionResumePayload(unerrDir, factStore) {
|
|
29
|
-
try {
|
|
30
|
-
const { readLastSession } = await import("../tracking/session-summary-writer.js");
|
|
31
|
-
const lastSession = readLastSession(unerrDir);
|
|
32
|
-
if (!lastSession)
|
|
33
|
-
return null;
|
|
34
|
-
const endedAt = new Date(lastSession.ended_at).getTime();
|
|
35
|
-
const elapsed = Date.now() - endedAt;
|
|
36
|
-
if (elapsed > STALENESS_THRESHOLD_MS)
|
|
37
|
-
return null;
|
|
38
|
-
const staleness = elapsed < WARM_THRESHOLD_MS ? "fresh" : "warm";
|
|
39
|
-
const hotFiles = computeHotFiles(lastSession);
|
|
40
|
-
const incompleteHint = generateIncompleteHint(lastSession);
|
|
41
|
-
const recalledFacts = await recallFactsForSession(lastSession, factStore);
|
|
42
|
-
// Query facts that recently decayed below recall threshold
|
|
43
|
-
let decayedFacts = [];
|
|
44
|
-
if (factStore?.recallDecaying) {
|
|
45
|
-
try {
|
|
46
|
-
const decaying = await factStore.recallDecaying(0.05, 0.2);
|
|
47
|
-
decayedFacts = decaying.slice(0, 5).map((f) => ({
|
|
48
|
-
fact_id: f.fact_id,
|
|
49
|
-
type: f.fact_type,
|
|
50
|
-
content: f.content,
|
|
51
|
-
confidence: Math.round(f.effective_confidence * 100) / 100,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Non-critical
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
session_resumed: true,
|
|
60
|
-
previous_session: {
|
|
61
|
-
session_id: lastSession.session_id,
|
|
62
|
-
duration_ms: lastSession.duration_ms,
|
|
63
|
-
tool_calls: lastSession.tool_calls,
|
|
64
|
-
chains: lastSession.chains,
|
|
65
|
-
files_modified: lastSession.files_modified.slice(0, 10),
|
|
66
|
-
entities_touched: lastSession.entities_touched.slice(0, 10),
|
|
67
|
-
tools_used: lastSession.tools_used,
|
|
68
|
-
feature_areas: lastSession.feature_areas,
|
|
69
|
-
revert_count: lastSession.revert_count,
|
|
70
|
-
facts_recorded: lastSession.facts_recorded,
|
|
71
|
-
branch: lastSession.branch,
|
|
72
|
-
ended_at: lastSession.ended_at,
|
|
73
|
-
},
|
|
74
|
-
continuity: {
|
|
75
|
-
hot_files: hotFiles,
|
|
76
|
-
incomplete_hint: incompleteHint,
|
|
77
|
-
staleness,
|
|
78
|
-
},
|
|
79
|
-
recalled_facts: recalledFacts,
|
|
80
|
-
decayed_since_last_session: decayedFacts,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// ── Internal Helpers ─────────────────────────────────────────────────
|
|
88
|
-
/**
|
|
89
|
-
* Compute the "hot files" — files most likely to be worked on again.
|
|
90
|
-
* Prioritizes files that appeared most frequently in tool calls.
|
|
91
|
-
*/
|
|
92
|
-
function computeHotFiles(session) {
|
|
93
|
-
return session.files_modified.slice(0, 5);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Generate a human-readable hint about what was incomplete.
|
|
97
|
-
*/
|
|
98
|
-
function generateIncompleteHint(session) {
|
|
99
|
-
const parts = [];
|
|
100
|
-
if (session.revert_count > 0) {
|
|
101
|
-
parts.push(`${session.revert_count} revert(s) in last session — approach may need rethinking`);
|
|
102
|
-
}
|
|
103
|
-
if (session.files_modified.length > 5) {
|
|
104
|
-
parts.push(`Large change set (${session.files_modified.length} files) — verify consistency`);
|
|
105
|
-
}
|
|
106
|
-
const highUseTools = Object.entries(session.tools_used)
|
|
107
|
-
.filter(([_, count]) => count > 10)
|
|
108
|
-
.map(([tool]) => tool);
|
|
109
|
-
if (highUseTools.length > 0) {
|
|
110
|
-
parts.push(`Heavy usage: ${highUseTools.join(", ")}`);
|
|
111
|
-
}
|
|
112
|
-
if (parts.length === 0) {
|
|
113
|
-
return session.files_modified.length > 0
|
|
114
|
-
? `Continuing work on ${session.files_modified.slice(0, 3).join(", ")}`
|
|
115
|
-
: "No specific continuity context";
|
|
116
|
-
}
|
|
117
|
-
return parts.join(". ");
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Recall facts relevant to the previous session's hot files.
|
|
121
|
-
* Returns up to 5 facts with highest effective confidence.
|
|
122
|
-
*/
|
|
123
|
-
async function recallFactsForSession(session, factStore) {
|
|
124
|
-
if (!factStore)
|
|
125
|
-
return [];
|
|
126
|
-
try {
|
|
127
|
-
const allFacts = [];
|
|
128
|
-
for (const file of session.files_modified.slice(0, 5)) {
|
|
129
|
-
const facts = await factStore.recallByScope(file);
|
|
130
|
-
for (const f of facts) {
|
|
131
|
-
if (!allFacts.some((existing) => existing.fact_id === f.fact_id)) {
|
|
132
|
-
allFacts.push(f);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const projectFacts = await factStore.recallByScope("project");
|
|
137
|
-
for (const f of projectFacts) {
|
|
138
|
-
if (f.fact_type === "negative" &&
|
|
139
|
-
!allFacts.some((existing) => existing.fact_id === f.fact_id)) {
|
|
140
|
-
allFacts.push(f);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return allFacts
|
|
144
|
-
.sort((a, b) => b.effective_confidence - a.effective_confidence)
|
|
145
|
-
.slice(0, 5)
|
|
146
|
-
.map((f) => ({
|
|
147
|
-
fact_id: f.fact_id,
|
|
148
|
-
type: f.fact_type,
|
|
149
|
-
content: f.content,
|
|
150
|
-
confidence: Math.round(f.effective_confidence * 100) / 100,
|
|
151
|
-
source: f.source,
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// ── Visible Resume Block ────────────────────────────────────────────
|
|
159
|
-
/**
|
|
160
|
-
* Format session resume payload as a visible text block that agents will read.
|
|
161
|
-
* This is prepended to the first tool response content so it's impossible to miss.
|
|
162
|
-
* Keeps total output under 500 chars.
|
|
163
|
-
*/
|
|
164
|
-
export function formatSessionResumeBlock(payload) {
|
|
165
|
-
if (!payload)
|
|
166
|
-
return "";
|
|
167
|
-
const parts = [];
|
|
168
|
-
// Elapsed time
|
|
169
|
-
const endedMs = new Date(payload.previous_session.ended_at).getTime();
|
|
170
|
-
const elapsedMs = Date.now() - endedMs;
|
|
171
|
-
const elapsed = formatElapsed(elapsedMs);
|
|
172
|
-
// Hot files
|
|
173
|
-
const hotFiles = payload.continuity.hot_files.slice(0, 3);
|
|
174
|
-
const filesStr = hotFiles.length > 0 ? hotFiles.join(", ") : "various files";
|
|
175
|
-
parts.push(`[unerr:session-resume] Previous session (${elapsed} ago): worked on ${filesStr}.`);
|
|
176
|
-
// High-confidence recalled facts
|
|
177
|
-
const importantFacts = payload.recalled_facts
|
|
178
|
-
.filter((f) => f.confidence >= 0.5)
|
|
179
|
-
.slice(0, 3);
|
|
180
|
-
for (const f of importantFacts) {
|
|
181
|
-
const pct = Math.round(f.confidence * 100);
|
|
182
|
-
parts.push(`▸ ${f.content} (${pct}%).`);
|
|
183
|
-
}
|
|
184
|
-
// Decayed facts warning
|
|
185
|
-
if (payload.decayed_since_last_session.length > 0) {
|
|
186
|
-
const n = payload.decayed_since_last_session.length;
|
|
187
|
-
const subjects = payload.decayed_since_last_session
|
|
188
|
-
.slice(0, 2)
|
|
189
|
-
.map((f) => `"${f.content.slice(0, 40)}"`);
|
|
190
|
-
parts.push(`⚠ ${n} fact(s) expired since last session: ${subjects.join(", ")}. Use record_fact to re-record if still relevant.`);
|
|
191
|
-
}
|
|
192
|
-
// Incomplete hint if meaningful
|
|
193
|
-
if (payload.continuity.incomplete_hint &&
|
|
194
|
-
payload.continuity.incomplete_hint !== "No specific continuity context") {
|
|
195
|
-
parts.push(`▸ ${payload.continuity.incomplete_hint}`);
|
|
196
|
-
}
|
|
197
|
-
const result = parts.join("\n");
|
|
198
|
-
// Truncate to 500 chars max
|
|
199
|
-
return result.length > 500 ? `${result.slice(0, 497)}...` : result;
|
|
200
|
-
}
|
|
201
|
-
function formatElapsed(ms) {
|
|
202
|
-
const minutes = Math.floor(ms / 60000);
|
|
203
|
-
if (minutes < 60)
|
|
204
|
-
return `${minutes}m`;
|
|
205
|
-
const hours = Math.floor(minutes / 60);
|
|
206
|
-
if (hours < 24)
|
|
207
|
-
return `${hours}h`;
|
|
208
|
-
const days = Math.floor(hours / 24);
|
|
209
|
-
return `${days}d`;
|
|
210
|
-
}
|