omegon 0.6.8 → 0.6.10

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 (133) hide show
  1. package/extensions/cleave/dispatcher.ts +32 -6
  2. package/extensions/cleave/index.ts +21 -3
  3. package/extensions/cleave/subprocess-tracker.ts +72 -0
  4. package/extensions/project-memory/extraction-v2.ts +206 -434
  5. package/extensions/project-memory/index.ts +89 -182
  6. package/extensions/project-memory/llm-direct.ts +260 -0
  7. package/extensions/project-memory/types.ts +3 -3
  8. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/README.md +3 -0
  9. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/clipboard.linux-x64-gnu.node +0 -0
  10. package/node_modules/@mariozechner/{clipboard-darwin-arm64 → clipboard-linux-x64-gnu}/package.json +8 -5
  11. package/node_modules/@styrene-lab/pi-ai/dist/cli.js +0 -0
  12. package/node_modules/@types/node/README.md +2 -2
  13. package/node_modules/@types/node/assert/strict.d.ts +5 -11
  14. package/node_modules/@types/node/assert.d.ts +11 -171
  15. package/node_modules/@types/node/async_hooks.d.ts +8 -8
  16. package/node_modules/@types/node/buffer.buffer.d.ts +1 -7
  17. package/node_modules/@types/node/buffer.d.ts +44 -168
  18. package/node_modules/@types/node/child_process.d.ts +21 -64
  19. package/node_modules/@types/node/cluster.d.ts +240 -332
  20. package/node_modules/@types/node/console.d.ts +49 -351
  21. package/node_modules/@types/node/constants.d.ts +3 -4
  22. package/node_modules/@types/node/crypto.d.ts +287 -1639
  23. package/node_modules/@types/node/dgram.d.ts +15 -51
  24. package/node_modules/@types/node/diagnostics_channel.d.ts +4 -4
  25. package/node_modules/@types/node/dns/promises.d.ts +3 -3
  26. package/node_modules/@types/node/dns.d.ts +131 -132
  27. package/node_modules/@types/node/domain.d.ts +13 -17
  28. package/node_modules/@types/node/events.d.ts +866 -795
  29. package/node_modules/@types/node/fs/promises.d.ts +7 -8
  30. package/node_modules/@types/node/fs.d.ts +419 -455
  31. package/node_modules/@types/node/globals.d.ts +6 -26
  32. package/node_modules/@types/node/globals.typedarray.d.ts +60 -0
  33. package/node_modules/@types/node/http.d.ts +310 -276
  34. package/node_modules/@types/node/http2.d.ts +528 -804
  35. package/node_modules/@types/node/https.d.ts +63 -243
  36. package/node_modules/@types/node/index.d.ts +15 -1
  37. package/node_modules/@types/node/inspector/promises.d.ts +41 -0
  38. package/node_modules/@types/node/inspector.d.ts +42 -50
  39. package/node_modules/@types/node/inspector.generated.d.ts +572 -417
  40. package/node_modules/@types/node/module.d.ts +47 -195
  41. package/node_modules/@types/node/net.d.ts +68 -189
  42. package/node_modules/@types/node/os.d.ts +6 -6
  43. package/node_modules/@types/node/package.json +3 -3
  44. package/node_modules/@types/node/path/posix.d.ts +8 -0
  45. package/node_modules/@types/node/path/win32.d.ts +8 -0
  46. package/node_modules/@types/node/path.d.ts +120 -133
  47. package/node_modules/@types/node/perf_hooks.d.ts +244 -643
  48. package/node_modules/@types/node/process.d.ts +219 -127
  49. package/node_modules/@types/node/punycode.d.ts +5 -5
  50. package/node_modules/@types/node/querystring.d.ts +4 -4
  51. package/node_modules/@types/node/quic.d.ts +910 -0
  52. package/node_modules/@types/node/readline/promises.d.ts +3 -3
  53. package/node_modules/@types/node/readline.d.ts +68 -120
  54. package/node_modules/@types/node/repl.d.ts +75 -98
  55. package/node_modules/@types/node/sea.d.ts +1 -1
  56. package/node_modules/@types/node/sqlite.d.ts +137 -30
  57. package/node_modules/@types/node/stream/consumers.d.ts +10 -10
  58. package/node_modules/@types/node/stream/promises.d.ts +136 -15
  59. package/node_modules/@types/node/stream/web.d.ts +176 -453
  60. package/node_modules/@types/node/stream.d.ts +565 -478
  61. package/node_modules/@types/node/string_decoder.d.ts +4 -4
  62. package/node_modules/@types/node/test/reporters.d.ts +96 -0
  63. package/node_modules/@types/node/test.d.ts +134 -199
  64. package/node_modules/@types/node/timers/promises.d.ts +4 -4
  65. package/node_modules/@types/node/timers.d.ts +4 -130
  66. package/node_modules/@types/node/tls.d.ts +109 -179
  67. package/node_modules/@types/node/trace_events.d.ts +9 -9
  68. package/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +1 -7
  69. package/node_modules/@types/node/ts5.6/index.d.ts +15 -1
  70. package/node_modules/@types/node/ts5.7/index.d.ts +15 -1
  71. package/node_modules/@types/node/tty.d.ts +58 -16
  72. package/node_modules/@types/node/url.d.ts +78 -594
  73. package/node_modules/@types/node/util/types.d.ts +558 -0
  74. package/node_modules/@types/node/util.d.ts +150 -796
  75. package/node_modules/@types/node/v8.d.ts +31 -21
  76. package/node_modules/@types/node/vm.d.ts +43 -30
  77. package/node_modules/@types/node/wasi.d.ts +4 -4
  78. package/node_modules/@types/node/web-globals/abortcontroller.d.ts +27 -2
  79. package/node_modules/@types/node/web-globals/blob.d.ts +23 -0
  80. package/node_modules/@types/node/web-globals/console.d.ts +9 -0
  81. package/node_modules/@types/node/web-globals/crypto.d.ts +7 -0
  82. package/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
  83. package/node_modules/@types/node/web-globals/events.d.ts +9 -0
  84. package/node_modules/@types/node/web-globals/fetch.d.ts +7 -0
  85. package/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
  86. package/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
  87. package/node_modules/@types/node/web-globals/performance.d.ts +45 -0
  88. package/node_modules/@types/node/web-globals/streams.d.ts +93 -0
  89. package/node_modules/@types/node/web-globals/timers.d.ts +44 -0
  90. package/node_modules/@types/node/web-globals/url.d.ts +24 -0
  91. package/node_modules/@types/node/worker_threads.d.ts +186 -363
  92. package/node_modules/@types/node/zlib.d.ts +8 -71
  93. package/node_modules/cliui/CHANGELOG.md +121 -0
  94. package/node_modules/color-convert/CHANGELOG.md +54 -0
  95. package/node_modules/esprima/ChangeLog +235 -0
  96. package/node_modules/mz/HISTORY.md +66 -0
  97. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  98. package/node_modules/source-map/CHANGELOG.md +301 -0
  99. package/node_modules/thenify/History.md +11 -0
  100. package/node_modules/thenify-all/History.md +11 -0
  101. package/node_modules/undici-types/agent.d.ts +1 -1
  102. package/node_modules/undici-types/api.d.ts +2 -2
  103. package/node_modules/undici-types/balanced-pool.d.ts +2 -1
  104. package/node_modules/undici-types/cache-interceptor.d.ts +1 -0
  105. package/node_modules/undici-types/client.d.ts +1 -1
  106. package/node_modules/undici-types/connector.d.ts +2 -2
  107. package/node_modules/undici-types/diagnostics-channel.d.ts +2 -2
  108. package/node_modules/undici-types/dispatcher.d.ts +12 -12
  109. package/node_modules/undici-types/fetch.d.ts +4 -4
  110. package/node_modules/undici-types/formdata.d.ts +1 -1
  111. package/node_modules/undici-types/h2c-client.d.ts +1 -1
  112. package/node_modules/undici-types/index.d.ts +9 -1
  113. package/node_modules/undici-types/interceptors.d.ts +36 -2
  114. package/node_modules/undici-types/package.json +1 -1
  115. package/node_modules/undici-types/pool.d.ts +1 -1
  116. package/node_modules/undici-types/readable.d.ts +2 -2
  117. package/node_modules/undici-types/round-robin-pool.d.ts +41 -0
  118. package/node_modules/undici-types/websocket.d.ts +9 -9
  119. package/node_modules/which/CHANGELOG.md +166 -0
  120. package/node_modules/y18n/CHANGELOG.md +100 -0
  121. package/node_modules/yargs/CHANGELOG.md +88 -0
  122. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  123. package/package.json +5 -5
  124. package/extensions/render/composition/package-lock.json +0 -534
  125. package/node_modules/@mariozechner/clipboard-darwin-arm64/README.md +0 -3
  126. package/node_modules/@mariozechner/clipboard-darwin-arm64/clipboard.darwin-arm64.node +0 -0
  127. package/node_modules/@mariozechner/clipboard-darwin-universal/README.md +0 -3
  128. package/node_modules/@mariozechner/clipboard-darwin-universal/clipboard.darwin-universal.node +0 -0
  129. package/node_modules/@mariozechner/clipboard-darwin-universal/package.json +0 -19
  130. /package/node_modules/{p-retry/node_modules/@types → @types}/retry/LICENSE +0 -0
  131. /package/node_modules/{p-retry/node_modules/@types → @types}/retry/README.md +0 -0
  132. /package/node_modules/{p-retry/node_modules/@types → @types}/retry/index.d.ts +0 -0
  133. /package/node_modules/{p-retry/node_modules/@types → @types}/retry/package.json +0 -0
@@ -67,7 +67,7 @@ import {
67
67
  createTriggerState,
68
68
  shouldExtract,
69
69
  } from "./triggers.ts";
70
- import { runExtractionV2, runGlobalExtraction, killActiveExtraction, killAllSubprocesses, generateEpisode, generateEpisodeDirect, generateEpisodeWithFallback, buildTemplateEpisode, runSectionPruningPass, type SessionTelemetry } from "./extraction-v2.ts";
70
+ import { runExtractionV2, runGlobalExtraction, killActiveExtraction, killAllSubprocesses, generateEpisodeDirect, generateEpisodeWithFallback, buildTemplateEpisode, runSectionPruningPass, type SessionTelemetry } from "./extraction-v2.ts";
71
71
  import { migrateToFactStore, needsMigration, markMigrated } from "./migration.ts";
72
72
  import { SECTIONS } from "./template.ts";
73
73
  import { serializeConversation, convertToLlm } from "@styrene-lab/pi-coding-agent";
@@ -156,23 +156,17 @@ Summarize the prefix to provide context for the retained suffix:
156
156
  Be concise. Focus on what's needed to understand the kept suffix.`;
157
157
 
158
158
  // ---------------------------------------------------------------------------
159
- // Ollama helpers for local-model compaction fallback
159
+ // Local model discovery for compaction fallback
160
160
  // ---------------------------------------------------------------------------
161
161
 
162
- const OLLAMA_URL = () => process.env.OLLAMA_HOST || process.env.LOCAL_INFERENCE_URL || "http://localhost:11434";
163
-
164
- /** Embedding model names that must not be used for chat completions */
165
- const EMBEDDING_MODEL_PATTERN = /embed|embedding/i;
166
-
167
- /** Preferred models for summarization, in priority order */
168
- // Canonical preference list + family prefix catch-alls from shared registry.
169
- // Specific tags first (largest/best wins via startsWith); families catch any
170
- // installed variant not explicitly listed (e.g. qwen3:14b-q4_k_m).
171
- // Edit extensions/lib/local-models.ts to update model preferences.
172
162
  import {
173
163
  PREFERRED_ORDER as LOCAL_MODELS_ORDER,
174
164
  PREFERRED_FAMILIES,
175
165
  } from "../lib/local-models.ts";
166
+ import { chatDirect } from "./llm-direct.ts";
167
+
168
+ const OLLAMA_URL = () => process.env.OLLAMA_HOST || process.env.LOCAL_INFERENCE_URL || "http://localhost:11434";
169
+ const EMBEDDING_MODEL_PATTERN = /embed|embedding/i;
176
170
  const PREFERRED_CHAT_MODELS = [...LOCAL_MODELS_ORDER, ...PREFERRED_FAMILIES];
177
171
 
178
172
  /**
@@ -187,49 +181,16 @@ async function discoverLocalChatModel(): Promise<string | null> {
187
181
  const available = (data.data?.map((m: { id: string }) => m.id) ?? [])
188
182
  .filter((id: string) => !EMBEDDING_MODEL_PATTERN.test(id));
189
183
  if (available.length === 0) return null;
190
-
191
- // Try preferred models first (startsWith for exact matching)
192
184
  for (const pref of PREFERRED_CHAT_MODELS) {
193
185
  const found = available.find((id: string) => id.startsWith(pref));
194
186
  if (found) return found;
195
187
  }
196
- return available[0]; // Any non-embedding model
188
+ return available[0];
197
189
  } catch {
198
190
  return null;
199
191
  }
200
192
  }
201
193
 
202
- /**
203
- * Send a chat completion to Ollama. Returns trimmed content or null.
204
- */
205
- async function ollamaChat(
206
- model: string,
207
- systemPrompt: string,
208
- userPrompt: string,
209
- opts: { maxTokens?: number; signal?: AbortSignal },
210
- ): Promise<string | null> {
211
- const resp = await fetch(`${OLLAMA_URL()}/v1/chat/completions`, {
212
- method: "POST",
213
- headers: { "Content-Type": "application/json" },
214
- body: JSON.stringify({
215
- model,
216
- messages: [
217
- { role: "system", content: systemPrompt },
218
- { role: "user", content: userPrompt },
219
- ],
220
- max_tokens: opts.maxTokens ?? 4096,
221
- temperature: 0.3,
222
- // Request a reasonable context window for the local model
223
- num_ctx: 32768,
224
- }),
225
- signal: opts.signal,
226
- });
227
- if (!resp.ok) return null;
228
- const data = await resp.json() as { choices?: Array<{ message?: { content?: string } }> };
229
- const content = data.choices?.[0]?.message?.content?.trim();
230
- return content || null;
231
- }
232
-
233
194
  /**
234
195
  * Format file operations for appending to compaction summary.
235
196
  * Mirrors pi core's formatFileOperations but inlined since it's not exported.
@@ -362,6 +323,8 @@ export default function (pi: ExtensionAPI) {
362
323
  let exitEpisodeDone = false;
363
324
  /** Pending embed promises — tracked so shutdown can await them before DB close */
364
325
  const pendingEmbeds = new Set<Promise<unknown>>();
326
+ /** Concurrency guard — prevents pruning, indexing, and extraction from overlapping */
327
+ let backgroundTaskRunning = false;
365
328
  let consecutiveExtractionFailures = 0;
366
329
  let memoryDir = "";
367
330
 
@@ -770,14 +733,11 @@ export default function (pi: ExtensionAPI) {
770
733
  }
771
734
 
772
735
  // --- Decay sweep + per-section pruning (background, non-blocking) ---
773
- // 1. Archive facts that have decayed below their profile's minimum confidence.
774
- // Converts passive decay (lower score on read) into active archival.
775
- // Without this, decayed facts accumulate forever as active but invisible.
776
- // 2. When any section exceeds 60 facts, run a targeted LLM archival pass
777
- // to bring it back under the ceiling. This prevents monotonic accumulation.
778
- // Runs fire-and-forget so it doesn't block session startup.
736
+ // Guarded by backgroundTaskRunning to prevent overlap with indexing/extraction.
779
737
  if (store) {
780
738
  (async () => {
739
+ if (backgroundTaskRunning) return;
740
+ backgroundTaskRunning = true;
781
741
  try {
782
742
  const mind = activeMind();
783
743
 
@@ -792,38 +752,28 @@ export default function (pi: ExtensionAPI) {
792
752
  const sectionCounts = store!.getSectionCounts(mind);
793
753
  for (const [section, count] of sectionCounts) {
794
754
  if (count <= SECTION_CEILING) continue;
795
- // Skip Recent Work — decay sweep handles it (fast decay, no LLM needed)
796
755
  if (section === "Recent Work") continue;
797
756
 
798
- // Single query — all subsequent phases operate on this in-memory list.
799
- // getFactsBySection already computes confidence via computeConfidence
800
- // (canonical formula from core.ts), so sorted.confidence is correct.
801
757
  const facts = store!.getFactsBySection(mind, section);
802
758
  const excess = count - SECTION_CEILING;
803
759
  let archived = 0;
804
760
  const archivedIds = new Set<string>();
805
761
 
806
- // Phase 2a: Deterministic cull — archive only facts with confidence
807
- // below a hard floor. This protects semantically important facts that
808
- // happen to have low confidence purely due to age. Facts above the
809
- // floor are sent to the LLM for judgment (phase 2b).
762
+ // Phase 2a: Deterministic cull — below confidence floor
810
763
  const CONFIDENCE_FLOOR = 0.25;
811
764
  const sorted = [...facts].sort((a, b) => a.confidence - b.confidence);
812
765
  for (const f of sorted) {
813
- if (archived >= excess) break; // enough culled
814
- if (f.confidence > CONFIDENCE_FLOOR) break; // above floor → LLM decides
766
+ if (archived >= excess) break;
767
+ if (f.confidence > CONFIDENCE_FLOOR) break;
815
768
  store!.archiveFact(f.id);
816
769
  archivedIds.add(f.id);
817
770
  archived++;
818
771
  }
819
772
 
820
- // Phase 2b: LLM-assisted pruning for the remaining excess.
821
- // Filter the in-memory array (no re-query) and send a manageable
822
- // batch (up to 80 facts) to the LLM for nuanced review.
773
+ // Phase 2b: LLM-assisted pruning for remaining excess
823
774
  const remaining = facts.filter(f => !archivedIds.has(f.id));
824
775
  const stillExcess = remaining.length - SECTION_CEILING;
825
776
  if (stillExcess > 0) {
826
- // Send the bottom 80 by confidence for LLM review
827
777
  const batch = [...remaining]
828
778
  .sort((a, b) => a.confidence - b.confidence)
829
779
  .slice(0, Math.min(80, remaining.length));
@@ -845,96 +795,14 @@ export default function (pi: ExtensionAPI) {
845
795
  }
846
796
  } catch {
847
797
  // Best effort — don't interrupt session
798
+ } finally {
799
+ backgroundTaskRunning = false;
848
800
  }
849
801
  })();
850
802
  }
851
803
 
852
- // --- Proactive startup injection (session-continuity) ---
853
- // Inject three layers before the user's first message so continuation
854
- // questions work without waiting for semantic retrieval.
855
- // Layer 1: last 1 session episode (most recent — use memory_episodes for more)
856
- // Layer 2: top-15 recently-reinforced facts (recency window, cross-section)
857
- // Layer 3: Decisions + Constraints + Known Issues always; Architecture capped at 10
858
- //
859
- // Architecture is the largest section by fact count. Loading all Architecture
860
- // facts unconditionally blows context on large projects. The top-10 by recency
861
- // covers active concerns; older facts are retrievable via memory_recall.
862
- //
863
- // This runs asynchronously so it doesn't block the TUI from appearing.
864
- // The payload is injected as a pre-prompt system message on the first turn.
865
- if (store) {
866
- try {
867
- const mind = activeMind();
868
- const recentEpisodes = store.getEpisodes(mind, 1);
869
- const allFacts = store.getActiveFacts(mind);
870
-
871
- // Recency window: top 15 by last_reinforced (any section)
872
- const recentFacts = [...allFacts]
873
- .sort((a, b) => new Date(b.last_reinforced).getTime() - new Date(a.last_reinforced).getTime())
874
- .slice(0, 15);
875
-
876
- // Core structural facts: Decisions + Constraints + Known Issues always loaded.
877
- // Architecture capped at 10 most-recently-reinforced (largest section by volume).
878
- const coreFacts = allFacts.filter(f =>
879
- f.section === "Decisions" || f.section === "Constraints" || f.section === "Known Issues"
880
- );
881
- const archFacts = [...allFacts]
882
- .filter(f => f.section === "Architecture")
883
- .sort((a, b) => new Date(b.last_reinforced).getTime() - new Date(a.last_reinforced).getTime())
884
- .slice(0, 10);
885
-
886
- // Merge: recent episodes + recency window + core sections (deduplicated)
887
- // Budget-capped to prevent context overflow on large fact stores.
888
- const STARTUP_MAX_CHARS = 12_000; // ~3K tokens — leaves room for system prompt + design-tree
889
- let startupChars = 0;
890
- const startupFactIds = new Set<string>();
891
- const startupFacts: typeof allFacts = [];
892
- for (const f of [...coreFacts, ...archFacts, ...recentFacts]) {
893
- if (startupFactIds.has(f.id)) continue;
894
- const cost = f.content.length + 20;
895
- if (startupChars + cost > STARTUP_MAX_CHARS) break;
896
- startupFacts.push(f);
897
- startupFactIds.add(f.id);
898
- startupChars += cost;
899
- }
900
-
901
- if (recentEpisodes.length > 0 || startupFacts.length > 0) {
902
- const lines: string[] = ["<!-- Startup Context — recent sessions and structural memory -->", ""];
903
-
904
- if (recentEpisodes.length > 0) {
905
- lines.push("## Recent Sessions");
906
- lines.push("_Episodic memory — what happened and why_");
907
- lines.push("");
908
- for (const ep of recentEpisodes) {
909
- lines.push(`### ${ep.date}: ${ep.title}`);
910
- lines.push(ep.narrative);
911
- lines.push("");
912
- }
913
- }
914
-
915
- if (startupFacts.length > 0) {
916
- const factsBySection = new Map<string, typeof startupFacts>();
917
- for (const f of startupFacts) {
918
- const sec = factsBySection.get(f.section) ?? [];
919
- sec.push(f);
920
- factsBySection.set(f.section, sec);
921
- }
922
- for (const [section, facts] of factsBySection) {
923
- lines.push(`## ${section}`);
924
- lines.push("");
925
- for (const f of facts) {
926
- lines.push(`- ${f.content}`);
927
- }
928
- lines.push("");
929
- }
930
- }
931
-
932
- startupInjectionPayload = lines.join("\n");
933
- }
934
- } catch {
935
- // Best effort — don't block startup
936
- }
937
- }
804
+ // Startup injection payload is built lazily in before_agent_start (first turn)
805
+ // to avoid loading all facts twice (once here, once in the injection pipeline).
938
806
 
939
807
  updateStatus(ctx);
940
808
  });
@@ -952,11 +820,9 @@ export default function (pi: ExtensionAPI) {
952
820
  customInstructions: string | undefined,
953
821
  signal: AbortSignal
954
822
  ): Promise<{ summary: string; details: any } | null> {
955
- // Build summarization prompt (same as existing logic)
956
823
  const llmMessages = convertToLlm(prep.messagesToSummarize);
957
824
  let conversationText = sanitizeCompactionText(serializeConversation(llmMessages));
958
825
 
959
- // Truncate to ~60k chars (~15k tokens) to fit local model context windows
960
826
  const MAX_CONVERSATION_CHARS = 60_000;
961
827
  if (conversationText.length > MAX_CONVERSATION_CHARS) {
962
828
  conversationText = "...[earlier conversation truncated]...\n\n"
@@ -968,7 +834,6 @@ export default function (pi: ExtensionAPI) {
968
834
  promptText += `<previous-summary>\n${prep.previousSummary}\n</previous-summary>\n\n`;
969
835
  }
970
836
 
971
- // Inject project memory context for richer summaries
972
837
  if (store) {
973
838
  const mind = activeMind();
974
839
  const facts = store.getActiveFacts(mind);
@@ -982,7 +847,7 @@ export default function (pi: ExtensionAPI) {
982
847
  const basePrompt = prep.previousSummary ? COMPACTION_UPDATE_PROMPT : COMPACTION_INITIAL_PROMPT;
983
848
  promptText += customInstructions ? `${basePrompt}\n\nAdditional focus: ${customInstructions}` : basePrompt;
984
849
 
985
- // Handle split turn prefix if needed
850
+ // Handle split turn prefix
986
851
  let turnPrefixSummary = "";
987
852
  if (prep.isSplitTurn && prep.turnPrefixMessages.length > 0) {
988
853
  const prefixMessages = convertToLlm(prep.turnPrefixMessages);
@@ -991,36 +856,45 @@ export default function (pi: ExtensionAPI) {
991
856
  prefixText = "...[truncated]...\n\n" + prefixText.slice(-MAX_CONVERSATION_CHARS);
992
857
  }
993
858
  const prefixPrompt = `<conversation>\n${prefixText}\n</conversation>\n\n${COMPACTION_TURN_PREFIX_PROMPT}`;
994
-
995
859
  try {
996
- const prefixResp = await ollamaChat(localModel, COMPACTION_SYSTEM_PROMPT, prefixPrompt, {
997
- maxTokens: 2048, signal,
860
+ const result = await chatDirect({
861
+ model: localModel,
862
+ systemPrompt: COMPACTION_SYSTEM_PROMPT,
863
+ userMessage: prefixPrompt,
864
+ maxTokens: 2048,
865
+ timeout: 30_000,
866
+ signal,
998
867
  });
999
- if (prefixResp) turnPrefixSummary = prefixResp;
868
+ turnPrefixSummary = result.content;
1000
869
  } catch {
1001
870
  // If turn prefix fails, continue without it
1002
871
  }
1003
872
  }
1004
873
 
1005
- // Generate main summary
1006
- const summary = await ollamaChat(localModel, COMPACTION_SYSTEM_PROMPT, promptText, {
1007
- maxTokens: 4096, signal,
1008
- });
874
+ // Generate main summary via direct HTTP (no subprocess)
875
+ try {
876
+ const result = await chatDirect({
877
+ model: localModel,
878
+ systemPrompt: COMPACTION_SYSTEM_PROMPT,
879
+ userMessage: promptText,
880
+ maxTokens: 4096,
881
+ timeout: 45_000,
882
+ signal,
883
+ });
1009
884
 
1010
- if (!summary) return null;
885
+ let fullSummary = result.content;
886
+ if (turnPrefixSummary) {
887
+ fullSummary += `\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixSummary}`;
888
+ }
889
+ fullSummary += formatFileOps(prep.fileOps);
1011
890
 
1012
- let fullSummary = summary;
1013
- if (turnPrefixSummary) {
1014
- fullSummary += `\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixSummary}`;
891
+ return {
892
+ summary: fullSummary,
893
+ details: buildFileDetails(prep.fileOps),
894
+ };
895
+ } catch {
896
+ return null;
1015
897
  }
1016
-
1017
- // Append file operations
1018
- fullSummary += formatFileOps(prep.fileOps);
1019
-
1020
- return {
1021
- summary: fullSummary,
1022
- details: buildFileDetails(prep.fileOps),
1023
- };
1024
898
  }
1025
899
 
1026
900
  /**
@@ -1671,11 +1545,42 @@ export default function (pi: ExtensionAPI) {
1671
1545
  }
1672
1546
  }
1673
1547
 
1674
- // Proactive startup payload — prepend on firstTurn if available.
1675
- const startupSection = startupInjectionPayload
1676
- ? `\n\n${startupInjectionPayload}`
1677
- : "";
1678
- startupInjectionPayload = null; // consume once
1548
+ // Proactive startup payload — built lazily on firstTurn from allFacts (already loaded above).
1549
+ // Avoids the old pattern of loading all facts in session_start AND again here.
1550
+ let startupSection = "";
1551
+ if (startupInjectionPayload) {
1552
+ startupSection = `\n\n${startupInjectionPayload}`;
1553
+ startupInjectionPayload = null;
1554
+ } else if (!postCompaction) {
1555
+ // First turn: build startup context from allFacts + episodes (already in memory)
1556
+ try {
1557
+ const recentEpisodes = store.getEpisodes(mind, 1);
1558
+ const lines: string[] = ["<!-- Startup Context — recent sessions and structural memory -->", ""];
1559
+ if (recentEpisodes.length > 0) {
1560
+ lines.push("## Recent Sessions");
1561
+ lines.push("_Episodic memory — what happened and why_");
1562
+ lines.push("");
1563
+ for (const ep of recentEpisodes) {
1564
+ lines.push(`### ${ep.date}: ${ep.title}`);
1565
+ lines.push(ep.narrative);
1566
+ lines.push("");
1567
+ }
1568
+ }
1569
+ // Decisions section from allFacts (already sorted/injected above — add any not yet included)
1570
+ const decisionsList = allFacts.filter(f => f.section === "Decisions" && !injectedIds.has(f.id)).slice(0, 5);
1571
+ if (decisionsList.length > 0) {
1572
+ lines.push("## Decisions");
1573
+ lines.push("");
1574
+ for (const f of decisionsList) lines.push(`- ${f.content}`);
1575
+ lines.push("");
1576
+ }
1577
+ if (lines.length > 2) {
1578
+ startupSection = `\n\n${lines.join("\n")}`;
1579
+ }
1580
+ } catch {
1581
+ // Best effort
1582
+ }
1583
+ }
1679
1584
 
1680
1585
  const budgetNote = currentChars >= MAX_MEMORY_CHARS
1681
1586
  ? ` (budget-capped at ~${Math.round(MAX_MEMORY_CHARS / 1000)}K chars)`
@@ -1855,8 +1760,9 @@ export default function (pi: ExtensionAPI) {
1855
1760
 
1856
1761
  if (shouldExtract(triggerState, usage.tokens ?? 0, config, consecutiveExtractionFailures)) {
1857
1762
  activeExtractionPromise = (async () => {
1858
- if (!store || triggerState.isRunning) return;
1763
+ if (!store || triggerState.isRunning || backgroundTaskRunning) return;
1859
1764
  triggerState.isRunning = true;
1765
+ backgroundTaskRunning = true;
1860
1766
  try {
1861
1767
  await runExtractionCycle(ctx, config);
1862
1768
  const usage = ctx.getContextUsage();
@@ -1869,6 +1775,7 @@ export default function (pi: ExtensionAPI) {
1869
1775
  consecutiveExtractionFailures++;
1870
1776
  } finally {
1871
1777
  triggerState.isRunning = false;
1778
+ backgroundTaskRunning = false;
1872
1779
  }
1873
1780
  })();
1874
1781
  activeExtractionPromise.catch(() => {}).finally(() => { activeExtractionPromise = null; });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Direct HTTP LLM chat — zero subprocess overhead.
3
+ *
4
+ * Replaces spawnExtraction() which launched a full Omegon runtime just to
5
+ * make a single chat completion. Supports Anthropic, OpenAI, and Ollama
6
+ * APIs directly via fetch().
7
+ *
8
+ * Provider auto-detection: resolves from model name prefix + available API keys.
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ChatRequest {
16
+ model: string;
17
+ systemPrompt: string;
18
+ userMessage: string;
19
+ maxTokens?: number;
20
+ temperature?: number;
21
+ timeout?: number;
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ export interface ChatResult {
26
+ content: string;
27
+ provider: "anthropic" | "openai" | "ollama";
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Provider detection
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const ANTHROPIC_PREFIXES = ["claude-"];
35
+ const OPENAI_PREFIXES = ["gpt-", "o1-", "o3-", "o4-"];
36
+
37
+ type Provider = "anthropic" | "openai" | "ollama";
38
+
39
+ function detectProvider(model: string): Provider {
40
+ for (const p of ANTHROPIC_PREFIXES) {
41
+ if (model.startsWith(p)) return "anthropic";
42
+ }
43
+ for (const p of OPENAI_PREFIXES) {
44
+ if (model.startsWith(p)) return "openai";
45
+ }
46
+ // Everything else assumed local/Ollama
47
+ return "ollama";
48
+ }
49
+
50
+ function getApiKey(provider: Provider): string | null {
51
+ switch (provider) {
52
+ case "anthropic":
53
+ return process.env.ANTHROPIC_API_KEY?.trim() || null;
54
+ case "openai":
55
+ return process.env.OPENAI_API_KEY?.trim() || null;
56
+ case "ollama":
57
+ return null; // No key needed
58
+ }
59
+ }
60
+
61
+ function getBaseUrl(provider: Provider): string {
62
+ switch (provider) {
63
+ case "anthropic":
64
+ return process.env.ANTHROPIC_BASE_URL?.trim() || "https://api.anthropic.com";
65
+ case "openai":
66
+ return process.env.OPENAI_BASE_URL?.trim() || "https://api.openai.com";
67
+ case "ollama":
68
+ return process.env.OLLAMA_HOST?.trim() || process.env.LOCAL_INFERENCE_URL?.trim() || "http://localhost:11434";
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Provider-specific chat implementations
74
+ // ---------------------------------------------------------------------------
75
+
76
+ async function chatAnthropic(
77
+ baseUrl: string,
78
+ apiKey: string,
79
+ req: ChatRequest,
80
+ signal: AbortSignal,
81
+ ): Promise<string> {
82
+ const resp = await fetch(`${baseUrl}/v1/messages`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "x-api-key": apiKey,
87
+ "anthropic-version": "2023-06-01",
88
+ },
89
+ body: JSON.stringify({
90
+ model: req.model,
91
+ max_tokens: req.maxTokens ?? 2048,
92
+ temperature: req.temperature ?? 0.2,
93
+ system: req.systemPrompt,
94
+ messages: [{ role: "user", content: req.userMessage }],
95
+ }),
96
+ signal,
97
+ });
98
+
99
+ if (!resp.ok) {
100
+ const body = await resp.text().catch(() => "");
101
+ throw new Error(`Anthropic API ${resp.status}: ${body.slice(0, 200)}`);
102
+ }
103
+
104
+ const data = await resp.json() as {
105
+ content?: Array<{ type: string; text?: string }>;
106
+ };
107
+ const text = data.content?.find(b => b.type === "text")?.text?.trim();
108
+ if (!text) throw new Error("Anthropic returned empty content");
109
+ return text;
110
+ }
111
+
112
+ async function chatOpenAI(
113
+ baseUrl: string,
114
+ apiKey: string,
115
+ req: ChatRequest,
116
+ signal: AbortSignal,
117
+ ): Promise<string> {
118
+ const resp = await fetch(`${baseUrl}/v1/chat/completions`, {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ "Authorization": `Bearer ${apiKey}`,
123
+ },
124
+ body: JSON.stringify({
125
+ model: req.model,
126
+ max_tokens: req.maxTokens ?? 2048,
127
+ temperature: req.temperature ?? 0.2,
128
+ messages: [
129
+ { role: "system", content: req.systemPrompt },
130
+ { role: "user", content: req.userMessage },
131
+ ],
132
+ }),
133
+ signal,
134
+ });
135
+
136
+ if (!resp.ok) {
137
+ const body = await resp.text().catch(() => "");
138
+ throw new Error(`OpenAI API ${resp.status}: ${body.slice(0, 200)}`);
139
+ }
140
+
141
+ const data = await resp.json() as {
142
+ choices?: Array<{ message?: { content?: string } }>;
143
+ };
144
+ const text = data.choices?.[0]?.message?.content?.trim();
145
+ if (!text) throw new Error("OpenAI returned empty content");
146
+ return text;
147
+ }
148
+
149
+ async function chatOllama(
150
+ baseUrl: string,
151
+ req: ChatRequest,
152
+ signal: AbortSignal,
153
+ ): Promise<string> {
154
+ const resp = await fetch(`${baseUrl}/api/chat`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({
158
+ model: req.model,
159
+ stream: false,
160
+ options: {
161
+ temperature: req.temperature ?? 0.2,
162
+ num_predict: req.maxTokens ?? 2048,
163
+ num_ctx: 32768,
164
+ },
165
+ messages: [
166
+ { role: "system", content: req.systemPrompt },
167
+ { role: "user", content: req.userMessage },
168
+ ],
169
+ }),
170
+ signal,
171
+ });
172
+
173
+ if (!resp.ok) {
174
+ const body = await resp.text().catch(() => "");
175
+ throw new Error(`Ollama API ${resp.status}: ${body.slice(0, 200)}`);
176
+ }
177
+
178
+ const data = await resp.json() as { message?: { content?: string } };
179
+ const text = data.message?.content?.trim();
180
+ if (!text) throw new Error("Ollama returned empty content");
181
+ return text;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Public API
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Clean model output: strip code fences and <think> blocks.
190
+ */
191
+ export function cleanModelOutput(raw: string): string {
192
+ return raw
193
+ .replace(/^```(?:jsonl?|json)?\n?/, "")
194
+ .replace(/\n?```\s*$/, "")
195
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
196
+ .trim();
197
+ }
198
+
199
+ /**
200
+ * Send a chat completion to the appropriate provider.
201
+ * Auto-detects provider from model name. Returns cleaned content.
202
+ * Throws on failure (caller handles fallback).
203
+ */
204
+ export async function chatDirect(req: ChatRequest): Promise<ChatResult> {
205
+ const provider = detectProvider(req.model);
206
+ const baseUrl = getBaseUrl(provider);
207
+ const timeout = req.timeout ?? 60_000;
208
+
209
+ // Build combined signal: external abort + timeout
210
+ const signals: AbortSignal[] = [AbortSignal.timeout(timeout)];
211
+ if (req.signal) signals.push(req.signal);
212
+ const combinedSignal = typeof AbortSignal.any === "function"
213
+ ? AbortSignal.any(signals)
214
+ : signals[0]; // Node <20.3 fallback
215
+
216
+ let content: string;
217
+
218
+ if (provider === "ollama") {
219
+ content = await chatOllama(baseUrl, req, combinedSignal);
220
+ } else {
221
+ const apiKey = getApiKey(provider);
222
+ if (!apiKey) {
223
+ throw new Error(`No API key for ${provider} (set ${provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"})`);
224
+ }
225
+ if (provider === "anthropic") {
226
+ content = await chatAnthropic(baseUrl, apiKey, req, combinedSignal);
227
+ } else {
228
+ content = await chatOpenAI(baseUrl, apiKey, req, combinedSignal);
229
+ }
230
+ }
231
+
232
+ return { content: cleanModelOutput(content), provider };
233
+ }
234
+
235
+ /**
236
+ * Check if a model ID is a cloud model (has API key available).
237
+ * Returns false for Ollama/local models.
238
+ */
239
+ export function isCloudModel(model: string): boolean {
240
+ const provider = detectProvider(model);
241
+ if (provider === "ollama") return false;
242
+ return getApiKey(provider) !== null;
243
+ }
244
+
245
+ /**
246
+ * Check if any cloud provider is available (has API key configured).
247
+ */
248
+ export function hasCloudProvider(): boolean {
249
+ return !!(process.env.ANTHROPIC_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
250
+ }
251
+
252
+ /**
253
+ * Get the best available cloud model for lightweight tasks.
254
+ * Prefers Haiku (cheapest), falls back to GPT models.
255
+ */
256
+ export function getBudgetCloudModel(): string | null {
257
+ if (process.env.ANTHROPIC_API_KEY?.trim()) return "claude-haiku-4-5";
258
+ if (process.env.OPENAI_API_KEY?.trim()) return "gpt-4.1-mini";
259
+ return null;
260
+ }
@@ -77,9 +77,9 @@ export const DEFAULT_CONFIG: MemoryConfig = {
77
77
  maxLines: 50,
78
78
  embeddingProvider: "voyage",
79
79
  embeddingModel: "voyage-3-lite",
80
- minimumTokensToInit: 10_000,
81
- minimumTokensBetweenUpdate: 5_000,
82
- toolCallsBetweenUpdates: 8,
80
+ minimumTokensToInit: 15_000,
81
+ minimumTokensBetweenUpdate: 10_000,
82
+ toolCallsBetweenUpdates: 15,
83
83
  manualStoreThreshold: 3,
84
84
  extractionModel: "gpt-5.3-codex-spark",
85
85
  extractionTimeout: 60_000,