@oratis/lisa 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +34 -5
  2. package/README.zh-CN.md +34 -5
  3. package/dist/advisor/detectors.d.ts +25 -0
  4. package/dist/advisor/detectors.d.ts.map +1 -0
  5. package/dist/advisor/detectors.js +161 -0
  6. package/dist/advisor/detectors.js.map +1 -0
  7. package/dist/advisor/engine.d.ts +41 -0
  8. package/dist/advisor/engine.d.ts.map +1 -0
  9. package/dist/advisor/engine.js +156 -0
  10. package/dist/advisor/engine.js.map +1 -0
  11. package/dist/advisor/types.d.ts +68 -0
  12. package/dist/advisor/types.d.ts.map +1 -0
  13. package/dist/advisor/types.js +19 -0
  14. package/dist/advisor/types.js.map +1 -0
  15. package/dist/channels/imessage.d.ts +16 -0
  16. package/dist/channels/imessage.d.ts.map +1 -1
  17. package/dist/channels/imessage.js +29 -8
  18. package/dist/channels/imessage.js.map +1 -1
  19. package/dist/heartbeat/config.d.ts +10 -0
  20. package/dist/heartbeat/config.d.ts.map +1 -1
  21. package/dist/heartbeat/config.js +8 -2
  22. package/dist/heartbeat/config.js.map +1 -1
  23. package/dist/heartbeat/runner.d.ts.map +1 -1
  24. package/dist/heartbeat/runner.js +30 -0
  25. package/dist/heartbeat/runner.js.map +1 -1
  26. package/dist/integrations/claude-code/observer.d.ts +19 -0
  27. package/dist/integrations/claude-code/observer.d.ts.map +1 -0
  28. package/dist/integrations/claude-code/observer.js +59 -0
  29. package/dist/integrations/claude-code/observer.js.map +1 -0
  30. package/dist/integrations/claude-code/parser.d.ts +2 -0
  31. package/dist/integrations/claude-code/parser.d.ts.map +1 -1
  32. package/dist/integrations/claude-code/parser.js +153 -0
  33. package/dist/integrations/claude-code/parser.js.map +1 -1
  34. package/dist/integrations/claude-code/watcher.d.ts +9 -0
  35. package/dist/integrations/claude-code/watcher.d.ts.map +1 -1
  36. package/dist/integrations/claude-code/watcher.js +25 -4
  37. package/dist/integrations/claude-code/watcher.js.map +1 -1
  38. package/dist/integrations/codex/observer.d.ts +48 -0
  39. package/dist/integrations/codex/observer.d.ts.map +1 -0
  40. package/dist/integrations/codex/observer.js +213 -0
  41. package/dist/integrations/codex/observer.js.map +1 -0
  42. package/dist/integrations/current-hub.d.ts +13 -0
  43. package/dist/integrations/current-hub.d.ts.map +1 -0
  44. package/dist/integrations/current-hub.js +17 -0
  45. package/dist/integrations/current-hub.js.map +1 -0
  46. package/dist/integrations/hub.d.ts +45 -0
  47. package/dist/integrations/hub.d.ts.map +1 -0
  48. package/dist/integrations/hub.js +110 -0
  49. package/dist/integrations/hub.js.map +1 -0
  50. package/dist/integrations/registry.d.ts +15 -0
  51. package/dist/integrations/registry.d.ts.map +1 -0
  52. package/dist/integrations/registry.js +38 -0
  53. package/dist/integrations/registry.js.map +1 -0
  54. package/dist/integrations/types.d.ts +90 -0
  55. package/dist/integrations/types.d.ts.map +1 -0
  56. package/dist/integrations/types.js +12 -0
  57. package/dist/integrations/types.js.map +1 -0
  58. package/dist/memory/vector.d.ts +5 -1
  59. package/dist/memory/vector.d.ts.map +1 -1
  60. package/dist/memory/vector.js +58 -1
  61. package/dist/memory/vector.js.map +1 -1
  62. package/dist/soul/lock.d.ts +38 -0
  63. package/dist/soul/lock.d.ts.map +1 -0
  64. package/dist/soul/lock.js +103 -0
  65. package/dist/soul/lock.js.map +1 -0
  66. package/dist/soul/paths.d.ts.map +1 -1
  67. package/dist/soul/paths.js +12 -6
  68. package/dist/soul/paths.js.map +1 -1
  69. package/dist/soul/slug.d.ts +40 -0
  70. package/dist/soul/slug.d.ts.map +1 -0
  71. package/dist/soul/slug.js +78 -0
  72. package/dist/soul/slug.js.map +1 -0
  73. package/dist/soul/store.d.ts +12 -1
  74. package/dist/soul/store.d.ts.map +1 -1
  75. package/dist/soul/store.js +40 -23
  76. package/dist/soul/store.js.map +1 -1
  77. package/dist/soul/tools.d.ts.map +1 -1
  78. package/dist/soul/tools.js +10 -3
  79. package/dist/soul/tools.js.map +1 -1
  80. package/dist/tools/advise_now.d.ts +11 -0
  81. package/dist/tools/advise_now.d.ts.map +1 -0
  82. package/dist/tools/advise_now.js +34 -0
  83. package/dist/tools/advise_now.js.map +1 -0
  84. package/dist/tools/dispatch_agent.d.ts +38 -0
  85. package/dist/tools/dispatch_agent.d.ts.map +1 -0
  86. package/dist/tools/dispatch_agent.js +145 -0
  87. package/dist/tools/dispatch_agent.js.map +1 -0
  88. package/dist/tools/registry.d.ts.map +1 -1
  89. package/dist/tools/registry.js.map +1 -1
  90. package/dist/tools/web_fetch.d.ts +10 -0
  91. package/dist/tools/web_fetch.d.ts.map +1 -1
  92. package/dist/tools/web_fetch.js +48 -16
  93. package/dist/tools/web_fetch.js.map +1 -1
  94. package/dist/web/island.d.ts +1 -1
  95. package/dist/web/island.d.ts.map +1 -1
  96. package/dist/web/island.js +69 -5
  97. package/dist/web/island.js.map +1 -1
  98. package/dist/web/server.d.ts.map +1 -1
  99. package/dist/web/server.js +79 -16
  100. package/dist/web/server.js.map +1 -1
  101. package/package.json +3 -1
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Integration registry — mirrors src/channels/registry.ts.
3
+ *
4
+ * Each agent adapter calls registerIntegration() at module-load time; the
5
+ * hub looks them up by name. Keeps adapters decoupled and lets community
6
+ * adapters slot in by importing one more module.
7
+ */
8
+ const FACTORIES = new Map();
9
+ export function registerIntegration(name, factory) {
10
+ FACTORIES.set(name, factory);
11
+ }
12
+ export async function makeIntegration(name, cfg) {
13
+ const factory = FACTORIES.get(name);
14
+ if (!factory) {
15
+ throw new Error(`unknown integration "${name}". Known: ${Array.from(FACTORIES.keys()).join(", ") || "(none registered)"}`);
16
+ }
17
+ return await factory(cfg);
18
+ }
19
+ export function listAvailableIntegrations() {
20
+ return Array.from(FACTORIES.keys()).sort();
21
+ }
22
+ /** Test hook — clear the registry between unit tests. */
23
+ export function _resetIntegrationsForTest() {
24
+ FACTORIES.clear();
25
+ builtinsRegistered = false;
26
+ }
27
+ // Lazy registration of built-in adapters. Each module calls
28
+ // registerIntegration() at import time.
29
+ let builtinsRegistered = false;
30
+ export async function registerBuiltinIntegrations() {
31
+ if (builtinsRegistered)
32
+ return;
33
+ builtinsRegistered = true;
34
+ await import("./claude-code/observer.js");
35
+ await import("./codex/observer.js");
36
+ // Additional adapters register here as they land (opencode, aider, …).
37
+ }
38
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/integrations/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE1D,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,OAA6B;IAE7B,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,GAA2B;IAE3B,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,aAC1B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,mBAC7C,EAAE,CACH,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,yBAAyB;IACvC,SAAS,CAAC,KAAK,EAAE,CAAC;IAClB,kBAAkB,GAAG,KAAK,CAAC;AAC7B,CAAC;AAED,4DAA4D;AAC5D,wCAAwC;AACxC,IAAI,kBAAkB,GAAG,KAAK,CAAC;AAC/B,MAAM,CAAC,KAAK,UAAU,2BAA2B;IAC/C,IAAI,kBAAkB;QAAE,OAAO;IAC/B,kBAAkB,GAAG,IAAI,CAAC;IAC1B,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACpC,uEAAuE;AACzE,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Cross-agent orchestration — shared types.
3
+ *
4
+ * LISA observes many CLI agents (Claude Code, Codex, OpenCode, Aider, …),
5
+ * each with its own on-disk session format. These types are the normalized,
6
+ * agent-agnostic shape every adapter produces, so the UI, the hub, and the
7
+ * advisor never care which agent a session came from.
8
+ *
9
+ * See docs/ORCHESTRATOR_PLAN.md for the full design (L1 OBSERVE, L2 UNDERSTAND).
10
+ */
11
+ /** Which agent produced a session. Open-ended string so community adapters
12
+ * can add kinds without touching this file. */
13
+ export type AgentKind = "claude-code" | "codex" | "opencode" | "aider" | "gemini" | "cursor" | "github-pr" | "mcp" | "lisa" | (string & {});
14
+ /** Normalized session state across all agents. Superset of any single
15
+ * agent's states; adapters map their native states onto these. */
16
+ export type AgentSessionState = "working" | "waiting" | "error" | "idle" | "done" | "unknown";
17
+ /**
18
+ * L2 — structural activity for a session. Populated by tier ≥ "activity"
19
+ * adapters. PRIVACY: every field here is structural metadata (tool NAMES,
20
+ * file PATHS, command argv[0], error strings, counts) — never a prompt, a
21
+ * model reply, or full command arguments. See parser privacy tests.
22
+ */
23
+ export interface SessionActivity {
24
+ /** Number of assistant/user turns observed (rough progress indicator). */
25
+ turnCount: number;
26
+ /** Most recent tool names, newest last, e.g. ["Read","Edit","Bash"]. */
27
+ lastTools: string[];
28
+ /** File paths touched by Edit/Write-style tools (deduped, capped). */
29
+ filesTouched: string[];
30
+ /** argv[0] of the most recent shell command, e.g. "npm" — not the full command. */
31
+ lastCommandName?: string;
32
+ /** Short error summary if the last meaningful activity errored. */
33
+ lastError?: string;
34
+ /** git branch recorded by the agent, if any. */
35
+ gitBranch?: string;
36
+ /** Token usage so far (for cost-spike detection). */
37
+ tokens?: {
38
+ input: number;
39
+ output: number;
40
+ };
41
+ /** Tool name awaiting a permission decision, if the session is blocked on one. */
42
+ pendingPermission?: string;
43
+ }
44
+ /**
45
+ * One normalized session. The merge target for every adapter's output.
46
+ */
47
+ export interface AgentSession {
48
+ agent: AgentKind;
49
+ /** Stable id within this agent (uuid, rollout filename, PR number, …). */
50
+ sessionId: string;
51
+ /** Human label — usually the basename of cwd. */
52
+ project: string;
53
+ cwd?: string;
54
+ state: AgentSessionState;
55
+ /** Short machine-ish reason for the state ("end_turn", "permission", …). */
56
+ stateReason: string;
57
+ /** Last activity time, epoch ms. */
58
+ lastMtime: number;
59
+ /** L2 activity, present when the adapter runs at tier ≥ "activity". */
60
+ activity?: SessionActivity;
61
+ }
62
+ /** Visibility tier — how deeply LISA may inspect a session. See plan §3. */
63
+ export type VisibilityTier = "off" | "metadata" | "activity" | "intent";
64
+ /** Per-integration config from ~/.lisa/agents.json. */
65
+ export interface AgentIntegrationConfig {
66
+ enabled?: boolean;
67
+ /** Home dir / data dir override (e.g. CODEX_HOME, OPENCODE_DATA_DIR). */
68
+ home?: string;
69
+ /** Extra roots to watch (e.g. Aider's per-project logs). */
70
+ watchRoots?: string[];
71
+ /** Per-integration visibility override; falls back to the global tier. */
72
+ visibility?: VisibilityTier;
73
+ /** Arbitrary adapter-specific options. */
74
+ [k: string]: unknown;
75
+ }
76
+ /**
77
+ * An adapter that watches one agent kind and emits normalized sessions.
78
+ * Stateful: holds file watchers / timers between start() and stop().
79
+ */
80
+ export interface AgentObserver {
81
+ readonly agent: AgentKind;
82
+ /** Begin watching. `emit` is called on every new/changed session. */
83
+ start(emit: (session: AgentSession) => void): Promise<void>;
84
+ /** Current snapshot of active sessions. */
85
+ list(): AgentSession[];
86
+ /** Tear down watchers/timers. Idempotent. */
87
+ stop(): Promise<void>;
88
+ }
89
+ export type AgentObserverFactory = (cfg: AgentIntegrationConfig) => AgentObserver | Promise<AgentObserver>;
90
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;gDACgD;AAChD,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,OAAO,GACP,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,KAAK,GACL,MAAM,GACN,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;mEACmE;AACnE,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,SAAS,GACT,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,CAAC;AAEd;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,iBAAiB,CAAC;IACzB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,4EAA4E;AAC5E,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAExE,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0CAA0C;IAC1C,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,qEAAqE;IACrE,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,2CAA2C;IAC3C,IAAI,IAAI,YAAY,EAAE,CAAC;IACvB,6CAA6C;IAC7C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,MAAM,oBAAoB,GAAG,CACjC,GAAG,EAAE,sBAAsB,KACxB,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Cross-agent orchestration — shared types.
3
+ *
4
+ * LISA observes many CLI agents (Claude Code, Codex, OpenCode, Aider, …),
5
+ * each with its own on-disk session format. These types are the normalized,
6
+ * agent-agnostic shape every adapter produces, so the UI, the hub, and the
7
+ * advisor never care which agent a session came from.
8
+ *
9
+ * See docs/ORCHESTRATOR_PLAN.md for the full design (L1 OBSERVE, L2 UNDERSTAND).
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/integrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -12,7 +12,11 @@ interface Index {
12
12
  docs: Document[];
13
13
  idf: Map<string, number>;
14
14
  }
15
- export declare function buildIndex(): Promise<Index>;
15
+ /** Drop the cached index (test hook / explicit invalidation). */
16
+ export declare function clearIndexCache(): void;
17
+ export declare function buildIndex(opts?: {
18
+ cache?: boolean;
19
+ }): Promise<Index>;
16
20
  export interface SearchHit {
17
21
  sessionId: string;
18
22
  startedAt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIlC,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,KAAK;IACb,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1B;AASD,wBAAsB,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC,CAkCjD;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,SAAS,EAAE,CAuBtE;AA2BD,eAAO,MAAM,qBAAqB,EAAE,OAAO,IAAI,GAAG,OAAO,EAAE,GAAG,SAAqB,CAAC"}
1
+ {"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAKlC,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,UAAU,KAAK;IACb,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1B;AA4CD,iEAAiE;AACjE,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED,wBAAsB,UAAU,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAW/E;AAsCD,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,SAAS,EAAE,CAuBtE;AA2BD,eAAO,MAAM,qBAAqB,EAAE,OAAO,IAAI,GAAG,OAAO,EAAE,GAAG,SAAqB,CAAC"}
@@ -1,4 +1,7 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
1
3
  import { listSessionsOnDisk, loadSessionMessages } from "../sessions/list.js";
4
+ import { SESSIONS_DIR } from "../paths.js";
2
5
  import { extractTextFromContent } from "../agent.js";
3
6
  const STOPWORDS = new Set([
4
7
  "the", "a", "an", "of", "to", "in", "and", "or", "for", "is", "it",
@@ -6,7 +9,61 @@ const STOPWORDS = new Set([
6
9
  "by", "from", "but", "not", "i", "you", "we", "they", "he", "she",
7
10
  "lisa", "tool", "use", "user", "assistant",
8
11
  ]);
9
- export async function buildIndex() {
12
+ // ── index cache ───────────────────────────────────────────────────────────
13
+ // buildIndex re-reads + re-tokenizes EVERY past session, which is O(total
14
+ // transcript bytes). memory_search was calling it fresh on every invocation,
15
+ // so a chat that searches its memory N times in one session paid that cost N
16
+ // times even when nothing changed. We cache the built index keyed by a cheap
17
+ // fingerprint of the sessions directory (each .jsonl's mtime + size). The
18
+ // fingerprint changes the instant any session is appended to or a new one
19
+ // appears, so the cache is correct, not just fast.
20
+ let cachedIndex = null;
21
+ let cachedFingerprint = "";
22
+ async function sessionsFingerprint() {
23
+ let files;
24
+ try {
25
+ files = await fs.readdir(SESSIONS_DIR);
26
+ }
27
+ catch {
28
+ // Missing dir and empty dir must fingerprint identically: buildIndex →
29
+ // listSessionsOnDisk → ensureDir CREATES the dir as a side effect, so a
30
+ // distinct "no-dir" sentinel would spuriously miss the cache on the very
31
+ // next call. Both collapse to the empty fingerprint.
32
+ return "";
33
+ }
34
+ const parts = [];
35
+ for (const f of files.sort()) {
36
+ if (!f.endsWith(".jsonl"))
37
+ continue;
38
+ try {
39
+ const st = await fs.stat(path.join(SESSIONS_DIR, f));
40
+ parts.push(`${f}:${st.mtimeMs}:${st.size}`);
41
+ }
42
+ catch {
43
+ // file vanished between readdir and stat — ignore
44
+ }
45
+ }
46
+ return parts.join("|");
47
+ }
48
+ /** Drop the cached index (test hook / explicit invalidation). */
49
+ export function clearIndexCache() {
50
+ cachedIndex = null;
51
+ cachedFingerprint = "";
52
+ }
53
+ export async function buildIndex(opts = {}) {
54
+ const useCache = opts.cache !== false;
55
+ if (useCache) {
56
+ const fp = await sessionsFingerprint();
57
+ if (cachedIndex && fp === cachedFingerprint)
58
+ return cachedIndex;
59
+ const built = await buildIndexUncached();
60
+ cachedIndex = built;
61
+ cachedFingerprint = fp;
62
+ return built;
63
+ }
64
+ return buildIndexUncached();
65
+ }
66
+ async function buildIndexUncached() {
10
67
  const sessions = await listSessionsOnDisk();
11
68
  const docs = [];
12
69
  const docFreq = new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"vector.js","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vector.js","sourceRoot":"","sources":["../../src/memory/vector.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAgBrD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI;IAClE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IACpE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK;IACjE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW;CAC3C,CAAC,CAAC;AAEH,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,0EAA0E;AAC1E,0EAA0E;AAC1E,mDAAmD;AACnD,IAAI,WAAW,GAAiB,IAAI,CAAC;AACrC,IAAI,iBAAiB,GAAG,EAAE,CAAC;AAE3B,KAAK,UAAU,mBAAmB;IAChC,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,wEAAwE;QACxE,yEAAyE;QACzE,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7B,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,SAAS;QACpC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC;YACrD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;QACpD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe;IAC7B,WAAW,GAAG,IAAI,CAAC;IACnB,iBAAiB,GAAG,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAA4B,EAAE;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC;IACtC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAC;QACvC,IAAI,WAAW,IAAI,EAAE,KAAK,iBAAiB;YAAE,OAAO,WAAW,CAAC;QAChE,MAAM,KAAK,GAAG,MAAM,kBAAkB,EAAE,CAAC;QACzC,WAAW,GAAG,KAAK,CAAC;QACpB,iBAAiB,GAAG,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,kBAAkB,EAAE,CAAC;AAC9B,CAAC;AAED,KAAK,UAAU,kBAAkB;IAC/B,MAAM,QAAQ,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxD,MAAM,IAAI,GAAG,QAAQ;iBAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;iBAC7C,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;YAC5B,MAAM,EAAE,GAAG,IAAI,GAAG,EAAkB,CAAC;YACrC,KAAK,MAAM,CAAC,IAAI,MAAM;gBAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACxD,KAAK,MAAM,CAAC,IAAI,GAAG;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/D,IAAI,CAAC,IAAI,CAAC;gBACR,SAAS,EAAE,IAAI,CAAC,EAAE;gBAClB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,IAAI;gBACJ,MAAM;gBACN,QAAQ,EAAE,GAAG;gBACb,QAAQ,EAAE,EAAE;aACb,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;QACjC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACvB,CAAC;AASD,MAAM,UAAU,MAAM,CAAC,KAAY,EAAE,KAAa,EAAE,CAAC,GAAG,CAAC;IACvD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAuC,EAAE,CAAC;IACtD,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;YAC/B,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,EAAE,KAAK,CAAC;gBAAE,SAAS;YACvB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrC,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;QACzB,CAAC;QACD,IAAI,KAAK,GAAG,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS;QAC1B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS;QAC1B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC;KACjD,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,aAAa,CACpB,IAAY,EACZ,OAAiB,EACjB,KAAa;IAEb,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC;YAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC;YACnD,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAwC,SAAS,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cross-process advisory file lock for soul writes.
3
+ *
4
+ * The in-process commit queue (git.ts enqueueCommit) serializes writes within
5
+ * ONE process, but LISA runs as several processes against the same
6
+ * ~/.lisa/soul/: the web server, the CLI/REPL, and — crucially — the
7
+ * heartbeat/idle runners launched by launchd/cron on their own clock. Two of
8
+ * those writing concurrently can:
9
+ * - lose a desire-progress append (read-modify-write race), or
10
+ * - collide on .git/index.lock (one commit fails).
11
+ *
12
+ * withFileLock implements a portable advisory mutex via exclusive file
13
+ * creation (`open(..., "wx")` succeeds only if the file doesn't exist). It
14
+ * self-heals from a crashed holder via a staleness timeout + a best-effort
15
+ * liveness check on the recorded pid.
16
+ */
17
+ export interface FileLockOpts {
18
+ /** Treat a held lock as abandoned after this many ms (crashed holder). */
19
+ staleMs?: number;
20
+ /** Give up acquiring after this many ms. */
21
+ timeoutMs?: number;
22
+ /** Poll interval while waiting for the lock. */
23
+ pollMs?: number;
24
+ }
25
+ /**
26
+ * Run `fn` while holding an exclusive lock at `lockPath`. Releases the lock
27
+ * (deletes the file) when `fn` settles, success or failure.
28
+ */
29
+ export declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, opts?: FileLockOpts): Promise<T>;
30
+ /** The canonical soul write-lock path. */
31
+ export declare const SOUL_LOCK_PATH: string;
32
+ /**
33
+ * Run `fn` while holding the soul write-lock. Use this around any
34
+ * read-modify-write of soul state (e.g. desire-progress appends) so a
35
+ * concurrent heartbeat/idle/chat process can't interleave and lose data.
36
+ */
37
+ export declare function withSoulLock<T>(fn: () => Promise<T>, opts?: FileLockOpts): Promise<T>;
38
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/soul/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAoCD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,CAAC,CAAC,CAmCZ;AAED,0CAA0C;AAC1C,eAAO,MAAM,cAAc,QAAqC,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAErF"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Cross-process advisory file lock for soul writes.
3
+ *
4
+ * The in-process commit queue (git.ts enqueueCommit) serializes writes within
5
+ * ONE process, but LISA runs as several processes against the same
6
+ * ~/.lisa/soul/: the web server, the CLI/REPL, and — crucially — the
7
+ * heartbeat/idle runners launched by launchd/cron on their own clock. Two of
8
+ * those writing concurrently can:
9
+ * - lose a desire-progress append (read-modify-write race), or
10
+ * - collide on .git/index.lock (one commit fails).
11
+ *
12
+ * withFileLock implements a portable advisory mutex via exclusive file
13
+ * creation (`open(..., "wx")` succeeds only if the file doesn't exist). It
14
+ * self-heals from a crashed holder via a staleness timeout + a best-effort
15
+ * liveness check on the recorded pid.
16
+ */
17
+ import fsp from "node:fs/promises";
18
+ import path from "node:path";
19
+ import { ensureDir } from "../fs-utils.js";
20
+ import { SOUL_DIR } from "./paths.js";
21
+ const DEFAULTS = { staleMs: 30_000, timeoutMs: 10_000, pollMs: 50 };
22
+ function delay(ms) {
23
+ return new Promise((r) => setTimeout(r, ms));
24
+ }
25
+ /** Is the process holding this lock gone or the lock too old to trust? */
26
+ async function isStale(lockPath, staleMs) {
27
+ let body;
28
+ try {
29
+ body = JSON.parse(await fsp.readFile(lockPath, "utf8"));
30
+ }
31
+ catch {
32
+ // Unreadable / malformed lock file → treat as stale so we can recover.
33
+ return true;
34
+ }
35
+ if (typeof body.ts !== "number" || Date.now() - body.ts > staleMs)
36
+ return true;
37
+ // Best-effort liveness: signal 0 throws ESRCH if the pid is dead. Only
38
+ // meaningful on the same host; on a different host kill() may report the
39
+ // wrong answer, so we still rely primarily on the ts timeout above.
40
+ if (typeof body.pid === "number" && body.pid > 0) {
41
+ try {
42
+ process.kill(body.pid, 0);
43
+ }
44
+ catch (e) {
45
+ if (e.code === "ESRCH")
46
+ return true; // holder dead
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ /**
52
+ * Run `fn` while holding an exclusive lock at `lockPath`. Releases the lock
53
+ * (deletes the file) when `fn` settles, success or failure.
54
+ */
55
+ export async function withFileLock(lockPath, fn, opts = {}) {
56
+ const { staleMs, timeoutMs, pollMs } = { ...DEFAULTS, ...opts };
57
+ await ensureDir(path.dirname(lockPath));
58
+ const deadline = Date.now() + timeoutMs;
59
+ // ── acquire ──
60
+ for (;;) {
61
+ try {
62
+ const fh = await fsp.open(lockPath, "wx");
63
+ try {
64
+ await fh.writeFile(JSON.stringify({ pid: process.pid, ts: Date.now() }));
65
+ }
66
+ finally {
67
+ await fh.close();
68
+ }
69
+ break; // acquired
70
+ }
71
+ catch (e) {
72
+ if (e.code !== "EEXIST")
73
+ throw e;
74
+ // Held by someone. Steal if stale, else wait.
75
+ if (await isStale(lockPath, staleMs)) {
76
+ await fsp.rm(lockPath, { force: true }).catch(() => { });
77
+ continue; // retry immediately
78
+ }
79
+ if (Date.now() >= deadline) {
80
+ throw new Error(`timed out acquiring lock ${lockPath} after ${timeoutMs}ms`);
81
+ }
82
+ await delay(pollMs);
83
+ }
84
+ }
85
+ // ── critical section ──
86
+ try {
87
+ return await fn();
88
+ }
89
+ finally {
90
+ await fsp.rm(lockPath, { force: true }).catch(() => { });
91
+ }
92
+ }
93
+ /** The canonical soul write-lock path. */
94
+ export const SOUL_LOCK_PATH = path.join(SOUL_DIR, ".write.lock");
95
+ /**
96
+ * Run `fn` while holding the soul write-lock. Use this around any
97
+ * read-modify-write of soul state (e.g. desire-progress appends) so a
98
+ * concurrent heartbeat/idle/chat process can't interleave and lose data.
99
+ */
100
+ export function withSoulLock(fn, opts) {
101
+ return withFileLock(SOUL_LOCK_PATH, fn, opts);
102
+ }
103
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/soul/lock.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,QAA+B,CAAC;AAErD,eAAO,MAAM,SAAS,QAAmC,CAAC;AAC1D,eAAO,MAAM,SAAS,QAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,QAAqC,CAAC;AAChE,eAAO,MAAM,YAAY,QAAoC,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAyC,CAAC;AACxE,eAAO,MAAM,aAAa,QAAuC,CAAC;AAClE,eAAO,MAAM,SAAS,QAAwC,CAAC;AAE/D,eAAO,MAAM,eAAe,QAAgC,CAAC;AAC7D,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AACjE,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,sBAAsB,QAAuC,CAAC;AAE3E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AACD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AACD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,QAAQ,QAA+B,CAAC;AAErD,eAAO,MAAM,SAAS,QAAmC,CAAC;AAC1D,eAAO,MAAM,SAAS,QAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,QAAqC,CAAC;AAChE,eAAO,MAAM,YAAY,QAAoC,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAyC,CAAC;AACxE,eAAO,MAAM,aAAa,QAAuC,CAAC;AAClE,eAAO,MAAM,SAAS,QAAwC,CAAC;AAE/D,eAAO,MAAM,eAAe,QAAgC,CAAC;AAC7D,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AACjE,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,gBAAgB,QAAiC,CAAC;AAC/D,eAAO,MAAM,sBAAsB,QAAuC,CAAC;AAK3E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AACD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhD;AACD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { LISA_HOME } from "../paths.js";
3
+ import { assertSafeSlug } from "./slug.js";
3
4
  export const SOUL_DIR = path.join(LISA_HOME, "soul");
4
5
  export const SOUL_SEED = path.join(SOUL_DIR, "seed.json");
5
6
  export const SOUL_NAME = path.join(SOUL_DIR, "name.md");
@@ -13,22 +14,27 @@ export const SOUL_OPINIONS_DIR = path.join(SOUL_DIR, "opinions");
13
14
  export const SOUL_DESIRES_DIR = path.join(SOUL_DIR, "desires");
14
15
  export const SOUL_JOURNAL_DIR = path.join(SOUL_DIR, "journal");
15
16
  export const SOUL_RELATIONSHIPS_DIR = path.join(SOUL_DIR, "relationships");
17
+ // Every slug-bearing path helper runs the slug through assertSafeSlug first.
18
+ // This is the single chokepoint for path-traversal defense: a slug like
19
+ // "../../../etc/x" or "a/b" throws here rather than escaping the soul dir.
16
20
  export function valueFile(slug) {
17
- return path.join(SOUL_VALUES_DIR, `${slug}.md`);
21
+ return path.join(SOUL_VALUES_DIR, `${assertSafeSlug(slug)}.md`);
18
22
  }
19
23
  export function opinionFile(slug) {
20
- return path.join(SOUL_OPINIONS_DIR, `${slug}.md`);
24
+ return path.join(SOUL_OPINIONS_DIR, `${assertSafeSlug(slug)}.md`);
21
25
  }
22
26
  export function desireFile(slug) {
23
- return path.join(SOUL_DESIRES_DIR, `${slug}.md`);
27
+ return path.join(SOUL_DESIRES_DIR, `${assertSafeSlug(slug)}.md`);
24
28
  }
25
29
  export function desireProgressFile(slug) {
26
- return path.join(SOUL_DESIRES_DIR, `${slug}.progress.md`);
30
+ return path.join(SOUL_DESIRES_DIR, `${assertSafeSlug(slug)}.progress.md`);
27
31
  }
28
32
  export function journalFile(date) {
29
- return path.join(SOUL_JOURNAL_DIR, `${date}.md`);
33
+ // Journal keys are dates (YYYY-MM-DD), which assertSafeSlug accepts (no
34
+ // separators/dots-leading), so this guards against a malformed date too.
35
+ return path.join(SOUL_JOURNAL_DIR, `${assertSafeSlug(date)}.md`);
30
36
  }
31
37
  export function relationshipFile(userKey) {
32
- return path.join(SOUL_RELATIONSHIPS_DIR, `${userKey}.md`);
38
+ return path.join(SOUL_RELATIONSHIPS_DIR, `${assertSafeSlug(userKey)}.md`);
33
39
  }
34
40
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAErD,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAE3E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;AAClD,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;AACpD,CAAC;AACD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;AACnD,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;AAC5D,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;AACnD,CAAC;AACD,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,GAAG,OAAO,KAAK,CAAC,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/soul/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAErD,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC7D,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC/D,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAE3E,6EAA6E;AAC7E,wEAAwE;AACxE,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AACD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,wEAAwE;IACxE,yEAAyE;IACzE,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC;AACD,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Slug safety for soul file paths.
3
+ *
4
+ * Soul state lives one-file-per-entry under ~/.lisa/soul/{values,opinions,
5
+ * desires,relationships}/<slug>.md. The slug comes from the LLM (via
6
+ * soul_patch / desire tools) or from reflect operations, so it is untrusted
7
+ * input that gets joined into a filesystem path. Without validation, a slug
8
+ * like "../../../etc/something" or "foo/bar" would let a write escape the
9
+ * soul directory (path traversal) or silently nest into subdirectories.
10
+ *
11
+ * Two functions, two jobs:
12
+ * - assertSafeSlug — a HARD GATE applied in every path helper (read AND
13
+ * write). Throws on anything that could traverse or escape. Deliberately
14
+ * permissive about case/style so it never breaks reads of slugs written
15
+ * by older versions; it only blocks genuinely dangerous shapes.
16
+ * - normalizeSlug — a SOFT CLEANER applied when a NEW slug is first minted
17
+ * from free-form input. Lowercases, strips to [a-z0-9-], collapses dashes,
18
+ * caps length — produces a tidy, collision-resistant slug.
19
+ */
20
+ export declare const MAX_SLUG_LEN = 64;
21
+ export declare class UnsafeSlugError extends Error {
22
+ constructor(slug: string, reason: string);
23
+ }
24
+ /**
25
+ * Throw if `slug` could escape its soul subdirectory or corrupt the path.
26
+ * This is the security boundary — applied in paths.ts so no code path can
27
+ * construct a soul file path from a dangerous slug.
28
+ */
29
+ export declare function assertSafeSlug(slug: string): string;
30
+ /**
31
+ * Produce a tidy slug from free-form text. Use when minting a NEW slug.
32
+ * - lowercases
33
+ * - replaces any run of non [a-z0-9] with a single dash
34
+ * - trims leading/trailing dashes
35
+ * - caps length
36
+ * Returns "" only if the input had no usable characters; callers should
37
+ * treat "" as "generate a fallback" (e.g. a timestamp-based slug).
38
+ */
39
+ export declare function normalizeSlug(raw: string): string;
40
+ //# sourceMappingURL=slug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../src/soul/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIzC;AAWD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Slug safety for soul file paths.
3
+ *
4
+ * Soul state lives one-file-per-entry under ~/.lisa/soul/{values,opinions,
5
+ * desires,relationships}/<slug>.md. The slug comes from the LLM (via
6
+ * soul_patch / desire tools) or from reflect operations, so it is untrusted
7
+ * input that gets joined into a filesystem path. Without validation, a slug
8
+ * like "../../../etc/something" or "foo/bar" would let a write escape the
9
+ * soul directory (path traversal) or silently nest into subdirectories.
10
+ *
11
+ * Two functions, two jobs:
12
+ * - assertSafeSlug — a HARD GATE applied in every path helper (read AND
13
+ * write). Throws on anything that could traverse or escape. Deliberately
14
+ * permissive about case/style so it never breaks reads of slugs written
15
+ * by older versions; it only blocks genuinely dangerous shapes.
16
+ * - normalizeSlug — a SOFT CLEANER applied when a NEW slug is first minted
17
+ * from free-form input. Lowercases, strips to [a-z0-9-], collapses dashes,
18
+ * caps length — produces a tidy, collision-resistant slug.
19
+ */
20
+ export const MAX_SLUG_LEN = 64;
21
+ export class UnsafeSlugError extends Error {
22
+ constructor(slug, reason) {
23
+ super(`unsafe soul slug ${JSON.stringify(slug)}: ${reason}`);
24
+ this.name = "UnsafeSlugError";
25
+ }
26
+ }
27
+ // Control characters (codepoints 0x00–0x1f): newlines, tabs, null, etc.
28
+ // Checked via codepoint so the source stays plain-ASCII.
29
+ function hasControlChar(s) {
30
+ for (let i = 0; i < s.length; i++) {
31
+ if (s.charCodeAt(i) < 0x20)
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ /**
37
+ * Throw if `slug` could escape its soul subdirectory or corrupt the path.
38
+ * This is the security boundary — applied in paths.ts so no code path can
39
+ * construct a soul file path from a dangerous slug.
40
+ */
41
+ export function assertSafeSlug(slug) {
42
+ if (typeof slug !== "string" || slug.length === 0) {
43
+ throw new UnsafeSlugError(String(slug), "empty");
44
+ }
45
+ if (slug.length > MAX_SLUG_LEN) {
46
+ throw new UnsafeSlugError(slug, `longer than ${MAX_SLUG_LEN} chars`);
47
+ }
48
+ if (hasControlChar(slug)) {
49
+ throw new UnsafeSlugError(slug, "contains control characters");
50
+ }
51
+ if (slug.includes("/") || slug.includes("\\")) {
52
+ throw new UnsafeSlugError(slug, "contains a path separator");
53
+ }
54
+ // Block "." / ".." and any leading-dot form — path-traversal primitives
55
+ // and dotfiles that would hide from directory listings.
56
+ if (slug.startsWith(".")) {
57
+ throw new UnsafeSlugError(slug, "starts with a dot");
58
+ }
59
+ return slug;
60
+ }
61
+ /**
62
+ * Produce a tidy slug from free-form text. Use when minting a NEW slug.
63
+ * - lowercases
64
+ * - replaces any run of non [a-z0-9] with a single dash
65
+ * - trims leading/trailing dashes
66
+ * - caps length
67
+ * Returns "" only if the input had no usable characters; callers should
68
+ * treat "" as "generate a fallback" (e.g. a timestamp-based slug).
69
+ */
70
+ export function normalizeSlug(raw) {
71
+ return raw
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, "-")
74
+ .replace(/^-+|-+$/g, "")
75
+ .slice(0, MAX_SLUG_LEN)
76
+ .replace(/-+$/g, ""); // re-trim in case slice landed mid-dash-run
77
+ }
78
+ //# sourceMappingURL=slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.js","sourceRoot":"","sources":["../../src/soul/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC;AAE/B,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,IAAY,EAAE,MAAc;QACtC,KAAK,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,wEAAwE;AACxE,yDAAyD;AACzD,SAAS,cAAc,CAAC,CAAS;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,OAAO,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,eAAe,YAAY,QAAQ,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,6BAA6B,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC;IAC/D,CAAC;IACD,wEAAwE;IACxE,wDAAwD;IACxD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,eAAe,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,GAAG;SACP,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC;SACtB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,4CAA4C;AACtE,CAAC"}
@@ -13,7 +13,18 @@ export declare function readConstitution(): Promise<string>;
13
13
  export declare function writeConstitution(text: string): Promise<void>;
14
14
  export declare function readEmotions(): Promise<EmotionState>;
15
15
  export declare function writeEmotions(state: EmotionState): Promise<void>;
16
- export declare function decayEmotions(state: EmotionState): Promise<EmotionState>;
16
+ /**
17
+ * Apply exponential decay to all emotion intensities based on elapsed time
18
+ * since `state.updatedAt`. Pure modulo the clock: pass `nowMs` to make it
19
+ * deterministic (tests / replay). Preserves the `events` trail and the
20
+ * per-emotion `decay` rates — only the `values` and `updatedAt` change.
21
+ *
22
+ * Called BOTH on read (display) and at the start of soul_feel (so the
23
+ * persisted baseline is always decay-correct before a new delta is added).
24
+ * Previously decay only ran on read, so soul_feel added deltas onto a stale
25
+ * (un-decayed) value and the stored intensity could be months out of date.
26
+ */
27
+ export declare function decayEmotions(state: EmotionState, nowMs?: number): EmotionState;
17
28
  export declare function listValues(): Promise<ValueEntry[]>;
18
29
  export declare function writeValue(entry: ValueEntry): Promise<void>;
19
30
  export declare function listOpinions(): Promise<OpinionEntry[]>;