Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ npm run visual:ci

- PRD: `_bmad-output/planning-artifacts/main/prd.md`
- 项目设置: `_bmad-output/planning-artifacts/main/PROJECT_SETUP.md`
- 帮助文档: [`docs/help/choose-extra-paths-or-knowledge-injection.md`](docs/help/choose-extra-paths-or-knowledge-injection.md)

## 环境要求

Expand Down
172 changes: 172 additions & 0 deletions docs/help/choose-extra-paths-or-knowledge-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Choose Extra Paths Or Knowledge Injection

When you want to bring more knowledge into ClawScope, there are two common options:

- **Extra paths**
- **Knowledge Injection**

They are related, but they do different jobs.

The short version:

- Use **Extra paths** when you want ClawScope to search an external folder.
- Use **Knowledge Injection** when you want a specific node to absorb a piece of knowledge into its own memory.

## Use extra paths when you want searchable external knowledge

**Extra paths** belong to the **Memory** module.

They tell ClawScope:

> "Also include this folder when you build recall and search."

Use extra paths when:

- your team already has a shared folder of Markdown notes, playbooks, or reference docs
- you want those files to become searchable
- you do **not** want to rewrite a node's own memory
- you want to keep the original files where they are

Think of **Extra paths** as:

- attaching an external knowledge shelf
- expanding where search can look
- managing retrieval sources, not editing memory truth

Extra paths are a good fit for:

- team playbooks
- shared product notes
- reusable troubleshooting guides
- read-only knowledge directories

Extra paths are **not** a good fit for:

- runtime cache folders
- `sessions/` or `qmd/` directories
- SQLite stores
- content that should become part of a node's own managed memory

## Use Knowledge Injection when you want the node to remember it

**Knowledge Injection** belongs to the **Evolution** module.

It tells ClawScope:

> "Take this structured knowledge package and write it into the selected node's memory."

Use Knowledge Injection when:

- you want one node to permanently learn a rule, workflow, or role-specific context
- you want to preview the change before applying it
- you want the action to appear in history and audit trails
- you want rollback support

Think of **Knowledge Injection** as:

- writing managed knowledge into the node
- turning a knowledge package into part of that node's memory state
- running a governed change, not just adding another search source

Knowledge Injection is a good fit for:

- operating rules
- role instructions
- domain-specific procedures
- curated summaries distilled from larger documents

## What is the difference?

| Question | Extra paths | Knowledge Injection |
|---|---|---|
| What does it do? | Adds an external folder to recall and search | Writes a managed knowledge block into the selected node's memory |
| Which module owns it? | **Memory** | **Evolution** |
| Does it change the node's memory content? | No | Yes |
| Does it need preview / execute / rollback? | No | Yes |
| What is it best for? | Shared searchable documents | Node-specific long-term knowledge |
| What should you expect? | "Search can see this folder" | "This node now remembers this knowledge" |

## Where they overlap

Both features can improve later search results.

That is where the overlap ends.

They do **not** work the same way:

- **Extra paths** change the search scope
- **Knowledge Injection** changes the node's memory content

At the moment, ClawScope does **not** automatically sync the two:

- adding an extra path does **not** create a knowledge injection task
- running a knowledge injection does **not** automatically add its source folder to extra paths

## Recommended workflow

If you are not sure which one to use, start here:

1. Put the full reference material in a shared folder.
2. Add that folder through **Extra paths** so it becomes searchable.
3. Watch how useful the material is in practice.
4. If part of it should become durable node knowledge, turn the key parts into a smaller knowledge package.
5. Apply that package with **Knowledge Injection**.

This gives you a clean split:

- the full source stays external
- the most important rules get written into the node's memory

## Avoid duplicate knowledge on both sides

You can use both features together, but avoid copying the same long document into both places unchanged.

If you:

- add the full document through **Extra paths**
- and inject the same full document into node memory

you may end up with duplicated recall hits and harder-to-read search results.

A better pattern is:

- keep the full document in **Extra paths**
- inject only the distilled summary, rules, or final operating guidance

## FAQ

### Does Knowledge Injection write to `MEMORY.md`?

Yes, in most cases it does.

More precisely, **Knowledge Injection** writes to the node's **root memory document**:

- ClawScope uses `MEMORY.md` first
- if `MEMORY.md` is not available, it falls back to `memory.md`

This means Knowledge Injection does **not**:

- write into an extra path folder
- create a separate external knowledge database entry
- store the knowledge only as a search source

Instead, it appends a managed knowledge block into the selected node's root memory document, and that content later becomes searchable through the normal memory indexing flow.

## Quick decision guide

Choose **Extra paths** if your question is:

- "How do I make this folder searchable?"
- "How do I let multiple nodes search the same knowledge base?"
- "How do I include shared docs without editing node memory?"

Choose **Knowledge Injection** if your question is:

- "How do I make this node remember this?"
- "How do I apply a knowledge package with preview and rollback?"
- "How do I turn a source document into managed node memory?"

## One-line summary

- If you want ClawScope to **search it**, use **Extra paths**.
- If you want a node to **remember it**, use **Knowledge Injection**.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "claw-scope",
"private": true,
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "claw-scope"
version = "0.1.3"
version = "0.1.4"
description = "ClawScope - 记忆可见,进化可期"
authors = ["milome"]
license = "MIT"
Expand Down
10 changes: 3 additions & 7 deletions src-tauri/src/evolution/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ pub async fn evolution_history_list(
let mut history =
load_history(&store_paths).map_err(|error| GatewayErrorSummary::from_error(&error))?;
history.retain(|entry| entry.agent_id == agent_id);
history.sort_by(|left, right| right.created_at_ms.cmp(&left.created_at_ms));
history.sort_by_key(|entry| std::cmp::Reverse(entry.created_at_ms));
Ok(history)
}

Expand All @@ -343,7 +343,7 @@ pub async fn evolution_audit_summary(
let mut audit =
load_audit(&store_paths).map_err(|error| GatewayErrorSummary::from_error(&error))?;
audit.retain(|entry| entry.agent_id == agent_id);
audit.sort_by(|left, right| right.ended_at_ms.cmp(&left.ended_at_ms));
audit.sort_by_key(|entry| std::cmp::Reverse(entry.ended_at_ms));
Ok(summarize_audit_entries(agent_id, audit))
}

Expand Down Expand Up @@ -2161,11 +2161,7 @@ fn summarize_audit_entries(
last_7d_operations,
last_7d_failures,
last_7d_overrides,
average_duration_ms: if duration_count > 0 {
Some(duration_total / duration_count)
} else {
None
},
average_duration_ms: duration_total.checked_div(duration_count),
status_breakdown: status_breakdown
.into_iter()
.map(|(key, count)| EvolutionMetricBucket { key, count })
Expand Down
51 changes: 45 additions & 6 deletions src-tauri/src/gateway/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ pub fn select_connect_auth(
.map(ToOwned::to_owned),
_ => None,
};
let stored_token = stored_device_token
.map(|entry| entry.token.trim())
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let bootstrap_gateway_token = match config.auth_mode {
GatewayAuthMode::PairedDevice if stored_token.is_none() && !retry_with_stored_device_token => config
.auth_secret
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
_ => None,
};
let auth_password = match config.auth_mode {
GatewayAuthMode::Password => config
.auth_secret
Expand All @@ -51,23 +64,22 @@ pub fn select_connect_auth(
.map(ToOwned::to_owned),
_ => None,
};
let stored_token = stored_device_token
.map(|entry| entry.token.trim())
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);

let should_use_stored_device_token = retry_with_stored_device_token
|| (matches!(config.auth_mode, GatewayAuthMode::PairedDevice)
&& explicit_gateway_token.is_none()
&& bootstrap_gateway_token.is_none()
&& auth_password.is_none());
let resolved_device_token = if should_use_stored_device_token {
stored_token.clone()
} else {
None
};
let auth_bootstrap_token = bootstrap_gateway_token.clone();
let auth_token = explicit_gateway_token
.clone()
.or_else(|| resolved_device_token.clone());
.or_else(|| resolved_device_token.clone())
.or_else(|| auth_bootstrap_token.clone());
let auth_device_token = if retry_with_stored_device_token {
stored_token.clone()
} else {
Expand All @@ -76,7 +88,7 @@ pub fn select_connect_auth(
let auth = if auth_token.is_some() || auth_password.is_some() || auth_device_token.is_some() {
Some(ConnectAuth {
token: auth_token.clone(),
bootstrap_token: None,
bootstrap_token: auth_bootstrap_token,
device_token: auth_device_token,
password: auth_password,
})
Expand Down Expand Up @@ -165,10 +177,37 @@ mod tests {
let selected = select_connect_auth(&GatewayConnectConfig::default(), Some(&stored_entry()), false);
let auth = selected.auth.expect("device auth payload");
assert_eq!(auth.token.as_deref(), Some("stored-device-token"));
assert_eq!(auth.bootstrap_token, None);
assert_eq!(auth.device_token.as_deref(), None);
assert_eq!(selected.resolved_device_token.as_deref(), Some("stored-device-token"));
}

#[test]
fn paired_device_auth_uses_bootstrap_token_when_not_yet_paired() {
let mut config = GatewayConnectConfig::default();
config.auth_secret = Some("shared-token".to_string());

let selected = select_connect_auth(&config, None, false);
let auth = selected.auth.expect("bootstrap auth payload");
assert_eq!(auth.token.as_deref(), Some("shared-token"));
assert_eq!(auth.bootstrap_token.as_deref(), Some("shared-token"));
assert_eq!(auth.device_token, None);
assert_eq!(selected.signature_token.as_deref(), Some("shared-token"));
assert!(selected.resolved_device_token.is_none());
}

#[test]
fn paired_device_auth_prefers_cached_device_token_over_bootstrap_secret() {
let mut config = GatewayConnectConfig::default();
config.auth_secret = Some("shared-token".to_string());

let selected = select_connect_auth(&config, Some(&stored_entry()), false);
let auth = selected.auth.expect("paired auth payload");
assert_eq!(auth.token.as_deref(), Some("stored-device-token"));
assert_eq!(auth.bootstrap_token, None);
assert_eq!(selected.resolved_device_token.as_deref(), Some("stored-device-token"));
}

#[test]
fn token_auth_retry_attaches_device_token_without_dropping_shared_token() {
let mut config = GatewayConnectConfig::default();
Expand Down
Loading
Loading