lsd-pi 1.1.4 → 1.1.6

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 (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -15,9 +15,63 @@ import { getMemoryDir } from './memory-paths.js';
15
15
  import { scanMemoryFiles, formatMemoryManifest } from './memory-scan.js';
16
16
  import { normalizeSubagentModel } from '../subagent/model-resolution.js';
17
17
  const AUTO_EXTRACT_ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
18
+ const AUTO_EXTRACT_CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
19
+ const AUTO_EXTRACT_SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
20
+ const AUTO_EXTRACT_HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
18
21
  export function stripAnsiForAutoExtractLog(text) {
19
22
  return text.replace(AUTO_EXTRACT_ANSI_PATTERN, '');
20
23
  }
24
+ export function classifyAutoExtractLogLine(rawLine) {
25
+ const stripped = stripAnsiForAutoExtractLog(rawLine).trim();
26
+ if (!stripped) {
27
+ return {
28
+ stripped,
29
+ keep: false,
30
+ completion: 'none',
31
+ completionReason: null,
32
+ };
33
+ }
34
+ if (AUTO_EXTRACT_CACHE_TIMER_RE.test(stripped)) {
35
+ return {
36
+ stripped,
37
+ keep: false,
38
+ completion: 'none',
39
+ completionReason: null,
40
+ };
41
+ }
42
+ if (AUTO_EXTRACT_SESSION_ENDED_RE.test(stripped)) {
43
+ return {
44
+ stripped,
45
+ keep: true,
46
+ completion: 'success',
47
+ completionReason: 'session_end_detected',
48
+ };
49
+ }
50
+ const headlessStatusMatch = stripped.match(AUTO_EXTRACT_HEADLESS_STATUS_RE);
51
+ if (headlessStatusMatch) {
52
+ const status = headlessStatusMatch[1].toLowerCase();
53
+ if (status === 'complete') {
54
+ return {
55
+ stripped,
56
+ keep: true,
57
+ completion: 'success',
58
+ completionReason: 'headless_status_complete',
59
+ };
60
+ }
61
+ return {
62
+ stripped,
63
+ keep: true,
64
+ completion: 'failure',
65
+ completionReason: `headless_status_${status}`,
66
+ };
67
+ }
68
+ return {
69
+ stripped,
70
+ keep: true,
71
+ completion: 'none',
72
+ completionReason: null,
73
+ };
74
+ }
21
75
  /**
22
76
  * Build a plain-text transcript from session entries, keeping only
23
77
  * human-readable message content (no tool_use / tool_result blocks).
@@ -72,7 +126,7 @@ This directory already exists — write files directly.
72
126
 
73
127
  Rules:
74
128
  - Save ONLY: user preferences/role, feedback/corrections, project context (deadlines, decisions), external references
75
- - Do NOT save: code patterns, architecture, file paths, git history, debugging steps, ephemeral task details
129
+ - Do NOT save: raw code snippets, low-level implementation details, file paths, git history, one-off debugging steps, ephemeral task details
76
130
  - Check existing memories below — update existing files rather than creating duplicates
77
131
  - Use frontmatter: ---\\nname: ...\\ndescription: ...\\ntype: user|feedback|project|reference\\n---
78
132
  - After writing topic files, update MEMORY.md with one-line index entries
@@ -124,62 +178,8 @@ export function readBudgetMemoryModel() {
124
178
  return undefined;
125
179
  }
126
180
  }
127
- /**
128
- * Main entry point — called from the session_shutdown hook.
129
- *
130
- * Reads the conversation transcript, builds an extraction prompt,
131
- * and spawns a detached headless agent to process it.
132
- * Fire-and-forget: the parent can exit without killing the child.
133
- */
134
- export function extractMemories(ctx, cwd) {
135
- // Guard: prevent recursive extraction
136
- if (process.env.LSD_MEMORY_EXTRACT === '1')
137
- return;
138
- // Guard: user opt-out
139
- if (process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY)
140
- return;
141
- const entries = ctx.sessionManager.getEntries();
142
- // Guard: need at least one user message to extract from
143
- const userMessageCount = entries.filter((e) => e.type === 'message' && e.message?.role === 'user').length;
144
- if (userMessageCount < 1)
145
- return;
146
- const transcript = buildTranscriptSummary(entries);
147
- if (!transcript)
148
- return;
149
- const memoryDir = getMemoryDir(cwd);
150
- mkdirSync(memoryDir, { recursive: true });
151
- const prompt = buildExtractionPrompt(memoryDir, transcript);
152
- const auditPath = join(memoryDir, '.last-auto-extract.txt');
153
- const logPath = join(memoryDir, '.last-auto-extract.log');
154
- // Write prompt to a temp file so the spawned agent can read it
155
- const tmpPromptPath = join(tmpdir(), `lsd-memory-extract-${randomUUID()}.md`);
156
- writeFileSync(tmpPromptPath, prompt, 'utf-8');
157
- const cliPath = resolveCliPath();
158
- const budgetModel = readBudgetMemoryModel();
159
- if (!cliPath) {
160
- writeFileSync(auditPath, [
161
- `timestamp: ${new Date().toISOString()}`,
162
- 'status: skipped',
163
- 'reason: cli_path_not_found',
164
- `cwd: ${cwd}`,
165
- `userMessages: ${userMessageCount}`,
166
- `transcriptLength: ${transcript.length}`,
167
- `budgetModel: ${budgetModel ?? 'default'}`,
168
- ].join('\n') + '\n', 'utf-8');
169
- return;
170
- }
171
- writeFileSync(auditPath, [
172
- `timestamp: ${new Date().toISOString()}`,
173
- 'status: spawning',
174
- `cwd: ${cwd}`,
175
- `userMessages: ${userMessageCount}`,
176
- `transcriptLength: ${transcript.length}`,
177
- `cliPath: ${cliPath}`,
178
- `budgetModel: ${budgetModel ?? 'default'}`,
179
- `logPath: ${logPath}`,
180
- ].join('\n') + '\n', 'utf-8');
181
- const instruction = 'Extract memories from the transcript above. Write any worth-saving memories to the memory directory, then update MEMORY.md.';
182
- const helperScript = `
181
+ export function buildAutoExtractHelperScript() {
182
+ return String.raw `
183
183
  const { spawn } = require('node:child_process');
184
184
  const { appendFileSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } = require('node:fs');
185
185
  const { join, delimiter } = require('node:path');
@@ -187,41 +187,67 @@ const { join, delimiter } = require('node:path');
187
187
  const [cliPath, cwd, tmpPromptPath, auditPath, logPath, memoryDir, instruction, model, userMessageCount, transcriptLength] = process.argv.slice(1);
188
188
  const startedAt = new Date().toISOString();
189
189
  let finalized = false;
190
- let sawSessionEnded = false;
191
- let sessionEndTimer = null;
190
+ let completionState = null;
191
+ let completionTimer = null;
192
192
  let hardTimeout = null;
193
193
  let pendingLogText = '';
194
194
  const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
195
+ const CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
196
+ const SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
197
+ const HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
195
198
 
196
199
  function stripAnsi(text) {
197
200
  return String(text).replace(ANSI_PATTERN, '');
198
201
  }
199
202
 
203
+ function classifyLogLine(rawLine) {
204
+ const stripped = stripAnsi(rawLine).trim();
205
+ if (!stripped) {
206
+ return { stripped, keep: false, completion: null, completionReason: null };
207
+ }
208
+ if (CACHE_TIMER_RE.test(stripped)) {
209
+ return { stripped, keep: false, completion: null, completionReason: null };
210
+ }
211
+ if (SESSION_ENDED_RE.test(stripped)) {
212
+ return { stripped, keep: true, completion: 'finished', completionReason: 'session_end_detected' };
213
+ }
214
+ const headlessStatusMatch = stripped.match(HEADLESS_STATUS_RE);
215
+ if (headlessStatusMatch) {
216
+ const status = String(headlessStatusMatch[1] || '').toLowerCase();
217
+ if (status === 'complete') {
218
+ return { stripped, keep: true, completion: 'finished', completionReason: 'headless_status_complete' };
219
+ }
220
+ return { stripped, keep: true, completion: 'failed', completionReason: 'headless_status_' + status };
221
+ }
222
+ return { stripped, keep: true, completion: null, completionReason: null };
223
+ }
224
+
225
+ function scheduleCompletion(completion, completionReason) {
226
+ if (!completion || completionState || completionTimer) return;
227
+ completionState = { completion, completionReason };
228
+ completionTimer = setTimeout(() => {
229
+ finalize(completion, completion === 'finished' ? 0 : 1, null, completionReason);
230
+ try { child.kill('SIGTERM'); } catch {}
231
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
232
+ }, 1500);
233
+ completionTimer.unref();
234
+ }
235
+
200
236
  function flushLogText(text, force = false) {
201
237
  pendingLogText += text;
202
- const parts = pendingLogText.split(/\r?\n/);
238
+ const parts = pendingLogText.split(/(?:\r?\n|\r)/);
203
239
  pendingLogText = force ? '' : (parts.pop() ?? '');
204
240
 
205
241
  const kept = [];
206
242
  for (const rawLine of parts) {
207
- const line = stripAnsi(rawLine).trim();
208
- if (!line) continue;
209
- if (/^\[phase\]\s+cache-timer\s*$/.test(line)) continue;
210
- kept.push(rawLine);
211
- if (/^\[agent\]\s+Session ended/.test(line)) {
212
- sawSessionEnded = true;
243
+ const classified = classifyLogLine(rawLine);
244
+ if (classified.keep) kept.push(rawLine);
245
+ if (classified.completion) {
246
+ scheduleCompletion(classified.completion, classified.completionReason);
213
247
  }
214
248
  }
215
249
 
216
250
  if (kept.length > 0) appendFileSync(logPath, kept.join('\n') + '\n');
217
- if (sawSessionEnded && !sessionEndTimer) {
218
- sessionEndTimer = setTimeout(() => {
219
- finalize('finished', 0, null, 'session_end_detected');
220
- try { child.kill('SIGTERM'); } catch {}
221
- setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
222
- }, 1500);
223
- sessionEndTimer.unref();
224
- }
225
251
  }
226
252
 
227
253
  function appendLog(chunk) {
@@ -243,7 +269,7 @@ function writeAudit(status, extra = []) {
243
269
  'model: ' + (model || 'default'),
244
270
  'logPath: ' + logPath,
245
271
  ...extra,
246
- ].join('\\n') + '\\n', 'utf-8');
272
+ ].join('\n') + '\n', 'utf-8');
247
273
  } catch {}
248
274
  }
249
275
 
@@ -265,7 +291,7 @@ function newestMemoryMtime(dir) {
265
291
  function finalize(status, code, signal, completionReason) {
266
292
  if (finalized) return;
267
293
  finalized = true;
268
- if (sessionEndTimer) clearTimeout(sessionEndTimer);
294
+ if (completionTimer) clearTimeout(completionTimer);
269
295
  if (hardTimeout) clearTimeout(hardTimeout);
270
296
  const afterMtime = newestMemoryMtime(memoryDir);
271
297
  const logText = existsSync(logPath) ? readFileSync(logPath, 'utf-8') : '';
@@ -292,7 +318,10 @@ const bundledPaths = Array.from(
292
318
  new Set(
293
319
  [process.env.GSD_BUNDLED_EXTENSION_PATHS, process.env.LSD_BUNDLED_EXTENSION_PATHS]
294
320
  .filter(Boolean)
295
- .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean)),
321
+ .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean))
322
+ // Explicitly disable cache-timer extension for auto-memory workers.
323
+ // It is noisy in headless logs and provides no value for maintenance runs.
324
+ .filter((entry) => !/[\\/]cache-timer[\\/]/i.test(entry)),
296
325
  ),
297
326
  );
298
327
  for (const extensionPath of bundledPaths) childArgs.push('--extension', extensionPath);
@@ -302,11 +331,25 @@ childArgs.push('--bare', '--context', tmpPromptPath, '--context-text', instructi
302
331
  const child = spawn(
303
332
  process.execPath,
304
333
  childArgs,
305
- { cwd, env: { ...process.env, LSD_MEMORY_EXTRACT: '1' }, stdio: ['ignore', 'pipe', 'pipe'] },
334
+ {
335
+ cwd,
336
+ env: {
337
+ ...process.env,
338
+ LSD_MEMORY_EXTRACT: '1',
339
+ // Hard-disable cache timer in maintenance workers.
340
+ LSD_DISABLE_CACHE_TIMER: '1',
341
+ // Memory maintenance workers must not run in auto permission mode.
342
+ // In auto mode, write/edit calls require classifier approval, but headless
343
+ // maintenance workers have no interactive classifier responder.
344
+ // Keep writes safe via extension-level path restrictions instead.
345
+ LUCENT_CODE_PERMISSION_MODE: 'danger-full-access',
346
+ },
347
+ stdio: ['ignore', 'pipe', 'pipe'],
348
+ },
306
349
  );
307
350
 
308
351
  hardTimeout = setTimeout(() => {
309
- finalize('failed', null, 'timeout', sawSessionEnded ? 'hung_after_session_end' : 'timeout');
352
+ finalize('failed', null, 'timeout', completionState?.completionReason ?? 'timeout');
310
353
  try { child.kill('SIGTERM'); } catch {}
311
354
  setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
312
355
  }, 120000);
@@ -315,7 +358,7 @@ hardTimeout.unref();
315
358
  child.stdout.on('data', appendLog);
316
359
  child.stderr.on('data', appendLog);
317
360
  child.on('error', (err) => {
318
- appendLog(String(err && err.stack ? err.stack : err) + '\\n');
361
+ appendLog(String(err && err.stack ? err.stack : err) + '\n');
319
362
  flushLogText('', true);
320
363
  finalize('failed', null, 'spawn_error', String(err && err.message ? err.message : err));
321
364
  });
@@ -324,6 +367,79 @@ child.on('exit', (code, signal) => {
324
367
  finalize(code === 0 ? 'finished' : 'failed', code, signal, 'child_exit');
325
368
  });
326
369
  `;
370
+ }
371
+ /**
372
+ * Main entry point — called from the session_shutdown hook.
373
+ *
374
+ * Reads the conversation transcript, builds an extraction prompt,
375
+ * and spawns a detached headless agent to process it.
376
+ * Fire-and-forget: the parent can exit without killing the child.
377
+ */
378
+ function readAutoMemoryEnabled() {
379
+ try {
380
+ const settingsPath = join(getAgentDir(), 'settings.json');
381
+ if (!existsSync(settingsPath))
382
+ return false;
383
+ const raw = readFileSync(settingsPath, 'utf-8');
384
+ const parsed = JSON.parse(raw);
385
+ return parsed.autoMemory === true;
386
+ }
387
+ catch {
388
+ return false;
389
+ }
390
+ }
391
+ export function extractMemories(ctx, cwd) {
392
+ // Guard: prevent recursive extraction
393
+ if (process.env.LSD_MEMORY_EXTRACT === '1')
394
+ return;
395
+ // Guard: user opt-out via env var
396
+ if (process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY)
397
+ return;
398
+ // Guard: auto memory must be enabled in settings (default: disabled)
399
+ if (!readAutoMemoryEnabled())
400
+ return;
401
+ const entries = ctx.sessionManager.getEntries();
402
+ // Guard: need at least one user message to extract from
403
+ const userMessageCount = entries.filter((e) => e.type === 'message' && e.message?.role === 'user').length;
404
+ if (userMessageCount < 1)
405
+ return;
406
+ const transcript = buildTranscriptSummary(entries);
407
+ if (!transcript)
408
+ return;
409
+ const memoryDir = getMemoryDir(cwd);
410
+ mkdirSync(memoryDir, { recursive: true });
411
+ const prompt = buildExtractionPrompt(memoryDir, transcript);
412
+ const auditPath = join(memoryDir, '.last-auto-extract.txt');
413
+ const logPath = join(memoryDir, '.last-auto-extract.log');
414
+ // Write prompt to a temp file so the spawned agent can read it
415
+ const tmpPromptPath = join(tmpdir(), `lsd-memory-extract-${randomUUID()}.md`);
416
+ writeFileSync(tmpPromptPath, prompt, 'utf-8');
417
+ const cliPath = resolveCliPath();
418
+ const budgetModel = readBudgetMemoryModel();
419
+ if (!cliPath) {
420
+ writeFileSync(auditPath, [
421
+ `timestamp: ${new Date().toISOString()}`,
422
+ 'status: skipped',
423
+ 'reason: cli_path_not_found',
424
+ `cwd: ${cwd}`,
425
+ `userMessages: ${userMessageCount}`,
426
+ `transcriptLength: ${transcript.length}`,
427
+ `budgetModel: ${budgetModel ?? 'default'}`,
428
+ ].join('\n') + '\n', 'utf-8');
429
+ return;
430
+ }
431
+ writeFileSync(auditPath, [
432
+ `timestamp: ${new Date().toISOString()}`,
433
+ 'status: spawning',
434
+ `cwd: ${cwd}`,
435
+ `userMessages: ${userMessageCount}`,
436
+ `transcriptLength: ${transcript.length}`,
437
+ `cliPath: ${cliPath}`,
438
+ `budgetModel: ${budgetModel ?? 'default'}`,
439
+ `logPath: ${logPath}`,
440
+ ].join('\n') + '\n', 'utf-8');
441
+ const instruction = 'Extract memories from the transcript above. Write any worth-saving memories to the memory directory, then update MEMORY.md.';
442
+ const helperScript = buildAutoExtractHelperScript();
327
443
  const proc = spawn(process.execPath, [
328
444
  '-e',
329
445
  helperScript,
@@ -34,19 +34,21 @@ function readJsonFile(path) {
34
34
  }
35
35
  }
36
36
  function parseAutoDreamSettings(source) {
37
+ // Check top-level autoDream field first (set via /settings UI)
38
+ const topLevel = source.autoDream;
37
39
  const memory = source.memory;
38
- if (!memory || typeof memory !== 'object')
39
- return {};
40
- const settings = memory;
40
+ const nested = (memory && typeof memory === 'object') ? memory : undefined;
41
+ // Top-level takes precedence for enabled; nested.memory for thresholds
42
+ const enabledSource = typeof topLevel === 'boolean' ? topLevel
43
+ : nested && typeof nested.autoDream === 'boolean' ? nested.autoDream
44
+ : undefined;
41
45
  return {
42
- enabled: typeof settings.autoDream === 'boolean'
43
- ? settings.autoDream
44
- : DEFAULT_AUTO_DREAM_SETTINGS.enabled,
45
- minHours: typeof settings.autoDreamMinHours === 'number' && Number.isFinite(settings.autoDreamMinHours)
46
- ? Math.max(1, settings.autoDreamMinHours)
46
+ ...(enabledSource !== undefined ? { enabled: enabledSource } : {}),
47
+ minHours: nested && typeof nested.autoDreamMinHours === 'number' && Number.isFinite(nested.autoDreamMinHours)
48
+ ? Math.max(1, nested.autoDreamMinHours)
47
49
  : DEFAULT_AUTO_DREAM_SETTINGS.minHours,
48
- minSessions: typeof settings.autoDreamMinSessions === 'number' && Number.isFinite(settings.autoDreamMinSessions)
49
- ? Math.max(1, Math.floor(settings.autoDreamMinSessions))
50
+ minSessions: nested && typeof nested.autoDreamMinSessions === 'number' && Number.isFinite(nested.autoDreamMinSessions)
51
+ ? Math.max(1, Math.floor(nested.autoDreamMinSessions))
50
52
  : DEFAULT_AUTO_DREAM_SETTINGS.minSessions,
51
53
  };
52
54
  }
@@ -320,6 +322,13 @@ const { join, delimiter } = require('node:path');
320
322
  const [cliPath, cwd, tmpPromptPath, auditPath, logPath, memoryDir, sessionDir, instruction, model, trigger, priorMtime, sessionCount] = process.argv.slice(1);
321
323
  let finalized = false;
322
324
  let pendingLogText = '';
325
+ let completionState = null;
326
+ let completionTimer = null;
327
+ let hardTimeout = null;
328
+ const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
329
+ const CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
330
+ const SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
331
+ const HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
323
332
 
324
333
  function newestMemoryMtime(dir) {
325
334
  try {
@@ -391,6 +400,43 @@ function pruneBrokenMemoryRefs(dir) {
391
400
  }
392
401
  }
393
402
 
403
+ function stripAnsi(text) {
404
+ return String(text).replace(ANSI_PATTERN, '');
405
+ }
406
+
407
+ function classifyLogLine(rawLine) {
408
+ const stripped = stripAnsi(rawLine).trim();
409
+ if (!stripped) {
410
+ return { stripped, keep: false, completion: null, completionReason: null };
411
+ }
412
+ if (CACHE_TIMER_RE.test(stripped)) {
413
+ return { stripped, keep: false, completion: null, completionReason: null };
414
+ }
415
+ if (SESSION_ENDED_RE.test(stripped)) {
416
+ return { stripped, keep: true, completion: 'finished', completionReason: 'session_end_detected' };
417
+ }
418
+ const headlessStatusMatch = stripped.match(HEADLESS_STATUS_RE);
419
+ if (headlessStatusMatch) {
420
+ const status = String(headlessStatusMatch[1] || '').toLowerCase();
421
+ if (status === 'complete') {
422
+ return { stripped, keep: true, completion: 'finished', completionReason: 'headless_status_complete' };
423
+ }
424
+ return { stripped, keep: true, completion: 'failed', completionReason: 'headless_status_' + status };
425
+ }
426
+ return { stripped, keep: true, completion: null, completionReason: null };
427
+ }
428
+
429
+ function scheduleCompletion(completion, completionReason) {
430
+ if (!completion || completionState || completionTimer) return;
431
+ completionState = { completion, completionReason };
432
+ completionTimer = setTimeout(() => {
433
+ finalize(completion, completion === 'finished' ? 0 : 1, null, completionReason);
434
+ try { child.kill('SIGTERM'); } catch {}
435
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
436
+ }, 1500);
437
+ completionTimer.unref();
438
+ }
439
+
394
440
  function writeAudit(status, extra = []) {
395
441
  try {
396
442
  writeFileSync(auditPath, [
@@ -425,9 +471,18 @@ function rollbackLock() {
425
471
 
426
472
  function flushLogText(text, force = false) {
427
473
  pendingLogText += text;
428
- const parts = pendingLogText.split(/\r?\n/);
474
+ const parts = pendingLogText.split(/(?:\r?\n|\r)/);
429
475
  pendingLogText = force ? '' : (parts.pop() ?? '');
430
- const kept = parts.filter((line) => line.trim());
476
+
477
+ const kept = [];
478
+ for (const rawLine of parts) {
479
+ const classified = classifyLogLine(rawLine);
480
+ if (classified.keep) kept.push(rawLine);
481
+ if (classified.completion) {
482
+ scheduleCompletion(classified.completion, classified.completionReason);
483
+ }
484
+ }
485
+
431
486
  if (kept.length > 0) appendFileSync(logPath, kept.join('\n') + '\n');
432
487
  }
433
488
 
@@ -441,6 +496,8 @@ function appendLog(chunk) {
441
496
  function finalize(status, code, signal, completionReason) {
442
497
  if (finalized) return;
443
498
  finalized = true;
499
+ if (completionTimer) clearTimeout(completionTimer);
500
+ if (hardTimeout) clearTimeout(hardTimeout);
444
501
  flushLogText('', true);
445
502
  const beforeBrokenRefs = listBrokenMemoryRefs(memoryDir);
446
503
  const prunedRefs = beforeBrokenRefs.length > 0 ? pruneBrokenMemoryRefs(memoryDir) : [];
@@ -476,7 +533,9 @@ const bundledPaths = Array.from(
476
533
  new Set(
477
534
  [process.env.GSD_BUNDLED_EXTENSION_PATHS, process.env.LSD_BUNDLED_EXTENSION_PATHS]
478
535
  .filter(Boolean)
479
- .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean)),
536
+ .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean))
537
+ // Explicitly disable cache-timer extension for dream workers.
538
+ .filter((entry) => !/[\\/]cache-timer[\\/]/i.test(entry)),
480
539
  ),
481
540
  );
482
541
  for (const extensionPath of bundledPaths) childArgs.push('--extension', extensionPath);
@@ -485,12 +544,20 @@ childArgs.push('--bare', '--context', tmpPromptPath, '--context-text', instructi
485
544
 
486
545
  const child = spawn(process.execPath, childArgs, {
487
546
  cwd,
488
- env: { ...process.env, LSD_MEMORY_DREAM: '1' },
547
+ env: {
548
+ ...process.env,
549
+ LSD_MEMORY_DREAM: '1',
550
+ // Hard-disable cache timer in maintenance workers.
551
+ LSD_DISABLE_CACHE_TIMER: '1',
552
+ // Dream workers run headless and cannot answer auto-mode classifier prompts.
553
+ // Force non-auto permissions and rely on memory-extension path/tool guards.
554
+ LUCENT_CODE_PERMISSION_MODE: 'danger-full-access',
555
+ },
489
556
  stdio: ['ignore', 'pipe', 'pipe'],
490
557
  });
491
558
 
492
- const hardTimeout = setTimeout(() => {
493
- finalize('failed', null, 'timeout', 'timeout');
559
+ hardTimeout = setTimeout(() => {
560
+ finalize('failed', null, 'timeout', completionState?.completionReason ?? 'timeout');
494
561
  try { child.kill('SIGTERM'); } catch {}
495
562
  setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
496
563
  }, 180000);
@@ -500,12 +567,12 @@ child.stdout.on('data', appendLog);
500
567
  child.stderr.on('data', appendLog);
501
568
  child.on('error', (err) => {
502
569
  appendLog(String(err && err.stack ? err.stack : err) + '\n');
503
- clearTimeout(hardTimeout);
570
+ flushLogText('', true);
504
571
  finalize('failed', null, 'spawn_error', String(err && err.message ? err.message : err));
505
572
  });
506
573
  child.on('exit', (code, signal) => {
507
- clearTimeout(hardTimeout);
508
- finalize(code === 0 ? 'finished' : 'failed', code, signal, 'child_exit');
574
+ flushLogText('', true);
575
+ finalize(code === 0 ? 'finished' : 'failed', code, signal, completionState?.completionReason ?? 'child_exit');
509
576
  });
510
577
  `;
511
578
  const proc = spawn(process.execPath, [