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 +187 -0
- package/package.json +15 -13
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +2 -3
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +0 -2
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +0 -1
- package/src/cli/cmd/tui/context/app/helper.tsx +2 -3
- package/src/cli/cmd/tui/context/app/sdk.tsx +2 -4
- package/src/cli/cmd/tui/context/app/sync.tsx +0 -1
- package/src/cli/cmd/tui/context/app/theme.tsx +0 -1
- package/src/cli/cmd/tui/context/runtime/local.tsx +0 -3
- package/src/ltm/config.ts +2 -12
- package/src/ltm/memory/procedural.ts +2 -12
- package/src/ltm/pipeline.ts +3 -19
- package/src/ltm/scheduler.ts +2 -14
- package/src/ltm/store.ts +2 -11
- package/src/ltm/types.ts +2 -8
- package/src/provider/local/embedder.ts +2 -18
- package/src/session/core/llm.ts +2 -6
- package/src/session/core/retry.ts +2 -6
- package/src/session/core/session-events.ts +101 -0
- package/src/session/core/session-types.ts +5 -7
- package/src/session/core/session.ts +5 -43
- package/src/session/message/message-errors.ts +2 -6
- package/src/session/message/message-parts.ts +2 -4
- package/src/session/prompt/prompt.ts +2 -3
- package/src/tool/file/apply_patch.ts +0 -21
- package/src/tool/file/edit-replacers.ts +2 -3
- package/src/tool/integration/package.ts +2 -4
- package/src/tool/search/warpgrep.ts +0 -8
- package/src/tool/search/webfetch.ts +2 -10
- package/tsconfig.json +19 -0
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.
|
|
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.
|
|
54
|
-
"@saeeol/core": "7.3.
|
|
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": "
|
|
136
|
-
"@saeeol/core": "7.3.
|
|
137
|
-
"@saeeol/gateway": "7.3.
|
|
138
|
-
"@saeeol/i18n": "7.3.
|
|
139
|
-
"@saeeol/indexing": "7.3.
|
|
140
|
-
"@saeeol/plugin": "7.3.
|
|
141
|
-
"@saeeol/script": "7.3.
|
|
142
|
-
"@saeeol/sdk": "7.3.
|
|
143
|
-
"@saeeol/telemetry": "7.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))),
|
|
@@ -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
|
+
}
|
package/src/ltm/pipeline.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/ltm/scheduler.ts
CHANGED
|
@@ -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
|
+
}
|