fix: auto-bootstrapped memory was accidentally putting the MEMORY.md directly in the repo root rather than .coyote/memory/MEMORY.md
This commit is contained in:
@@ -11,19 +11,25 @@ pub(crate) const DEFAULT_SKILL_INSTRUCTIONS: &str = indoc! {"
|
|||||||
pub(crate) const DEFAULT_MEMORY_INSTRUCTIONS: &str = indoc! {"
|
pub(crate) const DEFAULT_MEMORY_INSTRUCTIONS: &str = indoc! {"
|
||||||
## Memory
|
## Memory
|
||||||
A persistent memory file system survives across sessions. The MEMORY.md content shown above is
|
A persistent memory file system survives across sessions. The MEMORY.md content shown above is
|
||||||
your always-on context; put universal facts (user identity, hard rules, binding feedback) directly
|
your always-on context (universal facts, hard rules, binding feedback). Drill files hold deeper,
|
||||||
in MEMORY.md so they appear on every turn. Drill files hold deeper, on-demand context.
|
on-demand context that you fetch with `memory__read`.
|
||||||
|
|
||||||
Tools:
|
Tools:
|
||||||
- `memory__read(name)`: Read a specific drill file's full content.
|
- `memory__read(name)`: Read a specific drill file's full content.
|
||||||
- `memory__write(name, content, scope)`: Create or replace a drill file (scope: 'global' | 'workspace').
|
- `memory__write(name, content, scope)`: Create or replace a drill file (scope: 'global' | 'workspace').
|
||||||
|
The MEMORY.md index is appended automatically; do not also update the index by hand.
|
||||||
|
- `memory__edit_index(scope, content)`: Replace the entire MEMORY.md at the given scope.
|
||||||
|
Use this to add always-on facts, reorganize, prune stale entries, or fix descriptions.
|
||||||
- `memory__list()`: See all known drill files and their metadata.
|
- `memory__list()`: See all known drill files and their metadata.
|
||||||
- `memory__lint()`: Health-check memory for orphans, broken links, oversized files.
|
- `memory__lint()`: Health-check memory for orphans, broken links, oversized files.
|
||||||
|
|
||||||
RULES:
|
RULES:
|
||||||
- Every interaction has two outputs: your answer AND any memory updates the conversation warrants.
|
- Every interaction has two outputs: your answer AND any memory updates the conversation warrants.
|
||||||
Don't let learnings evaporate into chat history.
|
Don't let learnings evaporate into chat history.
|
||||||
- When you create or modify a drill file, also update MEMORY.md so the index stays accurate.
|
- All MEMORY.md edits MUST go through `memory__edit_index`. NEVER use `fs_write`, `fs_patch`,
|
||||||
|
or any other generic file tool on MEMORY.md — Coyote manages its location and a stray
|
||||||
|
MEMORY.md outside the managed path is invisible to memory.
|
||||||
|
- All drill files MUST go through `memory__write`. The index updates itself.
|
||||||
- Use [[wikilink]] notation in memory files to reference other memories by their `name:` slug.
|
- Use [[wikilink]] notation in memory files to reference other memories by their `name:` slug.
|
||||||
- NEVER write secrets, credentials, or API keys to memory — memory is plaintext on disk.
|
- NEVER write secrets, credentials, or API keys to memory — memory is plaintext on disk.
|
||||||
Use coyote's Vault for secrets.
|
Use coyote's Vault for secrets.
|
||||||
|
|||||||
@@ -129,6 +129,41 @@ pub fn memory_function_declarations() -> Vec<FunctionDeclaration> {
|
|||||||
},
|
},
|
||||||
agent: false,
|
agent: false,
|
||||||
},
|
},
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: format!("{MEMORY_FUNCTION_PREFIX}edit_index"),
|
||||||
|
description:
|
||||||
|
"Replace the entire MEMORY.md index at the given scope. Use to add always-on facts, \
|
||||||
|
reorganize, prune stale entries, or fix descriptions. Coyote manages the path; \
|
||||||
|
NEVER use fs_write or any other generic file tool on MEMORY.md."
|
||||||
|
.to_string(),
|
||||||
|
parameters: JsonSchema {
|
||||||
|
type_value: Some("object".to_string()),
|
||||||
|
properties: Some(IndexMap::from([
|
||||||
|
(
|
||||||
|
"scope".to_string(),
|
||||||
|
JsonSchema {
|
||||||
|
type_value: Some("string".to_string()),
|
||||||
|
description: Some(
|
||||||
|
"Where to edit: 'global' (user-level) or 'workspace' (project-level)"
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content".to_string(),
|
||||||
|
JsonSchema {
|
||||||
|
type_value: Some("string".to_string()),
|
||||||
|
description: Some("Full new contents of MEMORY.md".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
required: Some(vec!["scope".to_string(), "content".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
agent: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,11 +247,33 @@ pub fn handle_memory_tool(ctx: &mut RequestContext, cmd_name: &str, args: &Value
|
|||||||
"index_updated": index_updated,
|
"index_updated": index_updated,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
"edit_index" => {
|
||||||
|
let scope = arg_str(args, "scope")?;
|
||||||
|
let content = arg_str(args, "content")?;
|
||||||
|
let target_dir = match scope.as_str() {
|
||||||
|
"global" => paths::global_memory_dir(),
|
||||||
|
"workspace" => workspace_write_dir(&store, &cwd)?,
|
||||||
|
other => bail!("unknown scope '{}': use 'global' or 'workspace'", other),
|
||||||
|
};
|
||||||
|
let index_path = write_memory_index(&target_dir, &content)?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"status": "ok",
|
||||||
|
"path": index_path.display().to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
"lint" => lint_memory(&store),
|
"lint" => lint_memory(&store),
|
||||||
_ => bail!("unknown memory action: {action}"),
|
_ => bail!("unknown memory action: {action}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_memory_index(target_dir: &Path, content: &str) -> Result<PathBuf> {
|
||||||
|
fs::create_dir_all(target_dir)?;
|
||||||
|
let index_path = target_dir.join("MEMORY.md");
|
||||||
|
fs::write(&index_path, content)?;
|
||||||
|
Ok(index_path)
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_index_entry(index_path: &Path, name: &str, description: &str) -> Result<bool> {
|
fn ensure_index_entry(index_path: &Path, name: &str, description: &str) -> Result<bool> {
|
||||||
let existing = fs::read_to_string(index_path).unwrap_or_default();
|
let existing = fs::read_to_string(index_path).unwrap_or_default();
|
||||||
let already_referenced =
|
let already_referenced =
|
||||||
@@ -533,6 +590,39 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_memory_index_creates_dir_and_writes_content() {
|
||||||
|
let root = temp_root("write_index_create");
|
||||||
|
let target = root.join("nested").join(".coyote").join("memory");
|
||||||
|
|
||||||
|
let path =
|
||||||
|
write_memory_index(&target, "# Workspace Memory Index\n\n- [[foo]]: hello\n").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(path, target.join("MEMORY.md"));
|
||||||
|
assert!(path.exists());
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&path).unwrap(),
|
||||||
|
"# Workspace Memory Index\n\n- [[foo]]: hello\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_memory_index_replaces_existing_content() {
|
||||||
|
let root = temp_root("write_index_replace");
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
let index = root.join("MEMORY.md");
|
||||||
|
fs::write(&index, "# Old\n\n- [[stale]]: gone\n").unwrap();
|
||||||
|
|
||||||
|
let path = write_memory_index(&root, "# New\n").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(path, index);
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "# New\n");
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lint_flags_orphans_broken_links_and_oversized() {
|
fn lint_flags_orphans_broken_links_and_oversized() {
|
||||||
let root = temp_root("lint");
|
let root = temp_root("lint");
|
||||||
|
|||||||
Reference in New Issue
Block a user