recallnest 1.4.1
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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { boundedStringSchema, identifierSchema, normalizedStringListSchema, optionalBoundedStringSchema } from "./schema-utils.js";
|
|
4
|
+
|
|
5
|
+
export const WORKFLOW_OBSERVATION_OUTCOMES = [
|
|
6
|
+
"success",
|
|
7
|
+
"failure",
|
|
8
|
+
"corrected",
|
|
9
|
+
"missed",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export const WorkflowObservationOutcomeSchema = z.enum(WORKFLOW_OBSERVATION_OUTCOMES);
|
|
13
|
+
|
|
14
|
+
export const WorkflowObservationInputSchema = z.object({
|
|
15
|
+
workflowId: identifierSchema("workflowId", 120),
|
|
16
|
+
outcome: WorkflowObservationOutcomeSchema.default("success"),
|
|
17
|
+
summary: boundedStringSchema("summary", 400),
|
|
18
|
+
scope: optionalBoundedStringSchema(160),
|
|
19
|
+
source: boundedStringSchema("source", 40).default("manual"),
|
|
20
|
+
signal: optionalBoundedStringSchema(120),
|
|
21
|
+
task: optionalBoundedStringSchema(240),
|
|
22
|
+
tags: normalizedStringListSchema("tags", 8, 40),
|
|
23
|
+
tools: normalizedStringListSchema("tools", 6, 60),
|
|
24
|
+
recordedAt: z.string().datetime().default(() => new Date().toISOString()),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const WorkflowObservationRecordSchema = WorkflowObservationInputSchema.extend({
|
|
28
|
+
observationId: identifierSchema("observationId", 128),
|
|
29
|
+
resolvedScope: identifierSchema("resolvedScope", 160),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type WorkflowObservationOutcome = z.infer<typeof WorkflowObservationOutcomeSchema>;
|
|
33
|
+
export type WorkflowObservationInput = z.infer<typeof WorkflowObservationInputSchema>;
|
|
34
|
+
export type WorkflowObservationRecord = z.infer<typeof WorkflowObservationRecordSchema>;
|
|
35
|
+
|
|
36
|
+
export interface WorkflowHealthWindow {
|
|
37
|
+
days: number;
|
|
38
|
+
total: number;
|
|
39
|
+
successes: number;
|
|
40
|
+
failures: number;
|
|
41
|
+
corrected: number;
|
|
42
|
+
missed: number;
|
|
43
|
+
issueCount: number;
|
|
44
|
+
successRate: number;
|
|
45
|
+
issueRate: number;
|
|
46
|
+
latestAt?: string;
|
|
47
|
+
topSignals: Array<{ signal: string; count: number }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorkflowHealthReport {
|
|
51
|
+
workflowId: string;
|
|
52
|
+
scope?: string;
|
|
53
|
+
status: "healthy" | "watch" | "critical" | "no-data";
|
|
54
|
+
summary: string;
|
|
55
|
+
latestObservationAt?: string;
|
|
56
|
+
windows: WorkflowHealthWindow[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WorkflowHealthDashboardItem {
|
|
60
|
+
workflowId: string;
|
|
61
|
+
scope?: string;
|
|
62
|
+
status: "healthy" | "watch" | "critical" | "no-data";
|
|
63
|
+
total: number;
|
|
64
|
+
issueCount: number;
|
|
65
|
+
successRate: number;
|
|
66
|
+
latestObservationAt?: string;
|
|
67
|
+
summary: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WorkflowEvidencePack {
|
|
71
|
+
workflowId: string;
|
|
72
|
+
scope?: string;
|
|
73
|
+
generatedAt: string;
|
|
74
|
+
summary: string;
|
|
75
|
+
health: WorkflowHealthReport;
|
|
76
|
+
topSignals: Array<{ signal: string; count: number }>;
|
|
77
|
+
recentIssues: Array<{
|
|
78
|
+
observationId: string;
|
|
79
|
+
outcome: WorkflowObservationOutcome;
|
|
80
|
+
summary: string;
|
|
81
|
+
signal?: string;
|
|
82
|
+
recordedAt: string;
|
|
83
|
+
task?: string;
|
|
84
|
+
}>;
|
|
85
|
+
suggestions: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { buildLegacyScopeKeepReview, suppressesLegacyScopeIssue } from "./legacy-scope-review.js";
|
|
5
|
+
import { classifyLegacyScope } from "./store.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowObservationScopeCue {
|
|
8
|
+
needle: string;
|
|
9
|
+
scope: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WorkflowObservationScopeReviewEntry {
|
|
13
|
+
observationId: string;
|
|
14
|
+
workflowId: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
currentScope: string;
|
|
17
|
+
suggestedScope?: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
task?: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
recordedAt?: string;
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorkflowObservationScopeReview {
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
currentScope: string;
|
|
28
|
+
totalCount: number;
|
|
29
|
+
suggestedCount: number;
|
|
30
|
+
manualReviewCount: number;
|
|
31
|
+
invalidCount: number;
|
|
32
|
+
cues: WorkflowObservationScopeCue[];
|
|
33
|
+
entries: WorkflowObservationScopeReviewEntry[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WorkflowObservationScopeRewriteResult {
|
|
37
|
+
updatedCount: number;
|
|
38
|
+
skippedCount: number;
|
|
39
|
+
updated: Array<{
|
|
40
|
+
observationId: string;
|
|
41
|
+
fromScope: string;
|
|
42
|
+
toScope: string;
|
|
43
|
+
path: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkflowObservationScopeKeepResult {
|
|
48
|
+
reviewedCount: number;
|
|
49
|
+
skippedCount: number;
|
|
50
|
+
reviewed: Array<{
|
|
51
|
+
observationId: string;
|
|
52
|
+
path: string;
|
|
53
|
+
reason: string;
|
|
54
|
+
}>;
|
|
55
|
+
unmatchedIds: string[];
|
|
56
|
+
ambiguousIds: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WorkflowObservationScopeReviewOptions {
|
|
60
|
+
dir?: string;
|
|
61
|
+
currentScope?: string;
|
|
62
|
+
source?: string;
|
|
63
|
+
workflowId?: string;
|
|
64
|
+
cues?: WorkflowObservationScopeCue[];
|
|
65
|
+
neighborWindowSeconds?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function defaultDir(): string {
|
|
69
|
+
return resolve(import.meta.dir, "../data/workflow-observations");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clip(text: string, maxLen = 72): string {
|
|
73
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
74
|
+
if (!compact) return "-";
|
|
75
|
+
return compact.length <= maxLen ? compact : `${compact.slice(0, maxLen - 3)}...`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizedHaystack(record: Record<string, unknown>): string {
|
|
79
|
+
return `${typeof record.task === "string" ? record.task : ""}\n${typeof record.summary === "string" ? record.summary : ""}`
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeRecordedAt(value: unknown): string | undefined {
|
|
84
|
+
if (typeof value !== "string") return undefined;
|
|
85
|
+
return Number.isFinite(Date.parse(value)) ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ParsedObservationRecord {
|
|
89
|
+
observationId: string;
|
|
90
|
+
workflowId?: string;
|
|
91
|
+
source?: string;
|
|
92
|
+
resolvedScope?: string;
|
|
93
|
+
task?: string;
|
|
94
|
+
summary?: string;
|
|
95
|
+
recordedAt?: string;
|
|
96
|
+
path: string;
|
|
97
|
+
raw: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseObservationRecord(path: string, raw: Record<string, unknown>): ParsedObservationRecord {
|
|
101
|
+
return {
|
|
102
|
+
observationId: typeof raw.observationId === "string" ? raw.observationId : basename(path, ".json"),
|
|
103
|
+
workflowId: typeof raw.workflowId === "string" ? raw.workflowId : undefined,
|
|
104
|
+
source: typeof raw.source === "string" ? raw.source : undefined,
|
|
105
|
+
resolvedScope: typeof raw.resolvedScope === "string" ? raw.resolvedScope : undefined,
|
|
106
|
+
task: typeof raw.task === "string" ? raw.task : undefined,
|
|
107
|
+
summary: typeof raw.summary === "string" ? raw.summary : undefined,
|
|
108
|
+
recordedAt: normalizeRecordedAt(raw.recordedAt),
|
|
109
|
+
path,
|
|
110
|
+
raw,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function inferNeighborScope(
|
|
115
|
+
entry: ParsedObservationRecord,
|
|
116
|
+
records: ParsedObservationRecord[],
|
|
117
|
+
currentScope: string,
|
|
118
|
+
windowSeconds: number,
|
|
119
|
+
): { scope?: string; reason?: string } {
|
|
120
|
+
const entryMs = Date.parse(entry.recordedAt || "");
|
|
121
|
+
if (!Number.isFinite(entryMs) || windowSeconds <= 0) return {};
|
|
122
|
+
|
|
123
|
+
const windowMs = windowSeconds * 1000;
|
|
124
|
+
const neighbors = records
|
|
125
|
+
.filter((record) => record.observationId !== entry.observationId)
|
|
126
|
+
.filter((record) => record.resolvedScope && record.resolvedScope !== currentScope)
|
|
127
|
+
.filter((record) => record.source === entry.source)
|
|
128
|
+
.filter((record) => {
|
|
129
|
+
const recordMs = Date.parse(record.recordedAt || "");
|
|
130
|
+
return Number.isFinite(recordMs) && Math.abs(recordMs - entryMs) <= windowMs;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const scopes = [...new Set(neighbors.map((record) => record.resolvedScope!).filter(Boolean))];
|
|
134
|
+
if (scopes.length !== 1) return {};
|
|
135
|
+
|
|
136
|
+
const nearest = [...neighbors]
|
|
137
|
+
.sort((a, b) => {
|
|
138
|
+
const aDiff = Math.abs(Date.parse(a.recordedAt || "") - entryMs);
|
|
139
|
+
const bDiff = Math.abs(Date.parse(b.recordedAt || "") - entryMs);
|
|
140
|
+
return aDiff - bDiff;
|
|
141
|
+
})[0];
|
|
142
|
+
if (!nearest) return {};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
scope: scopes[0],
|
|
146
|
+
reason: `matched nearby ${nearest.workflowId || "workflow"} ${nearest.observationId.slice(0, 8)} within ${windowSeconds}s`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseWorkflowObservationScopeCue(raw: string): WorkflowObservationScopeCue {
|
|
151
|
+
const separator = raw.indexOf("=");
|
|
152
|
+
if (separator <= 0 || separator === raw.length - 1) {
|
|
153
|
+
throw new Error(`Invalid cue "${raw}". Use needle=scope, for example recallnest=project:recallnest.`);
|
|
154
|
+
}
|
|
155
|
+
const needle = raw.slice(0, separator).trim().toLowerCase();
|
|
156
|
+
const scope = raw.slice(separator + 1).trim();
|
|
157
|
+
if (!needle || !scope) {
|
|
158
|
+
throw new Error(`Invalid cue "${raw}". Use needle=scope, for example recallnest=project:recallnest.`);
|
|
159
|
+
}
|
|
160
|
+
return { needle, scope };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reviewEntry(
|
|
164
|
+
entry: ParsedObservationRecord,
|
|
165
|
+
currentScope: string,
|
|
166
|
+
cues: WorkflowObservationScopeCue[],
|
|
167
|
+
allRecords: ParsedObservationRecord[],
|
|
168
|
+
neighborWindowSeconds: number,
|
|
169
|
+
): WorkflowObservationScopeReviewEntry {
|
|
170
|
+
const haystack = normalizedHaystack(entry.raw);
|
|
171
|
+
const matches = cues.filter((cue) => haystack.includes(cue.needle));
|
|
172
|
+
const matchedScopes = [...new Set(matches.map((cue) => cue.scope))];
|
|
173
|
+
|
|
174
|
+
let suggestedScope: string | undefined;
|
|
175
|
+
let reason = "no cue match";
|
|
176
|
+
if (matchedScopes.length === 1) {
|
|
177
|
+
suggestedScope = matchedScopes[0];
|
|
178
|
+
const cue = matches.find((item) => item.scope === suggestedScope)?.needle || suggestedScope;
|
|
179
|
+
reason = `matched cue "${cue}"`;
|
|
180
|
+
} else if (matchedScopes.length > 1) {
|
|
181
|
+
reason = `conflicting cues: ${matchedScopes.join(", ")}`;
|
|
182
|
+
} else if (cues.length === 0) {
|
|
183
|
+
reason = "no cues configured";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!suggestedScope) {
|
|
187
|
+
const neighbor = inferNeighborScope(entry, allRecords, currentScope, neighborWindowSeconds);
|
|
188
|
+
if (neighbor.scope) {
|
|
189
|
+
suggestedScope = neighbor.scope;
|
|
190
|
+
reason = neighbor.reason || "matched nearby observation";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
observationId: entry.observationId,
|
|
196
|
+
workflowId: entry.workflowId || "workflow",
|
|
197
|
+
source: entry.source,
|
|
198
|
+
currentScope,
|
|
199
|
+
suggestedScope,
|
|
200
|
+
reason,
|
|
201
|
+
task: entry.task,
|
|
202
|
+
summary: entry.summary || "",
|
|
203
|
+
recordedAt: entry.recordedAt,
|
|
204
|
+
path: entry.path,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function reviewWorkflowObservationScopes(
|
|
209
|
+
options: WorkflowObservationScopeReviewOptions = {},
|
|
210
|
+
): WorkflowObservationScopeReview {
|
|
211
|
+
const dir = options.dir || defaultDir();
|
|
212
|
+
const currentScope = options.currentScope || "global";
|
|
213
|
+
const cues = options.cues || [];
|
|
214
|
+
|
|
215
|
+
if (!existsSync(dir)) {
|
|
216
|
+
return {
|
|
217
|
+
generatedAt: new Date().toISOString(),
|
|
218
|
+
currentScope,
|
|
219
|
+
totalCount: 0,
|
|
220
|
+
suggestedCount: 0,
|
|
221
|
+
manualReviewCount: 0,
|
|
222
|
+
invalidCount: 0,
|
|
223
|
+
cues,
|
|
224
|
+
entries: [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const files = readdirSync(dir)
|
|
229
|
+
.filter((name) => name.endsWith(".json"))
|
|
230
|
+
.map((name) => join(dir, name));
|
|
231
|
+
|
|
232
|
+
const parsedRecords: ParsedObservationRecord[] = [];
|
|
233
|
+
const entries: WorkflowObservationScopeReviewEntry[] = [];
|
|
234
|
+
let invalidCount = 0;
|
|
235
|
+
|
|
236
|
+
for (const path of files) {
|
|
237
|
+
let raw: Record<string, unknown>;
|
|
238
|
+
try {
|
|
239
|
+
raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
240
|
+
} catch {
|
|
241
|
+
invalidCount += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
parsedRecords.push(parseObservationRecord(path, raw));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const neighborWindowSeconds = Math.max(0, Math.trunc(options.neighborWindowSeconds ?? 90));
|
|
248
|
+
for (const record of parsedRecords) {
|
|
249
|
+
if (record.resolvedScope !== currentScope) continue;
|
|
250
|
+
if (options.source && record.source !== options.source) continue;
|
|
251
|
+
if (options.workflowId && record.workflowId !== options.workflowId) continue;
|
|
252
|
+
const kind = classifyLegacyScope(record.resolvedScope);
|
|
253
|
+
if (kind && suppressesLegacyScopeIssue(record.raw.legacyScopeReview, kind)) continue;
|
|
254
|
+
entries.push(reviewEntry(record, currentScope, cues, parsedRecords, neighborWindowSeconds));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
entries.sort((a, b) => {
|
|
258
|
+
const timeDiff = Date.parse(b.recordedAt || "") - Date.parse(a.recordedAt || "");
|
|
259
|
+
if (Number.isFinite(timeDiff) && timeDiff !== 0) return timeDiff;
|
|
260
|
+
return a.observationId.localeCompare(b.observationId);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const suggestedCount = entries.filter((entry) => entry.suggestedScope).length;
|
|
264
|
+
return {
|
|
265
|
+
generatedAt: new Date().toISOString(),
|
|
266
|
+
currentScope,
|
|
267
|
+
totalCount: entries.length,
|
|
268
|
+
suggestedCount,
|
|
269
|
+
manualReviewCount: entries.length - suggestedCount,
|
|
270
|
+
invalidCount,
|
|
271
|
+
cues,
|
|
272
|
+
entries,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function applyWorkflowObservationScopeSuggestions(
|
|
277
|
+
review: WorkflowObservationScopeReview,
|
|
278
|
+
): WorkflowObservationScopeRewriteResult {
|
|
279
|
+
const updated: WorkflowObservationScopeRewriteResult["updated"] = [];
|
|
280
|
+
let skippedCount = 0;
|
|
281
|
+
|
|
282
|
+
for (const entry of review.entries) {
|
|
283
|
+
if (!entry.suggestedScope) {
|
|
284
|
+
skippedCount += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const parsed = JSON.parse(readFileSync(entry.path, "utf-8")) as Record<string, unknown>;
|
|
289
|
+
parsed.scope = entry.suggestedScope;
|
|
290
|
+
parsed.resolvedScope = entry.suggestedScope;
|
|
291
|
+
writeFileSync(entry.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
292
|
+
updated.push({
|
|
293
|
+
observationId: entry.observationId,
|
|
294
|
+
fromScope: entry.currentScope,
|
|
295
|
+
toScope: entry.suggestedScope,
|
|
296
|
+
path: entry.path,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
updatedCount: updated.length,
|
|
302
|
+
skippedCount,
|
|
303
|
+
updated,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function matchesObservationSelector(observationId: string, selector: string): boolean {
|
|
308
|
+
const normalizedId = observationId.trim().toLowerCase();
|
|
309
|
+
const normalizedSelector = selector.trim().toLowerCase();
|
|
310
|
+
return normalizedSelector.length > 0
|
|
311
|
+
&& (normalizedId === normalizedSelector || normalizedId.startsWith(normalizedSelector));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function keepWorkflowObservationScopesGlobal(
|
|
315
|
+
review: WorkflowObservationScopeReview,
|
|
316
|
+
options: {
|
|
317
|
+
ids: string[];
|
|
318
|
+
reason: string;
|
|
319
|
+
reviewedAt?: string;
|
|
320
|
+
},
|
|
321
|
+
): WorkflowObservationScopeKeepResult {
|
|
322
|
+
const reviewed: WorkflowObservationScopeKeepResult["reviewed"] = [];
|
|
323
|
+
const unmatchedIds: string[] = [];
|
|
324
|
+
const ambiguousIds: string[] = [];
|
|
325
|
+
let skippedCount = 0;
|
|
326
|
+
|
|
327
|
+
const reason = options.reason.trim();
|
|
328
|
+
if (!reason) {
|
|
329
|
+
throw new Error("keepWorkflowObservationScopesGlobal requires a non-empty reason.");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const kind = classifyLegacyScope(review.currentScope);
|
|
333
|
+
if (!kind) {
|
|
334
|
+
throw new Error(`Current scope "${review.currentScope}" is not a legacy scope kind that can be kept.`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const uniqueSelectors = [...new Set(options.ids.map((value) => value.trim()).filter(Boolean))];
|
|
338
|
+
for (const selector of uniqueSelectors) {
|
|
339
|
+
const matches = review.entries.filter((entry) => matchesObservationSelector(entry.observationId, selector));
|
|
340
|
+
if (matches.length === 0) {
|
|
341
|
+
unmatchedIds.push(selector);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (matches.length > 1) {
|
|
345
|
+
ambiguousIds.push(selector);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const entry = matches[0]!;
|
|
350
|
+
if (entry.suggestedScope) {
|
|
351
|
+
skippedCount += 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const parsed = JSON.parse(readFileSync(entry.path, "utf-8")) as Record<string, unknown>;
|
|
356
|
+
parsed.legacyScopeReview = buildLegacyScopeKeepReview(kind, reason, options.reviewedAt);
|
|
357
|
+
writeFileSync(entry.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
358
|
+
reviewed.push({
|
|
359
|
+
observationId: entry.observationId,
|
|
360
|
+
path: entry.path,
|
|
361
|
+
reason,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
reviewedCount: reviewed.length,
|
|
367
|
+
skippedCount,
|
|
368
|
+
reviewed,
|
|
369
|
+
unmatchedIds,
|
|
370
|
+
ambiguousIds,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function formatWorkflowObservationScopeReview(review: WorkflowObservationScopeReview): string {
|
|
375
|
+
const lines = [
|
|
376
|
+
"Workflow observation scope review",
|
|
377
|
+
"",
|
|
378
|
+
` Generated at : ${review.generatedAt}`,
|
|
379
|
+
` Current scope: ${review.currentScope}`,
|
|
380
|
+
` Records : ${review.totalCount}`,
|
|
381
|
+
` Suggested : ${review.suggestedCount}`,
|
|
382
|
+
` Manual review: ${review.manualReviewCount}`,
|
|
383
|
+
` Invalid files: ${review.invalidCount}`,
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
if (review.cues.length > 0) {
|
|
387
|
+
lines.push(` Cues : ${review.cues.map((cue) => `${cue.needle}=${cue.scope}`).join(", ")}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (review.entries.length === 0) {
|
|
391
|
+
lines.push("", "No matching workflow observations found.");
|
|
392
|
+
return lines.join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
lines.push("", " Review rows:");
|
|
396
|
+
lines.push(" ID Workflow Source Suggestion Date Task / Summary");
|
|
397
|
+
lines.push(" -------- ------------------ --------- -------------------- ---------- --------------");
|
|
398
|
+
for (const entry of review.entries) {
|
|
399
|
+
const taskOrSummary = entry.task || entry.summary;
|
|
400
|
+
const suggestion = entry.suggestedScope
|
|
401
|
+
? `${entry.suggestedScope} (${entry.reason})`
|
|
402
|
+
: `manual (${entry.reason})`;
|
|
403
|
+
lines.push(
|
|
404
|
+
` ${entry.observationId.slice(0, 8)} ${clip(entry.workflowId, 18).padEnd(18)} ${clip(entry.source || "-", 9).padEnd(9)} ${clip(suggestion, 20).padEnd(20)} ${(entry.recordedAt || "-").slice(0, 10).padEnd(10)} ${clip(taskOrSummary, 56)}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return lines.join("\n");
|
|
409
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkflowObservationOutcome, WorkflowObservationRecord } from "./workflow-observation-schema.js";
|
|
5
|
+
import { WorkflowObservationRecordSchema } from "./workflow-observation-schema.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowObservationQuery {
|
|
8
|
+
workflowId?: string;
|
|
9
|
+
scope?: string;
|
|
10
|
+
outcome?: WorkflowObservationOutcome;
|
|
11
|
+
limit?: number;
|
|
12
|
+
since?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureDir(dir: string): string {
|
|
16
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sortNewestFirst(records: WorkflowObservationRecord[]): WorkflowObservationRecord[] {
|
|
21
|
+
return [...records].sort((a, b) => {
|
|
22
|
+
const timeDiff = Date.parse(b.recordedAt) - Date.parse(a.recordedAt);
|
|
23
|
+
if (timeDiff !== 0) return timeDiff;
|
|
24
|
+
return b.observationId.localeCompare(a.observationId);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WorkflowObservationStore {
|
|
29
|
+
constructor(private readonly dir = resolve(import.meta.dir, "../data/workflow-observations")) {}
|
|
30
|
+
|
|
31
|
+
get dataDir(): string {
|
|
32
|
+
return ensureDir(this.dir);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async save(record: WorkflowObservationRecord): Promise<WorkflowObservationRecord> {
|
|
36
|
+
const parsed = WorkflowObservationRecordSchema.parse(record);
|
|
37
|
+
const timestampToken = parsed.recordedAt.replace(/[:.]/g, "-");
|
|
38
|
+
const path = join(this.dataDir, `${timestampToken}-${parsed.observationId}.json`);
|
|
39
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listRecent(query: WorkflowObservationQuery = {}): Promise<WorkflowObservationRecord[]> {
|
|
44
|
+
const { workflowId, scope, outcome, limit = 200, since } = query;
|
|
45
|
+
const sinceTime = since ? Date.parse(since) : Number.NEGATIVE_INFINITY;
|
|
46
|
+
const files = readdirSync(this.dataDir)
|
|
47
|
+
.filter((name) => name.endsWith(".json"))
|
|
48
|
+
.map((name) => join(this.dataDir, name));
|
|
49
|
+
|
|
50
|
+
const items: WorkflowObservationRecord[] = [];
|
|
51
|
+
for (const path of files) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = WorkflowObservationRecordSchema.parse(
|
|
54
|
+
JSON.parse(readFileSync(path, "utf-8")),
|
|
55
|
+
);
|
|
56
|
+
if (workflowId && parsed.workflowId !== workflowId) continue;
|
|
57
|
+
if (scope && parsed.resolvedScope !== scope) continue;
|
|
58
|
+
if (outcome && parsed.outcome !== outcome) continue;
|
|
59
|
+
if (Date.parse(parsed.recordedAt) < sinceTime) continue;
|
|
60
|
+
items.push(parsed);
|
|
61
|
+
} catch {
|
|
62
|
+
// Skip corrupt observation files.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return sortNewestFirst(items).slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|