@gotgenes/pi-subagents 6.19.1 → 7.0.0
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/CHANGELOG.md +24 -0
- package/docs/architecture/architecture.md +5 -4
- package/docs/plans/0185-remove-persistent-agent-memory.md +161 -0
- package/docs/retro/0185-remove-persistent-agent-memory.md +39 -0
- package/docs/retro/0188-replace-any-casts-with-sdk-types.md +29 -0
- package/package.json +1 -1
- package/src/config/agent-types.ts +0 -20
- package/src/config/custom-agents.ts +1 -11
- package/src/index.ts +1 -3
- package/src/session/prompts.ts +1 -6
- package/src/session/safe-fs.ts +45 -0
- package/src/session/session-config.ts +3 -49
- package/src/session/skill-loader.ts +1 -1
- package/src/types.ts +0 -5
- package/src/ui/agent-config-editor.ts +0 -1
- package/src/ui/agent-creation-wizard.ts +0 -1
- package/src/session/memory.ts +0 -168
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [7.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.19.1...pi-subagents-v7.0.0) (2026-05-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* `src/session/memory.ts` and all persistent agent memory functionality (MEMORY.md, agent-memory directories) are removed from pi-subagents. This is scope reduction — agent spawning, execution, and result retrieval remain.
|
|
14
|
+
* `MemoryScope` is no longer exported from the package. The `memory` field is removed from `AgentConfig`. Custom agent .md files with a `memory:` frontmatter key will have it silently ignored.
|
|
15
|
+
* The `memory` field in agent configuration no longer has any effect. Memory block injection, memory tool augmentation, and the `AssemblerIO.buildMemoryBlock` / `buildReadOnlyMemoryBlock` collaborators are removed from the session assembler.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* delete memory module ([78ace55](https://github.com/gotgenes/pi-packages/commit/78ace558b1988bce8e002b28fe3152c5de708b84))
|
|
20
|
+
* remove memory from session assembly and config layers ([6ebeb91](https://github.com/gotgenes/pi-packages/commit/6ebeb91f28c56c1d21fc4e1a7b6bededa8eba025))
|
|
21
|
+
* remove MemoryScope type and memory config field ([d6e3bcb](https://github.com/gotgenes/pi-packages/commit/d6e3bcbb58902133a3704d89d88b548f8f2a4769))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
* plan remove persistent agent memory ([#185](https://github.com/gotgenes/pi-packages/issues/185)) ([0f6b3ad](https://github.com/gotgenes/pi-packages/commit/0f6b3adb6c35c3879c5d1e176e1c6d9da36a5cf1))
|
|
27
|
+
* **retro:** add planning stage notes for issue [#185](https://github.com/gotgenes/pi-packages/issues/185) ([58dcfbc](https://github.com/gotgenes/pi-packages/commit/58dcfbc3124ec7695048ad3df8fc6f397a883d1a))
|
|
28
|
+
* **retro:** add retro notes for issue [#188](https://github.com/gotgenes/pi-packages/issues/188) ([8eeaf6b](https://github.com/gotgenes/pi-packages/commit/8eeaf6b52f7b40a2126f3ffa3ca01a8e3b84f338))
|
|
29
|
+
* **retro:** add TDD stage notes for issue [#185](https://github.com/gotgenes/pi-packages/issues/185) ([8e75b18](https://github.com/gotgenes/pi-packages/commit/8e75b18b2c87b47f1a29df9d2c544a8ad2023f9f))
|
|
30
|
+
* update architecture after memory removal ([52716d5](https://github.com/gotgenes/pi-packages/commit/52716d5f89b729ce183d4a450d8539e6cabdbadc))
|
|
31
|
+
|
|
8
32
|
## [6.19.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.19.0...pi-subagents-v6.19.1) (2026-05-24)
|
|
9
33
|
|
|
10
34
|
|
|
@@ -42,7 +42,7 @@ flowchart TB
|
|
|
42
42
|
SessionConfig["assembleSessionConfig\n(pure assembler)"]
|
|
43
43
|
Prompts["prompts\n(system prompt)"]
|
|
44
44
|
Context["context\n(parent history)"]
|
|
45
|
-
|
|
45
|
+
SafeFs["safe-fs\n(symlink/name guards)"]
|
|
46
46
|
SkillLoader["skill-loader\n(preload skills)"]
|
|
47
47
|
Env["env\n(git/platform)"]
|
|
48
48
|
ModelResolver["model-resolver\n(fuzzy match)"]
|
|
@@ -86,7 +86,8 @@ flowchart TB
|
|
|
86
86
|
AgentManager --> AgentRunner
|
|
87
87
|
AgentRunner --> SessionConfig
|
|
88
88
|
SessionConfig --> AgentTypeRegistry
|
|
89
|
-
SessionConfig --> Prompts &
|
|
89
|
+
SessionConfig --> Prompts & SkillLoader & Env
|
|
90
|
+
SkillLoader --> SafeFs
|
|
90
91
|
AgentTypeRegistry --> DefaultAgents & CustomAgents
|
|
91
92
|
RecordObserver -.->|subscribes| AgentRunner
|
|
92
93
|
UIObserver -.->|subscribes| AgentRunner
|
|
@@ -246,7 +247,7 @@ src/
|
|
|
246
247
|
│ ├── prompts.ts system prompt building
|
|
247
248
|
│ ├── content-items.ts shared message content parsing (tool-call names, assistant content)
|
|
248
249
|
│ ├── context.ts parent conversation extraction
|
|
249
|
-
│ ├──
|
|
250
|
+
│ ├── safe-fs.ts symlink rejection and safe file reads
|
|
250
251
|
│ ├── skill-loader.ts skill preloading
|
|
251
252
|
│ ├── env.ts git/platform detection
|
|
252
253
|
│ ├── model-resolver.ts fuzzy model name resolution
|
|
@@ -338,7 +339,7 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
338
339
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
339
340
|
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
340
341
|
- Agent type registry — default agents, custom `.md` file loading.
|
|
341
|
-
- Prompt assembly, context extraction,
|
|
342
|
+
- Prompt assembly, context extraction, skills, environment.
|
|
342
343
|
- Worktree isolation.
|
|
343
344
|
- Token usage tracking.
|
|
344
345
|
- Session directory derivation and persisted `SessionManager` for subagent transcripts.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 185
|
|
3
|
+
issue_title: "pi-subagents: Remove persistent agent memory feature"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove persistent agent memory feature
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The `memory.ts` module and all supporting code for persistent agent memory (`MEMORY.md`, `agent-memory/` directories) should be removed from `pi-subagents`.
|
|
11
|
+
The memory feature invents its own filesystem layout, file format, and security model — all outside the stated scope of `pi-subagents`, which is agent spawning, execution, and result retrieval.
|
|
12
|
+
This follows the same scope-reduction rationale as the scheduling subsystem removal (issue #52).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Remove the `memory.ts` module and all memory-related code from the package.
|
|
17
|
+
- Remove `MemoryScope` type and `memory` field from `AgentConfig`.
|
|
18
|
+
- Remove memory block injection from session assembly.
|
|
19
|
+
- Remove memory tool augmentation from agent-types.
|
|
20
|
+
- Remove memory parsing from custom agent loading.
|
|
21
|
+
- Remove memory display from UI components.
|
|
22
|
+
- Extract `isSymlink`, `isUnsafeName`, and `safeReadFile` to a shared utility module — `skill-loader.ts` depends on them independently of memory.
|
|
23
|
+
- Update architecture documentation to reflect the removal.
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- Replacing memory with an alternative persistence mechanism — that is out of scope.
|
|
28
|
+
- Changing the `skill-loader.ts` logic beyond updating the import source for the extracted utilities.
|
|
29
|
+
|
|
30
|
+
## Background
|
|
31
|
+
|
|
32
|
+
The memory system spans six source modules and two UI modules:
|
|
33
|
+
|
|
34
|
+
| File | Memory surface |
|
|
35
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `src/session/memory.ts` | Core module: `buildMemoryBlock`, `buildReadOnlyMemoryBlock`, `resolveMemoryDir`, `ensureMemoryDir`, `readMemoryIndex`, plus shared utilities `isSymlink`, `isUnsafeName`, `safeReadFile` |
|
|
37
|
+
| `src/types.ts` | `MemoryScope` type, `memory?: MemoryScope` field on `AgentConfig` |
|
|
38
|
+
| `src/session/session-config.ts` | `AssemblerIO.buildMemoryBlock` / `buildReadOnlyMemoryBlock` fields, memory logic block (~25 lines), `MemoryScope` import |
|
|
39
|
+
| `src/config/agent-types.ts` | `getMemoryToolNames()`, `getReadOnlyMemoryToolNames()`, `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES` |
|
|
40
|
+
| `src/config/custom-agents.ts` | `parseMemory()` function, `memory: parseMemory(fm.memory)` assignment |
|
|
41
|
+
| `src/session/prompts.ts` | `memoryBlock` field on `PromptExtras`, injection into system prompt |
|
|
42
|
+
| `src/ui/agent-config-editor.ts` | `if (cfg.memory) fmFields.push(...)` |
|
|
43
|
+
| `src/ui/agent-creation-wizard.ts` | `memory:` line in frontmatter help text |
|
|
44
|
+
| `src/index.ts` | Imports `buildMemoryBlock`, `buildReadOnlyMemoryBlock`; wires them into `assemblerIO` |
|
|
45
|
+
|
|
46
|
+
The three utility functions (`isSymlink`, `isUnsafeName`, `safeReadFile`) are imported by `skill-loader.ts` for filesystem safety — they are not memory-specific and must survive the removal.
|
|
47
|
+
|
|
48
|
+
AGENTS.md constraint: the architecture doc in `docs/architecture/architecture.md` references `memory.ts` in the module listing and domain model diagram — both need updating.
|
|
49
|
+
|
|
50
|
+
## Design Overview
|
|
51
|
+
|
|
52
|
+
This is a pure removal with one extraction.
|
|
53
|
+
The only design decision is where to place the extracted utilities.
|
|
54
|
+
|
|
55
|
+
The three functions (`isSymlink`, `isUnsafeName`, `safeReadFile`) are filesystem safety primitives used by `skill-loader.ts`.
|
|
56
|
+
They belong in a new `src/session/safe-fs.ts` module in the session domain, co-located with their sole remaining consumer.
|
|
57
|
+
|
|
58
|
+
The removal works consumers-first, declaration-last:
|
|
59
|
+
|
|
60
|
+
1. Extract shared utilities to `safe-fs.ts` (additive, no behavior change).
|
|
61
|
+
2. Remove all memory consumers — session-config, prompts, agent-types, custom-agents, UI, index wiring.
|
|
62
|
+
3. Remove the declarations — `MemoryScope`, `AgentConfig.memory`, `memory.ts`.
|
|
63
|
+
4. Update architecture docs.
|
|
64
|
+
|
|
65
|
+
## Module-Level Changes
|
|
66
|
+
|
|
67
|
+
### New files
|
|
68
|
+
|
|
69
|
+
- `src/session/safe-fs.ts` — extracted `isSymlink`, `isUnsafeName`, `safeReadFile`.
|
|
70
|
+
- `test/session/safe-fs.test.ts` — tests moved from `memory.test.ts` for these three functions.
|
|
71
|
+
|
|
72
|
+
### Modified files
|
|
73
|
+
|
|
74
|
+
- `src/session/skill-loader.ts` — change import from `#src/session/memory` to `#src/session/safe-fs`.
|
|
75
|
+
- `src/session/session-config.ts` — remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` from `AssemblerIO`; remove `MemoryScope` import; remove entire memory logic block (~lines 215–242).
|
|
76
|
+
- `src/session/prompts.ts` — remove `memoryBlock` from `PromptExtras` interface; remove `extras?.memoryBlock` injection.
|
|
77
|
+
- `src/config/agent-types.ts` — remove `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES`, `getMemoryToolNames()`, `getReadOnlyMemoryToolNames()`.
|
|
78
|
+
- `src/config/custom-agents.ts` — remove `MemoryScope` import; remove `memory: parseMemory(fm.memory)` assignment; remove `parseMemory()` function.
|
|
79
|
+
- `src/types.ts` — remove `MemoryScope` type; remove `memory?: MemoryScope` from `AgentConfig`; remove associated doc comment.
|
|
80
|
+
- `src/ui/agent-config-editor.ts` — remove `if (cfg.memory)` line.
|
|
81
|
+
- `src/ui/agent-creation-wizard.ts` — remove `memory:` line from frontmatter help text.
|
|
82
|
+
- `src/index.ts` — remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` import; remove those fields from `assemblerIO` object.
|
|
83
|
+
- `docs/architecture/architecture.md` — remove `memory.ts` from module listing; remove Memory node from domain model Mermaid diagram; update session domain description.
|
|
84
|
+
|
|
85
|
+
### Deleted files
|
|
86
|
+
|
|
87
|
+
- `src/session/memory.ts` — entire module.
|
|
88
|
+
- `test/session/memory.test.ts` — entire test file (tests for `isSymlink`, `isUnsafeName`, `safeReadFile` are moved to `safe-fs.test.ts` first; remaining memory-specific tests are deleted).
|
|
89
|
+
|
|
90
|
+
### Test files modified
|
|
91
|
+
|
|
92
|
+
- `test/session/session-config.test.ts` — remove `mockBuildMemoryBlock` / `mockBuildReadOnlyMemoryBlock` mocks from `AssemblerIO` construction; remove "assembleSessionConfig — memory block selection" describe block (~lines 354–427).
|
|
93
|
+
- `test/session/prompts.test.ts` — remove "injects memory block in replace mode", "injects memory block in append mode", and "injects both memory and skills" test cases.
|
|
94
|
+
- `test/config/agent-types.test.ts` — remove `getMemoryToolNames` / `getReadOnlyMemoryToolNames` imports and test suite (~lines 26–51).
|
|
95
|
+
- `test/config/custom-agents.test.ts` — remove memory scope parsing tests (~lines 361–403).
|
|
96
|
+
|
|
97
|
+
## Test Impact Analysis
|
|
98
|
+
|
|
99
|
+
1. The extraction of `isSymlink`, `isUnsafeName`, `safeReadFile` to `safe-fs.ts` enables their tests to exist independently of the memory module — currently they are co-located with memory-specific tests in `memory.test.ts`.
|
|
100
|
+
2. The memory-specific tests in `memory.test.ts` (`resolveMemoryDir`, `ensureMemoryDir`, `readMemoryIndex`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`) become redundant and are deleted — the code they test is being removed.
|
|
101
|
+
3. The memory block selection tests in `session-config.test.ts` (~70 lines) test the memory branching logic in `assembleSessionConfig` — they are deleted because that logic is removed.
|
|
102
|
+
4. The memory injection tests in `prompts.test.ts` test `memoryBlock` injection — deleted because the field and injection code are removed.
|
|
103
|
+
5. The memory parsing tests in `custom-agents.test.ts` test `parseMemory` — deleted because the function is removed.
|
|
104
|
+
6. The memory tool name helper tests in `agent-types.test.ts` test `getMemoryToolNames` / `getReadOnlyMemoryToolNames` — deleted because the functions are removed.
|
|
105
|
+
7. All other tests remain as-is — they do not depend on memory functionality.
|
|
106
|
+
|
|
107
|
+
## TDD Order
|
|
108
|
+
|
|
109
|
+
1. **Extract utilities to `safe-fs.ts`.**
|
|
110
|
+
Create `src/session/safe-fs.ts` with `isSymlink`, `isUnsafeName`, `safeReadFile`.
|
|
111
|
+
Create `test/session/safe-fs.test.ts` with tests moved from `memory.test.ts` for these three functions.
|
|
112
|
+
Update `src/session/skill-loader.ts` import to point to `#src/session/safe-fs`.
|
|
113
|
+
Update `src/session/memory.ts` import to point to `#src/session/safe-fs` (temporary — keeps memory working until removal).
|
|
114
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
115
|
+
Commit: `refactor: extract safe-fs utilities from memory module`
|
|
116
|
+
|
|
117
|
+
2. **Remove memory from session assembly and config layers.**
|
|
118
|
+
Remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` from `AssemblerIO` in `session-config.ts`.
|
|
119
|
+
Remove `MemoryScope` import and all memory logic from `session-config.ts`.
|
|
120
|
+
Remove `memoryBlock` from `PromptExtras` in `prompts.ts` and its injection logic.
|
|
121
|
+
Remove `getMemoryToolNames`, `getReadOnlyMemoryToolNames`, `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES` from `agent-types.ts`.
|
|
122
|
+
Remove `getMemoryToolNames` / `getReadOnlyMemoryToolNames` import from `session-config.ts`.
|
|
123
|
+
Remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` import and `assemblerIO` fields from `index.ts`.
|
|
124
|
+
Update `test/session/session-config.test.ts`: remove memory mocks from IO construction and memory block selection test suite.
|
|
125
|
+
Update `test/session/prompts.test.ts`: remove memory injection tests.
|
|
126
|
+
Update `test/config/agent-types.test.ts`: remove memory tool name helper tests.
|
|
127
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
128
|
+
Commit: `feat!: remove memory from session assembly and config layers`
|
|
129
|
+
|
|
130
|
+
3. **Remove memory from types, custom-agents, and UI.**
|
|
131
|
+
Remove `MemoryScope` type and `memory` field from `AgentConfig` in `types.ts`.
|
|
132
|
+
Remove `parseMemory()` function, `MemoryScope` import, and `memory:` assignment from `custom-agents.ts`.
|
|
133
|
+
Remove `if (cfg.memory)` line from `agent-config-editor.ts`.
|
|
134
|
+
Remove `memory:` help text line from `agent-creation-wizard.ts`.
|
|
135
|
+
Update `test/config/custom-agents.test.ts`: remove memory parsing tests.
|
|
136
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
137
|
+
Commit: `feat!: remove MemoryScope type and memory config field`
|
|
138
|
+
|
|
139
|
+
4. **Delete `memory.ts` and its test file.**
|
|
140
|
+
Delete `src/session/memory.ts`.
|
|
141
|
+
Delete `test/session/memory.test.ts`.
|
|
142
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
143
|
+
Commit: `feat!: delete memory module`
|
|
144
|
+
|
|
145
|
+
5. **Update architecture documentation.**
|
|
146
|
+
Remove Memory node from the domain model Mermaid diagram.
|
|
147
|
+
Remove `memory.ts` from the module listing.
|
|
148
|
+
Update session domain description.
|
|
149
|
+
Commit: `docs: update architecture after memory removal`
|
|
150
|
+
|
|
151
|
+
## Risks and Mitigations
|
|
152
|
+
|
|
153
|
+
| Risk | Mitigation |
|
|
154
|
+
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| Users with `memory:` in custom agent frontmatter — the field silently becomes a no-op. | `parseMemory` removal means unknown frontmatter keys are ignored by the markdown parser. No error, no crash — just no memory. This is acceptable for a feature removal. |
|
|
156
|
+
| `skill-loader.ts` breaks if the utility extraction has a typo or missing re-export. | Step 1 runs full test suite before proceeding. The skill-loader tests exercise `isUnsafeName` and `safeReadFile` indirectly. |
|
|
157
|
+
| Architecture doc Mermaid diagram breaks after node removal. | Verify the diagram renders correctly after editing — remove both the node and all edges referencing it. |
|
|
158
|
+
|
|
159
|
+
## Open Questions
|
|
160
|
+
|
|
161
|
+
None — the issue scope is unambiguous and all affected files have been traced.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 185
|
|
3
|
+
issue_title: "pi-subagents: Remove persistent agent memory feature"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #185 — pi-subagents: Remove persistent agent memory feature
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T20:46:56Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Traced all memory-related code across 9 source files, 5 test files, and the architecture doc.
|
|
13
|
+
Produced a 5-step TDD plan: extract shared utilities (`isSymlink`, `isUnsafeName`, `safeReadFile`) to `safe-fs.ts`, then remove memory consumers (session assembly, config, UI), then delete the module, then update docs.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The three utility functions in `memory.ts` are the only complication — `skill-loader.ts` imports them independently of memory.
|
|
18
|
+
Extracting to `src/session/safe-fs.ts` keeps them co-located with their sole remaining consumer.
|
|
19
|
+
- The removal is consumers-first, declaration-last: session-config and prompts lose their memory logic before `MemoryScope` is removed from `types.ts`, avoiding intermediate type errors.
|
|
20
|
+
- No ambiguous design choices — the issue scope section is precise about what to remove and what to extract.
|
|
21
|
+
- Memory field in custom agent frontmatter will silently become a no-op (ignored by the YAML parser) — no user-facing error, just loss of the feature.
|
|
22
|
+
- The `AssemblerIO` interface shrinks from 4 fields to 2 after removal, which is a welcome simplification.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-24T22:41:23Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
All 5 TDD steps completed across 5 commits.
|
|
29
|
+
Test count went from 901 (54 files) to 848 (53 files) — a net reduction of 53 tests and 1 file, reflecting the deletion of the `memory.test.ts` file with its memory-specific tests and the removal of memory-related tests from `session-config.test.ts`, `prompts.test.ts`, `agent-types.test.ts`, and `custom-agents.test.ts`.
|
|
30
|
+
New file `safe-fs.test.ts` was created with 13 tests for the extracted utilities.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- Step 1 had a subtle bug: after re-exporting `isUnsafeName` from `safe-fs` in `memory.ts`, the function was not imported into the `memory.ts` module scope itself, so `resolveMemoryDir` got a `ReferenceError` at runtime.
|
|
35
|
+
Fix was trivial: add `isUnsafeName` to the import alongside `isSymlink` and `safeReadFile`.
|
|
36
|
+
- Step 3 introduced a type error in `memory.ts` (still alive at that point): `MemoryScope` was imported from `#src/types` which no longer exported it.
|
|
37
|
+
Fix: inline the literal union `"user" | "project" | "local"` directly in `memory.ts` as a local type, so it compiles cleanly until deletion in step 4.
|
|
38
|
+
- The `SKILL.md` for `package-pi-subagents` also listed `memory.ts` in the session domain table — updated alongside `architecture.md` in the docs commit.
|
|
39
|
+
- No deviations from the plan other than the two minor bugs above (both self-corrected within the same TDD step).
|
|
@@ -38,3 +38,32 @@ Test count: 902 → 901 (one test removed).
|
|
|
38
38
|
- `ui-observer.ts` had one analogous fix: `event.assistantMessageEvent?.type` → `.type` (the field is required on `MessageUpdateEvent`).
|
|
39
39
|
- No test file changes were needed for `ui-observer.ts` — its existing tests all emit conforming events.
|
|
40
40
|
- The plan's contravariance reasoning about mock session compatibility was correct: `pnpm run check` passed without updating `MockSession.subscribe`.
|
|
41
|
+
|
|
42
|
+
## Stage: Final Retrospective (2026-05-24T20:41:42Z)
|
|
43
|
+
|
|
44
|
+
### Session summary
|
|
45
|
+
|
|
46
|
+
Clean two-step refactoring shipped as `pi-subagents-v6.19.1`.
|
|
47
|
+
Both TDD steps completed in a single session with two minor deviations from the plan, both caught by pre-commit hooks.
|
|
48
|
+
Test count: 902 → 901 (one non-conforming test removed).
|
|
49
|
+
|
|
50
|
+
### Observations
|
|
51
|
+
|
|
52
|
+
#### What went well
|
|
53
|
+
|
|
54
|
+
- The plan's contravariance analysis of `MockSession.subscribe` vs `SubscribableSession` was correct — zero test infrastructure changes needed.
|
|
55
|
+
- Pre-commit hooks (ESLint) caught both deviations from the plan at commit time, before they could reach CI.
|
|
56
|
+
- The user providing the Pi source path (`~/development/pi/pi`) unblocked verification of `AssistantMessage.usage` field requirements without guesswork.
|
|
57
|
+
|
|
58
|
+
#### What caused friction (agent side)
|
|
59
|
+
|
|
60
|
+
- `missing-context` — The plan's Risk 3 said "keep `?? ""` for safety" on `TextContent.text`, but `@typescript-eslint/no-unnecessary-condition` rejected it at commit time because `TextContent.text: string` is non-optional.
|
|
61
|
+
Impact: one failed commit attempt, immediate fix (changed `.map((c) => c.text ?? "")` to `.map((c) => c.text)`).
|
|
62
|
+
- `missing-context` — The plan's step 2 didn't anticipate the five additional lint errors surfaced by properly typing the `record-observer` callback: unnecessary optional chain on `event.message?.role`, unnecessary `if (u)` guard, and three unnecessary `?? 0` guards on usage fields.
|
|
63
|
+
Impact: added ~5 minutes of investigation to verify the SDK types were truly non-optional before removing guards; required removing one test case (`"ignores message_end without usage"`).
|
|
64
|
+
Both instances stem from the same root cause: the plan checked that the SDK types *existed* but didn't trace the *field optionality* of types downstream of `AgentSessionEvent` (specifically `AgentMessage.usage` and `Usage.{input,output,cacheWrite}`).
|
|
65
|
+
|
|
66
|
+
#### What caused friction (user side)
|
|
67
|
+
|
|
68
|
+
- The Pi source path (`~/development/pi/pi`) was provided reactively after I'd already spent several tool calls trying to locate `AgentMessage` type definitions in `node_modules`.
|
|
69
|
+
Sharing this path earlier (or noting it in the package skill) would have saved ~4 `grep`/`find` calls.
|
package/package.json
CHANGED
|
@@ -133,23 +133,3 @@ export class AgentTypeRegistry implements AgentConfigLookup {
|
|
|
133
133
|
|
|
134
134
|
/** All known built-in tool names. */
|
|
135
135
|
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
136
|
-
|
|
137
|
-
/** Tool names required for memory management. */
|
|
138
|
-
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Get memory tool names (read/write/edit) not already in the provided set.
|
|
142
|
-
*/
|
|
143
|
-
export function getMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
144
|
-
return MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Tool names needed for read-only memory access. */
|
|
148
|
-
const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get read-only memory tool names not already in the provided set.
|
|
152
|
-
*/
|
|
153
|
-
export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
154
|
-
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
155
|
-
}
|
|
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
|
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
|
|
9
9
|
import { debugLog } from "#src/debug";
|
|
10
|
-
import type { AgentConfig,
|
|
10
|
+
import type { AgentConfig, ThinkingLevel } from "#src/types";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Scan for custom agent .md files from multiple locations.
|
|
@@ -69,7 +69,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
69
69
|
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
70
70
|
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
71
71
|
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
72
|
-
memory: parseMemory(fm.memory),
|
|
73
72
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
74
73
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
75
74
|
source,
|
|
@@ -119,15 +118,6 @@ function csvListOptional(val: unknown): string[] | undefined {
|
|
|
119
118
|
return parseCsvField(val);
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/**
|
|
123
|
-
* Parse a memory scope field.
|
|
124
|
-
* omitted → undefined; "user"/"project"/"local" → MemoryScope.
|
|
125
|
-
*/
|
|
126
|
-
function parseMemory(val: unknown): MemoryScope | undefined {
|
|
127
|
-
if (val === "user" || val === "project" || val === "local") return val;
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
/**
|
|
132
122
|
* Parse an inherit field (extensions, skills).
|
|
133
123
|
* omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { createSubagentRuntime } from "#src/runtime";
|
|
|
34
34
|
import { publishSubagentsService, unpublishSubagentsService } from "#src/service/service";
|
|
35
35
|
import { createSubagentsService } from "#src/service/service-adapter";
|
|
36
36
|
import { detectEnv } from "#src/session/env";
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
|
|
39
39
|
import { buildAgentPrompt } from "#src/session/prompts";
|
|
40
40
|
import { deriveSubagentSessionDir } from "#src/session/session-dir";
|
|
@@ -146,8 +146,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
146
|
createSession: (opts) => createAgentSession(opts as any),
|
|
147
147
|
assemblerIO: {
|
|
148
148
|
preloadSkills,
|
|
149
|
-
buildMemoryBlock,
|
|
150
|
-
buildReadOnlyMemoryBlock,
|
|
151
149
|
buildAgentPrompt,
|
|
152
150
|
},
|
|
153
151
|
};
|
package/src/session/prompts.ts
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
import type { EnvInfo } from "#src/session/env";
|
|
6
6
|
import type { AgentPromptConfig } from "#src/types";
|
|
7
7
|
|
|
8
|
-
/** Extra sections to inject into the system prompt (
|
|
8
|
+
/** Extra sections to inject into the system prompt (skills, etc.). */
|
|
9
9
|
export interface PromptExtras {
|
|
10
|
-
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
|
11
|
-
memoryBlock?: string;
|
|
12
10
|
/** Preloaded skill contents to inject. */
|
|
13
11
|
skillBlocks?: { name: string; content: string }[];
|
|
14
12
|
}
|
|
@@ -45,9 +43,6 @@ Platform: ${env.platform}`;
|
|
|
45
43
|
|
|
46
44
|
// Build optional extras suffix
|
|
47
45
|
const extraSections: string[] = [];
|
|
48
|
-
if (extras?.memoryBlock) {
|
|
49
|
-
extraSections.push(extras.memoryBlock);
|
|
50
|
-
}
|
|
51
46
|
if (extras?.skillBlocks?.length) {
|
|
52
47
|
for (const skill of extras.skillBlocks) {
|
|
53
48
|
extraSections.push(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safe-fs.ts — Filesystem safety utilities for reading untrusted paths.
|
|
3
|
+
*
|
|
4
|
+
* Used by skill-loader.ts to reject symlinks and path-traversal names
|
|
5
|
+
* before reading skill files from disk.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
9
|
+
import { debugLog } from "#src/debug";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
13
|
+
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
14
|
+
*/
|
|
15
|
+
export function isUnsafeName(name: string): boolean {
|
|
16
|
+
if (!name || name.length > 128) return true;
|
|
17
|
+
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
22
|
+
*/
|
|
23
|
+
export function isSymlink(filePath: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
return lstatSync(filePath).isSymbolicLink();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
debugLog("lstatSync", err);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Safely read a file, rejecting symlinks.
|
|
34
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
35
|
+
*/
|
|
36
|
+
export function safeReadFile(filePath: string): string | undefined {
|
|
37
|
+
if (!existsSync(filePath)) return undefined;
|
|
38
|
+
if (isSymlink(filePath)) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(filePath, "utf-8");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
debugLog("readFileSync", err);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -10,20 +10,11 @@
|
|
|
10
10
|
* before invoking this function, keeping the assembler synchronous.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
type AgentConfigLookup,
|
|
15
|
-
getMemoryToolNames,
|
|
16
|
-
getReadOnlyMemoryToolNames,
|
|
17
|
-
} from "#src/config/agent-types";
|
|
13
|
+
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
18
14
|
import type { EnvInfo } from "#src/session/env";
|
|
19
15
|
import type { PromptExtras } from "#src/session/prompts";
|
|
20
16
|
import type { PreloadedSkill } from "#src/session/skill-loader";
|
|
21
|
-
import type {
|
|
22
|
-
AgentPromptConfig,
|
|
23
|
-
MemoryScope,
|
|
24
|
-
SubagentType,
|
|
25
|
-
ThinkingLevel,
|
|
26
|
-
} from "#src/types";
|
|
17
|
+
import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types";
|
|
27
18
|
|
|
28
19
|
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
29
20
|
|
|
@@ -52,12 +43,6 @@ export interface ToolFilterConfig {
|
|
|
52
43
|
*/
|
|
53
44
|
export interface AssemblerIO {
|
|
54
45
|
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
55
|
-
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
56
|
-
buildReadOnlyMemoryBlock: (
|
|
57
|
-
name: string,
|
|
58
|
-
scope: MemoryScope,
|
|
59
|
-
cwd: string,
|
|
60
|
-
) => string;
|
|
61
46
|
buildAgentPrompt: (
|
|
62
47
|
config: AgentPromptConfig,
|
|
63
48
|
cwd: string,
|
|
@@ -210,38 +195,7 @@ export function assembleSessionConfig(
|
|
|
210
195
|
}
|
|
211
196
|
}
|
|
212
197
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Persistent memory: detect write capability and branch accordingly.
|
|
216
|
-
// Account for disallowedTools — a tool in the base set but on the denylist
|
|
217
|
-
// is not truly available.
|
|
218
|
-
if (agentConfig.memory) {
|
|
219
|
-
const existingNames = new Set(toolNames);
|
|
220
|
-
const denied = agentConfig.disallowedTools
|
|
221
|
-
? new Set(agentConfig.disallowedTools)
|
|
222
|
-
: undefined;
|
|
223
|
-
const effectivelyHas = (name: string) =>
|
|
224
|
-
existingNames.has(name) && !denied?.has(name);
|
|
225
|
-
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
226
|
-
|
|
227
|
-
if (hasWriteTools) {
|
|
228
|
-
const extraNames = getMemoryToolNames(existingNames);
|
|
229
|
-
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
230
|
-
extras.memoryBlock = io.buildMemoryBlock(
|
|
231
|
-
agentConfig.name,
|
|
232
|
-
agentConfig.memory,
|
|
233
|
-
effectiveCwd,
|
|
234
|
-
);
|
|
235
|
-
} else {
|
|
236
|
-
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
237
|
-
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
238
|
-
extras.memoryBlock = io.buildReadOnlyMemoryBlock(
|
|
239
|
-
agentConfig.name,
|
|
240
|
-
agentConfig.memory,
|
|
241
|
-
effectiveCwd,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
198
|
+
const toolNames = registry.getToolNamesForType(type);
|
|
245
199
|
|
|
246
200
|
// Build system prompt from the resolved agent config
|
|
247
201
|
const systemPrompt = io.buildAgentPrompt(
|
|
@@ -24,7 +24,7 @@ import { homedir } from "node:os";
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { debugLog } from "#src/debug";
|
|
27
|
-
import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/
|
|
27
|
+
import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/safe-fs";
|
|
28
28
|
|
|
29
29
|
export interface PreloadedSkill {
|
|
30
30
|
name: string;
|
package/src/types.ts
CHANGED
|
@@ -20,9 +20,6 @@ export interface SubscribableSession {
|
|
|
20
20
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
21
21
|
export type SubagentType = string;
|
|
22
22
|
|
|
23
|
-
/** Memory scope for persistent agent memory. */
|
|
24
|
-
export type MemoryScope = "user" | "project" | "local";
|
|
25
|
-
|
|
26
23
|
/** Isolation mode for agent execution. */
|
|
27
24
|
export type IsolationMode = "worktree";
|
|
28
25
|
|
|
@@ -59,8 +56,6 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
|
59
56
|
runInBackground?: boolean;
|
|
60
57
|
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
61
58
|
isolated?: boolean;
|
|
62
|
-
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
63
|
-
memory?: MemoryScope;
|
|
64
59
|
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
65
60
|
isolation?: IsolationMode;
|
|
66
61
|
/** true = this is an embedded default agent (informational) */
|
|
@@ -131,7 +131,6 @@ export function createAgentConfigEditor(
|
|
|
131
131
|
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
132
132
|
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
133
133
|
if (cfg.isolated) fmFields.push("isolated: true");
|
|
134
|
-
if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`);
|
|
135
134
|
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
136
135
|
|
|
137
136
|
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
@@ -115,7 +115,6 @@ disallowed_tools: <comma-separated tool names to block, even if otherwise availa
|
|
|
115
115
|
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
116
116
|
run_in_background: <true to run in background by default. Default: false>
|
|
117
117
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
118
|
-
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
|
|
119
118
|
isolation: <"worktree" to run in isolated git worktree. Omit for normal>
|
|
120
119
|
---
|
|
121
120
|
|
package/src/session/memory.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
-
*
|
|
4
|
-
* Memory scopes:
|
|
5
|
-
* - "user" → ~/.pi/agent-memory/{agent-name}/
|
|
6
|
-
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
-
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join, } from "node:path";
|
|
13
|
-
import { debugLog } from "#src/debug";
|
|
14
|
-
import type { MemoryScope } from "#src/types";
|
|
15
|
-
|
|
16
|
-
/** Maximum lines to read from MEMORY.md */
|
|
17
|
-
const MAX_MEMORY_LINES = 200;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
21
|
-
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
22
|
-
*/
|
|
23
|
-
export function isUnsafeName(name: string): boolean {
|
|
24
|
-
if (!name || name.length > 128) return true;
|
|
25
|
-
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
30
|
-
*/
|
|
31
|
-
export function isSymlink(filePath: string): boolean {
|
|
32
|
-
try {
|
|
33
|
-
return lstatSync(filePath).isSymbolicLink();
|
|
34
|
-
} catch (err) {
|
|
35
|
-
debugLog("lstatSync", err);
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Safely read a file, rejecting symlinks.
|
|
42
|
-
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
43
|
-
*/
|
|
44
|
-
export function safeReadFile(filePath: string): string | undefined {
|
|
45
|
-
if (!existsSync(filePath)) return undefined;
|
|
46
|
-
if (isSymlink(filePath)) return undefined;
|
|
47
|
-
try {
|
|
48
|
-
return readFileSync(filePath, "utf-8");
|
|
49
|
-
} catch (err) {
|
|
50
|
-
debugLog("readFileSync", err);
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
57
|
-
* Throws if agentName contains path traversal characters.
|
|
58
|
-
*/
|
|
59
|
-
export function resolveMemoryDir(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
60
|
-
if (isUnsafeName(agentName)) {
|
|
61
|
-
throw new Error(`Unsafe agent name for memory directory: "${agentName}"`);
|
|
62
|
-
}
|
|
63
|
-
switch (scope) {
|
|
64
|
-
case "user":
|
|
65
|
-
return join(homedir(), ".pi", "agent-memory", agentName);
|
|
66
|
-
case "project":
|
|
67
|
-
return join(cwd, ".pi", "agent-memory", agentName);
|
|
68
|
-
case "local":
|
|
69
|
-
return join(cwd, ".pi", "agent-memory-local", agentName);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Ensure the memory directory exists, creating it if needed.
|
|
75
|
-
* Refuses to create directories if any component in the path is a symlink
|
|
76
|
-
* to prevent symlink-based directory traversal attacks.
|
|
77
|
-
*/
|
|
78
|
-
export function ensureMemoryDir(memoryDir: string): void {
|
|
79
|
-
// If the directory already exists, verify it's not a symlink
|
|
80
|
-
if (existsSync(memoryDir)) {
|
|
81
|
-
if (isSymlink(memoryDir)) {
|
|
82
|
-
throw new Error(`Refusing to use symlinked memory directory: ${memoryDir}`);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
mkdirSync(memoryDir, { recursive: true });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
91
|
-
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
92
|
-
*/
|
|
93
|
-
export function readMemoryIndex(memoryDir: string): string | undefined {
|
|
94
|
-
// Reject symlinked memory directories
|
|
95
|
-
if (isSymlink(memoryDir)) return undefined;
|
|
96
|
-
|
|
97
|
-
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
98
|
-
const content = safeReadFile(memoryFile);
|
|
99
|
-
if (content === undefined) return undefined;
|
|
100
|
-
|
|
101
|
-
const lines = content.split("\n");
|
|
102
|
-
if (lines.length > MAX_MEMORY_LINES) {
|
|
103
|
-
return lines.slice(0, MAX_MEMORY_LINES).join("\n") + "\n... (truncated at 200 lines)";
|
|
104
|
-
}
|
|
105
|
-
return content;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build the memory block to inject into the agent's system prompt.
|
|
110
|
-
* Also ensures the memory directory exists (creates it if needed).
|
|
111
|
-
*/
|
|
112
|
-
export function buildMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
113
|
-
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
114
|
-
// Create the memory directory so the agent can immediately write to it
|
|
115
|
-
ensureMemoryDir(memoryDir);
|
|
116
|
-
|
|
117
|
-
const existingMemory = readMemoryIndex(memoryDir);
|
|
118
|
-
|
|
119
|
-
const header = `# Agent Memory
|
|
120
|
-
|
|
121
|
-
You have a persistent memory directory at: ${memoryDir}/
|
|
122
|
-
Memory scope: ${scope}
|
|
123
|
-
|
|
124
|
-
This memory persists across sessions. Use it to build up knowledge over time.`;
|
|
125
|
-
|
|
126
|
-
const memoryContent = existingMemory
|
|
127
|
-
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
128
|
-
: `\n\nNo MEMORY.md exists yet. Create one at ${join(memoryDir, "MEMORY.md")} to start building persistent memory.`;
|
|
129
|
-
|
|
130
|
-
const instructions = `
|
|
131
|
-
|
|
132
|
-
## Memory Instructions
|
|
133
|
-
- MEMORY.md is an index file — keep it concise (under 200 lines). Lines after 200 are truncated.
|
|
134
|
-
- Store detailed memories in separate files within ${memoryDir}/ and link to them from MEMORY.md.
|
|
135
|
-
- Each memory file should use this frontmatter format:
|
|
136
|
-
\`\`\`markdown
|
|
137
|
-
---
|
|
138
|
-
name: <memory name>
|
|
139
|
-
description: <one-line description>
|
|
140
|
-
type: <user|feedback|project|reference>
|
|
141
|
-
---
|
|
142
|
-
<memory content>
|
|
143
|
-
\`\`\`
|
|
144
|
-
- Update or remove memories that become outdated. Check for existing memories before creating duplicates.
|
|
145
|
-
- You have Read, Write, and Edit tools available for managing memory files.`;
|
|
146
|
-
|
|
147
|
-
return header + memoryContent + instructions;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Build a read-only memory block for agents that lack write/edit tools.
|
|
152
|
-
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
153
|
-
*/
|
|
154
|
-
export function buildReadOnlyMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
155
|
-
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
156
|
-
const existingMemory = readMemoryIndex(memoryDir);
|
|
157
|
-
|
|
158
|
-
const header = `# Agent Memory (read-only)
|
|
159
|
-
|
|
160
|
-
Memory scope: ${scope}
|
|
161
|
-
You have read-only access to memory. You can reference existing memories but cannot create or modify them.`;
|
|
162
|
-
|
|
163
|
-
const memoryContent = existingMemory
|
|
164
|
-
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
165
|
-
: `\n\nNo memory is available yet. Other agents or sessions with write access can create memories for you to consume.`;
|
|
166
|
-
|
|
167
|
-
return header + memoryContent;
|
|
168
|
-
}
|