claude-mem-lite 2.3.0 → 2.3.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +2 -7
- package/commands/mem.md +7 -0
- package/commands/memory.md +51 -0
- package/commands/tools.md +78 -0
- package/commands/update.md +38 -0
- package/dispatch-inject.mjs +5 -4
- package/dispatch-workflow.mjs +155 -0
- package/dispatch.mjs +37 -15
- package/hook-handoff.mjs +222 -0
- package/hook-shared.mjs +10 -6
- package/hook.mjs +6 -5
- package/hooks/hooks.json +1 -1
- package/install.mjs +440 -11
- package/package.json +6 -1
- package/registry/preinstalled.json +0 -13
- package/registry-retriever.mjs +0 -3
- package/registry.mjs +1 -1
- package/schema.mjs +1 -0
- package/scripts/setup.sh +20 -1
- package/server.mjs +153 -159
- package/tool-schemas.mjs +4 -2
- package/utils.mjs +10 -2
package/.mcp.json
CHANGED
package/commands/mem.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: mem
|
|
2
3
|
description: Search and manage project memory (observations, sessions, prompts)
|
|
3
4
|
---
|
|
4
5
|
|
|
@@ -13,6 +14,9 @@ Search and browse your project memory efficiently.
|
|
|
13
14
|
- `/mem save <text>` — Save a manual memory/note
|
|
14
15
|
- `/mem stats` — Show memory statistics
|
|
15
16
|
- `/mem timeline <query>` — Browse timeline around a matching observation
|
|
17
|
+
- `/mem cleanup` — Scan and interactively purge stale data
|
|
18
|
+
- `/mem cleanup [N]d` — Purge stale data older than N days (e.g. `cleanup 60d`)
|
|
19
|
+
- `/mem cleanup keep [N]d` — Purge stale data but retain last N days (e.g. `cleanup keep 14d`)
|
|
16
20
|
|
|
17
21
|
## Efficient Search Workflow (3 steps, saves 10x tokens)
|
|
18
22
|
|
|
@@ -29,6 +33,9 @@ When the user invokes `/mem`, parse their intent:
|
|
|
29
33
|
- `/mem save <text>` → call `mem_save` with the text as content
|
|
30
34
|
- `/mem stats` → call `mem_stats`
|
|
31
35
|
- `/mem timeline <query>` → call `mem_timeline` with the query
|
|
36
|
+
- `/mem cleanup` → run `mem_maintain(action="scan")`, report pending purge count and stale items to user, ask for confirmation, then run `mem_maintain(action="execute", operations=["purge_stale"])` if confirmed
|
|
37
|
+
- `/mem cleanup Nd` (e.g. `60d`) → same as above but use `retain_days=N` to only purge items older than N days
|
|
38
|
+
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `retain_days=N`
|
|
32
39
|
- `/mem <query>` (no subcommand) → treat as search, call `mem_search`
|
|
33
40
|
|
|
34
41
|
Always use the compact index from mem_search first, then mem_get for details only when needed. This minimizes token usage.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory
|
|
3
|
+
description: Save content to memory — with explicit content, instructions, or auto-summarize current session
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory Save
|
|
7
|
+
|
|
8
|
+
Save important content to your long-term memory database.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
- `/mem:memory <content>` — Save the given content directly to memory
|
|
13
|
+
- `/mem:memory` (no args) — Auto-summarize recent session highlights and save key findings
|
|
14
|
+
|
|
15
|
+
## Instructions
|
|
16
|
+
|
|
17
|
+
When the user invokes `/mem:memory`, determine the intent:
|
|
18
|
+
|
|
19
|
+
### With explicit content
|
|
20
|
+
|
|
21
|
+
If the user provides content after the command:
|
|
22
|
+
|
|
23
|
+
1. Analyze the content to determine appropriate type (decision, bugfix, feature, refactor, discovery, change)
|
|
24
|
+
2. Generate a concise title from the content
|
|
25
|
+
3. Call `mem_save` with:
|
|
26
|
+
- `content`: the provided text
|
|
27
|
+
- `title`: auto-generated title
|
|
28
|
+
- `type`: inferred type (default: "discovery")
|
|
29
|
+
- `importance`: 2 (notable — user explicitly requested save)
|
|
30
|
+
|
|
31
|
+
### With instructions/prompt
|
|
32
|
+
|
|
33
|
+
If the user provides instructions like "save the database schema we discussed" or "remember the fix for the auth bug":
|
|
34
|
+
|
|
35
|
+
1. Review recent conversation context
|
|
36
|
+
2. Extract the relevant information per the user's instruction
|
|
37
|
+
3. Call `mem_save` with extracted content, appropriate title and type, importance=2
|
|
38
|
+
|
|
39
|
+
### No arguments (auto-save)
|
|
40
|
+
|
|
41
|
+
If no content is provided:
|
|
42
|
+
|
|
43
|
+
1. Review the current session's recent key findings
|
|
44
|
+
2. Identify: decisions made, bugs fixed, patterns discovered, important code changes
|
|
45
|
+
3. For each significant finding (max 5), call `mem_save` with:
|
|
46
|
+
- Clear title and structured content
|
|
47
|
+
- Appropriate type and importance level (1=routine, 2=notable)
|
|
48
|
+
4. Skip trivial or already-saved items
|
|
49
|
+
5. Report what was saved in a concise summary
|
|
50
|
+
|
|
51
|
+
Always set importance=2 for explicit saves (user chose to save), importance=1 for auto-saves of routine items, importance=2 for auto-saves of notable discoveries.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tools
|
|
3
|
+
description: Import skills and agents from GitHub repositories into the tool resource registry
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tool Import
|
|
7
|
+
|
|
8
|
+
Import skills and agents from GitHub repositories into the resource registry for intelligent dispatch.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
- `/mem:tools <github-url>` — Import all skills/agents from a GitHub repo
|
|
13
|
+
- `/mem:tools <github-url> <instructions>` — Import with specific instructions (add/remove specific items)
|
|
14
|
+
- `/mem:tools <instructions>` — Directly add/remove/modify tools by prompt (no URL needed)
|
|
15
|
+
- `/mem:tools` (no args) — Show current registry stats and import help
|
|
16
|
+
|
|
17
|
+
## Instructions
|
|
18
|
+
|
|
19
|
+
When the user invokes `/mem:tools`:
|
|
20
|
+
|
|
21
|
+
### With GitHub URL
|
|
22
|
+
|
|
23
|
+
1. Use WebFetch to fetch the repository README and skill/agent files:
|
|
24
|
+
- Try `https://raw.githubusercontent.com/{owner}/{repo}/main/README.md`
|
|
25
|
+
- Look for skill definitions (`.md` files with frontmatter), agent definitions, plugin.json
|
|
26
|
+
- If the repo has a `commands/` directory, fetch skill files from there
|
|
27
|
+
2. Identify all skills and agents in the repository
|
|
28
|
+
3. For each tool found, extract metadata using your understanding of the content:
|
|
29
|
+
- `name`: tool name (lowercase, hyphenated)
|
|
30
|
+
- `resource_type`: "skill" or "agent"
|
|
31
|
+
- `repo_url`: the GitHub URL
|
|
32
|
+
- `intent_tags`: comma-separated intent keywords (what the tool helps with)
|
|
33
|
+
- `domain_tags`: comma-separated technology/domain tags
|
|
34
|
+
- `capability_summary`: one-line description of what the tool does
|
|
35
|
+
- `trigger_patterns`: when to recommend this tool (natural language)
|
|
36
|
+
- `keywords`: additional search terms
|
|
37
|
+
- `tech_stack`: technology stack tags
|
|
38
|
+
- `use_cases`: usage scenarios
|
|
39
|
+
4. Call `mem_registry(action="import", ...)` for each tool with extracted metadata
|
|
40
|
+
5. Call `mem_registry(action="reindex")` to update FTS5 index
|
|
41
|
+
6. Report imported tools in a table format
|
|
42
|
+
|
|
43
|
+
### With GitHub URL + instructions
|
|
44
|
+
|
|
45
|
+
If the user provides instructions after the URL:
|
|
46
|
+
- "only add the TDD skill" → import only matching tools from that repo
|
|
47
|
+
- "remove the old testing tool" → call `mem_registry(action="remove", ...)`
|
|
48
|
+
- Follow user instructions for selective add/remove/modify operations
|
|
49
|
+
|
|
50
|
+
### With instructions only (no URL)
|
|
51
|
+
|
|
52
|
+
If the user provides a prompt without a GitHub URL, parse the intent:
|
|
53
|
+
|
|
54
|
+
**Adding a tool:**
|
|
55
|
+
- "添加一个叫 my-linter 的 skill" or "add a skill called my-linter"
|
|
56
|
+
- → Ask for metadata (or infer from context): capability_summary, intent_tags, domain_tags, trigger_patterns
|
|
57
|
+
- → Call `mem_registry(action="import", name="my-linter", resource_type="skill", ...)`
|
|
58
|
+
|
|
59
|
+
**Removing a tool:**
|
|
60
|
+
- "删除 old-testing skill" or "remove the old-testing agent"
|
|
61
|
+
- → Call `mem_registry(action="remove", name="old-testing", resource_type="skill")`
|
|
62
|
+
|
|
63
|
+
**Listing/searching:**
|
|
64
|
+
- "有哪些 testing 相关的工具" or "list all agents"
|
|
65
|
+
- → Call `mem_registry(action="list", type="agent")` or search by keywords
|
|
66
|
+
|
|
67
|
+
**Modifying a tool:**
|
|
68
|
+
- "更新 my-linter 的描述" or "update tags for my-tool"
|
|
69
|
+
- → Call `mem_registry(action="import", ...)` with updated metadata (upsert)
|
|
70
|
+
|
|
71
|
+
Always call `mem_registry(action="reindex")` after any add/remove/modify operations.
|
|
72
|
+
|
|
73
|
+
### Without URL or instructions
|
|
74
|
+
|
|
75
|
+
If no arguments provided:
|
|
76
|
+
1. Call `mem_registry(action="stats")` to show current registry state
|
|
77
|
+
2. Call `mem_registry(action="list")` to show all registered tools
|
|
78
|
+
3. Explain usage examples
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: update
|
|
3
|
+
description: Auto-maintain memory and resource registry — deduplicate, merge, decay, cleanup, reindex
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory & Registry Maintenance
|
|
7
|
+
|
|
8
|
+
Run intelligent maintenance on both the memory database and tool resource registry.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
- `/mem:update` — Run full maintenance cycle
|
|
13
|
+
|
|
14
|
+
## Instructions
|
|
15
|
+
|
|
16
|
+
When the user invokes `/mem:update`, perform the following maintenance cycle:
|
|
17
|
+
|
|
18
|
+
### Phase 1: Memory Maintenance
|
|
19
|
+
|
|
20
|
+
1. Call `mem_maintain(action="scan")` to analyze maintenance candidates
|
|
21
|
+
2. Report scan results to the user (duplicates, stale items, broken items, boostable items, **pending purge** items)
|
|
22
|
+
3. Call `mem_maintain(action="execute", operations=["cleanup","decay","boost"])` to apply safe automatic changes
|
|
23
|
+
4. If duplicates were found in scan, review them and call `mem_maintain(action="execute", operations=["dedup"], merge_ids=[[keepId, removeId1, ...], ...])` — keep the more important/recent observation in each pair
|
|
24
|
+
5. Run `mem_compress(preview=false)` for old low-value observations
|
|
25
|
+
6. **If pending purge items > 0**: Report the count to the user and ask for confirmation. If confirmed, call `mem_maintain(action="execute", operations=["purge_stale"])`. User may optionally specify `retain_days` (default 30) to control how many days of data to keep. Do NOT purge without explicit user confirmation.
|
|
26
|
+
|
|
27
|
+
### Phase 2: Registry Maintenance
|
|
28
|
+
|
|
29
|
+
1. Call `mem_registry(action="stats")` to get registry overview
|
|
30
|
+
2. Call `mem_registry(action="reindex")` to rebuild FTS5 search index
|
|
31
|
+
3. Report updated stats
|
|
32
|
+
|
|
33
|
+
### Phase 3: Summary
|
|
34
|
+
|
|
35
|
+
Summarize all maintenance actions taken in zh-CN:
|
|
36
|
+
- Memory: observations cleaned, decayed, boosted, deduplicated, compressed
|
|
37
|
+
- Registry: total resources, adoption rates, reindex status
|
|
38
|
+
- Overall health assessment
|
package/dispatch-inject.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Formats resource recommendations for Claude Code's additionalContext
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { truncate } from './utils.mjs';
|
|
8
8
|
import { DB_DIR } from './schema.mjs';
|
|
@@ -18,13 +18,14 @@ function truncateContent(str, max) {
|
|
|
18
18
|
|
|
19
19
|
// Allowed base directories for resource file reads (defense-in-depth)
|
|
20
20
|
const ALLOWED_BASES = [
|
|
21
|
-
join(homedir(), '.claude'),
|
|
22
|
-
join(DB_DIR, 'managed'),
|
|
21
|
+
resolve(join(homedir(), '.claude')),
|
|
22
|
+
resolve(join(DB_DIR, 'managed')),
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
function isAllowedPath(filePath) {
|
|
26
26
|
if (!filePath) return false;
|
|
27
|
-
|
|
27
|
+
const resolved = resolve(filePath);
|
|
28
|
+
return ALLOWED_BASES.some(base => resolved === base || resolved.startsWith(base + '/'));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// ─── Template Detection ──────────────────────────────────────────────────────
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// claude-mem-lite: Workflow-aware dispatch intelligence
|
|
2
|
+
// Suite auto-flow protection, explicit request detection, stage model
|
|
3
|
+
|
|
4
|
+
// ─── Stage Model ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export const STAGES = ['ANALYZE', 'PLAN', 'REVIEW_PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE', 'COMMIT'];
|
|
7
|
+
|
|
8
|
+
// Skill invocation name → workflow stage
|
|
9
|
+
const SKILL_STAGE_MAP = {
|
|
10
|
+
'superpowers:brainstorming': 'ANALYZE',
|
|
11
|
+
'superpowers:writing-plans': 'PLAN',
|
|
12
|
+
'gsd:start': 'PLAN',
|
|
13
|
+
'gsd:prd': 'PLAN',
|
|
14
|
+
'superpowers:executing-plans': 'EXECUTE',
|
|
15
|
+
'superpowers:subagent-driven-development': 'EXECUTE',
|
|
16
|
+
'gsd:resume': 'EXECUTE',
|
|
17
|
+
'superpowers:test-driven-development': 'TEST',
|
|
18
|
+
'superpowers:systematic-debugging': 'EXECUTE',
|
|
19
|
+
'superpowers:verification-before-completion': 'TEST',
|
|
20
|
+
'superpowers:requesting-code-review': 'REVIEW_CODE',
|
|
21
|
+
'superpowers:receiving-code-review': 'REVIEW_CODE',
|
|
22
|
+
'superpowers:finishing-a-development-branch': 'COMMIT',
|
|
23
|
+
'commit-commands:commit': 'COMMIT',
|
|
24
|
+
'commit-commands:commit-push-pr': 'COMMIT',
|
|
25
|
+
'commit-commands:clean_gone': 'COMMIT',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// User intent → stage mapping (for stage inference from prompt)
|
|
29
|
+
export const INTENT_STAGE_MAP = {
|
|
30
|
+
'plan': 'PLAN',
|
|
31
|
+
'review': 'REVIEW_CODE',
|
|
32
|
+
'test': 'TEST',
|
|
33
|
+
'fix': 'EXECUTE',
|
|
34
|
+
'clean': 'EXECUTE',
|
|
35
|
+
'commit': 'COMMIT',
|
|
36
|
+
'deploy': 'COMMIT',
|
|
37
|
+
'design': 'ANALYZE',
|
|
38
|
+
'doc': 'COMMIT',
|
|
39
|
+
'build': 'EXECUTE',
|
|
40
|
+
'fast': 'EXECUTE',
|
|
41
|
+
'lint': 'EXECUTE',
|
|
42
|
+
'db': 'EXECUTE',
|
|
43
|
+
'api': 'EXECUTE',
|
|
44
|
+
'secure': 'EXECUTE',
|
|
45
|
+
'infra': 'EXECUTE',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─── Suite Auto-Flow Protection ──────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export const SUITE_AUTO_FLOWS = {
|
|
51
|
+
superpowers: {
|
|
52
|
+
stages: ['ANALYZE', 'PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE', 'COMMIT'],
|
|
53
|
+
gaps: ['REVIEW_PLAN'],
|
|
54
|
+
},
|
|
55
|
+
gsd: {
|
|
56
|
+
stages: ['PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE'],
|
|
57
|
+
gaps: ['ANALYZE', 'REVIEW_PLAN', 'COMMIT'],
|
|
58
|
+
},
|
|
59
|
+
'feature-dev': {
|
|
60
|
+
stages: ['ANALYZE', 'EXECUTE', 'REVIEW_CODE'],
|
|
61
|
+
gaps: ['PLAN', 'REVIEW_PLAN', 'TEST', 'COMMIT'],
|
|
62
|
+
},
|
|
63
|
+
'commit-commands': {
|
|
64
|
+
stages: ['COMMIT'],
|
|
65
|
+
gaps: ['ANALYZE', 'PLAN', 'REVIEW_PLAN', 'EXECUTE', 'TEST', 'REVIEW_CODE'],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect if a suite auto-flow is active based on recent Skill tool events.
|
|
71
|
+
* Scans backwards to find the most recent Skill invocation from a known suite.
|
|
72
|
+
* @param {object[]} sessionEvents Array of tool events
|
|
73
|
+
* @returns {{suite: string, flow: object, lastSkill: string}|null}
|
|
74
|
+
*/
|
|
75
|
+
export function detectActiveSuite(sessionEvents) {
|
|
76
|
+
if (!sessionEvents || sessionEvents.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
for (let i = sessionEvents.length - 1; i >= 0; i--) {
|
|
79
|
+
const e = sessionEvents[i];
|
|
80
|
+
if (e.tool_name === 'Skill' && e.tool_input?.skill) {
|
|
81
|
+
const skill = e.tool_input.skill;
|
|
82
|
+
const suite = skill.split(':')[0];
|
|
83
|
+
if (SUITE_AUTO_FLOWS[suite]) {
|
|
84
|
+
return { suite, flow: SUITE_AUTO_FLOWS[suite], lastSkill: skill };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Determine if a recommendation should be made for the current stage.
|
|
93
|
+
* @param {{suite: string, flow: object}|null} activeSuite Active suite info
|
|
94
|
+
* @param {string} currentStage Current workflow stage
|
|
95
|
+
* @returns {{shouldRecommend: boolean, reason: string}}
|
|
96
|
+
*/
|
|
97
|
+
export function shouldRecommendForStage(activeSuite, currentStage) {
|
|
98
|
+
if (!activeSuite) return { shouldRecommend: true, reason: 'no_suite' };
|
|
99
|
+
|
|
100
|
+
const { flow } = activeSuite;
|
|
101
|
+
if (flow.stages.includes(currentStage)) {
|
|
102
|
+
return { shouldRecommend: false, reason: 'suite_covers_stage' };
|
|
103
|
+
}
|
|
104
|
+
if (flow.gaps.includes(currentStage)) {
|
|
105
|
+
return { shouldRecommend: true, reason: 'suite_gap' };
|
|
106
|
+
}
|
|
107
|
+
return { shouldRecommend: true, reason: 'unknown_stage' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Infer the current workflow stage from intent or from the last skill used.
|
|
112
|
+
* @param {string} primaryIntent Primary intent from signal extraction
|
|
113
|
+
* @param {{lastSkill: string}|null} activeSuite Active suite info
|
|
114
|
+
* @returns {string|null} Stage name or null
|
|
115
|
+
*/
|
|
116
|
+
export function inferCurrentStage(primaryIntent, activeSuite) {
|
|
117
|
+
if (activeSuite?.lastSkill && SKILL_STAGE_MAP[activeSuite.lastSkill]) {
|
|
118
|
+
return SKILL_STAGE_MAP[activeSuite.lastSkill];
|
|
119
|
+
}
|
|
120
|
+
return INTENT_STAGE_MAP[primaryIntent] || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Explicit Request Detection ──────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const EXPLICIT_REQUEST_PATTERNS = [
|
|
126
|
+
// EN: "use the playwright skill", "try the ppt skill"
|
|
127
|
+
/(?:use|try|invoke|run|activate|load)\s+(?:the\s+)?(\S+?)\s+(?:skill|agent|tool|plugin)\b/i,
|
|
128
|
+
// CN: "用ppt的技能", "帮我用playwright的skill"
|
|
129
|
+
/(?:用|使用|帮我用|试试|启用)\s*(\S+?)\s*(?:的|的技能|的skill|的agent|技能|skill|agent|工具|插件)/,
|
|
130
|
+
// "有没有xxx的skill", "is there a xxx agent"
|
|
131
|
+
/(?:有没有|有无|是否有|do you have|is there)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
|
|
132
|
+
// "推荐一个xxx", "recommend a xxx agent"
|
|
133
|
+
/(?:推荐|suggest|recommend)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Detect if the user is explicitly requesting a specific tool/skill.
|
|
138
|
+
* Highest priority — bypasses all dispatch restrictions.
|
|
139
|
+
* @param {string} userPrompt User's prompt text
|
|
140
|
+
* @returns {{isExplicit: boolean, searchTerm?: string}}
|
|
141
|
+
*/
|
|
142
|
+
export function detectExplicitRequest(userPrompt) {
|
|
143
|
+
if (!userPrompt) return { isExplicit: false };
|
|
144
|
+
|
|
145
|
+
for (const pattern of EXPLICIT_REQUEST_PATTERNS) {
|
|
146
|
+
const match = userPrompt.match(pattern);
|
|
147
|
+
if (match && match[1]) {
|
|
148
|
+
const term = match[1].replace(/['"]/g, '').trim();
|
|
149
|
+
if (term.length >= 2 && term.length <= 30) {
|
|
150
|
+
return { isExplicit: true, searchTerm: term };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { isExplicit: false };
|
|
155
|
+
}
|
package/dispatch.mjs
CHANGED
|
@@ -32,22 +32,28 @@ export const BM25_MIN_THRESHOLD = 1.5;
|
|
|
32
32
|
// ─── Haiku Circuit Breaker ──────────────────────────────────────────────────
|
|
33
33
|
// Prevents cascading latency when Haiku API is down or slow.
|
|
34
34
|
// After BREAKER_THRESHOLD consecutive failures, disable for BREAKER_RESET_MS.
|
|
35
|
+
// KNOWN LIMITATION: File-based state has a TOCTOU race under concurrent hook
|
|
36
|
+
// processes. Worst case: breaker trips on failure N+1 instead of N. This is
|
|
37
|
+
// acceptable — the breaker is a latency guard, not a correctness mechanism.
|
|
35
38
|
|
|
36
39
|
const BREAKER_THRESHOLD = 3;
|
|
37
40
|
const BREAKER_RESET_MS = 5 * 60 * 1000; // 5 minutes
|
|
38
|
-
|
|
41
|
+
let breakerFile = join(RUNTIME_DIR, 'haiku-breaker.json');
|
|
39
42
|
|
|
40
43
|
function _readBreakerState() {
|
|
41
44
|
try {
|
|
42
|
-
if (!existsSync(
|
|
43
|
-
return JSON.parse(readFileSync(
|
|
45
|
+
if (!existsSync(breakerFile)) return { failures: 0, openUntil: 0 };
|
|
46
|
+
return JSON.parse(readFileSync(breakerFile, 'utf8'));
|
|
44
47
|
} catch { return { failures: 0, openUntil: 0 }; }
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function _writeBreakerState(state) {
|
|
48
|
-
try { writeFileSync(
|
|
51
|
+
try { writeFileSync(breakerFile, JSON.stringify(state)); } catch {}
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** Override breaker file path (for testing isolation). */
|
|
55
|
+
export function _setBreakerFile(path) { breakerFile = path; }
|
|
56
|
+
|
|
51
57
|
function isHaikuCircuitOpen() {
|
|
52
58
|
const state = _readBreakerState();
|
|
53
59
|
if (state.openUntil > 0 && Date.now() < state.openUntil) return true;
|
|
@@ -636,15 +642,27 @@ JSON: {"query":"search keywords for finding the right skill or agent","type":"sk
|
|
|
636
642
|
|
|
637
643
|
// ─── Cooldown & Dedup (DB-persisted, survives process restarts) ─────────────
|
|
638
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Check if session has hit the recommendation cap.
|
|
647
|
+
* Separated from per-resource check so callers in filter loops can hoist this.
|
|
648
|
+
* @param {Database} db Registry database
|
|
649
|
+
* @param {string} sessionId Session identifier
|
|
650
|
+
* @returns {boolean} true if session cap is reached
|
|
651
|
+
*/
|
|
652
|
+
export function isSessionCapped(db, sessionId) {
|
|
653
|
+
if (!sessionId) return false;
|
|
654
|
+
const sessionCount = db.prepare(
|
|
655
|
+
'SELECT COUNT(*) as cnt FROM invocations WHERE session_id = ? AND recommended = 1'
|
|
656
|
+
).get(sessionId);
|
|
657
|
+
return sessionCount.cnt >= SESSION_RECOMMEND_CAP;
|
|
658
|
+
}
|
|
659
|
+
|
|
639
660
|
export function isRecentlyRecommended(db, resourceId, sessionId) {
|
|
640
|
-
// Check 1
|
|
661
|
+
// Check 1: Session cap (loop-invariant — callers should prefer isSessionCapped for filter loops)
|
|
641
662
|
if (sessionId) {
|
|
642
|
-
|
|
643
|
-
'SELECT COUNT(*) as cnt FROM invocations WHERE session_id = ? AND recommended = 1'
|
|
644
|
-
).get(sessionId);
|
|
645
|
-
if (sessionCount.cnt >= SESSION_RECOMMEND_CAP) return true;
|
|
663
|
+
if (isSessionCapped(db, sessionId)) return true;
|
|
646
664
|
|
|
647
|
-
// Already recommended in this session (session dedup)
|
|
665
|
+
// Check 2: Already recommended in this session (session dedup)
|
|
648
666
|
const sessionHit = db.prepare(
|
|
649
667
|
'SELECT 1 FROM invocations WHERE resource_id = ? AND session_id = ? AND recommended = 1 LIMIT 1'
|
|
650
668
|
).get(resourceId, sessionId);
|
|
@@ -857,7 +875,8 @@ export async function dispatchOnSessionStart(db, userPrompt, sessionId, { hasHan
|
|
|
857
875
|
|
|
858
876
|
if (results.length === 0) return null;
|
|
859
877
|
|
|
860
|
-
// Filter by DB-persisted cooldown + session dedup
|
|
878
|
+
// Filter by DB-persisted cooldown + session dedup (hoisted cap check avoids N queries)
|
|
879
|
+
if (sessionId && isSessionCapped(db, sessionId)) return null;
|
|
861
880
|
const viable = sessionId
|
|
862
881
|
? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId))
|
|
863
882
|
: results;
|
|
@@ -895,12 +914,13 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionE
|
|
|
895
914
|
if (!userPrompt || !db) return null;
|
|
896
915
|
|
|
897
916
|
try {
|
|
898
|
-
// 1. Explicit request → highest priority, bypass
|
|
917
|
+
// 1. Explicit request → highest priority, bypass cooldown but apply adoption decay
|
|
899
918
|
const explicit = detectExplicitRequest(userPrompt);
|
|
900
919
|
if (explicit.isExplicit) {
|
|
901
920
|
const textQuery = buildQueryFromText(explicit.searchTerm);
|
|
902
921
|
if (textQuery) {
|
|
903
|
-
|
|
922
|
+
let explicitResults = retrieveResources(db, textQuery, { limit: 3, projectDomains: detectProjectDomains() });
|
|
923
|
+
explicitResults = applyAdoptionDecay(explicitResults, db);
|
|
904
924
|
if (explicitResults.length > 0) {
|
|
905
925
|
const best = explicitResults[0];
|
|
906
926
|
if (!sessionId || !isRecentlyRecommended(db, best.id, sessionId)) {
|
|
@@ -960,7 +980,8 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionE
|
|
|
960
980
|
// Low confidence → skip (no Haiku in user_prompt path — stay fast)
|
|
961
981
|
if (needsHaikuDispatch(results)) return null;
|
|
962
982
|
|
|
963
|
-
// Filter by cooldown + session dedup (
|
|
983
|
+
// Filter by cooldown + session dedup (hoisted cap check avoids N queries)
|
|
984
|
+
if (sessionId && isSessionCapped(db, sessionId)) return null;
|
|
964
985
|
const viable = sessionId
|
|
965
986
|
? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId))
|
|
966
987
|
: results;
|
|
@@ -1028,8 +1049,9 @@ export async function dispatchOnPreToolUse(db, event, sessionCtx = {}) {
|
|
|
1028
1049
|
// Low-confidence results: skip recommendation rather than suggest unreliable match
|
|
1029
1050
|
if (needsHaikuDispatch(results)) return null;
|
|
1030
1051
|
|
|
1031
|
-
// Apply DB-persisted cooldown and session dedup (
|
|
1052
|
+
// Apply DB-persisted cooldown and session dedup (hoisted cap check avoids N queries)
|
|
1032
1053
|
const sid = sessionCtx.sessionId || null;
|
|
1054
|
+
if (sid && isSessionCapped(db, sid)) return null;
|
|
1033
1055
|
const viable = sid
|
|
1034
1056
|
? results.filter(r => !isRecentlyRecommended(db, r.id, sid))
|
|
1035
1057
|
: results;
|