@soleri/core 9.11.0 → 9.13.0
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/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/persona/defaults.d.ts +8 -0
- package/dist/persona/defaults.d.ts.map +1 -1
- package/dist/persona/defaults.js +49 -0
- package/dist/persona/defaults.js.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +140 -9
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +12 -0
- package/src/persona/defaults.test.ts +39 -1
- package/src/persona/defaults.ts +65 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/planner.test.ts +2 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +131 -0
- package/src/runtime/orchestrate-ops.ts +158 -10
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- package/dist/vault/vault-interfaces.js.map +0 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process lifecycle integration tests.
|
|
3
|
+
*
|
|
4
|
+
* These tests spawn real child processes to verify orphan detection,
|
|
5
|
+
* kill escalation, process group management, and the full dispatch
|
|
6
|
+
* lifecycle. No mocking of process.kill — real signals, real PIDs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
10
|
+
import { spawn, ChildProcess, execSync } from 'node:child_process';
|
|
11
|
+
import { OrphanReaper } from '../../subagent/orphan-reaper.js';
|
|
12
|
+
|
|
13
|
+
// Collect all spawned processes for cleanup
|
|
14
|
+
const spawnedChildren: ChildProcess[] = [];
|
|
15
|
+
|
|
16
|
+
/** Spawn a long-running node process that does nothing. */
|
|
17
|
+
function spawnIdleProcess(opts?: { detached?: boolean }): ChildProcess {
|
|
18
|
+
const child = spawn('node', ['-e', 'setInterval(()=>{},1000)'], {
|
|
19
|
+
stdio: 'ignore',
|
|
20
|
+
detached: opts?.detached ?? false,
|
|
21
|
+
});
|
|
22
|
+
spawnedChildren.push(child);
|
|
23
|
+
return child;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Spawn a parent that spawns a grandchild, both idle. */
|
|
27
|
+
function spawnWithGrandchild(): ChildProcess {
|
|
28
|
+
// The parent spawns a child which also idles.
|
|
29
|
+
// Using detached so the parent becomes a process group leader.
|
|
30
|
+
const child = spawn(
|
|
31
|
+
'node',
|
|
32
|
+
[
|
|
33
|
+
'-e',
|
|
34
|
+
`
|
|
35
|
+
const { spawn } = require('child_process');
|
|
36
|
+
const gc = spawn('node', ['-e', 'setInterval(()=>{},1000)'], { stdio: 'ignore' });
|
|
37
|
+
// Write grandchild PID to stdout so the test can track it
|
|
38
|
+
process.stdout.write(String(gc.pid));
|
|
39
|
+
setInterval(()=>{}, 1000);
|
|
40
|
+
`,
|
|
41
|
+
],
|
|
42
|
+
{
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
+
detached: true,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
spawnedChildren.push(child);
|
|
48
|
+
return child;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if a PID is alive using signal 0. */
|
|
52
|
+
function isAlive(pid: number): boolean {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Wait until a condition is true, polling every intervalMs. */
|
|
62
|
+
async function waitFor(fn: () => boolean, timeoutMs = 10_000, intervalMs = 100): Promise<void> {
|
|
63
|
+
const start = Date.now();
|
|
64
|
+
while (!fn()) {
|
|
65
|
+
if (Date.now() - start > timeoutMs) {
|
|
66
|
+
throw new Error(`waitFor timed out after ${timeoutMs}ms`);
|
|
67
|
+
}
|
|
68
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Force-kill a PID, ignoring errors if already dead. */
|
|
73
|
+
function safeKill(pid: number, signal: NodeJS.Signals = 'SIGKILL'): void {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, signal);
|
|
76
|
+
} catch {
|
|
77
|
+
// already dead — fine
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
// Kill every process we spawned, best-effort
|
|
83
|
+
for (const child of spawnedChildren) {
|
|
84
|
+
if (child.pid) {
|
|
85
|
+
// Try group kill first (for detached), then single
|
|
86
|
+
try {
|
|
87
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
88
|
+
} catch {
|
|
89
|
+
safeKill(child.pid);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
spawnedChildren.length = 0;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Process Lifecycle Integration', { timeout: 15_000 }, () => {
|
|
97
|
+
// ── a. Orphan detection ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('orphan detection', () => {
|
|
100
|
+
it('detects a killed process via reap()', async () => {
|
|
101
|
+
const child = spawnIdleProcess();
|
|
102
|
+
const pid = child.pid!;
|
|
103
|
+
expect(pid).toBeGreaterThan(0);
|
|
104
|
+
|
|
105
|
+
const reaper = new OrphanReaper();
|
|
106
|
+
reaper.register(pid, 'orphan-test');
|
|
107
|
+
|
|
108
|
+
// Process should be alive initially
|
|
109
|
+
expect(isAlive(pid)).toBe(true);
|
|
110
|
+
const initialReap = reaper.reap();
|
|
111
|
+
expect(initialReap.reaped).toHaveLength(0);
|
|
112
|
+
expect(reaper.isTracked(pid)).toBe(true);
|
|
113
|
+
|
|
114
|
+
// Kill it externally
|
|
115
|
+
process.kill(pid, 'SIGKILL');
|
|
116
|
+
await waitFor(() => !isAlive(pid));
|
|
117
|
+
|
|
118
|
+
// Now reap should detect it
|
|
119
|
+
const result = reaper.reap();
|
|
120
|
+
expect(result.reaped).toHaveLength(1);
|
|
121
|
+
expect(result.reaped[0]).toBe('orphan-test');
|
|
122
|
+
expect(reaper.isTracked(pid)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('invokes onOrphan callback for dead processes', async () => {
|
|
126
|
+
const orphanEvents: Array<{ taskId: string; pid: number }> = [];
|
|
127
|
+
const reaper = new OrphanReaper((taskId, pid) => {
|
|
128
|
+
orphanEvents.push({ taskId, pid });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const child = spawnIdleProcess();
|
|
132
|
+
const pid = child.pid!;
|
|
133
|
+
reaper.register(pid, 'callback-test');
|
|
134
|
+
|
|
135
|
+
process.kill(pid, 'SIGKILL');
|
|
136
|
+
await waitFor(() => !isAlive(pid));
|
|
137
|
+
|
|
138
|
+
reaper.reap();
|
|
139
|
+
expect(orphanEvents).toHaveLength(1);
|
|
140
|
+
expect(orphanEvents[0]).toEqual({ taskId: 'callback-test', pid });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles multiple tracked processes with mixed liveness', async () => {
|
|
144
|
+
const alive = spawnIdleProcess();
|
|
145
|
+
const dead = spawnIdleProcess();
|
|
146
|
+
const alivePid = alive.pid!;
|
|
147
|
+
const deadPid = dead.pid!;
|
|
148
|
+
|
|
149
|
+
const reaper = new OrphanReaper();
|
|
150
|
+
reaper.register(alivePid, 'alive-task');
|
|
151
|
+
reaper.register(deadPid, 'dead-task');
|
|
152
|
+
|
|
153
|
+
// Kill only one
|
|
154
|
+
process.kill(deadPid, 'SIGKILL');
|
|
155
|
+
await waitFor(() => !isAlive(deadPid));
|
|
156
|
+
|
|
157
|
+
const result = reaper.reap();
|
|
158
|
+
expect(result.reaped).toHaveLength(1);
|
|
159
|
+
expect(result.reaped[0]).toBe('dead-task');
|
|
160
|
+
expect(reaper.isTracked(alivePid)).toBe(true);
|
|
161
|
+
expect(reaper.isTracked(deadPid)).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── b. Timeout escalation (killProcessGroup with SIGKILL) ──────
|
|
166
|
+
|
|
167
|
+
describe('kill escalation', () => {
|
|
168
|
+
it('kills a process with SIGTERM via killProcessGroup', async () => {
|
|
169
|
+
const child = spawnIdleProcess({ detached: true });
|
|
170
|
+
const pid = child.pid!;
|
|
171
|
+
|
|
172
|
+
const reaper = new OrphanReaper();
|
|
173
|
+
reaper.register(pid, 'kill-test');
|
|
174
|
+
|
|
175
|
+
const result = reaper.killProcessGroup(pid, 'SIGTERM');
|
|
176
|
+
expect(result.killed).toBe(true);
|
|
177
|
+
|
|
178
|
+
await waitFor(() => !isAlive(pid));
|
|
179
|
+
expect(isAlive(pid)).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('escalates to SIGKILL when SIGTERM is ignored', async () => {
|
|
183
|
+
// Spawn a process that traps SIGTERM
|
|
184
|
+
const child = spawn(
|
|
185
|
+
'node',
|
|
186
|
+
['-e', "process.on('SIGTERM', () => {}); setInterval(()=>{},1000)"],
|
|
187
|
+
{ stdio: 'ignore', detached: true },
|
|
188
|
+
);
|
|
189
|
+
spawnedChildren.push(child);
|
|
190
|
+
const pid = child.pid!;
|
|
191
|
+
|
|
192
|
+
const reaper = new OrphanReaper();
|
|
193
|
+
reaper.register(pid, 'escalation-test');
|
|
194
|
+
|
|
195
|
+
// Send SIGTERM — process should still be alive after a short wait
|
|
196
|
+
reaper.killProcessGroup(pid, 'SIGTERM');
|
|
197
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
198
|
+
|
|
199
|
+
// Process should still be alive (it traps SIGTERM)
|
|
200
|
+
if (isAlive(pid)) {
|
|
201
|
+
// Escalate to SIGKILL
|
|
202
|
+
const result = reaper.killProcessGroup(pid, 'SIGKILL');
|
|
203
|
+
expect(result.killed).toBe(true);
|
|
204
|
+
await waitFor(() => !isAlive(pid));
|
|
205
|
+
expect(isAlive(pid)).toBe(false);
|
|
206
|
+
}
|
|
207
|
+
// If it died from SIGTERM that's fine too — OS-dependent behavior
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('killProcessGroup returns killed:false for already-dead process', async () => {
|
|
211
|
+
const child = spawnIdleProcess({ detached: true });
|
|
212
|
+
const pid = child.pid!;
|
|
213
|
+
|
|
214
|
+
// Kill it first
|
|
215
|
+
process.kill(pid, 'SIGKILL');
|
|
216
|
+
await waitFor(() => !isAlive(pid));
|
|
217
|
+
|
|
218
|
+
const reaper = new OrphanReaper();
|
|
219
|
+
const result = reaper.killProcessGroup(pid, 'SIGTERM');
|
|
220
|
+
// Should indicate failure since process is dead
|
|
221
|
+
expect(result.killed).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ── c. Process group kill ──────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
describe('process group management', () => {
|
|
228
|
+
it('killProcessGroup kills the parent process group', async () => {
|
|
229
|
+
const child = spawnIdleProcess({ detached: true });
|
|
230
|
+
const pid = child.pid!;
|
|
231
|
+
|
|
232
|
+
const reaper = new OrphanReaper();
|
|
233
|
+
const result = reaper.killProcessGroup(pid);
|
|
234
|
+
|
|
235
|
+
expect(result.killed).toBe(true);
|
|
236
|
+
// Windows doesn't support negative-PID group signals, falls back to 'single'
|
|
237
|
+
expect(result.method).toBe(process.platform === 'win32' ? 'single' : 'group');
|
|
238
|
+
|
|
239
|
+
await waitFor(() => !isAlive(pid));
|
|
240
|
+
expect(isAlive(pid)).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('killProcessGroup with grandchild kills the entire tree', async () => {
|
|
244
|
+
const parent = spawnWithGrandchild();
|
|
245
|
+
const parentPid = parent.pid!;
|
|
246
|
+
|
|
247
|
+
// Read grandchild PID from stdout
|
|
248
|
+
const grandchildPid = await new Promise<number>((resolve, reject) => {
|
|
249
|
+
let data = '';
|
|
250
|
+
const timer = setTimeout(() => reject(new Error('timeout reading grandchild PID')), 5000);
|
|
251
|
+
parent.stdout!.on('data', (chunk: Buffer) => {
|
|
252
|
+
data += chunk.toString();
|
|
253
|
+
const pid = parseInt(data, 10);
|
|
254
|
+
if (!isNaN(pid) && pid > 0) {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
resolve(pid);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
parent.on('error', (err) => {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
reject(err);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(isAlive(parentPid)).toBe(true);
|
|
266
|
+
expect(isAlive(grandchildPid)).toBe(true);
|
|
267
|
+
|
|
268
|
+
// Kill the process group
|
|
269
|
+
const reaper = new OrphanReaper();
|
|
270
|
+
const result = reaper.killProcessGroup(parentPid, 'SIGKILL');
|
|
271
|
+
expect(result.killed).toBe(true);
|
|
272
|
+
// Windows doesn't support negative-PID group signals, falls back to 'single'
|
|
273
|
+
expect(result.method).toBe(process.platform === 'win32' ? 'single' : 'group');
|
|
274
|
+
|
|
275
|
+
// Both parent and grandchild should be dead
|
|
276
|
+
await waitFor(() => !isAlive(parentPid) && !isAlive(grandchildPid), 5000);
|
|
277
|
+
expect(isAlive(parentPid)).toBe(false);
|
|
278
|
+
expect(isAlive(grandchildPid)).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('killAll kills all tracked processes', async () => {
|
|
282
|
+
const child1 = spawnIdleProcess({ detached: true });
|
|
283
|
+
const child2 = spawnIdleProcess({ detached: true });
|
|
284
|
+
const pid1 = child1.pid!;
|
|
285
|
+
const pid2 = child2.pid!;
|
|
286
|
+
|
|
287
|
+
const reaper = new OrphanReaper();
|
|
288
|
+
reaper.register(pid1, 'task-1');
|
|
289
|
+
reaper.register(pid2, 'task-2');
|
|
290
|
+
|
|
291
|
+
const results = reaper.killAll('SIGKILL');
|
|
292
|
+
|
|
293
|
+
expect(results.size).toBe(2);
|
|
294
|
+
expect(results.get(pid1)!.killed).toBe(true);
|
|
295
|
+
expect(results.get(pid2)!.killed).toBe(true);
|
|
296
|
+
|
|
297
|
+
// Tracking should be cleared
|
|
298
|
+
expect(reaper.listTracked()).toHaveLength(0);
|
|
299
|
+
|
|
300
|
+
await waitFor(() => !isAlive(pid1) && !isAlive(pid2));
|
|
301
|
+
expect(isAlive(pid1)).toBe(false);
|
|
302
|
+
expect(isAlive(pid2)).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('killAll handles empty tracking gracefully', () => {
|
|
306
|
+
const reaper = new OrphanReaper();
|
|
307
|
+
const results = reaper.killAll();
|
|
308
|
+
expect(results.size).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── d. Full lifecycle ─────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
describe('full lifecycle', () => {
|
|
315
|
+
it('register → kill externally → reap → verify cleanup', async () => {
|
|
316
|
+
const reaper = new OrphanReaper();
|
|
317
|
+
|
|
318
|
+
// Simulate a dispatch wave: register multiple processes
|
|
319
|
+
const children = [spawnIdleProcess(), spawnIdleProcess(), spawnIdleProcess()];
|
|
320
|
+
const pids = children.map((c) => c.pid!);
|
|
321
|
+
|
|
322
|
+
pids.forEach((pid, i) => reaper.register(pid, `wave-task-${i}`));
|
|
323
|
+
expect(reaper.listTracked()).toHaveLength(3);
|
|
324
|
+
|
|
325
|
+
// All alive initially
|
|
326
|
+
const earlyReap = reaper.reap();
|
|
327
|
+
expect(earlyReap.reaped).toHaveLength(0);
|
|
328
|
+
|
|
329
|
+
// Kill two of three externally
|
|
330
|
+
process.kill(pids[0], 'SIGKILL');
|
|
331
|
+
process.kill(pids[2], 'SIGKILL');
|
|
332
|
+
await waitFor(() => !isAlive(pids[0]) && !isAlive(pids[2]));
|
|
333
|
+
|
|
334
|
+
// Reap should detect the two dead ones
|
|
335
|
+
const result = reaper.reap();
|
|
336
|
+
expect(result.reaped).toHaveLength(2);
|
|
337
|
+
expect(result.reaped.sort()).toEqual(['wave-task-0', 'wave-task-2']);
|
|
338
|
+
|
|
339
|
+
// One should still be tracked
|
|
340
|
+
expect(reaper.listTracked()).toHaveLength(1);
|
|
341
|
+
expect(reaper.isTracked(pids[1])).toBe(true);
|
|
342
|
+
|
|
343
|
+
// Clean up the survivor
|
|
344
|
+
reaper.killAll('SIGKILL');
|
|
345
|
+
expect(reaper.listTracked()).toHaveLength(0);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('reap in finally block pattern works correctly', async () => {
|
|
349
|
+
const reaper = new OrphanReaper();
|
|
350
|
+
const child = spawnIdleProcess();
|
|
351
|
+
const pid = child.pid!;
|
|
352
|
+
reaper.register(pid, 'finally-test');
|
|
353
|
+
|
|
354
|
+
let reapedInFinally: ReturnType<typeof reaper.reap> | null = null;
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Simulate dispatch work
|
|
358
|
+
process.kill(pid, 'SIGKILL');
|
|
359
|
+
await waitFor(() => !isAlive(pid));
|
|
360
|
+
} finally {
|
|
361
|
+
// This is what orchestrate-ops.ts does in the finally block
|
|
362
|
+
reapedInFinally = reaper.reap();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
expect(reapedInFinally!.reaped).toHaveLength(1);
|
|
366
|
+
expect(reapedInFinally!.reaped[0]).toBe('finally-test');
|
|
367
|
+
expect(reaper.listTracked()).toHaveLength(0);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── e. Facade integration (admin_reap_orphans structure) ──────
|
|
372
|
+
|
|
373
|
+
describe('admin facade integration', () => {
|
|
374
|
+
it('dispatcher.reapOrphans() returns correct report structure', async () => {
|
|
375
|
+
// We test the dispatcher's reapOrphans method shape which is what
|
|
376
|
+
// admin_reap_orphans calls. We create an OrphanReaper directly since
|
|
377
|
+
// the dispatcher requires a full RuntimeAdapterRegistry.
|
|
378
|
+
|
|
379
|
+
const reaper = new OrphanReaper();
|
|
380
|
+
const child = spawnIdleProcess();
|
|
381
|
+
const pid = child.pid!;
|
|
382
|
+
reaper.register(pid, 'facade-test');
|
|
383
|
+
|
|
384
|
+
// Kill it
|
|
385
|
+
process.kill(pid, 'SIGKILL');
|
|
386
|
+
await waitFor(() => !isAlive(pid));
|
|
387
|
+
|
|
388
|
+
// Simulate what dispatcher.reapOrphans() does internally
|
|
389
|
+
const result = reaper.reap();
|
|
390
|
+
|
|
391
|
+
// reap() returns { reaped: string[], alive: string[] }
|
|
392
|
+
expect(result.reaped).toHaveLength(1);
|
|
393
|
+
expect(result.reaped[0]).toBe('facade-test');
|
|
394
|
+
expect(result.alive).toHaveLength(0);
|
|
395
|
+
|
|
396
|
+
// Verify report structure matches what admin_reap_orphans builds
|
|
397
|
+
const report = {
|
|
398
|
+
reaped: result.reaped.length,
|
|
399
|
+
tasks: result.reaped,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
expect(report.reaped).toBe(1);
|
|
403
|
+
expect(report.tasks).toEqual(['facade-test']);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('returns empty report when no orphans exist', () => {
|
|
407
|
+
const reaper = new OrphanReaper();
|
|
408
|
+
const child = spawnIdleProcess();
|
|
409
|
+
reaper.register(child.pid!, 'alive-task');
|
|
410
|
+
|
|
411
|
+
// All alive — reap returns nothing reaped
|
|
412
|
+
const result = reaper.reap();
|
|
413
|
+
const report = {
|
|
414
|
+
reaped: result.reaped.length,
|
|
415
|
+
tasks: result.reaped,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
expect(report).toEqual({ reaped: 0, tasks: [] });
|
|
419
|
+
expect(result.alive).toHaveLength(1);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -54,7 +54,7 @@ describe('WorkspaceResolver', () => {
|
|
|
54
54
|
warnSpy.mockRestore();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('cleanup() calls git worktree remove and git branch -D', () => {
|
|
57
|
+
it('cleanup() calls git worktree remove and git branch -D but not git push', () => {
|
|
58
58
|
(execSync as ReturnType<typeof vi.fn>).mockReturnValue('');
|
|
59
59
|
|
|
60
60
|
// First create a worktree
|
|
@@ -71,6 +71,11 @@ describe('WorkspaceResolver', () => {
|
|
|
71
71
|
expect.stringContaining('git branch -D'),
|
|
72
72
|
expect.objectContaining({ cwd: baseDir }),
|
|
73
73
|
);
|
|
74
|
+
// Worktree branches are local-only — should NOT push to remote
|
|
75
|
+
expect(execSync).not.toHaveBeenCalledWith(
|
|
76
|
+
expect.stringContaining('git push'),
|
|
77
|
+
expect.anything(),
|
|
78
|
+
);
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
it('cleanup() silently handles errors', () => {
|
package/src/adapters/types.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface AdapterExecutionResult {
|
|
|
47
47
|
exitCode: number;
|
|
48
48
|
/** Whether execution timed out */
|
|
49
49
|
timedOut?: boolean;
|
|
50
|
+
/** PID of the spawned child process (if available) */
|
|
51
|
+
pid?: number;
|
|
50
52
|
/** Token usage */
|
|
51
53
|
usage?: AdapterTokenUsage;
|
|
52
54
|
/** Session state to persist for next run */
|
package/src/brain/brain.ts
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
cosineSimilarity,
|
|
11
11
|
jaccardSimilarity,
|
|
12
12
|
} from '../text/similarity.js';
|
|
13
|
-
import { rowToEntry } from '../vault/vault-entries.js';
|
|
13
|
+
import { rowToEntry, cosineSearch, getVector } from '../vault/vault-entries.js';
|
|
14
|
+
import type { EmbeddingProvider } from '../embeddings/types.js';
|
|
14
15
|
import type {
|
|
15
16
|
ScoringWeights,
|
|
16
17
|
ScoreBreakdown,
|
|
@@ -33,6 +34,21 @@ const SEVERITY_SCORES: Record<string, number> = {
|
|
|
33
34
|
suggestion: 0.4,
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
// ─── Vector cosine similarity (dense float arrays) ────────────────
|
|
38
|
+
|
|
39
|
+
function vectorCosineSimilarity(a: number[], b: number[]): number {
|
|
40
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
41
|
+
let dot = 0,
|
|
42
|
+
normA = 0,
|
|
43
|
+
normB = 0;
|
|
44
|
+
for (let i = 0; i < a.length; i++) {
|
|
45
|
+
dot += a[i] * b[i];
|
|
46
|
+
normA += a[i] * a[i];
|
|
47
|
+
normB += b[i] * b[i];
|
|
48
|
+
}
|
|
49
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB) || 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
// ─── Brain Class ─────────────────────────────────────────────────
|
|
37
53
|
|
|
38
54
|
const DEFAULT_WEIGHTS: ScoringWeights = {
|
|
@@ -44,6 +60,16 @@ const DEFAULT_WEIGHTS: ScoringWeights = {
|
|
|
44
60
|
domainMatch: 0.15,
|
|
45
61
|
};
|
|
46
62
|
|
|
63
|
+
/** Weights used when an embedding provider is active — vector gets 0.15, semantic drops to 0.25. */
|
|
64
|
+
const DEFAULT_WEIGHTS_HYBRID: ScoringWeights = {
|
|
65
|
+
semantic: 0.25,
|
|
66
|
+
vector: 0.15,
|
|
67
|
+
severity: 0.15,
|
|
68
|
+
temporalDecay: 0.15,
|
|
69
|
+
tagOverlap: 0.15,
|
|
70
|
+
domainMatch: 0.15,
|
|
71
|
+
};
|
|
72
|
+
|
|
47
73
|
const WEIGHT_BOUND = 0.15;
|
|
48
74
|
const FEEDBACK_THRESHOLD = 30;
|
|
49
75
|
const DUPLICATE_BLOCK_THRESHOLD = 0.8;
|
|
@@ -53,16 +79,24 @@ const RECENCY_HALF_LIFE_DAYS = 365;
|
|
|
53
79
|
export class Brain {
|
|
54
80
|
private vault: Vault;
|
|
55
81
|
private vaultManager: VaultManager | undefined;
|
|
82
|
+
private embeddingProvider: EmbeddingProvider | undefined;
|
|
56
83
|
private vocabulary: Map<string, number> = new Map();
|
|
57
84
|
private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
|
|
58
85
|
|
|
59
|
-
constructor(vault: Vault, vaultManager?: VaultManager) {
|
|
86
|
+
constructor(vault: Vault, vaultManager?: VaultManager, embeddingProvider?: EmbeddingProvider) {
|
|
60
87
|
this.vault = vault;
|
|
61
88
|
this.vaultManager = vaultManager;
|
|
89
|
+
this.embeddingProvider = embeddingProvider;
|
|
62
90
|
this.loadVocabularyFromDb();
|
|
63
91
|
this.recomputeWeights();
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
/** Set or replace the embedding provider at runtime. */
|
|
95
|
+
setEmbeddingProvider(provider: EmbeddingProvider | undefined): void {
|
|
96
|
+
this.embeddingProvider = provider;
|
|
97
|
+
this.recomputeWeights();
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
async intelligentSearch(query: string, options?: SearchOptions): Promise<RankedResult[]> {
|
|
67
101
|
const limit = options?.limit ?? 10;
|
|
68
102
|
const fetchLimit = Math.max(limit * 3, 30);
|
|
@@ -91,9 +125,8 @@ export class Brain {
|
|
|
91
125
|
});
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
if (rawResults.length === 0) return [];
|
|
128
|
+
if (rawResults.length === 0 && !this.embeddingProvider) return [];
|
|
95
129
|
|
|
96
|
-
const seedCount = rawResults.length;
|
|
97
130
|
const queryTokens = tokenize(query);
|
|
98
131
|
const queryTags = options?.tags ?? [];
|
|
99
132
|
const queryDomain = options?.domain;
|
|
@@ -105,9 +138,53 @@ export class Brain {
|
|
|
105
138
|
? calculateTfIdf(queryTokens, this.vocabulary)
|
|
106
139
|
: null;
|
|
107
140
|
|
|
141
|
+
// ── Vector recall: embed query and merge cosineSearch candidates ──
|
|
142
|
+
let queryEmbedding: number[] | null = null;
|
|
143
|
+
const vectorSimilarityMap = new Map<string, number>();
|
|
144
|
+
|
|
145
|
+
if (this.embeddingProvider) {
|
|
146
|
+
try {
|
|
147
|
+
const embResult = await this.embeddingProvider.embed([query]);
|
|
148
|
+
if (embResult.vectors.length > 0 && embResult.vectors[0].length > 0) {
|
|
149
|
+
queryEmbedding = embResult.vectors[0];
|
|
150
|
+
const provider = this.vault.getProvider();
|
|
151
|
+
const vectorHits = cosineSearch(provider, queryEmbedding, fetchLimit);
|
|
152
|
+
|
|
153
|
+
// Build similarity lookup and merge vector-only candidates into rawResults
|
|
154
|
+
const ftsIds = new Set(rawResults.map((r) => r.entry.id));
|
|
155
|
+
for (const hit of vectorHits) {
|
|
156
|
+
vectorSimilarityMap.set(hit.entryId, hit.similarity);
|
|
157
|
+
if (!ftsIds.has(hit.entryId)) {
|
|
158
|
+
// Vector-only candidate — fetch full entry and add to pool
|
|
159
|
+
const entry = this.vault.get(hit.entryId);
|
|
160
|
+
if (entry) {
|
|
161
|
+
rawResults.push({ entry, score: hit.similarity });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Embedding failed — graceful degradation, continue with FTS-only
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (rawResults.length === 0) return [];
|
|
172
|
+
|
|
173
|
+
const seedCount = rawResults.length;
|
|
174
|
+
|
|
108
175
|
const ranked = rawResults.map((result) => {
|
|
109
176
|
const entry = result.entry;
|
|
110
|
-
const
|
|
177
|
+
const vectorSim = vectorSimilarityMap.get(entry.id) ?? null;
|
|
178
|
+
const breakdown = this.scoreEntry(
|
|
179
|
+
entry,
|
|
180
|
+
queryTokens,
|
|
181
|
+
queryTags,
|
|
182
|
+
queryDomain,
|
|
183
|
+
now,
|
|
184
|
+
queryVec,
|
|
185
|
+
queryEmbedding,
|
|
186
|
+
vectorSim,
|
|
187
|
+
);
|
|
111
188
|
return { entry, score: breakdown.total, breakdown };
|
|
112
189
|
});
|
|
113
190
|
|
|
@@ -499,6 +576,8 @@ export class Brain {
|
|
|
499
576
|
queryDomain: string | undefined,
|
|
500
577
|
now: number,
|
|
501
578
|
queryVec: Map<string, number> | null = null,
|
|
579
|
+
queryEmbedding: number[] | null = null,
|
|
580
|
+
precomputedVectorSim: number | null = null,
|
|
502
581
|
): ScoreBreakdown {
|
|
503
582
|
const w = this.weights;
|
|
504
583
|
|
|
@@ -523,7 +602,22 @@ export class Brain {
|
|
|
523
602
|
|
|
524
603
|
const domainMatch = queryDomain && entry.domain === queryDomain ? 1.0 : 0;
|
|
525
604
|
|
|
526
|
-
|
|
605
|
+
// Use precomputed cosine similarity from the vector recall phase when available.
|
|
606
|
+
// If we have a query embedding but no precomputed similarity (entry wasn't in
|
|
607
|
+
// cosineSearch results), try to compute it from the entry's stored vector.
|
|
608
|
+
let vector = 0;
|
|
609
|
+
if (precomputedVectorSim !== null) {
|
|
610
|
+
vector = precomputedVectorSim;
|
|
611
|
+
} else if (queryEmbedding) {
|
|
612
|
+
try {
|
|
613
|
+
const stored = getVector(this.vault.getProvider(), entry.id);
|
|
614
|
+
if (stored) {
|
|
615
|
+
vector = vectorCosineSimilarity(queryEmbedding, stored.vector);
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
// No stored vector — vector stays 0
|
|
619
|
+
}
|
|
620
|
+
}
|
|
527
621
|
|
|
528
622
|
const total =
|
|
529
623
|
w.semantic * semantic +
|
|
@@ -681,7 +775,9 @@ export class Brain {
|
|
|
681
775
|
}
|
|
682
776
|
).count;
|
|
683
777
|
if (feedbackCount < FEEDBACK_THRESHOLD) {
|
|
684
|
-
this.weights =
|
|
778
|
+
this.weights = this.embeddingProvider
|
|
779
|
+
? { ...DEFAULT_WEIGHTS_HYBRID }
|
|
780
|
+
: { ...DEFAULT_WEIGHTS };
|
|
685
781
|
return;
|
|
686
782
|
}
|
|
687
783
|
|
|
@@ -707,8 +803,20 @@ export class Brain {
|
|
|
707
803
|
DEFAULT_WEIGHTS.semantic + WEIGHT_BOUND,
|
|
708
804
|
);
|
|
709
805
|
|
|
710
|
-
//
|
|
711
|
-
|
|
806
|
+
// When no embedding provider is configured, vector weight stays 0.
|
|
807
|
+
// When provider IS available, vector participates in weight adaptation.
|
|
808
|
+
if (!this.embeddingProvider) {
|
|
809
|
+
newWeights.vector = 0;
|
|
810
|
+
} else {
|
|
811
|
+
// With embeddings active, give vector a meaningful default weight
|
|
812
|
+
// by redistributing from semantic (the closest signal).
|
|
813
|
+
newWeights.vector = DEFAULT_WEIGHTS_HYBRID.vector;
|
|
814
|
+
newWeights.semantic = clamp(
|
|
815
|
+
newWeights.semantic - DEFAULT_WEIGHTS_HYBRID.vector,
|
|
816
|
+
DEFAULT_WEIGHTS.semantic - WEIGHT_BOUND,
|
|
817
|
+
DEFAULT_WEIGHTS.semantic + WEIGHT_BOUND,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
712
820
|
|
|
713
821
|
const remaining = 1.0 - newWeights.semantic - newWeights.vector;
|
|
714
822
|
const otherSum =
|