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
@@ -58,27 +58,29 @@ function readJsonFile(path: string): Record<string, unknown> {
58
58
  }
59
59
 
60
60
  function parseAutoDreamSettings(source: Record<string, unknown>): Partial<AutoDreamSettings> {
61
+ // Check top-level autoDream field first (set via /settings UI)
62
+ const topLevel = source.autoDream;
61
63
  const memory = source.memory;
62
- if (!memory || typeof memory !== 'object') return {};
63
-
64
- const settings = memory as {
64
+ const nested = (memory && typeof memory === 'object') ? memory as {
65
65
  autoDream?: unknown;
66
66
  autoDreamMinHours?: unknown;
67
67
  autoDreamMinSessions?: unknown;
68
- };
68
+ } : undefined;
69
+
70
+ // Top-level takes precedence for enabled; nested.memory for thresholds
71
+ const enabledSource = typeof topLevel === 'boolean' ? topLevel
72
+ : nested && typeof nested.autoDream === 'boolean' ? nested.autoDream
73
+ : undefined;
69
74
 
70
75
  return {
71
- enabled:
72
- typeof settings.autoDream === 'boolean'
73
- ? settings.autoDream
74
- : DEFAULT_AUTO_DREAM_SETTINGS.enabled,
76
+ ...(enabledSource !== undefined ? { enabled: enabledSource } : {}),
75
77
  minHours:
76
- typeof settings.autoDreamMinHours === 'number' && Number.isFinite(settings.autoDreamMinHours)
77
- ? Math.max(1, settings.autoDreamMinHours)
78
+ nested && typeof nested.autoDreamMinHours === 'number' && Number.isFinite(nested.autoDreamMinHours)
79
+ ? Math.max(1, nested.autoDreamMinHours)
78
80
  : DEFAULT_AUTO_DREAM_SETTINGS.minHours,
79
81
  minSessions:
80
- typeof settings.autoDreamMinSessions === 'number' && Number.isFinite(settings.autoDreamMinSessions)
81
- ? Math.max(1, Math.floor(settings.autoDreamMinSessions))
82
+ nested && typeof nested.autoDreamMinSessions === 'number' && Number.isFinite(nested.autoDreamMinSessions)
83
+ ? Math.max(1, Math.floor(nested.autoDreamMinSessions))
82
84
  : DEFAULT_AUTO_DREAM_SETTINGS.minSessions,
83
85
  };
84
86
  }
@@ -383,6 +385,13 @@ const { join, delimiter } = require('node:path');
383
385
  const [cliPath, cwd, tmpPromptPath, auditPath, logPath, memoryDir, sessionDir, instruction, model, trigger, priorMtime, sessionCount] = process.argv.slice(1);
384
386
  let finalized = false;
385
387
  let pendingLogText = '';
388
+ let completionState = null;
389
+ let completionTimer = null;
390
+ let hardTimeout = null;
391
+ const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
392
+ const CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
393
+ const SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
394
+ const HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
386
395
 
387
396
  function newestMemoryMtime(dir) {
388
397
  try {
@@ -454,6 +463,43 @@ function pruneBrokenMemoryRefs(dir) {
454
463
  }
455
464
  }
456
465
 
466
+ function stripAnsi(text) {
467
+ return String(text).replace(ANSI_PATTERN, '');
468
+ }
469
+
470
+ function classifyLogLine(rawLine) {
471
+ const stripped = stripAnsi(rawLine).trim();
472
+ if (!stripped) {
473
+ return { stripped, keep: false, completion: null, completionReason: null };
474
+ }
475
+ if (CACHE_TIMER_RE.test(stripped)) {
476
+ return { stripped, keep: false, completion: null, completionReason: null };
477
+ }
478
+ if (SESSION_ENDED_RE.test(stripped)) {
479
+ return { stripped, keep: true, completion: 'finished', completionReason: 'session_end_detected' };
480
+ }
481
+ const headlessStatusMatch = stripped.match(HEADLESS_STATUS_RE);
482
+ if (headlessStatusMatch) {
483
+ const status = String(headlessStatusMatch[1] || '').toLowerCase();
484
+ if (status === 'complete') {
485
+ return { stripped, keep: true, completion: 'finished', completionReason: 'headless_status_complete' };
486
+ }
487
+ return { stripped, keep: true, completion: 'failed', completionReason: 'headless_status_' + status };
488
+ }
489
+ return { stripped, keep: true, completion: null, completionReason: null };
490
+ }
491
+
492
+ function scheduleCompletion(completion, completionReason) {
493
+ if (!completion || completionState || completionTimer) return;
494
+ completionState = { completion, completionReason };
495
+ completionTimer = setTimeout(() => {
496
+ finalize(completion, completion === 'finished' ? 0 : 1, null, completionReason);
497
+ try { child.kill('SIGTERM'); } catch {}
498
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
499
+ }, 1500);
500
+ completionTimer.unref();
501
+ }
502
+
457
503
  function writeAudit(status, extra = []) {
458
504
  try {
459
505
  writeFileSync(auditPath, [
@@ -488,9 +534,18 @@ function rollbackLock() {
488
534
 
489
535
  function flushLogText(text, force = false) {
490
536
  pendingLogText += text;
491
- const parts = pendingLogText.split(/\r?\n/);
537
+ const parts = pendingLogText.split(/(?:\r?\n|\r)/);
492
538
  pendingLogText = force ? '' : (parts.pop() ?? '');
493
- const kept = parts.filter((line) => line.trim());
539
+
540
+ const kept = [];
541
+ for (const rawLine of parts) {
542
+ const classified = classifyLogLine(rawLine);
543
+ if (classified.keep) kept.push(rawLine);
544
+ if (classified.completion) {
545
+ scheduleCompletion(classified.completion, classified.completionReason);
546
+ }
547
+ }
548
+
494
549
  if (kept.length > 0) appendFileSync(logPath, kept.join('\n') + '\n');
495
550
  }
496
551
 
@@ -504,6 +559,8 @@ function appendLog(chunk) {
504
559
  function finalize(status, code, signal, completionReason) {
505
560
  if (finalized) return;
506
561
  finalized = true;
562
+ if (completionTimer) clearTimeout(completionTimer);
563
+ if (hardTimeout) clearTimeout(hardTimeout);
507
564
  flushLogText('', true);
508
565
  const beforeBrokenRefs = listBrokenMemoryRefs(memoryDir);
509
566
  const prunedRefs = beforeBrokenRefs.length > 0 ? pruneBrokenMemoryRefs(memoryDir) : [];
@@ -539,7 +596,9 @@ const bundledPaths = Array.from(
539
596
  new Set(
540
597
  [process.env.GSD_BUNDLED_EXTENSION_PATHS, process.env.LSD_BUNDLED_EXTENSION_PATHS]
541
598
  .filter(Boolean)
542
- .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean)),
599
+ .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean))
600
+ // Explicitly disable cache-timer extension for dream workers.
601
+ .filter((entry) => !/[\\/]cache-timer[\\/]/i.test(entry)),
543
602
  ),
544
603
  );
545
604
  for (const extensionPath of bundledPaths) childArgs.push('--extension', extensionPath);
@@ -548,12 +607,20 @@ childArgs.push('--bare', '--context', tmpPromptPath, '--context-text', instructi
548
607
 
549
608
  const child = spawn(process.execPath, childArgs, {
550
609
  cwd,
551
- env: { ...process.env, LSD_MEMORY_DREAM: '1' },
610
+ env: {
611
+ ...process.env,
612
+ LSD_MEMORY_DREAM: '1',
613
+ // Hard-disable cache timer in maintenance workers.
614
+ LSD_DISABLE_CACHE_TIMER: '1',
615
+ // Dream workers run headless and cannot answer auto-mode classifier prompts.
616
+ // Force non-auto permissions and rely on memory-extension path/tool guards.
617
+ LUCENT_CODE_PERMISSION_MODE: 'danger-full-access',
618
+ },
552
619
  stdio: ['ignore', 'pipe', 'pipe'],
553
620
  });
554
621
 
555
- const hardTimeout = setTimeout(() => {
556
- finalize('failed', null, 'timeout', 'timeout');
622
+ hardTimeout = setTimeout(() => {
623
+ finalize('failed', null, 'timeout', completionState?.completionReason ?? 'timeout');
557
624
  try { child.kill('SIGTERM'); } catch {}
558
625
  setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
559
626
  }, 180000);
@@ -563,12 +630,12 @@ child.stdout.on('data', appendLog);
563
630
  child.stderr.on('data', appendLog);
564
631
  child.on('error', (err) => {
565
632
  appendLog(String(err && err.stack ? err.stack : err) + '\n');
566
- clearTimeout(hardTimeout);
633
+ flushLogText('', true);
567
634
  finalize('failed', null, 'spawn_error', String(err && err.message ? err.message : err));
568
635
  });
569
636
  child.on('exit', (code, signal) => {
570
- clearTimeout(hardTimeout);
571
- finalize(code === 0 ? 'finished' : 'failed', code, signal, 'child_exit');
637
+ flushLogText('', true);
638
+ finalize(code === 0 ? 'finished' : 'failed', code, signal, completionState?.completionReason ?? 'child_exit');
572
639
  });
573
640
  `;
574
641
 
@@ -1,158 +1,214 @@
1
1
  import test, { describe } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import vm from 'node:vm';
3
4
  import { mkdirSync, rmSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { tmpdir } from 'node:os';
6
- import { buildTranscriptSummary, buildExtractionPrompt, stripAnsiForAutoExtractLog } from '../auto-extract.js';
7
+ import { buildTranscriptSummary, buildExtractionPrompt, stripAnsiForAutoExtractLog, classifyAutoExtractLogLine, buildAutoExtractHelperScript } from '../auto-extract.js';
7
8
 
8
9
  function makeTempDir(): string {
9
- const dir = join(tmpdir(), `mem-extract-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
- mkdirSync(dir, { recursive: true });
11
- return dir;
10
+ const dir = join(tmpdir(), `mem-extract-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
11
+ mkdirSync(dir, { recursive: true });
12
+ return dir;
12
13
  }
13
14
 
14
15
  describe('buildTranscriptSummary', () => {
15
- test('returns transcript even for a short conversation when there is user-authored content', () => {
16
- const entries = [
17
- { type: 'message', message: { role: 'user', content: 'hello' } },
18
- { type: 'message', message: { role: 'assistant', content: 'hi' } },
19
- ];
20
- const summary = buildTranscriptSummary(entries);
21
- assert.ok(summary.includes('User: hello'));
22
- assert.ok(summary.includes('Assistant: hi'));
23
- });
24
-
25
- test('returns empty string for empty array', () => {
26
- assert.equal(buildTranscriptSummary([]), '');
27
- });
28
-
29
- test('returns empty string when there is no user-authored content', () => {
30
- const entries = [
31
- { type: 'message', message: { role: 'assistant', content: 'hi' } },
32
- ];
33
- assert.equal(buildTranscriptSummary(entries), '');
34
- });
35
-
36
- test('extracts user and assistant text messages', () => {
37
- const entries = [
38
- { type: 'message', message: { role: 'user', content: 'hello' } },
39
- { type: 'message', message: { role: 'assistant', content: 'hi there' } },
40
- { type: 'message', message: { role: 'user', content: 'how are you' } },
41
- ];
42
- const summary = buildTranscriptSummary(entries);
43
- assert.ok(summary.includes('User: hello'));
44
- assert.ok(summary.includes('Assistant: hi there'));
45
- assert.ok(summary.includes('User: how are you'));
46
- });
47
-
48
- test('skips entries where type !== message', () => {
49
- const entries = [
50
- { type: 'other', data: 'ignored' },
51
- { type: 'message', message: { role: 'user', content: 'hello' } },
52
- { type: 'tool_result', id: '1', content: 'ignored' },
53
- { type: 'message', message: { role: 'assistant', content: 'hi' } },
54
- { type: 'message', message: { role: 'user', content: 'test' } },
55
- ];
56
- const summary = buildTranscriptSummary(entries);
57
- assert.ok(summary.includes('User: hello'));
58
- assert.ok(summary.includes('Assistant: hi'));
59
- assert.ok(summary.includes('User: test'));
60
- });
61
-
62
- test('handles array content (multi-part), includes only text', () => {
63
- const entries = [
64
- { type: 'message', message: { role: 'user', content: [{ type: 'text', text: 'hello' }, { type: 'tool_use', id: '1', name: 'bash', input: {} }] } },
65
- { type: 'message', message: { role: 'assistant', content: 'response' } },
66
- { type: 'message', message: { role: 'user', content: 'end' } },
67
- ];
68
- const summary = buildTranscriptSummary(entries);
69
- assert.ok(summary.includes('User: hello'));
70
- assert.ok(!summary.includes('tool_use'));
71
- assert.ok(summary.includes('Assistant: response'));
72
- assert.ok(summary.includes('User: end'));
73
- });
74
-
75
- test('truncates messages over 2000 chars', () => {
76
- const longText = 'a'.repeat(3000);
77
- const entries = [
78
- { type: 'message', message: { role: 'user', content: 'start' } },
79
- { type: 'message', message: { role: 'assistant', content: longText } },
80
- { type: 'message', message: { role: 'user', content: 'end' } },
81
- ];
82
- const summary = buildTranscriptSummary(entries);
83
- assert.ok(summary.includes('…'));
84
- assert.ok(summary.length < 4000); // Should be truncated
85
- });
86
-
87
- test('labels user messages with User: and assistant with Assistant:', () => {
88
- const entries = [
89
- { type: 'message', message: { role: 'user', content: 'question' } },
90
- { type: 'message', message: { role: 'assistant', content: 'answer' } },
91
- { type: 'message', message: { role: 'user', content: 'thanks' } },
92
- ];
93
- const summary = buildTranscriptSummary(entries);
94
- assert.ok(summary.includes('User:'));
95
- assert.ok(summary.includes('Assistant:'));
96
- });
16
+ test('returns transcript even for a short conversation when there is user-authored content', () => {
17
+ const entries = [
18
+ { type: 'message', message: { role: 'user', content: 'hello' } },
19
+ { type: 'message', message: { role: 'assistant', content: 'hi' } },
20
+ ];
21
+ const summary = buildTranscriptSummary(entries);
22
+ assert.ok(summary.includes('User: hello'));
23
+ assert.ok(summary.includes('Assistant: hi'));
24
+ });
25
+
26
+ test('returns empty string for empty array', () => {
27
+ assert.equal(buildTranscriptSummary([]), '');
28
+ });
29
+
30
+ test('returns empty string when there is no user-authored content', () => {
31
+ const entries = [
32
+ { type: 'message', message: { role: 'assistant', content: 'hi' } },
33
+ ];
34
+ assert.equal(buildTranscriptSummary(entries), '');
35
+ });
36
+
37
+ test('extracts user and assistant text messages', () => {
38
+ const entries = [
39
+ { type: 'message', message: { role: 'user', content: 'hello' } },
40
+ { type: 'message', message: { role: 'assistant', content: 'hi there' } },
41
+ { type: 'message', message: { role: 'user', content: 'how are you' } },
42
+ ];
43
+ const summary = buildTranscriptSummary(entries);
44
+ assert.ok(summary.includes('User: hello'));
45
+ assert.ok(summary.includes('Assistant: hi there'));
46
+ assert.ok(summary.includes('User: how are you'));
47
+ });
48
+
49
+ test('skips entries where type !== message', () => {
50
+ const entries = [
51
+ { type: 'other', data: 'ignored' },
52
+ { type: 'message', message: { role: 'user', content: 'hello' } },
53
+ { type: 'tool_result', id: '1', content: 'ignored' },
54
+ { type: 'message', message: { role: 'assistant', content: 'hi' } },
55
+ { type: 'message', message: { role: 'user', content: 'test' } },
56
+ ];
57
+ const summary = buildTranscriptSummary(entries);
58
+ assert.ok(summary.includes('User: hello'));
59
+ assert.ok(summary.includes('Assistant: hi'));
60
+ assert.ok(summary.includes('User: test'));
61
+ });
62
+
63
+ test('handles array content (multi-part), includes only text', () => {
64
+ const entries = [
65
+ { type: 'message', message: { role: 'user', content: [{ type: 'text', text: 'hello' }, { type: 'tool_use', id: '1', name: 'bash', input: {} }] } },
66
+ { type: 'message', message: { role: 'assistant', content: 'response' } },
67
+ { type: 'message', message: { role: 'user', content: 'end' } },
68
+ ];
69
+ const summary = buildTranscriptSummary(entries);
70
+ assert.ok(summary.includes('User: hello'));
71
+ assert.ok(!summary.includes('tool_use'));
72
+ assert.ok(summary.includes('Assistant: response'));
73
+ assert.ok(summary.includes('User: end'));
74
+ });
75
+
76
+ test('truncates messages over 2000 chars', () => {
77
+ const longText = 'a'.repeat(3000);
78
+ const entries = [
79
+ { type: 'message', message: { role: 'user', content: 'start' } },
80
+ { type: 'message', message: { role: 'assistant', content: longText } },
81
+ { type: 'message', message: { role: 'user', content: 'end' } },
82
+ ];
83
+ const summary = buildTranscriptSummary(entries);
84
+ assert.ok(summary.includes('…'));
85
+ assert.ok(summary.length < 4000); // Should be truncated
86
+ });
87
+
88
+ test('labels user messages with User: and assistant with Assistant:', () => {
89
+ const entries = [
90
+ { type: 'message', message: { role: 'user', content: 'question' } },
91
+ { type: 'message', message: { role: 'assistant', content: 'answer' } },
92
+ { type: 'message', message: { role: 'user', content: 'thanks' } },
93
+ ];
94
+ const summary = buildTranscriptSummary(entries);
95
+ assert.ok(summary.includes('User:'));
96
+ assert.ok(summary.includes('Assistant:'));
97
+ });
98
+ });
99
+
100
+ describe('buildAutoExtractHelperScript', () => {
101
+ test('preserves regex escapes in the generated helper source', () => {
102
+ const script = buildAutoExtractHelperScript();
103
+
104
+ assert.ok(script.includes(String.raw`const CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;`));
105
+ assert.ok(script.includes(String.raw`const SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;`));
106
+ assert.ok(script.includes(String.raw`const HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;`));
107
+ assert.ok(script.includes(String.raw`const parts = pendingLogText.split(/\r?\n/);`));
108
+ assert.ok(script.includes(`appendFileSync(logPath, kept.join('\\n') + '\\n')`));
109
+ });
110
+
111
+ test('generates syntactically valid helper code', () => {
112
+ const script = buildAutoExtractHelperScript();
113
+ assert.doesNotThrow(() => new vm.Script(script));
114
+ });
97
115
  });
98
116
 
99
117
  describe('buildExtractionPrompt', () => {
100
- test('strips ANSI codes so session-end and cache-timer lines can be classified', () => {
101
- const coloredSessionEnd = '\u001b[36m[agent] Session ended\u001b[0m';
102
- const coloredCacheTimer = '\u001b[36m[phase] cache-timer\u001b[0m';
103
-
104
- assert.equal(stripAnsiForAutoExtractLog(coloredSessionEnd), '[agent] Session ended');
105
- assert.equal(stripAnsiForAutoExtractLog(coloredCacheTimer), '[phase] cache-timer');
106
- });
107
-
108
- test('contains the memory directory path in output', () => {
109
- const memoryDir = '/tmp/test-memory';
110
- const entries = [
111
- { type: 'message', message: { role: 'user', content: 'test1' } },
112
- { type: 'message', message: { role: 'assistant', content: 'test2' } },
113
- { type: 'message', message: { role: 'user', content: 'test3' } },
114
- ];
115
- const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
116
- assert.ok(prompt.includes(memoryDir));
117
- });
118
-
119
- test('contains the transcript in output', () => {
120
- const memoryDir = '/tmp/test-memory';
121
- const entries = [
122
- { type: 'message', message: { role: 'user', content: 'hello world' } },
123
- { type: 'message', message: { role: 'assistant', content: 'response' } },
124
- { type: 'message', message: { role: 'user', content: 'end' } },
125
- ];
126
- const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
127
- assert.ok(prompt.includes('User: hello world'));
128
- assert.ok(prompt.includes('Assistant: response'));
129
- });
130
-
131
- test('contains "None yet" when memory dir is empty', () => {
132
- const memoryDir = makeTempDir();
133
- const cleanup = () => rmSync(memoryDir, { recursive: true, force: true });
134
-
135
- try {
136
- const entries = [
137
- { type: 'message', message: { role: 'user', content: 'test1' } },
138
- { type: 'message', message: { role: 'assistant', content: 'test2' } },
139
- { type: 'message', message: { role: 'user', content: 'test3' } },
140
- ];
141
- const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
142
- assert.ok(prompt.includes('None yet'));
143
- } finally {
144
- cleanup();
145
- }
146
- });
147
-
148
- test('contains extraction rules (Save ONLY)', () => {
149
- const memoryDir = '/tmp/test-memory';
150
- const entries = [
151
- { type: 'message', message: { role: 'user', content: 'test1' } },
152
- { type: 'message', message: { role: 'assistant', content: 'test2' } },
153
- { type: 'message', message: { role: 'user', content: 'test3' } },
154
- ];
155
- const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
156
- assert.ok(prompt.includes('Save ONLY'));
157
- });
118
+ test('strips ANSI codes so session-end and cache-timer lines can be classified', () => {
119
+ const coloredSessionEnd = '\u001b[36m[agent] Session ended\u001b[0m';
120
+ const coloredCacheTimer = '\u001b[36m[phase] cache-timer\u001b[0m';
121
+
122
+ assert.equal(stripAnsiForAutoExtractLog(coloredSessionEnd), '[agent] Session ended');
123
+ assert.equal(stripAnsiForAutoExtractLog(coloredCacheTimer), '[phase] cache-timer');
124
+ });
125
+
126
+ test('classifies cache-timer lines as ignorable noise', () => {
127
+ assert.deepEqual(classifyAutoExtractLogLine('[phase] cache-timer'), {
128
+ stripped: '[phase] cache-timer',
129
+ keep: false,
130
+ completion: 'none',
131
+ completionReason: null,
132
+ });
133
+ });
134
+
135
+ test('classifies cache-timer lines with rendered values as ignorable noise', () => {
136
+ assert.deepEqual(classifyAutoExtractLogLine('[phase] cache-timer: ⏱ 0:05'), {
137
+ stripped: '[phase] cache-timer: 0:05',
138
+ keep: false,
139
+ completion: 'none',
140
+ completionReason: null,
141
+ });
142
+ });
143
+
144
+ test('classifies headless completion as a successful terminal signal', () => {
145
+ assert.deepEqual(classifyAutoExtractLogLine('[headless] Status: complete'), {
146
+ stripped: '[headless] Status: complete',
147
+ keep: true,
148
+ completion: 'success',
149
+ completionReason: 'headless_status_complete',
150
+ });
151
+ });
152
+
153
+ test('classifies non-complete headless statuses as failures', () => {
154
+ assert.deepEqual(classifyAutoExtractLogLine('[headless] Status: timeout'), {
155
+ stripped: '[headless] Status: timeout',
156
+ keep: true,
157
+ completion: 'failure',
158
+ completionReason: 'headless_status_timeout',
159
+ });
160
+ });
161
+
162
+ test('contains the memory directory path in output', () => {
163
+ const memoryDir = '/tmp/test-memory';
164
+ const entries = [
165
+ { type: 'message', message: { role: 'user', content: 'test1' } },
166
+ { type: 'message', message: { role: 'assistant', content: 'test2' } },
167
+ { type: 'message', message: { role: 'user', content: 'test3' } },
168
+ ];
169
+ const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
170
+ assert.ok(prompt.includes(memoryDir));
171
+ });
172
+
173
+ test('contains the transcript in output', () => {
174
+ const memoryDir = '/tmp/test-memory';
175
+ const entries = [
176
+ { type: 'message', message: { role: 'user', content: 'hello world' } },
177
+ { type: 'message', message: { role: 'assistant', content: 'response' } },
178
+ { type: 'message', message: { role: 'user', content: 'end' } },
179
+ ];
180
+ const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
181
+ assert.ok(prompt.includes('User: hello world'));
182
+ assert.ok(prompt.includes('Assistant: response'));
183
+ });
184
+
185
+ test('contains "None yet" when memory dir is empty', () => {
186
+ const memoryDir = makeTempDir();
187
+ const cleanup = () => rmSync(memoryDir, { recursive: true, force: true });
188
+
189
+ try {
190
+ const entries = [
191
+ { type: 'message', message: { role: 'user', content: 'test1' } },
192
+ { type: 'message', message: { role: 'assistant', content: 'test2' } },
193
+ { type: 'message', message: { role: 'user', content: 'test3' } },
194
+ ];
195
+ const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
196
+ assert.ok(prompt.includes('None yet'));
197
+ } finally {
198
+ cleanup();
199
+ }
200
+ });
201
+
202
+ test('contains extraction rules (Save ONLY)', () => {
203
+ const memoryDir = '/tmp/test-memory';
204
+ const entries = [
205
+ { type: 'message', message: { role: 'user', content: 'test1' } },
206
+ { type: 'message', message: { role: 'assistant', content: 'test2' } },
207
+ { type: 'message', message: { role: 'user', content: 'test3' } },
208
+ ];
209
+ const prompt = buildExtractionPrompt(memoryDir, buildTranscriptSummary(entries));
210
+ assert.ok(prompt.includes('Save ONLY'));
211
+ assert.ok(prompt.includes('raw code snippets'));
212
+ assert.ok(!prompt.includes('Do NOT save: code patterns, architecture'));
213
+ });
158
214
  });
@@ -0,0 +1,112 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
4
+ import { delimiter, join } from 'node:path'
5
+
6
+ const GSD_RTK_PATH_ENV = 'GSD_RTK_PATH'
7
+ const GSD_RTK_DISABLED_ENV = 'GSD_RTK_DISABLED'
8
+ const GSD_RTK_REWRITE_TIMEOUT_MS_ENV = 'GSD_RTK_REWRITE_TIMEOUT_MS'
9
+ const RTK_TELEMETRY_DISABLED_ENV = 'RTK_TELEMETRY_DISABLED'
10
+ const RTK_REWRITE_TIMEOUT_MS = 5_000
11
+
12
+ function isTruthy(value) {
13
+ if (!value) return false
14
+ const normalized = value.trim().toLowerCase()
15
+ return normalized === '1' || normalized === 'true' || normalized === 'yes'
16
+ }
17
+
18
+ function getRewriteTimeoutMs(env = process.env) {
19
+ const configured = Number.parseInt(env[GSD_RTK_REWRITE_TIMEOUT_MS_ENV] ?? '', 10)
20
+ if (Number.isFinite(configured) && configured > 0) return configured
21
+ return RTK_REWRITE_TIMEOUT_MS
22
+ }
23
+
24
+ export function isRtkEnabled(env = process.env) {
25
+ return !isTruthy(env[GSD_RTK_DISABLED_ENV])
26
+ }
27
+
28
+ export function buildRtkEnv(env = process.env) {
29
+ return {
30
+ ...env,
31
+ [RTK_TELEMETRY_DISABLED_ENV]: '1',
32
+ }
33
+ }
34
+
35
+ function getManagedRtkDir(env = process.env) {
36
+ return join(env.GSD_HOME || join(homedir(), '.lsd'), 'agent', 'bin')
37
+ }
38
+
39
+ function getRtkBinaryName(platform = process.platform) {
40
+ return platform === 'win32' ? 'rtk.exe' : 'rtk'
41
+ }
42
+
43
+ function getPathValue(env) {
44
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path')
45
+ return pathKey ? env[pathKey] : env.PATH
46
+ }
47
+
48
+ function resolvePathCandidates(pathValue) {
49
+ if (!pathValue) return []
50
+ return pathValue
51
+ .split(delimiter)
52
+ .map((part) => part.trim())
53
+ .filter(Boolean)
54
+ }
55
+
56
+ function resolveSystemRtkPath(pathValue, platform = process.platform) {
57
+ const candidates = platform === 'win32'
58
+ ? ['rtk.exe', 'rtk.cmd', 'rtk.bat', 'rtk']
59
+ : ['rtk']
60
+
61
+ for (const dir of resolvePathCandidates(pathValue)) {
62
+ for (const candidate of candidates) {
63
+ const fullPath = join(dir, candidate)
64
+ if (existsSync(fullPath)) return fullPath
65
+ }
66
+ }
67
+
68
+ return null
69
+ }
70
+
71
+ export function resolveRtkBinaryPath(options = {}) {
72
+ const env = options.env ?? process.env
73
+ const platform = options.platform ?? process.platform
74
+
75
+ const explicitPath = options.binaryPath ?? env[GSD_RTK_PATH_ENV]
76
+ if (explicitPath && existsSync(explicitPath)) return explicitPath
77
+
78
+ const managedDir = getManagedRtkDir(env)
79
+ const managedPath = join(managedDir, getRtkBinaryName(platform))
80
+ if (existsSync(managedPath)) return managedPath
81
+ if (platform === 'win32') {
82
+ const managedCmd = join(managedDir, 'rtk.cmd')
83
+ if (existsSync(managedCmd)) return managedCmd
84
+ }
85
+
86
+ return resolveSystemRtkPath(options.pathValue ?? getPathValue(env), platform)
87
+ }
88
+
89
+ export function rewriteCommandWithRtk(command, options = {}) {
90
+ const env = options.env ?? process.env
91
+
92
+ if (!command.trim()) return command
93
+ if (!isRtkEnabled(env)) return command
94
+
95
+ const binaryPath = options.binaryPath ?? resolveRtkBinaryPath({ env })
96
+ if (!binaryPath) return command
97
+
98
+ const run = options.spawnSyncImpl ?? spawnSync
99
+ const result = run(binaryPath, ['rewrite', command], {
100
+ encoding: 'utf-8',
101
+ env: buildRtkEnv(env),
102
+ stdio: ['ignore', 'pipe', 'ignore'],
103
+ timeout: getRewriteTimeoutMs(env),
104
+ shell: /\.(cmd|bat)$/i.test(binaryPath),
105
+ })
106
+
107
+ if (result.error) return command
108
+ if (result.status !== 0 && result.status !== 3) return command
109
+
110
+ const rewritten = (result.stdout ?? '').trimEnd()
111
+ return rewritten || command
112
+ }