@remnic/core 9.3.645 → 9.3.647

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.
Files changed (98) hide show
  1. package/dist/access-cli.js +18 -17
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.js +13 -13
  4. package/dist/access-mcp.js +10 -10
  5. package/dist/access-schema.js +3 -3
  6. package/dist/access-service.js +8 -8
  7. package/dist/adapters/index.js +4 -4
  8. package/dist/adapters/registry.js +2 -2
  9. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
  10. package/dist/{chunk-6YLJZ2EU.js → chunk-3D6L7CEP.js} +92 -29
  11. package/dist/chunk-3D6L7CEP.js.map +1 -0
  12. package/dist/{chunk-3SF7QNKF.js → chunk-6BNFVP7Y.js} +2 -2
  13. package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
  14. package/dist/{chunk-HRXCPKN4.js → chunk-APWJRJFW.js} +2 -2
  15. package/dist/{chunk-VDX2J7OX.js → chunk-BUKK5SWA.js} +2 -2
  16. package/dist/{chunk-T22TZJWJ.js → chunk-DC66QVL2.js} +2 -2
  17. package/dist/chunk-EVWIEEKZ.js +315 -0
  18. package/dist/chunk-EVWIEEKZ.js.map +1 -0
  19. package/dist/{chunk-IZXOO6NP.js → chunk-FAYDM5WD.js} +2 -2
  20. package/dist/{chunk-Y2XKJJEI.js → chunk-IMWFHBG2.js} +10 -33
  21. package/dist/chunk-IMWFHBG2.js.map +1 -0
  22. package/dist/{chunk-W5ISYSIL.js → chunk-L7W5YW6Y.js} +4 -4
  23. package/dist/{chunk-DGNQRNLL.js → chunk-NT5TINK5.js} +2 -2
  24. package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
  25. package/dist/{chunk-CDHRUNBX.js → chunk-QKE4LHNR.js} +2 -2
  26. package/dist/{chunk-BTRL6GG7.js → chunk-RAELB5NX.js} +5 -5
  27. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  28. package/dist/chunk-S4DDLTPX.js +140 -0
  29. package/dist/chunk-S4DDLTPX.js.map +1 -0
  30. package/dist/{chunk-MBT3ASYV.js → chunk-TA4LQ5SR.js} +12 -12
  31. package/dist/{chunk-KVUG7FF7.js → chunk-U55D5UD5.js} +7 -7
  32. package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
  33. package/dist/{chunk-D2B22JDF.js → chunk-WEPMT6SC.js} +7 -7
  34. package/dist/{chunk-IHG27THY.js → chunk-XUGQQPGO.js} +77 -61
  35. package/dist/chunk-XUGQQPGO.js.map +1 -0
  36. package/dist/{chunk-YOXCJP3A.js → chunk-ZPPFKVSD.js} +3 -3
  37. package/dist/cli.js +30 -28
  38. package/dist/{first-start-migration-FF7YFGRP.js → first-start-migration-PG5HBC3K.js} +4 -4
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +77 -63
  41. package/dist/index.js.map +1 -1
  42. package/dist/lcm/engine.js +3 -3
  43. package/dist/lcm/index.js +11 -11
  44. package/dist/namespaces/migrate.js +5 -5
  45. package/dist/namespaces/search.js +4 -4
  46. package/dist/operator-toolkit.js +6 -6
  47. package/dist/orchestrator.js +14 -13
  48. package/dist/resume-bundles.js +3 -2
  49. package/dist/schemas.d.ts +22 -22
  50. package/dist/search/factory.js +3 -3
  51. package/dist/search/index.js +7 -7
  52. package/dist/session-identity.d.ts +118 -0
  53. package/dist/session-identity.js +21 -0
  54. package/dist/session-identity.js.map +1 -0
  55. package/dist/session-transcript-migration.d.ts +65 -0
  56. package/dist/session-transcript-migration.js +16 -0
  57. package/dist/session-transcript-migration.js.map +1 -0
  58. package/dist/summarizer.js +2 -1
  59. package/dist/transcript.d.ts +18 -6
  60. package/dist/transcript.js +2 -1
  61. package/dist/transfer/backup.js +2 -2
  62. package/dist/transfer/capsule-export.js +2 -2
  63. package/dist/transfer/capsule-import.js +2 -2
  64. package/dist/transfer/import-sqlite.js +2 -2
  65. package/dist/transfer/types.d.ts +12 -12
  66. package/package.json +1 -1
  67. package/src/cli.ts +79 -0
  68. package/src/index.ts +18 -0
  69. package/src/session-identity.test.ts +130 -0
  70. package/src/session-identity.ts +281 -0
  71. package/src/session-transcript-migration.test.ts +350 -0
  72. package/src/session-transcript-migration.ts +527 -0
  73. package/src/summarizer.ts +15 -35
  74. package/src/transcript-session-identity.test.ts +424 -0
  75. package/src/transcript.test.ts +110 -0
  76. package/src/transcript.ts +121 -73
  77. package/dist/chunk-6YLJZ2EU.js.map +0 -1
  78. package/dist/chunk-IHG27THY.js.map +0 -1
  79. package/dist/chunk-Y2XKJJEI.js.map +0 -1
  80. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  81. /package/dist/{chunk-3SF7QNKF.js.map → chunk-6BNFVP7Y.js.map} +0 -0
  82. /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
  83. /package/dist/{chunk-HRXCPKN4.js.map → chunk-APWJRJFW.js.map} +0 -0
  84. /package/dist/{chunk-VDX2J7OX.js.map → chunk-BUKK5SWA.js.map} +0 -0
  85. /package/dist/{chunk-T22TZJWJ.js.map → chunk-DC66QVL2.js.map} +0 -0
  86. /package/dist/{chunk-IZXOO6NP.js.map → chunk-FAYDM5WD.js.map} +0 -0
  87. /package/dist/{chunk-W5ISYSIL.js.map → chunk-L7W5YW6Y.js.map} +0 -0
  88. /package/dist/{chunk-DGNQRNLL.js.map → chunk-NT5TINK5.js.map} +0 -0
  89. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
  90. /package/dist/{chunk-CDHRUNBX.js.map → chunk-QKE4LHNR.js.map} +0 -0
  91. /package/dist/{chunk-BTRL6GG7.js.map → chunk-RAELB5NX.js.map} +0 -0
  92. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  93. /package/dist/{chunk-MBT3ASYV.js.map → chunk-TA4LQ5SR.js.map} +0 -0
  94. /package/dist/{chunk-KVUG7FF7.js.map → chunk-U55D5UD5.js.map} +0 -0
  95. /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
  96. /package/dist/{chunk-D2B22JDF.js.map → chunk-WEPMT6SC.js.map} +0 -0
  97. /package/dist/{chunk-YOXCJP3A.js.map → chunk-ZPPFKVSD.js.map} +0 -0
  98. /package/dist/{first-start-migration-FF7YFGRP.js.map → first-start-migration-PG5HBC3K.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  HourlySummarizer
3
- } from "./chunk-Y2XKJJEI.js";
3
+ } from "./chunk-IMWFHBG2.js";
4
4
  import "./chunk-XJNBEDFE.js";
5
5
  import "./chunk-XZ4WBBB5.js";
6
6
  import "./chunk-H3HDXD3U.js";
7
+ import "./chunk-S4DDLTPX.js";
7
8
  import "./chunk-A3BS64GV.js";
8
9
  import "./chunk-B5XMS73R.js";
9
10
  import "./chunk-7SI52C65.js";
@@ -26,13 +26,17 @@ declare class TranscriptManager {
26
26
  private static readonly CHARS_PER_TOKEN;
27
27
  constructor(config: PluginConfig);
28
28
  /**
29
- * Parse a sessionKey to extract channel type and ID.
29
+ * Resolve the storage path pieces for a sessionKey via the shared
30
+ * {@link sessionStoragePaths} helper (issue #1496, rule #22).
30
31
  *
31
- * SessionKey patterns:
32
- * - agent:<agent-id>:main type="main", id="default"
33
- * - agent:<agent-id>:discord:channel:<channel-id> type="discord", id="<channel-id>"
34
- * - agent:<agent-id>:cron:<job-id> type="cron", id="<job-id>"
35
- * - agent:<agent-id>:slack:channel:<channel-id> type="slack", id="<channel-id>"
32
+ * Legacy `agent:<id>:...` shapes keep their existing readable channel paths.
33
+ * Arbitrary / non-legacy keys (e.g. `pi-geek:abc123`) route to
34
+ * `transcripts/session/<hash>/YYYY-MM-DD.jsonl` from the FIRST write — they
35
+ * never start life in `other/default`. For those keys a list of
36
+ * read-back-only `readbackDirs` is returned so data written by older builds
37
+ * (under `other/default`, or under an old `parts.length >= 3` directory such
38
+ * as `baz/default`) stays discoverable until migrated. New writes never
39
+ * target read-back dirs. See `session-identity.ts`.
36
40
  *
37
41
  * @returns Object with raw channel identifiers and encoded storage path pieces.
38
42
  */
@@ -43,6 +47,7 @@ declare class TranscriptManager {
43
47
  channelId: string;
44
48
  alternateDir: string;
45
49
  legacyDir?: string;
50
+ readbackDirs: string[];
46
51
  };
47
52
  /**
48
53
  * Initialize the transcript manager by ensuring directories exist.
@@ -59,6 +64,7 @@ declare class TranscriptManager {
59
64
  file: string;
60
65
  alternateDir: string;
61
66
  legacyDir?: string;
67
+ readbackDirs: string[];
62
68
  };
63
69
  private selectStorageDirForWrite;
64
70
  private directorySessionStatus;
@@ -102,6 +108,12 @@ declare class TranscriptManager {
102
108
  * Read transcript entries for a date range.
103
109
  * Returns entries within the time range, optionally filtered by sessionKey.
104
110
  * Reads from all channel subdirectories in the hierarchical structure.
111
+ *
112
+ * The window is half-open `[start, end)`: the upper bound is exclusive so
113
+ * caller-specified ranges (explicit dates, analytics buckets) don't
114
+ * double-count at shared boundaries (CLAUDE.md rule #35 / AGENTS.md rule 23).
115
+ * readRecent's inclusive-of-now behavior is handled by readRecent itself
116
+ * extending its end, not by relaxing this bound.
105
117
  */
106
118
  readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]>;
107
119
  /**
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  TranscriptManager
3
- } from "./chunk-IHG27THY.js";
3
+ } from "./chunk-XUGQQPGO.js";
4
+ import "./chunk-S4DDLTPX.js";
4
5
  import "./chunk-KFY3SGN7.js";
5
6
  import "./chunk-FF4KLI5W.js";
6
7
  import "./chunk-2ODBA7MQ.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  backupMemoryDir
3
- } from "../chunk-BXLOS5AJ.js";
3
+ } from "../chunk-OWHERGF2.js";
4
4
  import "../chunk-765K3SAT.js";
5
- import "../chunk-DQEMWVMT.js";
5
+ import "../chunk-UVYI6VIX.js";
6
6
  import "../chunk-J4EB7DNW.js";
7
7
  import "../chunk-BJMBJZ2Y.js";
8
8
  import "../chunk-UKJAGEXH.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  exportCapsule,
3
3
  isValidCapsuleSince
4
- } from "../chunk-VDX2J7OX.js";
4
+ } from "../chunk-BUKK5SWA.js";
5
5
  import "../chunk-WEHSQBFR.js";
6
- import "../chunk-DQEMWVMT.js";
6
+ import "../chunk-UVYI6VIX.js";
7
7
  import "../chunk-J4EB7DNW.js";
8
8
  import "../chunk-BJMBJZ2Y.js";
9
9
  import "../chunk-UKJAGEXH.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  importCapsule
3
- } from "../chunk-DGNQRNLL.js";
3
+ } from "../chunk-NT5TINK5.js";
4
4
  import "../chunk-ALEPI75L.js";
5
5
  import "../chunk-WEHSQBFR.js";
6
- import "../chunk-DQEMWVMT.js";
6
+ import "../chunk-UVYI6VIX.js";
7
7
  import "../chunk-BJMBJZ2Y.js";
8
8
  import "../chunk-UKJAGEXH.js";
9
9
  import "../chunk-FP2373TW.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  importSqlite
3
- } from "../chunk-AGNBY3VG.js";
4
- import "../chunk-Z734BLO3.js";
3
+ } from "../chunk-APJQ6UEA.js";
5
4
  import "../chunk-G4IAEX6D.js";
5
+ import "../chunk-Z734BLO3.js";
6
6
  import "../chunk-6KYMPV2O.js";
7
7
  import "../chunk-GCGJW34D.js";
8
8
  import "../chunk-PZ5AY32C.js";
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
- schemaVersion: string;
317
316
  includes: {
318
317
  procedural: boolean;
319
318
  taxonomy: boolean;
320
319
  identityAnchors: boolean;
321
320
  peerProfiles: boolean;
322
321
  };
322
+ schemaVersion: string;
323
323
  id: string;
324
324
  description: string;
325
325
  version: string;
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
- schemaVersion: string;
338
337
  includes: {
339
338
  procedural: boolean;
340
339
  taxonomy: boolean;
341
340
  identityAnchors: boolean;
342
341
  peerProfiles: boolean;
343
342
  };
343
+ schemaVersion: string;
344
344
  id: string;
345
345
  description: string;
346
346
  version: string;
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
- schemaVersion: string;
468
467
  includes: {
469
468
  procedural: boolean;
470
469
  taxonomy: boolean;
471
470
  identityAnchors: boolean;
472
471
  peerProfiles: boolean;
473
472
  };
473
+ schemaVersion: string;
474
474
  id: string;
475
475
  description: string;
476
476
  version: string;
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
- schemaVersion: string;
489
488
  includes: {
490
489
  procedural: boolean;
491
490
  taxonomy: boolean;
492
491
  identityAnchors: boolean;
493
492
  peerProfiles: boolean;
494
493
  };
494
+ schemaVersion: string;
495
495
  id: string;
496
496
  description: string;
497
497
  version: string;
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
- schemaVersion: string;
522
521
  includes: {
523
522
  procedural: boolean;
524
523
  taxonomy: boolean;
525
524
  identityAnchors: boolean;
526
525
  peerProfiles: boolean;
527
526
  };
527
+ schemaVersion: string;
528
528
  id: string;
529
529
  description: string;
530
530
  version: string;
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
- schemaVersion: string;
555
554
  includes: {
556
555
  procedural: boolean;
557
556
  taxonomy: boolean;
558
557
  identityAnchors: boolean;
559
558
  peerProfiles: boolean;
560
559
  };
560
+ schemaVersion: string;
561
561
  id: string;
562
562
  description: string;
563
563
  version: string;
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
- schemaVersion: string;
687
686
  includes: {
688
687
  procedural: boolean;
689
688
  taxonomy: boolean;
690
689
  identityAnchors: boolean;
691
690
  peerProfiles: boolean;
692
691
  };
692
+ schemaVersion: string;
693
693
  id: string;
694
694
  description: string;
695
695
  version: string;
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
- schemaVersion: string;
708
707
  includes: {
709
708
  procedural: boolean;
710
709
  taxonomy: boolean;
711
710
  identityAnchors: boolean;
712
711
  peerProfiles: boolean;
713
712
  };
713
+ schemaVersion: string;
714
714
  id: string;
715
715
  description: string;
716
716
  version: string;
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
- schemaVersion: string;
741
740
  includes: {
742
741
  procedural: boolean;
743
742
  taxonomy: boolean;
744
743
  identityAnchors: boolean;
745
744
  peerProfiles: boolean;
746
745
  };
746
+ schemaVersion: string;
747
747
  id: string;
748
748
  description: string;
749
749
  version: string;
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
- schemaVersion: string;
774
773
  includes: {
775
774
  procedural: boolean;
776
775
  taxonomy: boolean;
777
776
  identityAnchors: boolean;
778
777
  peerProfiles: boolean;
779
778
  };
779
+ schemaVersion: string;
780
780
  id: string;
781
781
  description: string;
782
782
  version: string;
@@ -815,13 +815,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
815
815
  pluginVersion: string;
816
816
  includesTranscripts: boolean;
817
817
  capsule: {
818
- schemaVersion: string;
819
818
  includes: {
820
819
  procedural: boolean;
821
820
  taxonomy: boolean;
822
821
  identityAnchors: boolean;
823
822
  peerProfiles: boolean;
824
823
  };
824
+ schemaVersion: string;
825
825
  id: string;
826
826
  description: string;
827
827
  version: string;
@@ -854,13 +854,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
854
854
  pluginVersion: string;
855
855
  includesTranscripts: boolean;
856
856
  capsule: {
857
- schemaVersion: string;
858
857
  includes: {
859
858
  procedural: boolean;
860
859
  taxonomy: boolean;
861
860
  identityAnchors: boolean;
862
861
  peerProfiles: boolean;
863
862
  };
863
+ schemaVersion: string;
864
864
  id: string;
865
865
  description: string;
866
866
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.645",
3
+ "version": "9.3.647",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -202,6 +202,10 @@ import {
202
202
  type SessionRepairApplyResult,
203
203
  type SessionRepairPlan,
204
204
  } from "./session-integrity.js";
205
+ import {
206
+ migrateSessionTranscripts,
207
+ planSessionTranscriptMigration,
208
+ } from "./session-transcript-migration.js";
205
209
  import type { TierMigrationCycleSummary, TierMigrationStatusSnapshot } from "./recall-state.js";
206
210
  import {
207
211
  readRuntimePolicySnapshot as readPolicyRuntimeSnapshot,
@@ -8650,6 +8654,81 @@ export function registerCli(
8650
8654
  }
8651
8655
  });
8652
8656
 
8657
+ // ── Sessions subcommand (issue #1496) ───────────────────────────────
8658
+ const sessionsCmd = cmd
8659
+ .command("sessions")
8660
+ .description("Inspect and migrate session transcript storage");
8661
+
8662
+ sessionsCmd
8663
+ .command("migrate-transcripts")
8664
+ .description(
8665
+ "Split conflated other/default transcripts into first-class session/<hash> dirs",
8666
+ )
8667
+ .option("--dry-run", "Report the migration plan without moving files (default)")
8668
+ .option("--apply", "Apply the migration (move files; default is dry-run)")
8669
+ .action(async (...args: unknown[]) => {
8670
+ const options = (args[0] ?? {}) as Record<string, unknown>;
8671
+ const dryRunFlag = options.dryRun === true;
8672
+ const applyFlag = options.apply === true;
8673
+
8674
+ // Rule #51: reject contradictory flags rather than silently picking one.
8675
+ if (dryRunFlag && applyFlag) {
8676
+ console.error("Cannot pass both --dry-run and --apply. Choose one.");
8677
+ process.exit(1);
8678
+ }
8679
+
8680
+ // Safe default: dry-run when neither flag is given.
8681
+ const apply = applyFlag;
8682
+ const memoryDir = orchestrator.config.memoryDir;
8683
+
8684
+ if (!apply) {
8685
+ const plan = await planSessionTranscriptMigration({ memoryDir });
8686
+ console.log("=== Session Transcript Migration (DRY RUN) ===\n");
8687
+ if (!dryRunFlag && !applyFlag) {
8688
+ console.log("No flag given — defaulting to --dry-run. Pass --apply to migrate.\n");
8689
+ }
8690
+ console.log(`transcripts dir: ${plan.transcriptsDir}`);
8691
+ console.log(`files to migrate: ${plan.files.length}`);
8692
+ console.log(`distinct sessions: ${plan.distinctSessions}`);
8693
+ console.log(`entries to move: ${plan.movedEntries}`);
8694
+ if (plan.files.length > 0) {
8695
+ console.log("\nPlanned splits:");
8696
+ for (const file of plan.files) {
8697
+ console.log(`- ${file.sourceRelPath}`);
8698
+ for (const group of file.groups) {
8699
+ console.log(
8700
+ ` ${group.entryCount} entr${group.entryCount === 1 ? "y" : "ies"} → ${group.destDir} (${group.legacy ? "legacy" : "session"})`,
8701
+ );
8702
+ }
8703
+ if (file.unmovableLines > 0) {
8704
+ console.log(` ${file.unmovableLines} unmovable line(s) retained in source`);
8705
+ }
8706
+ }
8707
+ }
8708
+ console.log("\nDRY RUN — no files changed.");
8709
+ return;
8710
+ }
8711
+
8712
+ const result = await migrateSessionTranscripts({ memoryDir, apply: true });
8713
+ console.log("=== Session Transcript Migration (APPLY) ===\n");
8714
+ console.log(`files rewritten: ${result.filesRewritten}`);
8715
+ console.log(`files removed: ${result.filesRemoved}`);
8716
+ console.log(`distinct sessions: ${result.plan.distinctSessions}`);
8717
+ console.log(`entries moved: ${result.plan.movedEntries}`);
8718
+ if (result.manifestPath) {
8719
+ console.log(`manifest: ${result.manifestPath}`);
8720
+ }
8721
+ if (result.errors.length > 0) {
8722
+ console.log("\nErrors:");
8723
+ for (const err of result.errors) {
8724
+ console.log(`- ${err}`);
8725
+ }
8726
+ process.exitCode = 1;
8727
+ return;
8728
+ }
8729
+ console.log("\nOK");
8730
+ });
8731
+
8653
8732
  // Checkpoint command
8654
8733
  cmd
8655
8734
  .command("checkpoint")
package/src/index.ts CHANGED
@@ -324,6 +324,24 @@ export { MeilisearchBackend } from "./search/meilisearch-backend.js";
324
324
  export { buildEntityRecallSection } from "./entity-retrieval.js";
325
325
  export { resolvePrincipal } from "./namespaces/principal.js";
326
326
 
327
+ // ---------------------------------------------------------------------------
328
+ // Session identity / transcript pathing (issue #1496)
329
+ // ---------------------------------------------------------------------------
330
+
331
+ // Shared, deterministic session-key → storage-path resolution. Exported so
332
+ // downstream consumers (the @remnic/bench adapter + its tests, host adapters)
333
+ // resolve transcript directories through the SAME layer the core writer uses
334
+ // instead of hard-coding `other/default` (rule #20, #22, #26).
335
+ export {
336
+ SESSION_CHANNEL_TYPE,
337
+ parseSessionIdentity,
338
+ sessionStoragePaths,
339
+ legacyParserReadbackDir,
340
+ type SessionIdentity,
341
+ type SessionStoragePaths,
342
+ } from "./session-identity.js";
343
+ export { storagePathHash } from "./storage-paths.js";
344
+
327
345
  // ---------------------------------------------------------------------------
328
346
  // Trust zones
329
347
  // ---------------------------------------------------------------------------
@@ -0,0 +1,130 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ LEGACY_FALLBACK_CHANNEL_ID,
6
+ LEGACY_FALLBACK_CHANNEL_TYPE,
7
+ SESSION_CHANNEL_TYPE,
8
+ legacyParserReadbackDir,
9
+ parseSessionIdentity,
10
+ sessionStoragePaths,
11
+ } from "./session-identity.js";
12
+
13
+ test("legacy agent:<id>:main keeps main/default channel identity", () => {
14
+ const id = parseSessionIdentity("agent:generalist:main");
15
+ assert.equal(id.legacy, true);
16
+ assert.equal(id.channelType, "main");
17
+ assert.equal(id.channelId, "default");
18
+ assert.equal(id.canonicalSessionKey, "agent:generalist:main");
19
+ });
20
+
21
+ test("legacy discord channel keeps discord/<channelId> identity", () => {
22
+ const id = parseSessionIdentity("agent:generalist:discord:channel:998877");
23
+ assert.equal(id.legacy, true);
24
+ assert.equal(id.channelType, "discord");
25
+ assert.equal(id.channelId, "998877");
26
+ });
27
+
28
+ test("legacy slack channel keeps slack/<channelId> identity", () => {
29
+ const id = parseSessionIdentity("agent:generalist:slack:channel:C12345");
30
+ assert.equal(id.legacy, true);
31
+ assert.equal(id.channelType, "slack");
32
+ assert.equal(id.channelId, "C12345");
33
+ });
34
+
35
+ test("legacy cron job keeps cron/<jobId> identity", () => {
36
+ const id = parseSessionIdentity("agent:generalist:cron:nightly-sync");
37
+ assert.equal(id.legacy, true);
38
+ assert.equal(id.channelType, "cron");
39
+ assert.equal(id.channelId, "nightly-sync");
40
+ });
41
+
42
+ test("arbitrary key becomes a first-class session/<hash> identity, never other/default", () => {
43
+ const id = parseSessionIdentity("pi-geek:abc123");
44
+ assert.equal(id.legacy, false);
45
+ assert.equal(id.channelType, SESSION_CHANNEL_TYPE);
46
+ assert.notEqual(id.channelType, LEGACY_FALLBACK_CHANNEL_TYPE);
47
+ assert.notEqual(id.channelId, LEGACY_FALLBACK_CHANNEL_ID);
48
+ assert.match(id.channelId, /^[0-9a-f]{16}$/);
49
+ });
50
+
51
+ test("distinct arbitrary keys get distinct, collision-resistant hashes", () => {
52
+ const geek = parseSessionIdentity("pi-geek:abc123");
53
+ const friend = parseSessionIdentity("pi-friend:def456");
54
+ assert.notEqual(geek.channelId, friend.channelId);
55
+ });
56
+
57
+ test("identity is deterministic for the same key", () => {
58
+ const a = parseSessionIdentity("pi-geek:abc123");
59
+ const b = parseSessionIdentity("pi-geek:abc123");
60
+ assert.deepEqual(a, b);
61
+ });
62
+
63
+ test("a bare token without the agent prefix is treated as arbitrary, not legacy", () => {
64
+ // "foo:bar:baz" must NOT be misread as channelType="baz".
65
+ const id = parseSessionIdentity("foo:bar:baz");
66
+ assert.equal(id.legacy, false);
67
+ assert.equal(id.channelType, SESSION_CHANNEL_TYPE);
68
+ });
69
+
70
+ test("empty session key resolves without throwing", () => {
71
+ const id = parseSessionIdentity("");
72
+ assert.equal(id.legacy, false);
73
+ assert.equal(id.channelType, SESSION_CHANNEL_TYPE);
74
+ assert.equal(typeof id.channelId, "string");
75
+ });
76
+
77
+ test("sessionStoragePaths routes arbitrary keys to session/<hash>", () => {
78
+ const paths = sessionStoragePaths("pi-geek:abc123");
79
+ assert.equal(paths.channelType, SESSION_CHANNEL_TYPE);
80
+ assert.match(paths.dir, /^session\/[0-9a-f]{16}$/);
81
+ });
82
+
83
+ test("sessionStoragePaths keeps legacy main at main/default", () => {
84
+ const paths = sessionStoragePaths("agent:generalist:main");
85
+ assert.equal(paths.dir, "main/default");
86
+ });
87
+
88
+ test("two arbitrary keys never share a storage dir", () => {
89
+ const a = sessionStoragePaths("pi-geek:abc123");
90
+ const b = sessionStoragePaths("pi-friend:def456");
91
+ assert.notEqual(a.dir, b.dir);
92
+ assert.notEqual(a.dir, "other/default");
93
+ assert.notEqual(b.dir, "other/default");
94
+ });
95
+
96
+ test("display label strips control characters and separators", () => {
97
+ const id = parseSessionIdentity("pi-geek:abc/123");
98
+ assert.ok(!id.displayLabel.includes("/"));
99
+ assert.ok(id.displayLabel.length > 0);
100
+ });
101
+
102
+ test("legacyParserReadbackDir reconstructs the OLD parser dir for >=3-part keys", () => {
103
+ // foo:bar:baz → channelType=baz, channelId=default
104
+ assert.equal(legacyParserReadbackDir("foo:bar:baz"), "baz/default");
105
+ // foo:bar:baz:qux → channelType=baz, channelId=qux
106
+ assert.equal(legacyParserReadbackDir("foo:bar:baz:qux"), "baz/qux");
107
+ });
108
+
109
+ test("legacyParserReadbackDir returns undefined for <3-part keys (old build used other/default)", () => {
110
+ assert.equal(legacyParserReadbackDir("pi-geek:abc123"), undefined);
111
+ assert.equal(legacyParserReadbackDir("bare"), undefined);
112
+ assert.equal(legacyParserReadbackDir(""), undefined);
113
+ });
114
+
115
+ test("readbackDirs for a >=3-part arbitrary key include other/default AND the old parser dir", () => {
116
+ const paths = sessionStoragePaths("foo:bar:baz");
117
+ assert.match(paths.dir, /^session\/[0-9a-f]{16}$/);
118
+ assert.ok(paths.readbackDirs.includes("other/default"));
119
+ assert.ok(paths.readbackDirs.includes("baz/default"));
120
+ });
121
+
122
+ test("readbackDirs for a 2-part arbitrary key include only other/default", () => {
123
+ const paths = sessionStoragePaths("pi-geek:abc123");
124
+ assert.deepEqual(paths.readbackDirs, ["other/default"]);
125
+ });
126
+
127
+ test("legacy agent:<id>:... keys expose no read-back dirs (their location never moved)", () => {
128
+ assert.deepEqual(sessionStoragePaths("agent:generalist:main").readbackDirs, []);
129
+ assert.deepEqual(sessionStoragePaths("agent:generalist:discord:channel:998877").readbackDirs, []);
130
+ });