@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,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Intent Detector (ST-4) — cross-session task stitching.
|
|
3
|
-
*
|
|
4
|
-
* Walks closed turns + recorded session_files + markers in timeline.db and
|
|
5
|
-
* groups sessions into `intents`. A session attaches to an intent if EITHER:
|
|
6
|
-
*
|
|
7
|
-
* a) it emitted a `mark_intent` whose text matches an existing intent's
|
|
8
|
-
* title (anchored stitch, source="agent_marker"); OR
|
|
9
|
-
* b) its file set has Jaccard overlap > 0.4 with an intent's file_set AND
|
|
10
|
-
* the intent was last active within the freshness window (default 14 d).
|
|
11
|
-
*
|
|
12
|
-
* Otherwise a new intent is created with source="file_jaccard". After each
|
|
13
|
-
* run, intents inactive for > 21 d move to "dormant".
|
|
14
|
-
*
|
|
15
|
-
* Pure functions are exported for testing; the orchestrator `runIntentStitch`
|
|
16
|
-
* does the I/O against CozoTimelineStore.
|
|
17
|
-
*/
|
|
18
|
-
import { randomUUID } from "node:crypto";
|
|
19
|
-
const DEFAULT_JACCARD = 0.4;
|
|
20
|
-
const DEFAULT_FRESHNESS_MS = 14 * 24 * 60 * 60_000;
|
|
21
|
-
const DEFAULT_DORMANT_MS = 21 * 24 * 60 * 60_000;
|
|
22
|
-
export function jaccard(a, b) {
|
|
23
|
-
if (a.size === 0 && b.size === 0)
|
|
24
|
-
return 0;
|
|
25
|
-
let intersection = 0;
|
|
26
|
-
const [small, big] = a.size <= b.size ? [a, b] : [b, a];
|
|
27
|
-
for (const v of small)
|
|
28
|
-
if (big.has(v))
|
|
29
|
-
intersection += 1;
|
|
30
|
-
const union = a.size + b.size - intersection;
|
|
31
|
-
return union === 0 ? 0 : intersection / union;
|
|
32
|
-
}
|
|
33
|
-
export function hashFileSet(files) {
|
|
34
|
-
const sorted = [...new Set(files)].sort();
|
|
35
|
-
// Tiny deterministic hash — enough to dedupe identical file sets without
|
|
36
|
-
// hauling in a crypto dep. Stable across processes since it's data-only.
|
|
37
|
-
let h = 0;
|
|
38
|
-
for (const f of sorted) {
|
|
39
|
-
for (let i = 0; i < f.length; i++) {
|
|
40
|
-
h = (h * 31 + f.charCodeAt(i)) | 0;
|
|
41
|
-
}
|
|
42
|
-
h = (h * 31 + 0x5f) | 0;
|
|
43
|
-
}
|
|
44
|
-
return `${sorted.length}-${(h >>> 0).toString(36)}`;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Pure stitch — given session summaries + existing intents (with their session
|
|
48
|
-
* lists), return updated intents + new attachments. Does NOT call the DB.
|
|
49
|
-
*/
|
|
50
|
-
export function stitchIntents(sessions, existingIntents, existingAttachments, opts = {}) {
|
|
51
|
-
const jaccardThreshold = opts.jaccardThreshold ?? DEFAULT_JACCARD;
|
|
52
|
-
const freshnessMs = opts.freshnessMs ?? DEFAULT_FRESHNESS_MS;
|
|
53
|
-
const dormantAfterMs = opts.dormantAfterMs ?? DEFAULT_DORMANT_MS;
|
|
54
|
-
const now = opts.nowMs ?? Date.now();
|
|
55
|
-
const attachedSessions = new Set(existingAttachments.map((a) => a.session_id));
|
|
56
|
-
const titleIndex = new Map();
|
|
57
|
-
for (const i of existingIntents) {
|
|
58
|
-
if (i.title.length > 0)
|
|
59
|
-
titleIndex.set(i.title.toLowerCase(), i);
|
|
60
|
-
}
|
|
61
|
-
// Working copy of intents; we mutate file_set + last_active_at.
|
|
62
|
-
const intents = new Map();
|
|
63
|
-
for (const i of existingIntents) {
|
|
64
|
-
let files = [];
|
|
65
|
-
try {
|
|
66
|
-
const parsed = JSON.parse(i.file_set || "[]");
|
|
67
|
-
if (Array.isArray(parsed))
|
|
68
|
-
files = parsed.map((x) => String(x));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
files = [];
|
|
72
|
-
}
|
|
73
|
-
intents.set(i.intent_id, { ...i, _filesSet: new Set(files) });
|
|
74
|
-
}
|
|
75
|
-
const newAttachments = [];
|
|
76
|
-
// Stitch each session, oldest-first so deterministic merge order.
|
|
77
|
-
const ordered = [...sessions].sort((a, b) => a.started_at - b.started_at);
|
|
78
|
-
for (const s of ordered) {
|
|
79
|
-
if (attachedSessions.has(s.session_id))
|
|
80
|
-
continue;
|
|
81
|
-
// 1) marker anchor
|
|
82
|
-
if (s.intent_text && s.intent_text.length > 0) {
|
|
83
|
-
const existing = titleIndex.get(s.intent_text.toLowerCase());
|
|
84
|
-
if (existing) {
|
|
85
|
-
attachSession(intents.get(existing.intent_id), s);
|
|
86
|
-
newAttachments.push({
|
|
87
|
-
intent_id: existing.intent_id,
|
|
88
|
-
session_id: s.session_id,
|
|
89
|
-
});
|
|
90
|
-
attachedSessions.add(s.session_id);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const created = createIntent(s, s.intent_text, "agent_marker");
|
|
94
|
-
intents.set(created.intent_id, created);
|
|
95
|
-
titleIndex.set(s.intent_text.toLowerCase(), created);
|
|
96
|
-
newAttachments.push({
|
|
97
|
-
intent_id: created.intent_id,
|
|
98
|
-
session_id: s.session_id,
|
|
99
|
-
});
|
|
100
|
-
attachedSessions.add(s.session_id);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
// 2) Jaccard
|
|
104
|
-
let best = null;
|
|
105
|
-
for (const i of intents.values()) {
|
|
106
|
-
if (s.started_at - i.last_active_at > freshnessMs)
|
|
107
|
-
continue;
|
|
108
|
-
const score = jaccard(s.files, i._filesSet);
|
|
109
|
-
if (score >= jaccardThreshold && (best === null || score > best.score)) {
|
|
110
|
-
best = { intent: i, score };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (best) {
|
|
114
|
-
attachSession(best.intent, s);
|
|
115
|
-
newAttachments.push({
|
|
116
|
-
intent_id: best.intent.intent_id,
|
|
117
|
-
session_id: s.session_id,
|
|
118
|
-
});
|
|
119
|
-
attachedSessions.add(s.session_id);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
// 3) Create fresh
|
|
123
|
-
const created = createIntent(s, deriveTitle(s.files), "file_jaccard");
|
|
124
|
-
intents.set(created.intent_id, created);
|
|
125
|
-
if (created.title.length > 0)
|
|
126
|
-
titleIndex.set(created.title.toLowerCase(), created);
|
|
127
|
-
newAttachments.push({
|
|
128
|
-
intent_id: created.intent_id,
|
|
129
|
-
session_id: s.session_id,
|
|
130
|
-
});
|
|
131
|
-
attachedSessions.add(s.session_id);
|
|
132
|
-
}
|
|
133
|
-
// Pass — dormant transitions
|
|
134
|
-
const dormantTransitions = [];
|
|
135
|
-
for (const i of intents.values()) {
|
|
136
|
-
if (i.status === "active" && now - i.last_active_at > dormantAfterMs) {
|
|
137
|
-
i.status = "dormant";
|
|
138
|
-
dormantTransitions.push(i.intent_id);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
intents: [...intents.values()].map((i) => stripWorkingFields(i)),
|
|
143
|
-
attachments: newAttachments,
|
|
144
|
-
dormantTransitions,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function attachSession(intent, session) {
|
|
148
|
-
for (const f of session.files)
|
|
149
|
-
intent._filesSet.add(f);
|
|
150
|
-
intent.last_active_at = Math.max(intent.last_active_at, session.last_active_at);
|
|
151
|
-
intent.confidence = Math.min(1, intent.confidence + 0.05);
|
|
152
|
-
intent.file_set = JSON.stringify([...intent._filesSet].sort());
|
|
153
|
-
intent.file_set_hash = hashFileSet(intent._filesSet);
|
|
154
|
-
}
|
|
155
|
-
function createIntent(session, title, source) {
|
|
156
|
-
const filesSorted = [...session.files].sort();
|
|
157
|
-
return {
|
|
158
|
-
intent_id: randomUUID(),
|
|
159
|
-
title,
|
|
160
|
-
started_at: session.started_at,
|
|
161
|
-
last_active_at: session.last_active_at,
|
|
162
|
-
file_set: JSON.stringify(filesSorted),
|
|
163
|
-
file_set_hash: hashFileSet(filesSorted),
|
|
164
|
-
status: "active",
|
|
165
|
-
confidence: source === "agent_marker" ? 0.8 : 0.5,
|
|
166
|
-
source,
|
|
167
|
-
_filesSet: new Set(session.files),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
function stripWorkingFields(i) {
|
|
171
|
-
const { _filesSet, ...row } = i;
|
|
172
|
-
void _filesSet;
|
|
173
|
-
return row;
|
|
174
|
-
}
|
|
175
|
-
function deriveTitle(files) {
|
|
176
|
-
if (files.size === 0)
|
|
177
|
-
return "Misc";
|
|
178
|
-
// Pick the most common directory prefix as a title hint.
|
|
179
|
-
const dirCounts = new Map();
|
|
180
|
-
for (const f of files) {
|
|
181
|
-
const dir = f.split("/").slice(0, -1).join("/") || "/";
|
|
182
|
-
dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
|
|
183
|
-
}
|
|
184
|
-
let bestDir = "";
|
|
185
|
-
let bestCount = -1;
|
|
186
|
-
for (const [d, c] of dirCounts) {
|
|
187
|
-
if (c > bestCount) {
|
|
188
|
-
bestDir = d;
|
|
189
|
-
bestCount = c;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return bestDir ? `Work in ${bestDir}` : "Misc";
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Build session summaries from turns + markers fetched from the store.
|
|
196
|
-
*/
|
|
197
|
-
export function buildSessionSummaries(turns, markers, filesBySession) {
|
|
198
|
-
const intentBySession = new Map();
|
|
199
|
-
for (const m of markers) {
|
|
200
|
-
if (m.type === "mark_intent" && !intentBySession.has(m.session_id)) {
|
|
201
|
-
intentBySession.set(m.session_id, m.text);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const byId = new Map();
|
|
205
|
-
for (const t of turns) {
|
|
206
|
-
let s = byId.get(t.session_id);
|
|
207
|
-
if (!s) {
|
|
208
|
-
s = {
|
|
209
|
-
session_id: t.session_id,
|
|
210
|
-
started_at: t.started_at,
|
|
211
|
-
last_active_at: t.ended_at,
|
|
212
|
-
files: filesBySession.get(t.session_id) ?? new Set(),
|
|
213
|
-
intent_text: intentBySession.get(t.session_id),
|
|
214
|
-
};
|
|
215
|
-
byId.set(t.session_id, s);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
s.started_at = Math.min(s.started_at, t.started_at);
|
|
219
|
-
s.last_active_at = Math.max(s.last_active_at, t.ended_at);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return [...byId.values()];
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* IO orchestrator — fetches state from the store, runs stitchIntents, writes
|
|
226
|
-
* back. Safe to call repeatedly (idempotent: previously-attached sessions are
|
|
227
|
-
* skipped).
|
|
228
|
-
*/
|
|
229
|
-
export async function runIntentStitch(store, opts = {}) {
|
|
230
|
-
const turns = await store.listTurns({ limit: 500 });
|
|
231
|
-
const markers = await store.listMarkers({ limit: 1000 });
|
|
232
|
-
const sessionIds = new Set(turns.map((t) => t.session_id));
|
|
233
|
-
const filesBySession = new Map();
|
|
234
|
-
for (const sid of sessionIds) {
|
|
235
|
-
const files = await store.getSessionFiles(sid);
|
|
236
|
-
filesBySession.set(sid, new Set(files));
|
|
237
|
-
}
|
|
238
|
-
const summaries = buildSessionSummaries(turns, markers, filesBySession);
|
|
239
|
-
const existingIntents = await store.listIntents({ limit: 500 });
|
|
240
|
-
const existingAttachments = [];
|
|
241
|
-
for (const i of existingIntents) {
|
|
242
|
-
const sessions = await store.listIntentSessions(i.intent_id);
|
|
243
|
-
for (const sid of sessions) {
|
|
244
|
-
existingAttachments.push({ intent_id: i.intent_id, session_id: sid });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const result = stitchIntents(summaries, existingIntents, existingAttachments, opts);
|
|
248
|
-
let created = 0;
|
|
249
|
-
const existingIds = new Set(existingIntents.map((i) => i.intent_id));
|
|
250
|
-
for (const i of result.intents) {
|
|
251
|
-
await store.upsertIntent(i);
|
|
252
|
-
if (!existingIds.has(i.intent_id))
|
|
253
|
-
created += 1;
|
|
254
|
-
}
|
|
255
|
-
for (const a of result.attachments) {
|
|
256
|
-
await store.attachSession(a.intent_id, a.session_id);
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
created,
|
|
260
|
-
attached: result.attachments.length,
|
|
261
|
-
dormant: result.dormantTransitions.length,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loop Miner (ST-3a).
|
|
3
|
-
*
|
|
4
|
-
* Pure functions over shadow-ledger entries. Surfaces two confusion shapes:
|
|
5
|
-
* 1. File-read loops — same file read ≥5 times within a 10-minute window
|
|
6
|
-
* with no edit in between. Signal that the agent is rereading instead of
|
|
7
|
-
* progressing.
|
|
8
|
-
* 2. Query-search loops — same `search_code` query issued ≥3 times within
|
|
9
|
-
* a 20-minute window. Signal of a gap in the agent's mental model (the
|
|
10
|
-
* function was renamed, the symbol doesn't exist, etc.).
|
|
11
|
-
*
|
|
12
|
-
* Read-only: never writes anything. Callers (the dashboard route, the future
|
|
13
|
-
* insights panel) pass in entries from `ShadowLedger.getRecentEntries()`.
|
|
14
|
-
*/
|
|
15
|
-
const READ_TOOLS = new Set(["file_read", "file_outline", "get_file", "Read"]);
|
|
16
|
-
const EDIT_TOOLS = new Set([
|
|
17
|
-
"Edit",
|
|
18
|
-
"Write",
|
|
19
|
-
"MultiEdit",
|
|
20
|
-
"NotebookEdit",
|
|
21
|
-
"edit_file",
|
|
22
|
-
"write_file",
|
|
23
|
-
]);
|
|
24
|
-
const DEFAULT_READ_WINDOW_MS = 10 * 60_000;
|
|
25
|
-
const DEFAULT_QUERY_WINDOW_MS = 20 * 60_000;
|
|
26
|
-
const DEFAULT_READ_THRESHOLD = 5;
|
|
27
|
-
const DEFAULT_QUERY_THRESHOLD = 3;
|
|
28
|
-
function entryFilePath(entry) {
|
|
29
|
-
const fp = entry.args_summary?.file_path ??
|
|
30
|
-
entry.args_summary?.path;
|
|
31
|
-
return typeof fp === "string" && fp.length > 0 ? fp : null;
|
|
32
|
-
}
|
|
33
|
-
function entryTsMs(entry) {
|
|
34
|
-
return Date.parse(entry.ts);
|
|
35
|
-
}
|
|
36
|
-
export function detectFileReadLoops(entries, opts = {}) {
|
|
37
|
-
const window = opts.readWindowMs ?? DEFAULT_READ_WINDOW_MS;
|
|
38
|
-
const threshold = opts.readThreshold ?? DEFAULT_READ_THRESHOLD;
|
|
39
|
-
const now = opts.nowMs ?? maxTs(entries) ?? Date.now();
|
|
40
|
-
const cutoff = now - window;
|
|
41
|
-
const grouped = new Map();
|
|
42
|
-
for (const e of entries) {
|
|
43
|
-
const ts = entryTsMs(e);
|
|
44
|
-
if (!Number.isFinite(ts) || ts < cutoff)
|
|
45
|
-
continue;
|
|
46
|
-
const fp = entryFilePath(e);
|
|
47
|
-
if (!fp)
|
|
48
|
-
continue;
|
|
49
|
-
const key = `${e.session_id}::${fp}`;
|
|
50
|
-
let bucket = grouped.get(key);
|
|
51
|
-
if (!bucket) {
|
|
52
|
-
bucket = { reads: [], lastEditTs: 0, session_id: e.session_id };
|
|
53
|
-
grouped.set(key, bucket);
|
|
54
|
-
}
|
|
55
|
-
if (EDIT_TOOLS.has(e.tool)) {
|
|
56
|
-
// Edit clears the read run — only reads AFTER the last edit count.
|
|
57
|
-
bucket.reads = [];
|
|
58
|
-
bucket.lastEditTs = ts;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (READ_TOOLS.has(e.tool)) {
|
|
62
|
-
bucket.reads.push(e);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const out = [];
|
|
66
|
-
for (const [key, bucket] of grouped) {
|
|
67
|
-
if (bucket.reads.length < threshold)
|
|
68
|
-
continue;
|
|
69
|
-
const fp = key.split("::").slice(1).join("::");
|
|
70
|
-
const first = bucket.reads[0];
|
|
71
|
-
const last = bucket.reads[bucket.reads.length - 1];
|
|
72
|
-
out.push({
|
|
73
|
-
kind: "file_reread",
|
|
74
|
-
file_path: fp,
|
|
75
|
-
count: bucket.reads.length,
|
|
76
|
-
first_ts: first.ts,
|
|
77
|
-
last_ts: last.ts,
|
|
78
|
-
session_id: bucket.session_id,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return out.sort((a, b) => b.count - a.count);
|
|
82
|
-
}
|
|
83
|
-
export function detectQueryLoops(entries, opts = {}) {
|
|
84
|
-
const window = opts.queryWindowMs ?? DEFAULT_QUERY_WINDOW_MS;
|
|
85
|
-
const threshold = opts.queryThreshold ?? DEFAULT_QUERY_THRESHOLD;
|
|
86
|
-
const now = opts.nowMs ?? maxTs(entries) ?? Date.now();
|
|
87
|
-
const cutoff = now - window;
|
|
88
|
-
const grouped = new Map();
|
|
89
|
-
for (const e of entries) {
|
|
90
|
-
if (e.tool !== "search_code")
|
|
91
|
-
continue;
|
|
92
|
-
const ts = entryTsMs(e);
|
|
93
|
-
if (!Number.isFinite(ts) || ts < cutoff)
|
|
94
|
-
continue;
|
|
95
|
-
const q = e.args_summary?.query;
|
|
96
|
-
if (typeof q !== "string" || q.length === 0)
|
|
97
|
-
continue;
|
|
98
|
-
const key = `${e.session_id}::${q}`;
|
|
99
|
-
const bucket = grouped.get(key) ?? [];
|
|
100
|
-
bucket.push(e);
|
|
101
|
-
grouped.set(key, bucket);
|
|
102
|
-
}
|
|
103
|
-
const out = [];
|
|
104
|
-
for (const [key, bucket] of grouped) {
|
|
105
|
-
if (bucket.length < threshold)
|
|
106
|
-
continue;
|
|
107
|
-
const query = key.split("::").slice(1).join("::");
|
|
108
|
-
const first = bucket[0];
|
|
109
|
-
const last = bucket[bucket.length - 1];
|
|
110
|
-
out.push({
|
|
111
|
-
kind: "search_repeat",
|
|
112
|
-
query,
|
|
113
|
-
count: bucket.length,
|
|
114
|
-
first_ts: first.ts,
|
|
115
|
-
last_ts: last.ts,
|
|
116
|
-
session_id: first.session_id,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
return out.sort((a, b) => b.count - a.count);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Convenience aggregator — runs both detectors and returns the union sorted by
|
|
123
|
-
* recency.
|
|
124
|
-
*/
|
|
125
|
-
export function detectLoops(entries, opts = {}) {
|
|
126
|
-
const all = [
|
|
127
|
-
...detectFileReadLoops(entries, opts),
|
|
128
|
-
...detectQueryLoops(entries, opts),
|
|
129
|
-
];
|
|
130
|
-
return all.sort((a, b) => Date.parse(b.last_ts) - Date.parse(a.last_ts));
|
|
131
|
-
}
|
|
132
|
-
function maxTs(entries) {
|
|
133
|
-
let max = Number.NEGATIVE_INFINITY;
|
|
134
|
-
for (const e of entries) {
|
|
135
|
-
const t = entryTsMs(e);
|
|
136
|
-
if (Number.isFinite(t) && t > max)
|
|
137
|
-
max = t;
|
|
138
|
-
}
|
|
139
|
-
return Number.isFinite(max) ? max : null;
|
|
140
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open Threads (ST-3a).
|
|
3
|
-
*
|
|
4
|
-
* Reads markers from timeline.db and returns mark_blocker rows that do not yet
|
|
5
|
-
* have a matching mark_resolution (matched by blocker_ref → marker_id).
|
|
6
|
-
* Resume strip + insights panel use this to surface unfinished work.
|
|
7
|
-
*
|
|
8
|
-
* Read-only: never writes anything.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Compute open threads from a pre-fetched list of markers. Pure — testable
|
|
12
|
-
* without a database. Resolutions whose blocker_ref points at a blocker outside
|
|
13
|
-
* this list are still counted (lookup by id, not membership).
|
|
14
|
-
*/
|
|
15
|
-
export function computeOpenThreads(markers) {
|
|
16
|
-
const resolvedRefs = new Set();
|
|
17
|
-
for (const m of markers) {
|
|
18
|
-
if (m.type === "mark_resolution" && m.blocker_ref.length > 0) {
|
|
19
|
-
resolvedRefs.add(m.blocker_ref);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
const out = [];
|
|
23
|
-
for (const m of markers) {
|
|
24
|
-
if (m.type !== "mark_blocker")
|
|
25
|
-
continue;
|
|
26
|
-
if (resolvedRefs.has(m.marker_id))
|
|
27
|
-
continue;
|
|
28
|
-
out.push({
|
|
29
|
-
marker_id: m.marker_id,
|
|
30
|
-
text: m.text,
|
|
31
|
-
session_id: m.session_id,
|
|
32
|
-
turn_id: m.turn_id,
|
|
33
|
-
ts: m.ts,
|
|
34
|
-
file_path: m.file_path,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return out.sort((a, b) => b.ts - a.ts);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Async convenience wrapper: pulls markers from the store, then runs
|
|
41
|
-
* computeOpenThreads. Limit caps the underlying marker fetch (default 500).
|
|
42
|
-
*/
|
|
43
|
-
export async function getOpenThreads(store, opts = {}) {
|
|
44
|
-
const markers = await store.listMarkers({
|
|
45
|
-
sessionId: opts.sessionId,
|
|
46
|
-
limit: opts.limit ?? 500,
|
|
47
|
-
});
|
|
48
|
-
return computeOpenThreads(markers);
|
|
49
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Signal Reinforcer (ST-5).
|
|
3
|
-
*
|
|
4
|
-
* Reinforce / contradict derived timeline signals (hot files, loops, co-changes,
|
|
5
|
-
* conventions) in `timeline.db`. Never touches `facts.db` — Layer 9 reinforcement
|
|
6
|
-
* stays on its own path.
|
|
7
|
-
*
|
|
8
|
-
* Confidence clamps to [0, 1]. Reinforcement history is unbounded in storage but
|
|
9
|
-
* the API + UI surface only the most recent 10 events. Stale signals (no
|
|
10
|
-
* reinforcement in `staleAfterMs`) are evicted by `pruneStaleSignals`.
|
|
11
|
-
*/
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
const DEFAULT_STALE_MS = 14 * 24 * 60 * 60_000;
|
|
14
|
-
/**
|
|
15
|
-
* Reinforce (or contradict) a signal. Identity is either an explicit
|
|
16
|
-
* `signal_id` or a `(type, scope)` natural key — the function upserts a row
|
|
17
|
-
* if it doesn't exist yet, then appends a reinforcement event.
|
|
18
|
-
*/
|
|
19
|
-
export async function reinforceSignal(store, identity, delta, source, opts = {}) {
|
|
20
|
-
const now = opts.nowMs ?? Date.now();
|
|
21
|
-
const signal = await resolveSignal(store, identity);
|
|
22
|
-
if (!signal) {
|
|
23
|
-
const created = {
|
|
24
|
-
signal_id: "signal_id" in identity ? identity.signal_id : randomUUID(),
|
|
25
|
-
type: "type" in identity ? identity.type : "unknown",
|
|
26
|
-
scope: "scope" in identity ? identity.scope : "",
|
|
27
|
-
content: "type" in identity ? (identity.content ?? "") : "",
|
|
28
|
-
confidence: clamp01(0.5 + delta),
|
|
29
|
-
first_seen_at: now,
|
|
30
|
-
last_seen_at: now,
|
|
31
|
-
};
|
|
32
|
-
await store.upsertSignal(created);
|
|
33
|
-
await store.appendReinforcement(created.signal_id, now, delta, source);
|
|
34
|
-
return created;
|
|
35
|
-
}
|
|
36
|
-
signal.confidence = clamp01(signal.confidence + delta);
|
|
37
|
-
signal.last_seen_at = now;
|
|
38
|
-
await store.upsertSignal(signal);
|
|
39
|
-
await store.appendReinforcement(signal.signal_id, now, delta, source);
|
|
40
|
-
return signal;
|
|
41
|
-
}
|
|
42
|
-
async function resolveSignal(store, identity) {
|
|
43
|
-
if ("signal_id" in identity) {
|
|
44
|
-
return store.getSignal(identity.signal_id);
|
|
45
|
-
}
|
|
46
|
-
const all = await store.listSignals({ type: identity.type, limit: 500 });
|
|
47
|
-
return all.find((s) => s.scope === identity.scope) ?? null;
|
|
48
|
-
}
|
|
49
|
-
export async function pruneStaleSignals(store, opts = {}) {
|
|
50
|
-
const now = opts.nowMs ?? Date.now();
|
|
51
|
-
const cutoff = now - (opts.staleAfterMs ?? DEFAULT_STALE_MS);
|
|
52
|
-
return store.deleteSignalsBefore(cutoff);
|
|
53
|
-
}
|
|
54
|
-
function clamp01(v) {
|
|
55
|
-
if (!Number.isFinite(v))
|
|
56
|
-
return 0;
|
|
57
|
-
if (v < 0)
|
|
58
|
-
return 0;
|
|
59
|
-
if (v > 1)
|
|
60
|
-
return 1;
|
|
61
|
-
return v;
|
|
62
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timeline subsystem bootstrap.
|
|
3
|
-
*
|
|
4
|
-
* Wires together the three pieces of the Timeline Layer:
|
|
5
|
-
* 1. CozoTimelineStore (timeline.db)
|
|
6
|
-
* 2. The TurnSegmenter embedded in the active ShadowLedger
|
|
7
|
-
* 3. A turn-close listener that computes a rollup and upserts it into turns
|
|
8
|
-
*
|
|
9
|
-
* Kill-switch: `UNERR_TIMELINE_V2=0` short-circuits everything — no db is
|
|
10
|
-
* opened, no listener attaches, the existing proxy/mcp-server code paths are
|
|
11
|
-
* unaffected. Same is true if startup throws — the bootstrap swallows errors
|
|
12
|
-
* after logging so the host process never crashes on timeline issues.
|
|
13
|
-
*
|
|
14
|
-
* No reads or writes to graph.db / facts.db. No edits to the shadow ledger.
|
|
15
|
-
*/
|
|
16
|
-
import { CozoTimelineStore } from "./timeline-store.js";
|
|
17
|
-
const EDIT_TOOLS = new Set([
|
|
18
|
-
"Edit",
|
|
19
|
-
"Write",
|
|
20
|
-
"MultiEdit",
|
|
21
|
-
"NotebookEdit",
|
|
22
|
-
"edit_file",
|
|
23
|
-
"write_file",
|
|
24
|
-
]);
|
|
25
|
-
function defaultLog(level, msg) {
|
|
26
|
-
process.stderr.write(`[unerr:timeline] ${level.toUpperCase()}: ${msg}\n`);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Start the timeline subsystem. Returns null when disabled via env, or when
|
|
30
|
-
* initialisation fails (after logging the error). Callers should treat null
|
|
31
|
-
* as "timeline is off" and continue normally.
|
|
32
|
-
*/
|
|
33
|
-
export async function startTimelineBootstrap(opts) {
|
|
34
|
-
if (process.env.UNERR_TIMELINE_V2 === "0")
|
|
35
|
-
return null;
|
|
36
|
-
const log = opts.log ?? defaultLog;
|
|
37
|
-
let store;
|
|
38
|
-
try {
|
|
39
|
-
store = await CozoTimelineStore.create(opts.projectRoot);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
log("warn", `init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const segmenter = opts.ledger.getTurnSegmenter();
|
|
46
|
-
const unsubscribe = segmenter.onTurnClose((event) => {
|
|
47
|
-
const entries = opts.ledger
|
|
48
|
-
.getRecentEntries(100)
|
|
49
|
-
.filter((e) => e.turn_id === event.turn_id);
|
|
50
|
-
const rollup = computeTurnRollup(event, entries);
|
|
51
|
-
store.upsertTurn(rollup).catch((err) => {
|
|
52
|
-
log("warn", `upsertTurn failed for ${rollup.turn_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
-
});
|
|
54
|
-
// ST-4: persist distinct files touched in this turn so the intent stitcher
|
|
55
|
-
// has a file set to Jaccard against.
|
|
56
|
-
const files = collectFilePaths(entries);
|
|
57
|
-
if (files.length > 0) {
|
|
58
|
-
store
|
|
59
|
-
.recordSessionFiles(event.session_id, files)
|
|
60
|
-
.catch((err) => {
|
|
61
|
-
log("warn", `recordSessionFiles failed for ${event.session_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
// UX-2: best-effort agent name capture. Lazy resolver so late identity
|
|
65
|
-
// (after MCP `initialize`) still gets recorded.
|
|
66
|
-
const agentName = opts.getAgentName?.();
|
|
67
|
-
if (agentName) {
|
|
68
|
-
store
|
|
69
|
-
.setSessionAgent(event.session_id, agentName)
|
|
70
|
-
.catch((err) => {
|
|
71
|
-
log("warn", `setSessionAgent failed for ${event.session_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
log("info", `active (db ${store.dbPath})`);
|
|
76
|
-
return {
|
|
77
|
-
store,
|
|
78
|
-
stop: () => {
|
|
79
|
-
unsubscribe();
|
|
80
|
-
store.close();
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Build a turn rollup from a close event + the entries that belong to it.
|
|
86
|
-
* Exported for testing.
|
|
87
|
-
*/
|
|
88
|
-
export function computeTurnRollup(event, entries) {
|
|
89
|
-
const tsValues = entries
|
|
90
|
-
.map((e) => Date.parse(e.ts))
|
|
91
|
-
.filter((n) => Number.isFinite(n));
|
|
92
|
-
const started_at = tsValues.length > 0 ? Math.min(...tsValues) : event.closed_at;
|
|
93
|
-
const filePaths = new Set();
|
|
94
|
-
let edit_count = 0;
|
|
95
|
-
let intentText;
|
|
96
|
-
const opened_by = entries[0]?.turn_confidence ?? "first_call";
|
|
97
|
-
for (const e of entries) {
|
|
98
|
-
const fp = e.args_summary?.file_path ??
|
|
99
|
-
e.args_summary?.path;
|
|
100
|
-
if (typeof fp === "string" && fp.length > 0)
|
|
101
|
-
filePaths.add(fp);
|
|
102
|
-
if (EDIT_TOOLS.has(e.tool))
|
|
103
|
-
edit_count += 1;
|
|
104
|
-
if (!intentText &&
|
|
105
|
-
e.tool === "mark_intent" &&
|
|
106
|
-
typeof e.args_summary?.text === "string") {
|
|
107
|
-
intentText = e.args_summary.text;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const title = intentText ?? deriveTitleFromFiles(filePaths);
|
|
111
|
-
return {
|
|
112
|
-
turn_id: event.turn_id,
|
|
113
|
-
session_id: event.session_id,
|
|
114
|
-
started_at,
|
|
115
|
-
ended_at: event.closed_at,
|
|
116
|
-
opened_by,
|
|
117
|
-
closed_reason: event.reason,
|
|
118
|
-
tool_count: entries.length,
|
|
119
|
-
file_count: filePaths.size,
|
|
120
|
-
edit_count,
|
|
121
|
-
title,
|
|
122
|
-
outcome: "unknown",
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
/** Extract distinct file paths touched by these entries. */
|
|
126
|
-
export function collectFilePaths(entries) {
|
|
127
|
-
const set = new Set();
|
|
128
|
-
for (const e of entries) {
|
|
129
|
-
const fp = e.args_summary?.file_path ??
|
|
130
|
-
e.args_summary?.path;
|
|
131
|
-
if (typeof fp === "string" && fp.length > 0)
|
|
132
|
-
set.add(fp);
|
|
133
|
-
}
|
|
134
|
-
return [...set];
|
|
135
|
-
}
|
|
136
|
-
function deriveTitleFromFiles(paths) {
|
|
137
|
-
if (paths.size === 0)
|
|
138
|
-
return "";
|
|
139
|
-
// Pick the shortest path as a rough proxy for "most important file" (entry
|
|
140
|
-
// points, index files, top-level modules tend to be shorter). Cheap heuristic
|
|
141
|
-
// until ST-3 adds a smarter title miner.
|
|
142
|
-
let shortest = null;
|
|
143
|
-
for (const p of paths) {
|
|
144
|
-
if (shortest === null || p.length < shortest.length)
|
|
145
|
-
shortest = p;
|
|
146
|
-
}
|
|
147
|
-
if (!shortest)
|
|
148
|
-
return "";
|
|
149
|
-
const base = shortest.split("/").pop() ?? shortest;
|
|
150
|
-
return base;
|
|
151
|
-
}
|