threadnote 0.3.2 → 0.3.4
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/README.md +36 -12
- package/dist/threadnote.cjs +239 -4
- package/docs/agent-instructions.md +14 -0
- package/docs/migration.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,18 +6,28 @@ and it does not index whole repositories by default.
|
|
|
6
6
|
|
|
7
7
|
## Real-World Uses
|
|
8
8
|
|
|
9
|
-
Want to continue work in a fresh agent session
|
|
10
|
-
new agents automatically recall recent handoffs and relevant memories before they start changing code.
|
|
9
|
+
**Want to continue work in a fresh agent session?**
|
|
10
|
+
`threadnote install` adds user-level Codex, Claude, and Cursor instructions so new agents automatically recall recent handoffs and relevant memories before they start changing code.
|
|
11
11
|
|
|
12
|
-
Implemented a feature a while ago and need to pick it up again
|
|
13
|
-
Threadnote returns auditable `viking://` pointers that the agent can read before deciding what still matters.
|
|
12
|
+
**Implemented a feature a while ago and need to pick it up again?**
|
|
13
|
+
Ask the agent to recall the feature, branch, or repo. Threadnote returns auditable `viking://` pointers that the agent can read before deciding what still matters.
|
|
14
14
|
|
|
15
|
-
Switching between Codex
|
|
16
|
-
handoff before they pause, so the next agent can search the same local memory layer instead of reconstructing context
|
|
15
|
+
**Switching between Codex, Claude, and Cursor?**\
|
|
16
|
+
Install the MCP adapter for each agent you use. The user-level instructions tell agents to store a handoff before they pause, so the next agent can search the same local memory layer instead of reconstructing context
|
|
17
17
|
from chat history.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
**Working through a long task until the agent context window fills up?**
|
|
20
|
+
After compaction, the next agent turn can recall the relevant Threadnote memories and handoffs instead of relying only on the compressed conversation summary.
|
|
21
|
+
|
|
22
|
+
**Found a durable workflow fact, like how a repo runs tests or where release notes live?**
|
|
23
|
+
Ask the agent to remember it. Threadnote keeps that memory local and searchable without editing unrelated repo files.
|
|
24
|
+
|
|
25
|
+
**Have reusable agent workflows already installed as skills?**\
|
|
26
|
+
Run `threadnote seed-skills` to make local `SKILL.md` guidance discoverable through recall. Agents can find relevant testing, release, on-call, debugging, or plugin-provided workflows without you reopening the same skill files by hand.
|
|
27
|
+
|
|
28
|
+
**Recall returned several overlapping memories?**
|
|
29
|
+
Agents are instructed to compact them into one concise replacement memory and forget only the clearly redundant originals,
|
|
30
|
+
keeping future recall sharper without losing useful detail.
|
|
21
31
|
|
|
22
32
|
## Safety Model
|
|
23
33
|
|
|
@@ -50,7 +60,7 @@ threadnote start
|
|
|
50
60
|
threadnote doctor --dry-run
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
If install or health checks fail, see
|
|
63
|
+
If install or health checks fail, see [Troubleshooting](docs/troubleshooting.md).
|
|
54
64
|
|
|
55
65
|
To force a runtime:
|
|
56
66
|
|
|
@@ -157,7 +167,19 @@ threadnote seed --dry-run
|
|
|
157
167
|
threadnote seed
|
|
158
168
|
```
|
|
159
169
|
|
|
160
|
-
|
|
170
|
+
For Git worktrees, seed the stable checkout by default:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
threadnote init-manifest --repo ~/src/coda
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
You usually do not need to add every temporary worktree such as `~/src/worktrees/coda/my-branch`. Threadnote treats each
|
|
177
|
+
worktree path as its own manifest project today, so adding every worktree can duplicate seeded repo resources and make
|
|
178
|
+
recall noisier. Add a worktree only when it is long-lived or has branch-specific docs, instructions, or repo-local skills
|
|
179
|
+
that should be recalled separately.
|
|
180
|
+
|
|
181
|
+
Optionally seed shared and repo-local skills. This imports existing `SKILL.md` files as a searchable resource catalog so
|
|
182
|
+
agents can discover relevant workflow guidance; it does not install or activate skills in the agent runtime.
|
|
161
183
|
|
|
162
184
|
```bash
|
|
163
185
|
threadnote seed-skills --dry-run
|
|
@@ -183,10 +205,12 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
183
205
|
preserved by default; pass `--erase-memories` to delete `THREADNOTE_HOME`.
|
|
184
206
|
- `init-manifest`: creates or updates `~/.openviking/seed-manifest.yaml` from one or more developer repo roots.
|
|
185
207
|
- `seed`: imports curated repo guidance and docs from the manifest.
|
|
186
|
-
- `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog
|
|
187
|
-
`seed-skills --native` only after configuring a working VLM provider.
|
|
208
|
+
- `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog so agents can discover
|
|
209
|
+
reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
|
|
188
210
|
- `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
|
|
189
211
|
- `remember`: stores a durable memory.
|
|
212
|
+
- `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
|
|
213
|
+
`migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
|
|
190
214
|
- `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
|
|
191
215
|
`skills for api service`; use `--uri` or `--no-infer-scope` to override.
|
|
192
216
|
- `read`: reads a `viking://` URI returned by `recall` or `list`.
|
package/dist/threadnote.cjs
CHANGED
|
@@ -7047,6 +7047,75 @@ async function runRemember(config, options) {
|
|
|
7047
7047
|
].join("\n");
|
|
7048
7048
|
await storeMemory(config, memory, options.dryRun === true);
|
|
7049
7049
|
}
|
|
7050
|
+
async function runMigrateMemories(config, options) {
|
|
7051
|
+
const dryRun = options.dryRun === true;
|
|
7052
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "migration limit") : void 0;
|
|
7053
|
+
const sourceAccounts = await legacySourceAccounts(config, options);
|
|
7054
|
+
if (sourceAccounts.length === 0) {
|
|
7055
|
+
console.log("No local OpenViking accounts found to scan.");
|
|
7056
|
+
return;
|
|
7057
|
+
}
|
|
7058
|
+
const candidates = await legacyMemoryCandidates(config, sourceAccounts);
|
|
7059
|
+
const existingHashes = await existingDurableMemoryHashes(config);
|
|
7060
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
7061
|
+
const migrationPath = (0, import_node_path4.join)(config.agentContextHome, "legacy-memory-migration.txt");
|
|
7062
|
+
let duplicateCount = 0;
|
|
7063
|
+
let migratedCount = 0;
|
|
7064
|
+
let sensitiveCount = 0;
|
|
7065
|
+
if (!dryRun && candidates.length > 0) {
|
|
7066
|
+
await ensureDurableMemoryDirectory(ov, config);
|
|
7067
|
+
}
|
|
7068
|
+
try {
|
|
7069
|
+
for (const candidate of candidates) {
|
|
7070
|
+
if (existingHashes.has(candidate.hash)) {
|
|
7071
|
+
duplicateCount += 1;
|
|
7072
|
+
continue;
|
|
7073
|
+
}
|
|
7074
|
+
if (existingHashes.has(candidate.comparableHash)) {
|
|
7075
|
+
duplicateCount += 1;
|
|
7076
|
+
continue;
|
|
7077
|
+
}
|
|
7078
|
+
const sensitiveReason = sensitiveMemoryReason(candidate.text);
|
|
7079
|
+
if (sensitiveReason) {
|
|
7080
|
+
sensitiveCount += 1;
|
|
7081
|
+
console.log(
|
|
7082
|
+
`SKIP ${legacySourceLabel(candidate)}: possible ${sensitiveReason}; inspect the source archive manually if needed.`
|
|
7083
|
+
);
|
|
7084
|
+
continue;
|
|
7085
|
+
}
|
|
7086
|
+
if (limit !== void 0 && migratedCount >= limit) {
|
|
7087
|
+
break;
|
|
7088
|
+
}
|
|
7089
|
+
const memoryUri = migratedDurableMemoryUri(config, candidate.hash);
|
|
7090
|
+
if (!dryRun && await vikingResourceExists(ov, config, memoryUri)) {
|
|
7091
|
+
duplicateCount += 1;
|
|
7092
|
+
existingHashes.add(candidate.hash);
|
|
7093
|
+
continue;
|
|
7094
|
+
}
|
|
7095
|
+
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${legacySourceLabel(candidate)} -> ${memoryUri}`);
|
|
7096
|
+
if (!dryRun) {
|
|
7097
|
+
await (0, import_promises4.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
|
|
7098
|
+
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7099
|
+
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath);
|
|
7100
|
+
existingHashes.add(candidate.hash);
|
|
7101
|
+
}
|
|
7102
|
+
migratedCount += 1;
|
|
7103
|
+
}
|
|
7104
|
+
} finally {
|
|
7105
|
+
if (!dryRun) {
|
|
7106
|
+
await (0, import_promises4.rm)(migrationPath, { force: true });
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
console.log(
|
|
7110
|
+
[
|
|
7111
|
+
`Migration summary: ${migratedCount} ${dryRun ? "would be migrated" : "migrated"}`,
|
|
7112
|
+
`${duplicateCount} duplicate(s) skipped`,
|
|
7113
|
+
`${sensitiveCount} sensitive-looking item(s) skipped`,
|
|
7114
|
+
`${candidates.length} legacy Threadnote item(s) scanned`,
|
|
7115
|
+
`source account(s): ${sourceAccounts.join(", ")}`
|
|
7116
|
+
].join("; ")
|
|
7117
|
+
);
|
|
7118
|
+
}
|
|
7050
7119
|
async function runRecall(config, options) {
|
|
7051
7120
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7052
7121
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
|
|
@@ -7238,13 +7307,13 @@ async function waitForOpenVikingQueue(ov, config) {
|
|
|
7238
7307
|
}
|
|
7239
7308
|
}
|
|
7240
7309
|
async function vikingResourceExists(ov, config, uri) {
|
|
7241
|
-
const
|
|
7242
|
-
return
|
|
7310
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7311
|
+
return stat3.exitCode === 0;
|
|
7243
7312
|
}
|
|
7244
7313
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7245
7314
|
const directoryUri = durableMemoryDirectoryUri(config);
|
|
7246
|
-
const
|
|
7247
|
-
if (
|
|
7315
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
|
|
7316
|
+
if (stat3.exitCode === 0) {
|
|
7248
7317
|
return;
|
|
7249
7318
|
}
|
|
7250
7319
|
await maybeRun(
|
|
@@ -7264,6 +7333,164 @@ function durableMemoryUri(config, memory) {
|
|
|
7264
7333
|
function durableMemoryDirectoryUri(config) {
|
|
7265
7334
|
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
7266
7335
|
}
|
|
7336
|
+
function migratedDurableMemoryUri(config, hash) {
|
|
7337
|
+
return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
|
|
7338
|
+
}
|
|
7339
|
+
async function legacySourceAccounts(config, options) {
|
|
7340
|
+
const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
|
|
7341
|
+
if (explicitAccounts.length > 0) {
|
|
7342
|
+
return uniqueStrings(explicitAccounts);
|
|
7343
|
+
}
|
|
7344
|
+
if (options.allAccounts === true) {
|
|
7345
|
+
const accounts = await childDirectoryNames(localVikingDataRoot(config));
|
|
7346
|
+
return accounts.filter((account) => !account.startsWith("_"));
|
|
7347
|
+
}
|
|
7348
|
+
return [config.account];
|
|
7349
|
+
}
|
|
7350
|
+
async function legacyMemoryCandidates(config, sourceAccounts) {
|
|
7351
|
+
const candidates = [];
|
|
7352
|
+
for (const sourceAccount of sourceAccounts) {
|
|
7353
|
+
const sessionRoot = (0, import_node_path4.join)(localVikingDataRoot(config), sourceAccount, "session");
|
|
7354
|
+
for (const sourceSession of await childDirectoryNames(sessionRoot)) {
|
|
7355
|
+
const historyRoot = (0, import_node_path4.join)(sessionRoot, sourceSession, "history");
|
|
7356
|
+
for (const sourceArchive of await childDirectoryNames(historyRoot)) {
|
|
7357
|
+
if (!sourceArchive.startsWith("archive_")) {
|
|
7358
|
+
continue;
|
|
7359
|
+
}
|
|
7360
|
+
const sourcePath = (0, import_node_path4.join)(historyRoot, sourceArchive, "messages.jsonl");
|
|
7361
|
+
for (const text of await legacyMemoryTexts(sourcePath)) {
|
|
7362
|
+
candidates.push({
|
|
7363
|
+
comparableHash: sha256(comparableMemoryText(text)),
|
|
7364
|
+
hash: sha256(text),
|
|
7365
|
+
sourceAccount,
|
|
7366
|
+
sourceArchive,
|
|
7367
|
+
sourceSession,
|
|
7368
|
+
text
|
|
7369
|
+
});
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
return candidates.sort((left, right) => legacySourceLabel(left).localeCompare(legacySourceLabel(right)));
|
|
7375
|
+
}
|
|
7376
|
+
async function legacyMemoryTexts(sourcePath) {
|
|
7377
|
+
const raw = await readTextIfExists(sourcePath);
|
|
7378
|
+
if (!raw) {
|
|
7379
|
+
return [];
|
|
7380
|
+
}
|
|
7381
|
+
const memories = [];
|
|
7382
|
+
for (const line of raw.split("\n")) {
|
|
7383
|
+
const trimmedLine = line.trim();
|
|
7384
|
+
if (!trimmedLine) {
|
|
7385
|
+
continue;
|
|
7386
|
+
}
|
|
7387
|
+
try {
|
|
7388
|
+
const parsed = JSON.parse(trimmedLine);
|
|
7389
|
+
const text = legacyMessageText(parsed)?.trim();
|
|
7390
|
+
if (text && isLegacyThreadnoteMemory(text)) {
|
|
7391
|
+
memories.push(text);
|
|
7392
|
+
}
|
|
7393
|
+
} catch (_err) {
|
|
7394
|
+
continue;
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
return memories;
|
|
7398
|
+
}
|
|
7399
|
+
function legacyMessageText(value) {
|
|
7400
|
+
if (!isJsonObject(value)) {
|
|
7401
|
+
return void 0;
|
|
7402
|
+
}
|
|
7403
|
+
if (typeof value.content === "string") {
|
|
7404
|
+
return value.content;
|
|
7405
|
+
}
|
|
7406
|
+
if (!Array.isArray(value.parts)) {
|
|
7407
|
+
return void 0;
|
|
7408
|
+
}
|
|
7409
|
+
const parts = value.parts.map((part) => isJsonObject(part) && part.type === "text" && typeof part.text === "string" ? part.text : void 0).filter((text) => text !== void 0);
|
|
7410
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
7411
|
+
}
|
|
7412
|
+
function isLegacyThreadnoteMemory(text) {
|
|
7413
|
+
return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
|
|
7414
|
+
}
|
|
7415
|
+
async function existingDurableMemoryHashes(config) {
|
|
7416
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
7417
|
+
await collectDurableMemoryHashes(localVikingDataRoot(config), hashes);
|
|
7418
|
+
return hashes;
|
|
7419
|
+
}
|
|
7420
|
+
async function collectDurableMemoryHashes(root, hashes) {
|
|
7421
|
+
let entries;
|
|
7422
|
+
try {
|
|
7423
|
+
entries = await (0, import_promises4.readdir)(root, { withFileTypes: true });
|
|
7424
|
+
} catch (_err) {
|
|
7425
|
+
return;
|
|
7426
|
+
}
|
|
7427
|
+
for (const entry of entries) {
|
|
7428
|
+
const path = (0, import_node_path4.join)(root, entry.name);
|
|
7429
|
+
if (entry.isDirectory()) {
|
|
7430
|
+
await collectDurableMemoryHashes(path, hashes);
|
|
7431
|
+
continue;
|
|
7432
|
+
}
|
|
7433
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md") || !isDurableMemoryPath(path)) {
|
|
7434
|
+
continue;
|
|
7435
|
+
}
|
|
7436
|
+
const content = await readTextIfExists(path);
|
|
7437
|
+
if (content) {
|
|
7438
|
+
const trimmedContent = content.trim();
|
|
7439
|
+
hashes.add(sha256(trimmedContent));
|
|
7440
|
+
hashes.add(sha256(comparableMemoryText(trimmedContent)));
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
function isDurableMemoryPath(path) {
|
|
7445
|
+
return path.split(import_node_path4.sep).includes("memories");
|
|
7446
|
+
}
|
|
7447
|
+
async function childDirectoryNames(path) {
|
|
7448
|
+
let entries;
|
|
7449
|
+
try {
|
|
7450
|
+
entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
|
|
7451
|
+
} catch (_err) {
|
|
7452
|
+
return [];
|
|
7453
|
+
}
|
|
7454
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
7455
|
+
}
|
|
7456
|
+
async function readTextIfExists(path) {
|
|
7457
|
+
try {
|
|
7458
|
+
const pathStat = await (0, import_promises4.stat)(path);
|
|
7459
|
+
if (!pathStat.isFile()) {
|
|
7460
|
+
return void 0;
|
|
7461
|
+
}
|
|
7462
|
+
return await (0, import_promises4.readFile)(path, "utf8");
|
|
7463
|
+
} catch (_err) {
|
|
7464
|
+
return void 0;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
function sensitiveMemoryReason(text) {
|
|
7468
|
+
const patterns = [
|
|
7469
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
7470
|
+
{ name: "API key", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
7471
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
7472
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
7473
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }
|
|
7474
|
+
];
|
|
7475
|
+
return patterns.find((pattern) => pattern.regex.test(text))?.name;
|
|
7476
|
+
}
|
|
7477
|
+
function comparableMemoryText(text) {
|
|
7478
|
+
const trimmed = text.trim();
|
|
7479
|
+
if (!trimmed.startsWith("MEMORY\n")) {
|
|
7480
|
+
return trimmed;
|
|
7481
|
+
}
|
|
7482
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
7483
|
+
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 2).trim();
|
|
7484
|
+
}
|
|
7485
|
+
function legacySourceLabel(candidate) {
|
|
7486
|
+
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
7487
|
+
}
|
|
7488
|
+
function localVikingDataRoot(config) {
|
|
7489
|
+
return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
|
|
7490
|
+
}
|
|
7491
|
+
function uniqueStrings(values) {
|
|
7492
|
+
return [...new Set(values)].sort();
|
|
7493
|
+
}
|
|
7267
7494
|
function isResourceBusy(stderr, stdout) {
|
|
7268
7495
|
return `${stderr}
|
|
7269
7496
|
${stdout}`.includes("resource is busy");
|
|
@@ -8766,6 +8993,14 @@ async function main() {
|
|
|
8766
8993
|
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
|
|
8767
8994
|
await runRemember(getRuntimeConfig(program2), options);
|
|
8768
8995
|
});
|
|
8996
|
+
program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
|
|
8997
|
+
"--source-account <account>",
|
|
8998
|
+
"Source OpenViking account to scan; repeat for multiple accounts",
|
|
8999
|
+
collectOption,
|
|
9000
|
+
[]
|
|
9001
|
+
).action(async (options) => {
|
|
9002
|
+
await runMigrateMemories(getRuntimeConfig(program2), options);
|
|
9003
|
+
});
|
|
8769
9004
|
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
|
|
8770
9005
|
await runRecall(getRuntimeConfig(program2), options);
|
|
8771
9006
|
});
|
|
@@ -36,6 +36,19 @@ logs, or other sensitive data.
|
|
|
36
36
|
Also remember durable workflow facts you discover during work when they would help future agents and are not already in
|
|
37
37
|
canonical docs. Prefer updating checked-in docs for canonical repo rules.
|
|
38
38
|
|
|
39
|
+
## Memory Compaction
|
|
40
|
+
|
|
41
|
+
When recall/read surfaces several memories that describe the same durable fact, incident, branch, or handoff, compact
|
|
42
|
+
them when it is safe:
|
|
43
|
+
|
|
44
|
+
- Store one concise replacement memory that preserves the current status, the important facts, and the source `viking://`
|
|
45
|
+
URIs you merged.
|
|
46
|
+
- Remove superseded duplicates with `threadnote forget <uri>` only when they are clearly redundant or stale and contain
|
|
47
|
+
no unique useful detail.
|
|
48
|
+
- If the memories disagree or you are not sure what can be deleted, keep them and mention the possible cleanup instead.
|
|
49
|
+
|
|
50
|
+
Never compact secrets, credentials, customer data, raw production logs, or checked-in canonical docs into memory.
|
|
51
|
+
|
|
39
52
|
## Handoff
|
|
40
53
|
|
|
41
54
|
Before pausing, switching agents, or ending meaningful work with local changes, store a concise handoff. Include:
|
|
@@ -59,5 +72,6 @@ threadnote recall --query "last handoff for this branch"
|
|
|
59
72
|
threadnote read viking://agent/threadnote/memories/.abstract.md
|
|
60
73
|
threadnote list viking://agent/threadnote/memories --all --recursive
|
|
61
74
|
threadnote remember --text "Durable engineering note..."
|
|
75
|
+
threadnote forget viking://user/example/memories/events/duplicate.md
|
|
62
76
|
threadnote handoff --task "short task summary" --tests "checks run" --next-step "what to do next"
|
|
63
77
|
```
|
package/docs/migration.md
CHANGED
|
@@ -171,6 +171,8 @@ Preferred agent behavior is automatic after `threadnote install` has updated the
|
|
|
171
171
|
|
|
172
172
|
- On non-trivial task start, search OpenViking for recent handoffs and relevant repo guidance.
|
|
173
173
|
- When the user says "remember", store the memory after checking that it contains no secret or customer data.
|
|
174
|
+
- When recall surfaces clearly duplicate or stale memories, store one concise replacement memory and forget only the
|
|
175
|
+
redundant originals.
|
|
174
176
|
- Before pausing, switching agents, or finishing meaningful code changes, store a concise handoff with status, tests,
|
|
175
177
|
blockers, and next steps.
|
|
176
178
|
|