docs(memory): add pi personal assistant memory implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,733 @@
|
||||
# Pi-Inspired Personal Assistant Memory — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add two-tier personal-assistant memory: a unified `user/*` namespace shared across channels and a working-memory layer (`user/working`) that survives restarts and is injected at session start.
|
||||
|
||||
**Architecture:** `user/working` is written on every compaction (TTL-based flat file with header metadata). On the first message of a new session, `user/profile` and `user/working` are injected into the base system prompt once. All behavior is behind a `memory.user_namespace` config key; when unset the feature is entirely inert.
|
||||
|
||||
**Tech Stack:** TypeScript, Vitest, existing `MemoryStore` (`src/memory/store.ts`), `AgentOrchestrator` (`src/backends/native/orchestrator.ts`), Zod config schema
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Config schema — new memory fields
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/config/schema.ts:592-613`
|
||||
|
||||
Add four new fields to `memorySchema`. They must come before `.default({})` at line 613.
|
||||
|
||||
**Step 1: Write the failing type check**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: passes (baseline). We'll check again after the edit.
|
||||
|
||||
**Step 2: Add fields to memorySchema**
|
||||
|
||||
In `src/config/schema.ts`, inside `memorySchema` (after `qmd: qmdSchema,` on line 612, before `}).default({});`), add:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* When set, all channels share user/* memory (unified identity namespace).
|
||||
* Absent = current session-scoped behavior, unchanged.
|
||||
*/
|
||||
user_namespace: z.string().optional(),
|
||||
/** How long working memory remains valid after the last compaction (days). */
|
||||
working_memory_ttl_days: z.number().min(1).max(365).default(14),
|
||||
/** Token budget for working memory injection at session start. */
|
||||
working_memory_max_tokens: z.number().min(100).max(4000).default(1000),
|
||||
/** When true, instruct the model to acknowledge prior context on session start. */
|
||||
proactive_session_greeting: z.boolean().default(false),
|
||||
```
|
||||
|
||||
**Step 3: Run typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/config/schema.ts
|
||||
git commit -m "feat(memory): add user_namespace and working memory config fields"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: WorkingMemory module
|
||||
|
||||
**Files:**
|
||||
- Create: `src/memory/workingMemory.ts`
|
||||
- Create: `src/memory/workingMemory.test.ts`
|
||||
|
||||
Working memory file format (stored at `{ns}/working.md`):
|
||||
|
||||
```
|
||||
# Working Memory
|
||||
Updated: 2026-02-25T11:30:00Z
|
||||
Expires: 2026-03-10T11:30:00Z
|
||||
|
||||
[content]
|
||||
```
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Create `src/memory/workingMemory.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { join } from 'path';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { MemoryStore } from './store.js';
|
||||
import { writeWorkingMemory, readWorkingMemory } from './workingMemory.js';
|
||||
|
||||
function makeStore(): { store: MemoryStore; dir: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'wm-test-'));
|
||||
const store = new MemoryStore({ dir, maxContextTokens: 2000 });
|
||||
return { store, dir };
|
||||
}
|
||||
|
||||
describe('writeWorkingMemory', () => {
|
||||
it('writes a file with Updated/Expires headers', () => {
|
||||
const { store, dir } = makeStore();
|
||||
writeWorkingMemory(store, 'user/working', 'some content', 14, 1000);
|
||||
const raw = store.read('user/working');
|
||||
expect(raw).toContain('# Working Memory');
|
||||
expect(raw).toContain('Updated:');
|
||||
expect(raw).toContain('Expires:');
|
||||
expect(raw).toContain('some content');
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it('truncates content to token budget', () => {
|
||||
const { store, dir } = makeStore();
|
||||
const longContent = 'x'.repeat(10000);
|
||||
writeWorkingMemory(store, 'user/working', longContent, 14, 100);
|
||||
const raw = store.read('user/working');
|
||||
// 100 tokens * 4 chars = 400 chars budget for content
|
||||
const contentPart = raw.split('\n\n').slice(1).join('\n\n');
|
||||
expect(contentPart.length).toBeLessThanOrEqual(400 + 10); // small tolerance
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('readWorkingMemory', () => {
|
||||
it('returns null when file does not exist', () => {
|
||||
const { store, dir } = makeStore();
|
||||
expect(readWorkingMemory(store, 'user/working')).toBeNull();
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it('returns content when not expired', () => {
|
||||
const { store, dir } = makeStore();
|
||||
writeWorkingMemory(store, 'user/working', 'hello world', 14, 1000);
|
||||
const result = readWorkingMemory(store, 'user/working');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.content).toBe('hello world');
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it('returns null when expired', () => {
|
||||
const { store, dir } = makeStore();
|
||||
// Write with 0 TTL days (expires immediately)
|
||||
writeWorkingMemory(store, 'user/working', 'stale content', 0, 1000);
|
||||
const result = readWorkingMemory(store, 'user/working');
|
||||
expect(result).toBeNull();
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it('returns null for malformed file', () => {
|
||||
const { store, dir } = makeStore();
|
||||
store.write('user/working', 'no headers here', 'replace');
|
||||
expect(readWorkingMemory(store, 'user/working')).toBeNull();
|
||||
rmSync(dir, { recursive: true });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/memory/workingMemory.test.ts
|
||||
```
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
**Step 3: Implement `src/memory/workingMemory.ts`**
|
||||
|
||||
```typescript
|
||||
import type { MemoryStore } from './store.js';
|
||||
|
||||
export interface WorkingMemoryEntry {
|
||||
content: string;
|
||||
updatedAt: Date;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
const HEADER_PREFIX = '# Working Memory\n';
|
||||
|
||||
/**
|
||||
* Write a compaction summary to the working memory namespace.
|
||||
* Content is capped at maxTokens (estimated at 4 chars/token).
|
||||
* The file format includes Updated/Expires timestamps for lazy expiry checks.
|
||||
*/
|
||||
export function writeWorkingMemory(
|
||||
store: MemoryStore,
|
||||
namespace: string,
|
||||
content: string,
|
||||
ttlDays: number,
|
||||
maxTokens: number,
|
||||
): void {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + ttlDays * 24 * 60 * 60 * 1000);
|
||||
const maxChars = maxTokens * 4;
|
||||
const truncatedContent = content.length > maxChars ? content.slice(0, maxChars) : content;
|
||||
|
||||
const file = [
|
||||
'# Working Memory',
|
||||
`Updated: ${now.toISOString()}`,
|
||||
`Expires: ${expiresAt.toISOString()}`,
|
||||
'',
|
||||
truncatedContent,
|
||||
].join('\n');
|
||||
|
||||
store.write(namespace, file, 'replace');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read working memory. Returns null if the file is absent, malformed, or expired.
|
||||
* Expiry is checked lazily here — no background cleanup needed.
|
||||
*/
|
||||
export function readWorkingMemory(
|
||||
store: MemoryStore,
|
||||
namespace: string,
|
||||
): WorkingMemoryEntry | null {
|
||||
const raw = store.read(namespace);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!raw.startsWith(HEADER_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = raw.split('\n');
|
||||
let updatedAt: Date | null = null;
|
||||
let expiresAt: Date | null = null;
|
||||
let contentStartLine = 0;
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith('Updated: ')) {
|
||||
updatedAt = new Date(line.slice('Updated: '.length));
|
||||
} else if (line.startsWith('Expires: ')) {
|
||||
expiresAt = new Date(line.slice('Expires: '.length));
|
||||
} else if (line === '' && expiresAt !== null) {
|
||||
contentStartLine = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updatedAt || !expiresAt || isNaN(expiresAt.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expiresAt.getTime() <= Date.now()) {
|
||||
console.debug('[Flynn:working-memory] Working memory expired, skipping injection');
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = lines.slice(contentStartLine).join('\n').trim();
|
||||
|
||||
return { content, updatedAt, expiresAt };
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run tests**
|
||||
|
||||
```bash
|
||||
pnpm test:run src/memory/workingMemory.test.ts
|
||||
```
|
||||
Expected: all pass.
|
||||
|
||||
**Step 5: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/memory/workingMemory.ts src/memory/workingMemory.test.ts
|
||||
git commit -m "feat(memory): add working memory read/write with TTL expiry"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: PA-focused compaction prompt
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/backends/native/prompts.ts`
|
||||
|
||||
The existing `COMPACTION_SYSTEM_PROMPT` is a generic summarizer. We need a personal-assistant variant that captures what the user was working on, decisions made, expressed preferences, and open threads.
|
||||
|
||||
**Step 1: Add the new prompt constant**
|
||||
|
||||
In `src/backends/native/prompts.ts`, after `COMPACTION_SYSTEM_PROMPT`, add:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Personal-assistant variant of the compaction prompt.
|
||||
* Used when `memory.user_namespace` is configured.
|
||||
* Captures continuity context: what the user was working on, decisions,
|
||||
* preferences, and open threads — not just a generic recap.
|
||||
*/
|
||||
export const PA_COMPACTION_SYSTEM_PROMPT = `You are summarising a conversation for a personal assistant. Your summary will be injected at the start of the next session so the assistant can pick up exactly where things left off.
|
||||
|
||||
Focus on:
|
||||
- What the user was working on and its current status (be specific: which files, commands, or steps were involved)
|
||||
- Decisions made and why (include rationale when stated)
|
||||
- Preferences or constraints the user expressed (tools, styles, approaches to avoid or prefer)
|
||||
- Open threads, unresolved questions, or explicit follow-up items
|
||||
|
||||
Rules:
|
||||
- Preserve key facts, file paths, error messages, and specific values verbatim.
|
||||
- Be concise but complete — aim for roughly 20% of the original length.
|
||||
- Use bullet points under short descriptive headings.
|
||||
- Never invent information not present in the conversation.
|
||||
- Skip purely transient content (one-off commands, status messages with no lasting significance).
|
||||
|
||||
Output format:
|
||||
Return a markdown summary. No preamble — output only the summary.`;
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/backends/native/prompts.ts
|
||||
git commit -m "feat(memory): add personal-assistant compaction prompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Thread working memory through compaction
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/context/compaction.ts`
|
||||
|
||||
Add `summary` to `CompactionResult` so the orchestrator can write it to `user/working` without re-computing it. Also thread the PA prompt option.
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
There is no direct unit test for `compactHistory()` currently (it requires a live orchestrator). We'll test via the orchestrator integration in Task 5. For now, just verify the type change works.
|
||||
|
||||
**Step 2: Update `CompactionResult` interface**
|
||||
|
||||
In `src/context/compaction.ts`, add `summary` to `CompactionResult`:
|
||||
|
||||
```typescript
|
||||
export interface CompactionResult {
|
||||
/** The compacted messages: [summary, ...recentMessages]. */
|
||||
messages: Message[];
|
||||
/** Number of messages that were compacted (removed). */
|
||||
compactedCount: number;
|
||||
/** Estimated tokens before compaction. */
|
||||
tokensBefore: number;
|
||||
/** Estimated tokens after compaction. */
|
||||
tokensAfter: number;
|
||||
/** The raw summary text produced by the compaction model (populated when compaction ran). */
|
||||
summary?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add `usePersonalAssistantPrompt` option and populate `summary`**
|
||||
|
||||
Update the `compactHistory` function signature to accept the option and return the summary:
|
||||
|
||||
```typescript
|
||||
export async function compactHistory(opts: {
|
||||
messages: Message[];
|
||||
orchestrator: AgentOrchestrator;
|
||||
config: CompactionConfig;
|
||||
memoryStore?: MemoryStore;
|
||||
autoExtract?: boolean;
|
||||
usePersonalAssistantPrompt?: boolean; // ← new
|
||||
}): Promise<CompactionResult> {
|
||||
```
|
||||
|
||||
In the body, replace the `COMPACTION_SYSTEM_PROMPT` reference in the `orchestrator.delegate()` call:
|
||||
|
||||
```typescript
|
||||
import { COMPACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_PROMPT, PA_COMPACTION_SYSTEM_PROMPT } from '../backends/native/prompts.js';
|
||||
|
||||
// ...inside compactHistory, where orchestrator.delegate is called:
|
||||
const systemPrompt = opts.usePersonalAssistantPrompt
|
||||
? PA_COMPACTION_SYSTEM_PROMPT
|
||||
: COMPACTION_SYSTEM_PROMPT;
|
||||
|
||||
const result = await orchestrator.delegate({
|
||||
task: 'compaction',
|
||||
tier,
|
||||
systemPrompt,
|
||||
message: formattedConversation,
|
||||
maxTokens: config.summaryMaxTokens,
|
||||
});
|
||||
```
|
||||
|
||||
And populate `summary` in the return value:
|
||||
|
||||
```typescript
|
||||
return {
|
||||
messages: [...preservedMessages, summaryMessage, ...toKeep],
|
||||
compactedCount: toSummarize.length,
|
||||
tokensBefore: estimateMessageTokens(messages),
|
||||
tokensAfter: estimateMessageTokens([...preservedMessages, summaryMessage, ...toKeep]),
|
||||
summary: result.content, // ← new
|
||||
};
|
||||
```
|
||||
|
||||
**Step 4: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 5: Run full test suite**
|
||||
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
Expected: all pass (summary field is additive, existing tests unaffected).
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/context/compaction.ts src/backends/native/prompts.ts
|
||||
git commit -m "feat(memory): thread summary and PA prompt through compaction result"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Orchestrator — session-start injection + post-compaction write
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/backends/native/orchestrator.ts`
|
||||
|
||||
This task has two parts:
|
||||
1. Write working memory after every compaction (when `userNamespace` is set)
|
||||
2. Inject `user/profile` + `user/working` into the base system prompt on first message
|
||||
|
||||
**Step 1: Write the failing test — post-compaction working memory write**
|
||||
|
||||
In `src/backends/native/orchestrator.test.ts` (or the closest existing test file — check with `ls src/backends/native/*.test.ts`), add a test. First, find the test file:
|
||||
|
||||
```bash
|
||||
ls src/backends/native/*.test.ts
|
||||
```
|
||||
|
||||
If `orchestrator.test.ts` exists, add to it. The test should verify that after calling `compact()`, the working memory namespace contains the summary. Because this requires a functioning orchestrator setup, write an integration-style test using vitest mocks rather than a full live model:
|
||||
|
||||
Locate the `describe('compact')` block in `src/backends/native/orchestrator.test.ts`. Add:
|
||||
|
||||
```typescript
|
||||
it('writes summary to user/working when userNamespace is set', async () => {
|
||||
// Setup: orchestrator with a mock delegate that returns a fixed summary
|
||||
// and a real MemoryStore in a temp dir
|
||||
// (follow the existing orchestrator test setup patterns)
|
||||
// After orchestrator.compact(), read store.read('user/working')
|
||||
// Expect it to contain the summary content
|
||||
});
|
||||
```
|
||||
|
||||
You'll need to examine the existing test setup patterns carefully before writing the full test body. See the file for the existing mock structure.
|
||||
|
||||
**Step 2: Add new fields to `OrchestratorConfig`**
|
||||
|
||||
In `src/backends/native/orchestrator.ts`, in `OrchestratorConfig` (after `attachmentCollector` on line ~160), add:
|
||||
|
||||
```typescript
|
||||
/** Shared identity namespace for cross-channel memory (e.g. 'user'). Absent = session-scoped. */
|
||||
userNamespace?: string;
|
||||
/** TTL in days for working memory. Defaults to 14. */
|
||||
workingMemoryTtlDays?: number;
|
||||
/** Token budget for working memory injection. Defaults to 1000. */
|
||||
workingMemoryMaxTokens?: number;
|
||||
/** When true, instruct the model to acknowledge prior context on session start. */
|
||||
proactiveSessionGreeting?: boolean;
|
||||
```
|
||||
|
||||
**Step 3: Store new config fields on the class**
|
||||
|
||||
In the `AgentOrchestrator` class, add private fields (near the other `_memory*` fields):
|
||||
|
||||
```typescript
|
||||
private _userNamespace?: string;
|
||||
private _workingMemoryTtlDays: number;
|
||||
private _workingMemoryMaxTokens: number;
|
||||
private _proactiveSessionGreeting: boolean;
|
||||
private _sessionContextInjected = false;
|
||||
```
|
||||
|
||||
In the constructor, after the existing memory field assignments:
|
||||
|
||||
```typescript
|
||||
this._userNamespace = config.userNamespace;
|
||||
this._workingMemoryTtlDays = config.workingMemoryTtlDays ?? 14;
|
||||
this._workingMemoryMaxTokens = config.workingMemoryMaxTokens ?? 1000;
|
||||
this._proactiveSessionGreeting = config.proactiveSessionGreeting ?? false;
|
||||
```
|
||||
|
||||
**Step 4: Post-compaction write in `compact()`**
|
||||
|
||||
Import `writeWorkingMemory` at the top of orchestrator.ts:
|
||||
|
||||
```typescript
|
||||
import { writeWorkingMemory } from '../../memory/workingMemory.js';
|
||||
```
|
||||
|
||||
In the `compact()` method, after the `auditLogger?.sessionCompact(...)` call (around line 500), add:
|
||||
|
||||
```typescript
|
||||
// Write working memory when user namespace is configured
|
||||
if (result.summary && this._userNamespace && this._memoryStore) {
|
||||
const workingNs = `${this._userNamespace}/working`;
|
||||
writeWorkingMemory(
|
||||
this._memoryStore,
|
||||
workingNs,
|
||||
result.summary,
|
||||
this._workingMemoryTtlDays,
|
||||
this._workingMemoryMaxTokens,
|
||||
);
|
||||
console.log(`[Flynn:working-memory] Updated ${workingNs} after compaction`);
|
||||
}
|
||||
```
|
||||
|
||||
Also, pass `usePersonalAssistantPrompt` to `compactHistory()` in `compact()`:
|
||||
|
||||
```typescript
|
||||
const result = await compactHistory({
|
||||
messages,
|
||||
orchestrator: this,
|
||||
config,
|
||||
memoryStore: this._memoryStore,
|
||||
autoExtract: this._memoryAutoExtract,
|
||||
usePersonalAssistantPrompt: Boolean(this._userNamespace), // ← new
|
||||
});
|
||||
```
|
||||
|
||||
**Step 5: Session-start injection**
|
||||
|
||||
Add `_injectSessionContext()` private method to `AgentOrchestrator` (after `_injectMemoryContext`):
|
||||
|
||||
```typescript
|
||||
private _injectSessionContext(): void {
|
||||
if (this._sessionContextInjected) {
|
||||
return;
|
||||
}
|
||||
this._sessionContextInjected = true;
|
||||
|
||||
if (!this._memoryStore || !this._userNamespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
// User profile block
|
||||
const profile = this._memoryStore.read(`${this._userNamespace}/profile`);
|
||||
if (profile.length > 0) {
|
||||
sections.push(`--- Who you're talking to ---\n${profile}`);
|
||||
}
|
||||
|
||||
// Working memory block
|
||||
const working = readWorkingMemory(this._memoryStore, `${this._userNamespace}/working`);
|
||||
if (working) {
|
||||
sections.push(`--- Recent context ---\n${working.content}`);
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._systemPromptBase = `${this._systemPromptBase}\n\n${sections.join('\n\n')}`;
|
||||
|
||||
if (this._proactiveSessionGreeting) {
|
||||
this._systemPromptBase +=
|
||||
'\n\n[If relevant, briefly acknowledge what the user was last working on before responding to their first message.]';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add the import at the top of orchestrator.ts:
|
||||
|
||||
```typescript
|
||||
import { writeWorkingMemory, readWorkingMemory } from '../../memory/workingMemory.js';
|
||||
```
|
||||
|
||||
**Step 6: Call `_injectSessionContext()` from `process()`**
|
||||
|
||||
In the `process()` method, find the first place where `_injectMemoryContext()` is called. Add `_injectSessionContext()` just before it:
|
||||
|
||||
```typescript
|
||||
// One-time session-start context injection (user/profile + user/working)
|
||||
this._injectSessionContext();
|
||||
this._injectMemoryContext(userMessage);
|
||||
```
|
||||
|
||||
Also reset `_sessionContextInjected` in `reset()`:
|
||||
|
||||
```typescript
|
||||
reset(): void {
|
||||
this._agent.reset();
|
||||
this._usageByTier.clear();
|
||||
this._lastContextAlertLevel = null;
|
||||
this._pendingContextAlert = undefined;
|
||||
this._lastCheckpointAt = 0;
|
||||
this._sessionContextInjected = false; // ← add
|
||||
}
|
||||
```
|
||||
|
||||
**Step 7: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 8: Run full test suite**
|
||||
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
Expected: all pass.
|
||||
|
||||
**Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/backends/native/orchestrator.ts src/memory/workingMemory.ts
|
||||
git commit -m "feat(memory): inject session context and write working memory after compaction"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Daemon routing — wire new config fields
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/daemon/routing.ts`
|
||||
|
||||
**Step 1: Add the new fields after `memoryDailyLogMaxAssistantChars`**
|
||||
|
||||
In `src/daemon/routing.ts` around line 756, after:
|
||||
```typescript
|
||||
memoryDailyLogMaxAssistantChars: deps.config.memory?.daily_log?.max_assistant_chars,
|
||||
```
|
||||
|
||||
Add:
|
||||
```typescript
|
||||
userNamespace: deps.config.memory?.user_namespace,
|
||||
workingMemoryTtlDays: deps.config.memory?.working_memory_ttl_days,
|
||||
workingMemoryMaxTokens: deps.config.memory?.working_memory_max_tokens,
|
||||
proactiveSessionGreeting: deps.config.memory?.proactive_session_greeting,
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 3: Run full test suite**
|
||||
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
Expected: all pass.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/daemon/routing.ts
|
||||
git commit -m "feat(memory): wire user_namespace and working memory config to orchestrator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Smoke test and state.json update
|
||||
|
||||
**Step 1: Run full test suite one final time**
|
||||
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
Expected: all pass.
|
||||
|
||||
**Step 2: Manual smoke test (optional but recommended)**
|
||||
|
||||
Add this to your `config.yaml`:
|
||||
```yaml
|
||||
memory:
|
||||
user_namespace: "user"
|
||||
working_memory_ttl_days: 14
|
||||
working_memory_max_tokens: 1000
|
||||
proactive_session_greeting: false
|
||||
```
|
||||
|
||||
Start a session, chat for a while, trigger a compaction (or wait for auto-compaction), then restart and start a new session. Verify `user/working.md` exists in your memory dir and is injected on the new session's first turn.
|
||||
|
||||
**Step 3: Update state.json**
|
||||
|
||||
In `docs/plans/state.json`, add an entry to `completed`:
|
||||
|
||||
```json
|
||||
"pi_personal_assistant_memory": {
|
||||
"status": "complete",
|
||||
"commit": "<sha>",
|
||||
"summary": "Two-tier personal assistant memory: working memory (user/working, TTL-based) written on compaction, injected at session start; unified user/* namespace across channels; PA-focused compaction prompt; proactive session greeting option."
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Final commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/state.json
|
||||
git commit -m "docs(state): mark pi personal assistant memory as complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria Checklist
|
||||
|
||||
- [ ] `user_namespace` absent → zero behavior change (all new code paths guarded by `this._userNamespace` check)
|
||||
- [ ] After compaction with `user_namespace` set, `user/working.md` exists in memory dir
|
||||
- [ ] Daemon restart + new session → `user/working` content appears in the first-turn system prompt
|
||||
- [ ] Telegram and web UI sessions share the same `user/working` file
|
||||
- [ ] Expired working memory (TTL elapsed) is silently ignored
|
||||
- [ ] All existing tests pass
|
||||
|
||||
---
|
||||
|
||||
## Reference: Config
|
||||
|
||||
```yaml
|
||||
memory:
|
||||
# Enables shared identity and working memory. Absent = unchanged behavior.
|
||||
user_namespace: "user"
|
||||
working_memory_ttl_days: 14 # default
|
||||
working_memory_max_tokens: 1000 # default
|
||||
proactive_session_greeting: false # default
|
||||
```
|
||||
Reference in New Issue
Block a user