saeeol 1.2.7 → 1.2.9

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.
package/bin/saeeol ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ function run(target) {
9
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
10
+ stdio: "inherit",
11
+ })
12
+ if (result.error) {
13
+ console.error(result.error.message)
14
+ process.exit(1)
15
+ }
16
+ const code = typeof result.status === "number" ? result.status : 0
17
+ process.exit(code)
18
+ }
19
+
20
+ const envPath = process.env.SAEEOL_BIN_PATH
21
+ if (envPath) {
22
+ run(envPath)
23
+ }
24
+
25
+ const scriptPath = fs.realpathSync(__filename)
26
+ const scriptDir = path.dirname(scriptPath)
27
+
28
+ // fall through to findBinary() if cached binary fails
29
+ const cached = path.join(scriptDir, ".saeeol")
30
+ if (fs.existsSync(cached)) {
31
+ const result = childProcess.spawnSync(cached, process.argv.slice(2), {
32
+ stdio: "inherit",
33
+ })
34
+ if (!result.error) {
35
+ const code = typeof result.status === "number" ? result.status : 0
36
+ process.exit(code)
37
+ }
38
+ // cached binary failed (e.g. wrong platform/arch, missing dynamic linker),
39
+ // fall through to findBinary() which has better variant detection
40
+ }
41
+
42
+ const platformMap = {
43
+ darwin: "darwin",
44
+ linux: "linux",
45
+ win32: "windows",
46
+ }
47
+ const archMap = {
48
+ x64: "x64",
49
+ arm64: "arm64",
50
+ arm: "arm",
51
+ }
52
+
53
+ let platform = platformMap[os.platform()]
54
+ if (!platform) {
55
+ platform = os.platform()
56
+ }
57
+ let arch = archMap[os.arch()]
58
+ if (!arch) {
59
+ arch = os.arch()
60
+ }
61
+ const base = "saeeol-" + platform + "-" + arch
62
+ const binary = platform === "windows" ? "saeeol.exe" : "saeeol"
63
+
64
+ function supportsAvx2() {
65
+ if (arch !== "x64") return false
66
+
67
+ if (platform === "linux") {
68
+ try {
69
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ if (platform === "darwin") {
76
+ try {
77
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
78
+ encoding: "utf8",
79
+ timeout: 1500,
80
+ })
81
+ if (result.status !== 0) return false
82
+ return (result.stdout || "").trim() === "1"
83
+ } catch {
84
+ return false
85
+ }
86
+ }
87
+
88
+ if (platform === "windows") {
89
+ const cmd =
90
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
91
+
92
+ for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
93
+ try {
94
+ const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], {
95
+ encoding: "utf8",
96
+ timeout: 3000,
97
+ windowsHide: true,
98
+ })
99
+ if (result.status !== 0) continue
100
+ const out = (result.stdout || "").trim().toLowerCase()
101
+ if (out === "true" || out === "1") return true
102
+ if (out === "false" || out === "0") return false
103
+ } catch {
104
+ continue
105
+ }
106
+ }
107
+
108
+ return false
109
+ }
110
+
111
+ return false
112
+ }
113
+
114
+ const names = (() => {
115
+ const avx2 = supportsAvx2()
116
+ const baseline = arch === "x64" && !avx2
117
+
118
+ if (platform === "linux") {
119
+ const musl = (() => {
120
+ try {
121
+ if (fs.existsSync("/etc/alpine-release")) return true
122
+ } catch {
123
+ // ignore
124
+ }
125
+
126
+ try {
127
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
128
+ const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
129
+ if (text.includes("musl")) return true
130
+ } catch {
131
+ // ignore
132
+ }
133
+
134
+ return false
135
+ })()
136
+
137
+ if (musl) {
138
+ if (arch === "x64") {
139
+ if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
140
+ return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
141
+ }
142
+ return [`${base}-musl`, base]
143
+ }
144
+
145
+ if (arch === "x64") {
146
+ if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
147
+ return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
148
+ }
149
+ return [base, `${base}-musl`]
150
+ }
151
+
152
+ if (arch === "x64") {
153
+ if (baseline) return [`${base}-baseline`, base]
154
+ return [base, `${base}-baseline`]
155
+ }
156
+ return [base]
157
+ })()
158
+
159
+ function findBinary(startDir) {
160
+ let current = startDir
161
+ for (;;) {
162
+ const modules = path.join(current, "node_modules")
163
+ if (fs.existsSync(modules)) {
164
+ for (const name of names) {
165
+ const candidate = path.join(modules, name, "bin", binary)
166
+ if (fs.existsSync(candidate)) return candidate
167
+ }
168
+ }
169
+ const parent = path.dirname(current)
170
+ if (parent === current) {
171
+ return
172
+ }
173
+ current = parent
174
+ }
175
+ }
176
+
177
+ const resolved = findBinary(scriptDir)
178
+ if (!resolved) {
179
+ console.error(
180
+ "It seems that your package manager failed to install the right version of the SAEEOL CLI for your platform. You can try manually installing " +
181
+ names.map((n) => `\"${n}\"`).join(" or ") +
182
+ " package",
183
+ )
184
+ process.exit(1)
185
+ }
186
+
187
+ run(resolved)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "name": "saeeol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -50,8 +50,8 @@
50
50
  "@babel/core": "7.28.4",
51
51
  "@effect/language-service": "0.84.2",
52
52
  "@octokit/webhooks-types": "7.6.1",
53
- "@saeeol/script": "7.3.4",
54
- "@saeeol/core": "7.3.4",
53
+ "@saeeol/script": "7.3.5",
54
+ "@saeeol/core": "7.3.5",
55
55
  "@parcel/watcher-darwin-arm64": "2.5.1",
56
56
  "@parcel/watcher-darwin-x64": "2.5.1",
57
57
  "@parcel/watcher-linux-arm64-glibc": "2.5.1",
@@ -132,15 +132,15 @@
132
132
  "@opentui/solid": "0.2.2",
133
133
  "@parcel/watcher": "2.5.1",
134
134
  "@pierre/diffs": "1.1.0-beta.18",
135
- "@saeeol/boxes": "0.2.0",
136
- "@saeeol/core": "7.3.4",
137
- "@saeeol/gateway": "7.3.3",
138
- "@saeeol/i18n": "7.3.4",
139
- "@saeeol/indexing": "7.3.3",
140
- "@saeeol/plugin": "7.3.4",
141
- "@saeeol/script": "7.3.4",
142
- "@saeeol/sdk": "7.3.4",
143
- "@saeeol/telemetry": "7.3.3",
135
+ "@saeeol/boxes": "7.3.5",
136
+ "@saeeol/core": "7.3.5",
137
+ "@saeeol/gateway": "7.3.5",
138
+ "@saeeol/i18n": "7.3.5",
139
+ "@saeeol/indexing": "7.3.5",
140
+ "@saeeol/plugin": "7.3.5",
141
+ "@saeeol/script": "7.3.5",
142
+ "@saeeol/sdk": "7.3.5",
143
+ "@saeeol/telemetry": "7.3.5",
144
144
  "@solid-primitives/event-bus": "1.1.2",
145
145
  "@solid-primitives/scheduled": "1.5.2",
146
146
  "@standard-schema/spec": "1.0.0",
@@ -204,6 +204,8 @@
204
204
  },
205
205
  "peerDependencies": {},
206
206
  "files": [
207
- "src"
207
+ "src",
208
+ "bin",
209
+ "tsconfig.json"
208
210
  ]
209
211
  }
@@ -1,4 +1,4 @@
1
- import { useDialog } from "@tui/ui/dialog"
1
+ import { useDialog } from "@tui/ui/dialog"
2
2
  import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
3
3
  import {
4
4
  createContext,
@@ -74,7 +74,6 @@ function init() {
74
74
  for (const option of entries()) {
75
75
  if (!isEnabled(option)) continue
76
76
  if (option.keybind && keybind.match(option.keybind, evt)) {
77
- // Require double-tap for agent cycle keybinds
78
77
  if (DOUBLE_TAB_KEYS.has(option.keybind)) {
79
78
  const now = Date.now()
80
79
  const match = option.keybind === lastTabKey && now - lastTabTime < DOUBLE_TAB_WINDOW
@@ -187,4 +186,4 @@ function DialogCommand(props: { options: CommandOption[]; suggestedOptions: Comm
187
186
  return [...props.suggestedOptions, ...props.options]
188
187
  }
189
188
  return <DialogSelect ref={(r) => (ref = r)} title={t("cmd.title")} options={list()} />
190
- }
189
+ }
@@ -27,7 +27,6 @@ export function DialogMcp() {
27
27
  const [loading, setLoading] = createSignal<string | null>(null)
28
28
 
29
29
  const options = createMemo(() => {
30
- // Track sync data and loading state to trigger re-render when they change
31
30
  const mcpData = sync.data.mcp
32
31
  const loadingMcp = loading()
33
32
 
@@ -56,7 +55,6 @@ export function DialogMcp() {
56
55
  setLoading(option.value)
57
56
  try {
58
57
  await local.mcp.toggle(option.value)
59
- // Refresh MCP status from server
60
58
  const status = await sdk.client.mcp.status()
61
59
  if (status.data) {
62
60
  sync.set("mcp", status.data)
@@ -36,7 +36,6 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
36
36
 
37
37
  const options = createMemo(() => {
38
38
  const entries = stash.list()
39
- // Show most recent first
40
39
  return entries
41
40
  .map((entry, index) => {
42
41
  const isDeleting = toDelete() === index
@@ -1,4 +1,4 @@
1
- import { createContext, Show, useContext, type ParentProps } from "solid-js"
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
2
 
3
3
  export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
4
  name: string
@@ -10,7 +10,6 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
10
10
  provider: (props: ParentProps<Props>) => {
11
11
  const init = input.init(props)
12
12
  return (
13
- // @ts-expect-error
14
13
  <Show when={init.ready === undefined || init.ready === true}>
15
14
  <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
15
  </Show>
@@ -22,4 +21,4 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
22
21
  return value
23
22
  },
24
23
  }
25
- }
24
+ }
@@ -1,4 +1,4 @@
1
- import { createSaeeolClient } from "@saeeol/sdk/v2"
1
+ import { createSaeeolClient } from "@saeeol/sdk/v2"
2
2
  import type { GlobalEvent } from "@saeeol/sdk/v2"
3
3
  import { createSimpleContext } from "./helper"
4
4
  import { createGlobalEmitter } from "@solid-primitives/event-bus"
@@ -100,8 +100,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
100
100
  if (queue.length > 0) flush()
101
101
  attempt += 1
102
102
  if (abort.signal.aborted || ctrl.signal.aborted) break
103
-
104
- // Exponential backoff
105
103
  const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
106
104
  await new Promise((resolve) => setTimeout(resolve, backoff))
107
105
  }
@@ -139,4 +137,4 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
139
137
  url: props.url,
140
138
  }
141
139
  },
142
- })
140
+ })
@@ -565,7 +565,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
565
565
  })
566
566
  .then(() => {
567
567
  if (store.status !== "complete") setStore("status", "partial")
568
- // non-blocking
569
568
  void Promise.all([
570
569
  ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
571
570
  consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
@@ -234,7 +234,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
234
234
  return {
235
235
  theme: new Proxy({} as Theme, {
236
236
  get(_target, prop) {
237
- // @ts-expect-error
238
237
  return values()[prop]
239
238
  },
240
239
  }),
@@ -99,7 +99,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
99
99
  if (agent?.color) {
100
100
  const color = agent.color
101
101
  if (color.startsWith("#")) return RGBA.fromHex(color)
102
- // already validated by config, just satisfying TS here
103
102
  return theme[color as keyof typeof theme] as RGBA
104
103
  }
105
104
  return colors()[index % colors().length]
@@ -440,10 +439,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
440
439
  async toggle(name: string) {
441
440
  const status = sync.data.mcp[name]
442
441
  if (status?.status === "connected") {
443
- // Disable: disconnect the MCP
444
442
  await sdk.client.mcp.disconnect({ name })
445
443
  } else {
446
- // Enable/Retry: connect the MCP (handles disabled, failed, and other states)
447
444
  await sdk.client.mcp.connect({ name })
448
445
  }
449
446
  },
package/src/ltm/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** LTM — hardware auto-detection + deterministic LLM parameter calculation */
1
+ /** LTM — hardware auto-detection + deterministic LLM parameter calculation */
2
2
 
3
3
  import os from "os"
4
4
  import { Effect } from "effect"
@@ -10,8 +10,6 @@ import type { HardwareProfile, LLMBakeParams, LTMConfig } from "./types"
10
10
 
11
11
  const log = Log.create({ service: "ltm/config" })
12
12
 
13
- // ── Default LTM config ──
14
-
15
13
  export const DEFAULT_LTM_CONFIG: LTMConfig = {
16
14
  enabled: false,
17
15
  embeddingModel: "auto",
@@ -23,8 +21,6 @@ export const DEFAULT_LTM_CONFIG: LTMConfig = {
23
21
  retrieval: { topK: 5, minScore: 0.7, maxTokens: 2000 },
24
22
  }
25
23
 
26
- // ── Hardware profiling ──
27
-
28
24
  /** Collect system hardware information */
29
25
  export async function profileHardware(): Promise<HardwareProfile> {
30
26
  const gpu = await Effect.runPromise(GPU.profile)
@@ -51,8 +47,6 @@ export function hardwareHash(hw: HardwareProfile): string {
51
47
  return h.toString(36)
52
48
  }
53
49
 
54
- // ── Embedding model selection ──
55
-
56
50
  /** Auto-select embedding model based on available VRAM */
57
51
  export function selectEmbeddingModel(hw: HardwareProfile) {
58
52
  const vram = hw.availableVRAMMB
@@ -63,8 +57,6 @@ export function selectEmbeddingModel(hw: HardwareProfile) {
63
57
  return RAG.EMBEDDING_MODELS[5] // all-minilm-l6, 384d, 80MB
64
58
  }
65
59
 
66
- // ── Bake (deterministic parameter calculation) ──
67
-
68
60
  /**
69
61
  * Compute hardware-based LLM parameters and persist to disk.
70
62
  * Once computed, the same values are reused as long as hardware doesn't change.
@@ -96,8 +88,6 @@ export async function bake(hw: HardwareProfile): Promise<LLMBakeParams> {
96
88
  return params
97
89
  }
98
90
 
99
- // ── Persistence ──
100
-
101
91
  import path from "path"
102
92
  import { mkdir, readFile, writeFile } from "fs/promises"
103
93
 
@@ -121,4 +111,4 @@ async function readBake(): Promise<LLMBakeParams | undefined> {
121
111
  async function writeBake(params: LLMBakeParams): Promise<void> {
122
112
  await mkdir(bakeDir(), { recursive: true })
123
113
  await writeFile(bakePath(), JSON.stringify(params, null, 2))
124
- }
114
+ }
@@ -1,4 +1,4 @@
1
- /** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
1
+ /** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
2
2
 
3
3
  import * as Log from "@saeeol/core/util/log"
4
4
  import * as Embedder from "@/provider/local/embedder"
@@ -20,8 +20,6 @@ export function extractStyleSignals(
20
20
  ): StyleSignal[] {
21
21
  const signals: StyleSignal[] = []
22
22
  const ext = filePath.split(".").pop() ?? ""
23
-
24
- // Comment language
25
23
  if (content.includes("//") && (ext === "ts" || ext === "js")) {
26
24
  const hasKoreanComment = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(content)
27
25
  signals.push({
@@ -30,15 +28,11 @@ export function extractStyleSignals(
30
28
  evidence: hasKoreanComment ? "comments in Korean" : "comments in English",
31
29
  })
32
30
  }
33
-
34
- // Indentation
35
31
  if (content.includes(" ") && !content.includes("\t")) {
36
32
  signals.push({ language: ext, pattern: "indent", evidence: "2-space indentation" })
37
33
  } else if (content.includes("\t")) {
38
34
  signals.push({ language: ext, pattern: "indent", evidence: "tab indentation" })
39
35
  }
40
-
41
- // Semicolons
42
36
  if (ext === "ts" || ext === "js") {
43
37
  const hasSemicolons = /;\s*\n/.test(content)
44
38
  signals.push({
@@ -47,8 +41,6 @@ export function extractStyleSignals(
47
41
  evidence: hasSemicolons ? "uses semicolons" : "no semicolons",
48
42
  })
49
43
  }
50
-
51
- // Quote style
52
44
  const singleQuotes = (content.match(/'/g) ?? []).length
53
45
  const doubleQuotes = (content.match(/"/g) ?? []).length
54
46
  if (singleQuotes > doubleQuotes * 2) {
@@ -56,8 +48,6 @@ export function extractStyleSignals(
56
48
  } else if (doubleQuotes > singleQuotes * 2) {
57
49
  signals.push({ language: ext, pattern: "quotes", evidence: "prefers double quotes" })
58
50
  }
59
-
60
- // Naming: camelCase vs snake_case
61
51
  const camelCase = (content.match(/[a-z][A-Z]/g) ?? []).length
62
52
  const snakeCase = (content.match(/_[a-z]/g) ?? []).length
63
53
  if (camelCase > snakeCase * 3) {
@@ -99,4 +89,4 @@ export async function fromStyleSignals(
99
89
  }
100
90
 
101
91
  return memories
102
- }
92
+ }
@@ -1,4 +1,4 @@
1
- /** LTM — background collection pipeline */
1
+ /** LTM — background collection pipeline */
2
2
 
3
3
  import { Effect } from "effect"
4
4
  import * as Log from "@saeeol/core/util/log"
@@ -19,8 +19,6 @@ let running = false
19
19
  let config: LTMConfig | undefined
20
20
  let unsubscribers: Array<() => void> = []
21
21
 
22
- // ── Pipeline lifecycle ──
23
-
24
22
  /** Start the pipeline */
25
23
  export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void> {
26
24
  if (running) return
@@ -31,24 +29,18 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
31
29
 
32
30
  log.info("pipeline starting", { model: bake.embeddingModel })
33
31
 
34
- // Start embedding server
35
32
  const server = await Embedder.start(bake)
36
33
  if (server.status !== "running") {
37
34
  log.error("embedding server failed to start, pipeline disabled")
38
35
  running = false
39
36
  return
40
37
  }
41
-
42
- // Prune old memories
43
- if (cfg.episodic.enabled) {
44
- const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
38
+ if (cfg.episodic.enabled) { const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
45
39
  if (pruned > 0) {
46
40
  log.info("pruned old episodic memories", { count: pruned })
47
41
  void Bus.publish(LTMEvent.MemoryPruned, { count: pruned, type: "episodic" })
48
42
  }
49
43
  }
50
-
51
- // Enforce memory limit
52
44
  const count = await Store.count()
53
45
  if (count > cfg.maxMemories) {
54
46
  const excess = count - cfg.maxMemories
@@ -58,8 +50,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
58
50
  await Store.remove(toRemove)
59
51
  log.info("trimmed memories to max", { removed: toRemove.length })
60
52
  }
61
-
62
- // Bind BusEvent subscriptions
63
53
  bindSubscriptions()
64
54
 
65
55
  log.info("pipeline started", { memoryCount: await Store.count() })
@@ -69,8 +59,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
69
59
  export async function stop(): Promise<void> {
70
60
  if (!running) return
71
61
  running = false
72
-
73
- // Unsubscribe
74
62
  for (const unsub of unsubscribers) {
75
63
  try { unsub() } catch { /* ignore */ }
76
64
  }
@@ -86,8 +74,6 @@ export function isActive(): boolean {
86
74
  return running
87
75
  }
88
76
 
89
- // ── BusEvent subscription binding ──
90
-
91
77
  function bindSubscriptions() {
92
78
  if (!config) return
93
79
 
@@ -169,8 +155,6 @@ function bindSubscriptions() {
169
155
  log.info("bus subscriptions bound", { count: unsubscribers.length })
170
156
  }
171
157
 
172
- // ── Direct-call handlers (called explicitly from external modules) ──
173
-
174
158
  /** Conversation message → episodic memory */
175
159
  export async function onMessageCompleted(
176
160
  sessionID: string,
@@ -254,4 +238,4 @@ export async function onCodeEdit(
254
238
  for (const memory of memories) {
255
239
  await Store.upsert(memory)
256
240
  }
257
- }
241
+ }
@@ -1,4 +1,4 @@
1
- /** LTM — VRAM/task scheduler */
1
+ /** LTM — VRAM/task scheduler */
2
2
 
3
3
  import * as Log from "@saeeol/core/util/log"
4
4
  import type { HardwareProfile } from "./types"
@@ -18,8 +18,6 @@ export interface Allocation {
18
18
  /** Scheduling strategy */
19
19
  export type Strategy = "concurrent" | "alternating" | "cpu-fallback" | "no-gpu"
20
20
 
21
- // ── VRAM gauge ──
22
-
23
21
  /** Query current VRAM allocation state */
24
22
  export async function allocation(): Promise<Allocation> {
25
23
  const gpu = await Effect.runPromise(GPU.profile)
@@ -40,14 +38,11 @@ export async function strategy(): Promise<Strategy> {
40
38
  return "cpu-fallback"
41
39
  }
42
40
 
43
- // ── Execution decisions ──
44
-
45
41
  /** Whether embedding background work can run */
46
42
  export async function canRunEmbedding(): Promise<boolean> {
47
43
  const strat = await strategy()
48
44
  // no-gpu or cpu-fallback: always allowed (CPU processing)
49
45
  if (strat === "no-gpu" || strat === "cpu-fallback") return true
50
- // concurrent: always allowed
51
46
  if (strat === "concurrent") return true
52
47
  // alternating: need at least 2GB VRAM
53
48
  const alloc = await allocation()
@@ -61,20 +56,13 @@ export async function canRunConcurrent(hw: HardwareProfile): Promise<boolean> {
61
56
  return false
62
57
  }
63
58
 
64
- // ── LLM ↔ embedding time-sharing ──
65
-
66
59
  /** Called when LLM requests VRAM — pauses embedding if needed */
67
60
  export async function requestLLM(vramNeededMB: number): Promise<boolean> {
68
61
  const strat = await strategy()
69
-
70
- // concurrent strategy: both can run
71
62
  if (strat === "concurrent") return true
72
- // no-gpu: GPU not needed
73
63
  if (strat === "no-gpu") return true
74
64
 
75
65
  const alloc = await allocation()
76
-
77
- // Sufficient VRAM — proceed as-is
78
66
  if (alloc.available >= vramNeededMB) {
79
67
  log.info("LLM request: enough VRAM", { available: alloc.available, needed: vramNeededMB })
80
68
  return true
@@ -126,4 +114,4 @@ export async function resumeEmbedding(bake: {
126
114
  } catch (e) {
127
115
  log.error("failed to resume embedding server", { error: e })
128
116
  }
129
- }
117
+ }
package/src/ltm/store.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** LTM — filesystem-based vector store */
1
+ /** LTM — filesystem-based vector store */
2
2
 
3
3
  import path from "path"
4
4
  import { mkdir, readFile, writeFile, readdir, rm, stat } from "fs/promises"
@@ -8,8 +8,6 @@ import type { Memory, MemoryType } from "./types"
8
8
 
9
9
  const log = Log.create({ service: "ltm/store" })
10
10
 
11
- // ── Cosine similarity ──
12
-
13
11
  function cosine(a: number[], b: number[]): number {
14
12
  let dot = 0
15
13
  let na = 0
@@ -24,14 +22,11 @@ function cosine(a: number[], b: number[]): number {
24
22
  return denom === 0 ? 0 : dot / denom
25
23
  }
26
24
 
27
- // ── File paths ──
28
-
29
25
  function storeDir(): string {
30
26
  return path.join(Global.Path.data, "ltm", "memories")
31
27
  }
32
28
 
33
29
  function memoryPath(id: string): string {
34
- // Sanitize special characters for safe filenames
35
30
  const safe = id.replace(/[:<>\"|?*]/g, "_")
36
31
  return path.join(storeDir(), `${safe}.json`)
37
32
  }
@@ -44,8 +39,6 @@ async function ensure(): Promise<void> {
44
39
  await mkdir(storeDir(), { recursive: true })
45
40
  }
46
41
 
47
- // ── Index ──
48
-
49
42
  interface Index {
50
43
  memories: Array<{ id: string; type: MemoryType; timestamp: number; source: string }>
51
44
  }
@@ -64,8 +57,6 @@ async function writeIndex(idx: Index): Promise<void> {
64
57
  await writeFile(indexPath(), JSON.stringify(idx, null, 2))
65
58
  }
66
59
 
67
- // ── Public API ──
68
-
69
60
  export async function upsert(memory: Memory): Promise<void> {
70
61
  await ensure()
71
62
  await writeFile(memoryPath(memory.id), JSON.stringify(memory, null, 2))
@@ -149,4 +140,4 @@ export async function prune(olderThanMs: number): Promise<number> {
149
140
  export async function count(): Promise<number> {
150
141
  const idx = await readIndex()
151
142
  return idx.memories.length
152
- }
143
+ }