threadnote 0.3.6 → 0.3.7

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 CHANGED
@@ -33,6 +33,33 @@ keeping future recall sharper without losing useful detail.
33
33
  Use `threadnote remember --replace <uri>` or `threadnote handoff --replace <uri>` to keep one current-state memory fresh
34
34
  instead of accumulating near-duplicate progress notes.
35
35
 
36
+ ## Memory Lifecycle
37
+
38
+ Threadnote separates current durable knowledge from the historical handoff trail.
39
+
40
+ New `remember` records default to `kind: durable` and `status: active`. New `handoff` records use `kind: handoff` and
41
+ `status: active`. Add `--project` and `--topic` when the memory represents an ongoing issue or stable fact:
42
+
43
+ ```bash
44
+ threadnote remember --project threadnote --topic install-health --text "Install/repair waits for OpenViking health."
45
+ threadnote handoff --project threadnote --topic lifecycle-storage --task "Implement lifecycle-aware memories"
46
+ ```
47
+
48
+ Active memories with the same project/topic write to a stable lifecycle path, so later updates replace the current
49
+ version instead of creating another timestamped note. Untagged memories still use timestamped files when you want a
50
+ historical trail.
51
+
52
+ Use `archive` when an old handoff is useful for provenance but should stop being treated as current working context:
53
+
54
+ ```bash
55
+ threadnote archive viking://user/example/memories/handoffs/active/threadnote/lifecycle-storage.md
56
+ threadnote recall --query "threadnote lifecycle storage"
57
+ threadnote recall --query "threadnote lifecycle storage" --include-archived
58
+ ```
59
+
60
+ If OpenViking is still processing the original file, `archive` keeps the archived copy and tells you to retry
61
+ `threadnote forget <uri>` later.
62
+
36
63
  ## Why Not Just CLAUDE.md Or AGENTS.md?
37
64
 
38
65
  Use them. Threadnote is not a replacement for checked-in instructions.
@@ -120,6 +147,15 @@ threadnote update --dry-run
120
147
 
121
148
  After updating, restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.
122
149
 
150
+ Some releases include post-update memory migrations. `threadnote update` runs the new package's migration prompt after
151
+ repair, explains what will change, and asks before applying it. Use `threadnote update --yes` for unattended local
152
+ migrations or `threadnote update --no-post-update` to skip them.
153
+
154
+ Applied migrations are tracked by id under `THREADNOTE_HOME`, and migration commands are written to be safe to rerun.
155
+
156
+ If you update from an older Threadnote version that only knew how to run `repair`, the new repair step will still detect
157
+ applicable migrations and print the manual command to run next.
158
+
123
159
  ### MCP
124
160
 
125
161
  Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh
@@ -231,15 +267,20 @@ This is it! Start working with your agents as usual. The agent will automaticall
231
267
  reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
232
268
  - `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
233
269
  - `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded one
234
- after the new memory succeeds.
270
+ after the new memory succeeds. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
235
271
  - `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
236
272
  `migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
273
+ - `migrate-lifecycle`: moves clear legacy handoff memories from the old events path into archived lifecycle handoff
274
+ paths. It dry-runs by default; use `--apply` after reviewing the output.
237
275
  - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
238
- `skills for api service`; use `--uri` or `--no-infer-scope` to override.
276
+ `skills for api service`; use `--uri` or `--no-infer-scope` to override. Exact durable-memory matches skip archived
277
+ lifecycle paths unless `--include-archived` is passed.
239
278
  - `read`: reads a `viking://` URI returned by `recall` or `list`.
240
279
  - `list` / `ls`: lists a `viking://` directory.
241
280
  - `handoff`: stores current git state and next-step notes as a durable handoff. Use `--replace <uri>` to update the
242
- current handoff for the same active issue.
281
+ current handoff for the same active issue, or `--project` and `--topic` to keep one active handoff file updated.
282
+ - `archive`: copies a memory into the archived lifecycle tree, then removes the original after the archive write
283
+ succeeds.
243
284
  - `forget`: removes a `viking://` URI.
244
285
  - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
245
286
 
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": 1,
3
+ "migrations": [
4
+ {
5
+ "id": "memory-lifecycle-handoff-archive-v1",
6
+ "introducedIn": "0.3.7",
7
+ "title": "Move legacy handoff memories into lifecycle archive paths",
8
+ "description": [
9
+ "Threadnote now separates current durable knowledge from the historical handoff trail.",
10
+ "This migration scans legacy viking://user/<you>/memories/events/*.md files for clear handoff records only.",
11
+ "Matching handoffs are copied to viking://user/<you>/memories/handoffs/archived/<project>/legacy-<hash>.md with lifecycle metadata.",
12
+ "After each archived copy is stored, Threadnote removes the original legacy events file to reduce duplicate recall results.",
13
+ "Ambiguous MEMORY records, preferences, durable facts, incidents, and sensitive-looking content are left untouched for manual review."
14
+ ],
15
+ "commandArgs": ["migrate-lifecycle", "--apply"],
16
+ "instructions": [
17
+ "Post-update lifecycle migration finished. Future agents should store durable facts with --kind durable --project <name> --topic <topic> and active work logs with handoff --project <name> --topic <topic>.",
18
+ "If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
19
+ ],
20
+ "requiresLegacyHandoffs": true
21
+ }
22
+ ]
23
+ }
@@ -31095,7 +31095,8 @@ async function main() {
31095
31095
  "Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
31096
31096
  'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
31097
31097
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
31098
- "When updating the same active issue, pass replaceUri to remember_context so the superseded memory is forgotten after the replacement is stored.",
31098
+ 'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
31099
+ "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate handoffs do not accumulate.",
31099
31100
  "Do not store secrets, customer data, raw production logs, or credentials."
31100
31101
  ].join("\n")
31101
31102
  }
@@ -31140,6 +31141,13 @@ function registerTools(server, config2) {
31140
31141
  "Store a durable Threadnote memory. Required: pass JSON arguments with text."
31141
31142
  );
31142
31143
  registerStoreTool(server, config2, "store", "Compatibility alias for remember_context.");
31144
+ registerArchiveTool(
31145
+ server,
31146
+ config2,
31147
+ "archive_context",
31148
+ "Archive a memory so it remains readable as provenance but is no longer current working context."
31149
+ );
31150
+ registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
31143
31151
  server.registerTool(
31144
31152
  "forget",
31145
31153
  {
@@ -31154,7 +31162,21 @@ function registerTools(server, config2) {
31154
31162
  if (!checkedUri.ok) {
31155
31163
  return checkedUri.error;
31156
31164
  }
31157
- return runOpenVikingTool(config2, ["rm", checkedUri.value]);
31165
+ try {
31166
+ const ov = await requiredOpenVikingCli();
31167
+ const removed = await removeVikingResourceWithRetry(ov, config2, checkedUri.value);
31168
+ return {
31169
+ content: [
31170
+ {
31171
+ type: "text",
31172
+ text: removed ? `Removed: ${checkedUri.value}` : `Resource is still being processed; retry later: ${checkedUri.value}`
31173
+ }
31174
+ ],
31175
+ isError: !removed
31176
+ };
31177
+ } catch (err) {
31178
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
31179
+ }
31158
31180
  }
31159
31181
  );
31160
31182
  server.registerTool(
@@ -31260,10 +31282,11 @@ function registerSearchTool(server, config2, name, description) {
31260
31282
  inputSchema: {
31261
31283
  query: external_exports.string().optional().describe('Required search query, for example "unity-ui-ccc latest handoff"'),
31262
31284
  uri: external_exports.string().optional().describe("Optional viking:// subtree to search"),
31263
- nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count")
31285
+ nodeLimit: external_exports.number().int().positive().max(100).optional().describe("Maximum result count"),
31286
+ includeArchived: external_exports.boolean().optional().describe("Include archived memories in exact durable-memory matches")
31264
31287
  }
31265
31288
  },
31266
- async ({ nodeLimit, query, uri }) => {
31289
+ async ({ includeArchived, nodeLimit, query, uri }) => {
31267
31290
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
31268
31291
  if (!checkedQuery.ok) {
31269
31292
  return checkedQuery.error;
@@ -31272,22 +31295,26 @@ function registerSearchTool(server, config2, name, description) {
31272
31295
  if (!checkedUri.ok) {
31273
31296
  return checkedUri.error;
31274
31297
  }
31275
- return runRecallTool(config2, [
31276
- "search",
31277
- checkedQuery.value,
31278
- ...checkedUri.value ? ["--uri", checkedUri.value] : [],
31279
- ...nodeLimit ? ["--node-limit", String(nodeLimit)] : []
31280
- ]);
31298
+ return runRecallTool(
31299
+ config2,
31300
+ [
31301
+ "search",
31302
+ checkedQuery.value,
31303
+ ...checkedUri.value ? ["--uri", checkedUri.value] : [],
31304
+ ...nodeLimit ? ["--node-limit", String(nodeLimit)] : []
31305
+ ],
31306
+ includeArchived === true
31307
+ );
31281
31308
  }
31282
31309
  );
31283
31310
  }
31284
- async function runRecallTool(config2, args) {
31311
+ async function runRecallTool(config2, args, includeArchived) {
31285
31312
  const semanticResult = await runOpenVikingTool(config2, args);
31286
31313
  if (semanticResult.isError === true) {
31287
31314
  return semanticResult;
31288
31315
  }
31289
31316
  const query = args[1];
31290
- const exactMatches = typeof query === "string" ? await exactMemoryMatchesText(config2, query) : void 0;
31317
+ const exactMatches = typeof query === "string" ? await exactMemoryMatchesText(config2, query, includeArchived) : void 0;
31291
31318
  if (!exactMatches) {
31292
31319
  return semanticResult;
31293
31320
  }
@@ -31303,16 +31330,13 @@ Exact durable memory matches:
31303
31330
  ${exactMatches}` }]
31304
31331
  };
31305
31332
  }
31306
- async function exactMemoryMatchesText(config2, query) {
31333
+ async function exactMemoryMatchesText(config2, query, includeArchived) {
31307
31334
  const terms = exactRecallTerms(query);
31308
31335
  if (terms.length === 0) {
31309
31336
  return void 0;
31310
31337
  }
31311
31338
  const ov = await requiredOpenVikingCli();
31312
- const scopes = [
31313
- `viking://user/${uriSegment(config2.user)}/memories`,
31314
- `viking://agent/${uriSegment(config2.agentId)}/memories`
31315
- ];
31339
+ const scopes = exactMemoryScopes(config2, includeArchived);
31316
31340
  const outputs = [];
31317
31341
  for (const term of terms) {
31318
31342
  for (const scope of scopes) {
@@ -31383,12 +31407,16 @@ function registerStoreTool(server, config2, name, description) {
31383
31407
  annotations: { readOnlyHint: false, destructiveHint: true },
31384
31408
  description: `${description} Never store secrets, credentials, customer data, or raw logs.`,
31385
31409
  inputSchema: {
31410
+ kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
31411
+ project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
31386
31412
  replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
31387
31413
  text: external_exports.string().optional().describe("Required memory text to store"),
31388
- sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, codex, or claude")
31414
+ sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, codex, or claude"),
31415
+ status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
31416
+ topic: external_exports.string().optional().describe("Stable topic; active project/topic memories update one file")
31389
31417
  }
31390
31418
  },
31391
- async ({ replaceUri, sourceAgentClient, text }) => {
31419
+ async ({ kind, project, replaceUri, sourceAgentClient, status, text, topic }) => {
31392
31420
  const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
31393
31421
  if (!checkedText.ok) {
31394
31422
  return checkedText.error;
@@ -31397,45 +31425,120 @@ function registerStoreTool(server, config2, name, description) {
31397
31425
  if (!checkedReplaceUri.ok) {
31398
31426
  return checkedReplaceUri.error;
31399
31427
  }
31400
- const header = [
31401
- "MEMORY",
31402
- `source_agent_client: ${sourceAgentClient ?? "mcp"}`,
31403
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
31404
- ];
31405
- if (checkedReplaceUri.value) {
31406
- header.push(`supersedes: ${checkedReplaceUri.value}`);
31428
+ const metadata = {
31429
+ kind: kind ?? "durable",
31430
+ project: normalizeOptionalMetadata(project),
31431
+ sourceAgentClient: sourceAgentClient ?? "mcp",
31432
+ status: status ?? "active",
31433
+ supersedes: checkedReplaceUri.value,
31434
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
31435
+ topic: normalizeOptionalMetadata(topic)
31436
+ };
31437
+ return writeDurableMemory(
31438
+ config2,
31439
+ formatMemoryDocument("MEMORY", metadata, checkedText.value),
31440
+ metadata,
31441
+ checkedReplaceUri.value
31442
+ );
31443
+ }
31444
+ );
31445
+ }
31446
+ function registerArchiveTool(server, config2, name, description) {
31447
+ server.registerTool(
31448
+ name,
31449
+ {
31450
+ annotations: { readOnlyHint: false, destructiveHint: true },
31451
+ description: `${description} The archive is written before the original URI is removed.`,
31452
+ inputSchema: {
31453
+ kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional(),
31454
+ project: external_exports.string().optional().describe("Project/repo namespace for the archived copy"),
31455
+ topic: external_exports.string().optional().describe("Topic for the archived copy"),
31456
+ uri: external_exports.string().optional().describe("Required viking:// memory URI to archive")
31457
+ }
31458
+ },
31459
+ async ({ kind, project, topic, uri }) => {
31460
+ const checkedUri = requiredVikingUri(uri, name, "viking://user/example/memories/handoffs/active/repo/topic.md");
31461
+ if (!checkedUri.ok) {
31462
+ return checkedUri.error;
31463
+ }
31464
+ try {
31465
+ const ov = await requiredOpenVikingCli();
31466
+ const readResult = await runCommand(ov, withIdentity(config2, ["read", checkedUri.value]));
31467
+ const original = readResult.stdout.trim();
31468
+ if (!original) {
31469
+ return {
31470
+ content: [{ type: "text", text: `Could not read ${checkedUri.value} before archiving.` }],
31471
+ isError: true
31472
+ };
31473
+ }
31474
+ const metadata = {
31475
+ archivedFrom: checkedUri.value,
31476
+ kind: kind ?? "handoff",
31477
+ project: normalizeOptionalMetadata(project),
31478
+ sourceAgentClient: "mcp",
31479
+ status: "archived",
31480
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
31481
+ topic: normalizeOptionalMetadata(topic)
31482
+ };
31483
+ const archiveResult = await writeDurableMemory(
31484
+ config2,
31485
+ formatMemoryDocument("MEMORY", metadata, ["Archived original Threadnote memory.", "", original].join("\n")),
31486
+ metadata,
31487
+ void 0
31488
+ );
31489
+ if (archiveResult.isError === true) {
31490
+ return archiveResult;
31491
+ }
31492
+ const removedOriginal = await removeVikingResourceWithRetry(ov, config2, checkedUri.value);
31493
+ const [content] = archiveResult.content;
31494
+ const text = content?.type === "text" ? content.text : "Archived memory stored.";
31495
+ return {
31496
+ content: [
31497
+ {
31498
+ type: "text",
31499
+ text: removedOriginal ? `${text}
31500
+ Archived original memory: ${checkedUri.value}` : `${text}
31501
+ Archive stored, but original memory is still processing. Retry later with forget: ${checkedUri.value}`
31502
+ }
31503
+ ]
31504
+ };
31505
+ } catch (err) {
31506
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
31407
31507
  }
31408
- return writeDurableMemory(config2, [...header, "", checkedText.value].join("\n"), checkedReplaceUri.value);
31409
31508
  }
31410
31509
  );
31411
31510
  }
31412
- async function writeDurableMemory(config2, memory, replaceUri) {
31511
+ async function writeDurableMemory(config2, memory, metadata, replaceUri) {
31413
31512
  try {
31414
31513
  const ov = await requiredOpenVikingCli();
31415
- const directoryUri = durableMemoryDirectoryUri(config2);
31416
- const stat = await runCommand(ov, withIdentity(config2, ["stat", directoryUri]), { allowFailure: true });
31417
- if (stat.exitCode !== 0) {
31418
- await runCommand(
31419
- ov,
31420
- withIdentity(config2, [
31421
- "mkdir",
31422
- directoryUri,
31423
- "--description",
31424
- "Threadnote durable handoffs, memories, and cross-agent notes."
31425
- ])
31426
- );
31427
- }
31428
- const memoryUri = durableMemoryUri(config2, memory);
31514
+ const directoryUri = memoryDirectoryUri(config2, metadata);
31515
+ await ensureMemoryDirectory(ov, config2, directoryUri);
31516
+ const memoryUri = memoryUriFor(config2, memory, metadata);
31517
+ const writeMode = await memoryWriteMode(ov, config2, memoryUri, metadata);
31429
31518
  const result = await runOpenVikingWriteWithRetry(
31430
31519
  ov,
31431
31520
  config2,
31432
31521
  memoryUri,
31433
- withIdentity(config2, ["write", memoryUri, "--content", memory, "--mode", "create", "--wait", "--timeout", "120"])
31522
+ withIdentity(config2, [
31523
+ "write",
31524
+ memoryUri,
31525
+ "--content",
31526
+ memory,
31527
+ "--mode",
31528
+ writeMode,
31529
+ "--wait",
31530
+ "--timeout",
31531
+ "120"
31532
+ ])
31434
31533
  );
31435
- const messages = [`Stored durable memory: ${memoryUri}`];
31436
- if (replaceUri) {
31437
- await runCommand(ov, withIdentity(config2, ["rm", replaceUri]));
31438
- messages.push(`Forgot replaced memory: ${replaceUri}`);
31534
+ const messages = [`Stored memory: ${memoryUri}`];
31535
+ if (replaceUri && replaceUri !== memoryUri) {
31536
+ const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config2, replaceUri);
31537
+ messages.push(
31538
+ removedReplacedMemory ? `Forgot replaced memory: ${replaceUri}` : `Replacement stored, but superseded memory is still processing. Retry later with forget: ${replaceUri}`
31539
+ );
31540
+ } else if (replaceUri === memoryUri) {
31541
+ messages.push(`Updated existing memory in place: ${memoryUri}`);
31439
31542
  }
31440
31543
  const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
31441
31544
  return { content: [{ type: "text", text: [...messages, text].filter(Boolean).join("\n") }] };
@@ -31464,15 +31567,111 @@ async function vikingResourceExists(ov, config2, uri) {
31464
31567
  const stat = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
31465
31568
  return stat.exitCode === 0;
31466
31569
  }
31570
+ async function removeVikingResourceWithRetry(ov, config2, uri) {
31571
+ const args = withIdentity(config2, ["rm", uri]);
31572
+ for (let attempt = 0; attempt < 4; attempt += 1) {
31573
+ const result = await runCommand(ov, args, { allowFailure: true });
31574
+ if (result.exitCode === 0) {
31575
+ return true;
31576
+ }
31577
+ if (isResourceBusy(result.stderr, result.stdout) && attempt === 3) {
31578
+ return false;
31579
+ }
31580
+ if (!isResourceBusy(result.stderr, result.stdout)) {
31581
+ throw new Error(`${ov} ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
31582
+ }
31583
+ await sleep(1e3 * (attempt + 1));
31584
+ }
31585
+ return false;
31586
+ }
31467
31587
  function isResourceBusy(stderr, stdout) {
31468
- return `${stderr}
31469
- ${stdout}`.includes("resource is busy");
31588
+ const output = `${stderr}
31589
+ ${stdout}`.toLowerCase();
31590
+ return output.includes("resource is busy") || output.includes("resource is being processed");
31591
+ }
31592
+ async function ensureMemoryDirectory(ov, config2, directoryUri) {
31593
+ for (const uri of vikingDirectoryChain(directoryUri)) {
31594
+ const statResult = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
31595
+ if (statResult.exitCode === 0) {
31596
+ continue;
31597
+ }
31598
+ await runCommand(
31599
+ ov,
31600
+ withIdentity(config2, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
31601
+ );
31602
+ }
31470
31603
  }
31471
- function durableMemoryUri(config2, memory) {
31472
- return `${durableMemoryDirectoryUri(config2)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
31604
+ function memoryUriFor(config2, memory, metadata) {
31605
+ const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
31606
+ return `${memoryDirectoryUri(config2, metadata)}/${filename}`;
31607
+ }
31608
+ function memoryDirectoryUri(config2, metadata) {
31609
+ const baseUri = `viking://user/${uriSegment(config2.user)}/memories`;
31610
+ const projectSegment = uriSegment(metadata.project ?? "general");
31611
+ switch (metadata.kind) {
31612
+ case "preference":
31613
+ return metadata.status === "active" ? `${baseUri}/preferences` : `${baseUri}/preferences/${uriSegment(metadata.status)}`;
31614
+ case "handoff":
31615
+ return `${baseUri}/handoffs/${uriSegment(metadata.status)}/${projectSegment}`;
31616
+ case "incident":
31617
+ return `${baseUri}/incidents/${uriSegment(metadata.status)}/${projectSegment}`;
31618
+ case "smoke":
31619
+ return `${baseUri}/smoke/${uriSegment(metadata.status)}`;
31620
+ case "durable":
31621
+ return metadata.status === "active" ? `${baseUri}/durable/projects/${projectSegment}` : `${baseUri}/durable/${uriSegment(metadata.status)}/${projectSegment}`;
31622
+ }
31473
31623
  }
31474
- function durableMemoryDirectoryUri(config2) {
31475
- return `viking://user/${uriSegment(config2.user)}/memories/events`;
31624
+ function shouldUseStableMemoryUri(metadata) {
31625
+ return metadata.status === "active" && metadata.topic !== void 0 && metadata.kind !== "smoke";
31626
+ }
31627
+ async function memoryWriteMode(ov, config2, memoryUri, metadata) {
31628
+ if (!shouldUseStableMemoryUri(metadata)) {
31629
+ return "create";
31630
+ }
31631
+ return await vikingResourceExists(ov, config2, memoryUri) ? "replace" : "create";
31632
+ }
31633
+ function vikingDirectoryChain(directoryUri) {
31634
+ const prefix = "viking://";
31635
+ if (!directoryUri.startsWith(prefix)) {
31636
+ return [directoryUri];
31637
+ }
31638
+ const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
31639
+ const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
31640
+ const chain = [];
31641
+ for (let index = startIndex; index <= parts.length; index += 1) {
31642
+ chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
31643
+ }
31644
+ return chain;
31645
+ }
31646
+ function exactMemoryScopes(config2, includeArchived) {
31647
+ const userBase = `viking://user/${uriSegment(config2.user)}/memories`;
31648
+ const scopes = [
31649
+ `${userBase}/preferences`,
31650
+ `${userBase}/durable/projects`,
31651
+ `${userBase}/handoffs/active`,
31652
+ `${userBase}/incidents/active`,
31653
+ `${userBase}/events`,
31654
+ `viking://agent/${uriSegment(config2.agentId)}/memories`
31655
+ ];
31656
+ return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
31657
+ }
31658
+ function formatMemoryDocument(title, metadata, body) {
31659
+ const header = [
31660
+ title,
31661
+ `kind: ${metadata.kind}`,
31662
+ `status: ${metadata.status}`,
31663
+ metadata.project ? `project: ${metadata.project}` : void 0,
31664
+ metadata.topic ? `topic: ${metadata.topic}` : void 0,
31665
+ `source_agent_client: ${metadata.sourceAgentClient}`,
31666
+ `timestamp: ${metadata.timestamp}`,
31667
+ metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
31668
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
31669
+ ].filter((line) => line !== void 0);
31670
+ return [...header, "", body.trim()].join("\n");
31671
+ }
31672
+ function normalizeOptionalMetadata(value) {
31673
+ const trimmed = value?.trim();
31674
+ return trimmed ? trimmed : void 0;
31476
31675
  }
31477
31676
  function uriSegment(value) {
31478
31677
  const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");