@soleri/core 9.4.0 → 9.6.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/claude-code-adapter.d.ts +27 -0
- package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code-adapter.js +111 -0
- package/dist/adapters/claude-code-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/registry.d.ts +21 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +44 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +10 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/brain/brain.d.ts +12 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +106 -44
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +36 -30
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/chat/agent-loop.js +1 -1
- package/dist/chat/agent-loop.js.map +1 -1
- package/dist/chat/notifications.d.ts.map +1 -1
- package/dist/chat/notifications.js +4 -0
- package/dist/chat/notifications.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +11 -5
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +141 -27
- package/dist/curator/curator.js.map +1 -1
- package/dist/hooks/candidate-scorer.d.ts +28 -0
- package/dist/hooks/candidate-scorer.d.ts.map +1 -0
- package/dist/hooks/candidate-scorer.js +20 -0
- package/dist/hooks/candidate-scorer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +1 -0
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/packs/index.d.ts +3 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +3 -2
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +23 -1
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js +50 -4
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/pack-installer.d.ts +10 -0
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +69 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/packs/pack-lifecycle.d.ts +50 -0
- package/dist/packs/pack-lifecycle.d.ts.map +1 -0
- package/dist/packs/pack-lifecycle.js +91 -0
- package/dist/packs/pack-lifecycle.js.map +1 -0
- package/dist/packs/types.d.ts +64 -44
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +9 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/persistence/sqlite-provider.d.ts +5 -1
- package/dist/persistence/sqlite-provider.d.ts.map +1 -1
- package/dist/persistence/sqlite-provider.js +22 -2
- package/dist/persistence/sqlite-provider.js.map +1 -1
- package/dist/planning/github-projection.d.ts +8 -8
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +42 -42
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +6 -1
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/plugins/types.d.ts +21 -21
- package/dist/queue/pipeline-runner.d.ts.map +1 -1
- package/dist/queue/pipeline-runner.js +4 -0
- package/dist/queue/pipeline-runner.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +9 -1
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +169 -0
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +133 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +128 -90
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +44 -11
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/shutdown-registry.d.ts +36 -0
- package/dist/runtime/shutdown-registry.d.ts.map +1 -0
- package/dist/runtime/shutdown-registry.js +74 -0
- package/dist/runtime/shutdown-registry.js.map +1 -0
- package/dist/runtime/types.d.ts +10 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/subagent/concurrency-manager.d.ts +29 -0
- package/dist/subagent/concurrency-manager.d.ts.map +1 -0
- package/dist/subagent/concurrency-manager.js +73 -0
- package/dist/subagent/concurrency-manager.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +41 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -0
- package/dist/subagent/dispatcher.js +259 -0
- package/dist/subagent/dispatcher.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +15 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/orphan-reaper.d.ts +37 -0
- package/dist/subagent/orphan-reaper.d.ts.map +1 -0
- package/dist/subagent/orphan-reaper.js +71 -0
- package/dist/subagent/orphan-reaper.js.map +1 -0
- package/dist/subagent/result-aggregator.d.ts +7 -0
- package/dist/subagent/result-aggregator.d.ts.map +1 -0
- package/dist/subagent/result-aggregator.js +57 -0
- package/dist/subagent/result-aggregator.js.map +1 -0
- package/dist/subagent/task-checkout.d.ts +36 -0
- package/dist/subagent/task-checkout.d.ts.map +1 -0
- package/dist/subagent/task-checkout.js +52 -0
- package/dist/subagent/task-checkout.js.map +1 -0
- package/dist/subagent/types.d.ts +114 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +9 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/workspace-resolver.d.ts +35 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -0
- package/dist/subagent/workspace-resolver.js +99 -0
- package/dist/subagent/workspace-resolver.js.map +1 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +49 -3
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/ws-server.d.ts.map +1 -1
- package/dist/transport/ws-server.js +7 -0
- package/dist/transport/ws-server.js.map +1 -1
- package/dist/vault/linking.d.ts +3 -4
- package/dist/vault/linking.d.ts.map +1 -1
- package/dist/vault/linking.js +79 -32
- package/dist/vault/linking.js.map +1 -1
- package/dist/vault/vault-maintenance.d.ts.map +1 -1
- package/dist/vault/vault-maintenance.js +7 -14
- package/dist/vault/vault-maintenance.js.map +1 -1
- package/dist/vault/vault-memories.d.ts.map +1 -1
- package/dist/vault/vault-memories.js +19 -9
- package/dist/vault/vault-memories.js.map +1 -1
- package/dist/vault/vault-schema.d.ts +1 -0
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +20 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +7 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
- package/src/__tests__/adapters/registry.test.ts +100 -0
- package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
- package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
- package/src/__tests__/subagent/dispatcher.test.ts +195 -0
- package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
- package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
- package/src/__tests__/subagent/task-checkout.test.ts +86 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
- package/src/adapters/claude-code-adapter.ts +163 -0
- package/src/adapters/index.ts +22 -0
- package/src/adapters/registry.ts +53 -0
- package/src/adapters/types.ts +114 -0
- package/src/brain/brain.ts +120 -46
- package/src/brain/intelligence.ts +42 -34
- package/src/chat/agent-loop.ts +1 -1
- package/src/chat/notifications.ts +4 -0
- package/src/control/intent-router.ts +10 -8
- package/src/curator/curator.ts +146 -29
- package/src/hooks/candidate-scorer.test.ts +76 -0
- package/src/hooks/candidate-scorer.ts +39 -0
- package/src/index.ts +40 -1
- package/src/llm/llm-client.ts +1 -0
- package/src/packs/index.ts +5 -1
- package/src/packs/lockfile.ts +70 -5
- package/src/packs/pack-installer.ts +78 -2
- package/src/packs/pack-lifecycle.ts +115 -0
- package/src/packs/pack-lockfile.test.ts +1 -1
- package/src/packs/pack-system.test.ts +1 -1
- package/src/packs/types.ts +40 -2
- package/src/persistence/sqlite-provider.ts +27 -2
- package/src/planning/github-projection.ts +48 -44
- package/src/planning/plan-lifecycle.ts +14 -1
- package/src/queue/pipeline-runner.ts +4 -0
- package/src/runtime/admin-setup-ops.test.ts +9 -4
- package/src/runtime/curator-extra-ops.test.ts +7 -0
- package/src/runtime/curator-extra-ops.ts +10 -1
- package/src/runtime/facades/curator-facade.test.ts +7 -0
- package/src/runtime/facades/memory-facade.ts +187 -0
- package/src/runtime/orchestrate-ops.ts +156 -4
- package/src/runtime/runtime.test.ts +50 -2
- package/src/runtime/runtime.ts +132 -89
- package/src/runtime/session-briefing.test.ts +94 -2
- package/src/runtime/session-briefing.ts +48 -12
- package/src/runtime/shutdown-registry.test.ts +151 -0
- package/src/runtime/shutdown-registry.ts +85 -0
- package/src/runtime/types.ts +10 -1
- package/src/subagent/concurrency-manager.ts +89 -0
- package/src/subagent/dispatcher.ts +326 -0
- package/src/subagent/index.ts +28 -0
- package/src/subagent/orphan-reaper.ts +82 -0
- package/src/subagent/result-aggregator.ts +66 -0
- package/src/subagent/task-checkout.ts +60 -0
- package/src/subagent/types.ts +138 -0
- package/src/subagent/workspace-resolver.ts +117 -0
- package/src/transport/http-server.ts +50 -3
- package/src/transport/ws-server.ts +8 -0
- package/src/vault/linking.test.ts +12 -0
- package/src/vault/linking.ts +90 -44
- package/src/vault/vault-maintenance.ts +11 -18
- package/src/vault/vault-memories.ts +21 -13
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +21 -0
- package/src/vault/vault.ts +8 -3
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent runtime engine — types for spawning, managing, and aggregating
|
|
3
|
+
* results from child agent processes.
|
|
4
|
+
*
|
|
5
|
+
* The SubagentDispatcher composes: TaskCheckout, WorkspaceResolver,
|
|
6
|
+
* ConcurrencyManager, OrphanReaper, and RuntimeAdapterRegistry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AdapterSessionState, AdapterTokenUsage } from '../adapters/types.js';
|
|
10
|
+
|
|
11
|
+
// ─── Task ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** A task to be dispatched to a subagent */
|
|
14
|
+
export interface SubagentTask {
|
|
15
|
+
/** Unique task identifier */
|
|
16
|
+
taskId: string;
|
|
17
|
+
/** The prompt or task description for the subagent */
|
|
18
|
+
prompt: string;
|
|
19
|
+
/** Working directory for execution */
|
|
20
|
+
workspace: string;
|
|
21
|
+
/** Runtime adapter type (e.g., 'claude-code', 'codex'). Falls back to registry default. */
|
|
22
|
+
runtime?: string;
|
|
23
|
+
/** Task IDs this task depends on (must complete first) */
|
|
24
|
+
dependencies?: string[];
|
|
25
|
+
/** Timeout in milliseconds. Default: 300_000 (5 min) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Additional context to pass to the adapter */
|
|
28
|
+
config?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Status ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Lifecycle status of a subagent task */
|
|
34
|
+
export type SubagentStatus = 'queued' | 'claimed' | 'running' | 'completed' | 'failed' | 'orphaned';
|
|
35
|
+
|
|
36
|
+
// ─── Result ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Result from a single subagent execution */
|
|
39
|
+
export interface SubagentResult {
|
|
40
|
+
/** Task ID this result belongs to */
|
|
41
|
+
taskId: string;
|
|
42
|
+
/** Final status */
|
|
43
|
+
status: SubagentStatus;
|
|
44
|
+
/** Exit code from the adapter (0 = success) */
|
|
45
|
+
exitCode: number;
|
|
46
|
+
/** Human-readable summary of what the subagent did */
|
|
47
|
+
summary?: string;
|
|
48
|
+
/** Token usage */
|
|
49
|
+
usage?: AdapterTokenUsage;
|
|
50
|
+
/** Session state for potential resume */
|
|
51
|
+
sessionState?: AdapterSessionState;
|
|
52
|
+
/** Files changed by this subagent */
|
|
53
|
+
filesChanged?: string[];
|
|
54
|
+
/** Error message if failed */
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Duration in milliseconds */
|
|
57
|
+
durationMs: number;
|
|
58
|
+
/** PID of the child process (if spawned) */
|
|
59
|
+
pid?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Dispatch Options ───────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Options controlling how tasks are dispatched */
|
|
65
|
+
export interface DispatchOptions {
|
|
66
|
+
/** Run tasks in parallel (default: true) */
|
|
67
|
+
parallel?: boolean;
|
|
68
|
+
/** Max concurrent subagents (default: 3) */
|
|
69
|
+
maxConcurrent?: number;
|
|
70
|
+
/** Isolate each task in a git worktree (default: false) */
|
|
71
|
+
worktreeIsolation?: boolean;
|
|
72
|
+
/** Global timeout per task in ms (default: 300_000) */
|
|
73
|
+
timeout?: number;
|
|
74
|
+
/** Callback for per-task status updates */
|
|
75
|
+
onTaskUpdate?: (taskId: string, status: SubagentStatus) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Aggregated Result ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Aggregated result from multiple subagent executions */
|
|
81
|
+
export interface AggregatedResult {
|
|
82
|
+
/** Overall status */
|
|
83
|
+
status: 'all-passed' | 'partial' | 'all-failed';
|
|
84
|
+
/** Total tasks dispatched */
|
|
85
|
+
totalTasks: number;
|
|
86
|
+
/** Count of completed tasks */
|
|
87
|
+
completed: number;
|
|
88
|
+
/** Count of failed tasks */
|
|
89
|
+
failed: number;
|
|
90
|
+
/** Sum of all token usage */
|
|
91
|
+
totalUsage: AdapterTokenUsage;
|
|
92
|
+
/** Deduplicated list of all files changed */
|
|
93
|
+
filesChanged: string[];
|
|
94
|
+
/** Combined summary from all tasks */
|
|
95
|
+
combinedSummary: string;
|
|
96
|
+
/** Total duration in milliseconds */
|
|
97
|
+
durationMs: number;
|
|
98
|
+
/** Per-task results */
|
|
99
|
+
results: SubagentResult[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Claim Info ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/** Information about a task claim */
|
|
105
|
+
export interface ClaimInfo {
|
|
106
|
+
/** Task ID */
|
|
107
|
+
taskId: string;
|
|
108
|
+
/** Agent/process that claimed this task */
|
|
109
|
+
claimerId: string;
|
|
110
|
+
/** When the claim was made */
|
|
111
|
+
claimedAt: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Worktree Info ──────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** Information about an active git worktree */
|
|
117
|
+
export interface WorktreeInfo {
|
|
118
|
+
/** Task ID this worktree is for */
|
|
119
|
+
taskId: string;
|
|
120
|
+
/** Absolute path to the worktree */
|
|
121
|
+
path: string;
|
|
122
|
+
/** Branch name (if created) */
|
|
123
|
+
branch?: string;
|
|
124
|
+
/** When the worktree was created */
|
|
125
|
+
createdAt: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Tracked Process ────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** A tracked child process for orphan detection */
|
|
131
|
+
export interface TrackedProcess {
|
|
132
|
+
/** Process ID */
|
|
133
|
+
pid: number;
|
|
134
|
+
/** Task ID this process is executing */
|
|
135
|
+
taskId: string;
|
|
136
|
+
/** When the process was registered */
|
|
137
|
+
registeredAt: number;
|
|
138
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceResolver — Git worktree isolation for subagent tasks.
|
|
3
|
+
*
|
|
4
|
+
* When isolation is requested, creates a dedicated git worktree per task
|
|
5
|
+
* at `<baseDir>/.soleri/worktrees/<taskId>/`. Falls back gracefully to the
|
|
6
|
+
* original workspace if git worktree creation fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import type { WorktreeInfo } from './types.js';
|
|
14
|
+
|
|
15
|
+
const EXEC_OPTS = { encoding: 'utf-8' as const, timeout: 30_000 };
|
|
16
|
+
|
|
17
|
+
export class WorkspaceResolver {
|
|
18
|
+
private readonly baseDir: string;
|
|
19
|
+
private readonly worktrees = new Map<string, WorktreeInfo>();
|
|
20
|
+
|
|
21
|
+
constructor(baseDir: string) {
|
|
22
|
+
this.baseDir = baseDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a workspace path for a task.
|
|
27
|
+
*
|
|
28
|
+
* If `isolate` is true, creates a git worktree at
|
|
29
|
+
* `<baseDir>/.soleri/worktrees/<taskId>/` on branch `subagent/<taskId>`.
|
|
30
|
+
* Returns the worktree path on success, or the original `workspace` on failure.
|
|
31
|
+
*
|
|
32
|
+
* If `isolate` is false, returns `workspace` as-is.
|
|
33
|
+
*/
|
|
34
|
+
resolve(taskId: string, workspace: string, isolate: boolean): string {
|
|
35
|
+
if (!isolate) {
|
|
36
|
+
return workspace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const worktreePath = join(this.baseDir, '.soleri', 'worktrees', taskId);
|
|
41
|
+
const branch = `subagent/${taskId}`;
|
|
42
|
+
|
|
43
|
+
// Ensure parent directory exists
|
|
44
|
+
const parentDir = join(this.baseDir, '.soleri', 'worktrees');
|
|
45
|
+
if (!existsSync(parentDir)) {
|
|
46
|
+
mkdirSync(parentDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
execSync(`git worktree add "${worktreePath}" -b "${branch}"`, {
|
|
50
|
+
...EXEC_OPTS,
|
|
51
|
+
cwd: this.baseDir,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const info: WorktreeInfo = {
|
|
55
|
+
taskId,
|
|
56
|
+
path: worktreePath,
|
|
57
|
+
branch,
|
|
58
|
+
createdAt: Date.now(),
|
|
59
|
+
};
|
|
60
|
+
this.worktrees.set(taskId, info);
|
|
61
|
+
|
|
62
|
+
return worktreePath;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Graceful fallback — log warning and return original workspace
|
|
65
|
+
console.warn(
|
|
66
|
+
`[WorkspaceResolver] Failed to create worktree for task "${taskId}":`,
|
|
67
|
+
err instanceof Error ? err.message : err,
|
|
68
|
+
);
|
|
69
|
+
return workspace;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove the worktree for a given task.
|
|
75
|
+
* Silently handles errors (e.g., worktree already removed).
|
|
76
|
+
*/
|
|
77
|
+
cleanup(taskId: string): void {
|
|
78
|
+
const info = this.worktrees.get(taskId);
|
|
79
|
+
if (!info) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execSync(`git worktree remove "${info.path}" --force`, { ...EXEC_OPTS, cwd: this.baseDir });
|
|
85
|
+
} catch {
|
|
86
|
+
// Silently ignore — worktree may already be gone
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Clean up the branch as well
|
|
90
|
+
if (info.branch) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(`git branch -D "${info.branch}"`, { ...EXEC_OPTS, cwd: this.baseDir });
|
|
93
|
+
} catch {
|
|
94
|
+
// Silently ignore — branch may not exist
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.worktrees.delete(taskId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Remove all active worktrees. */
|
|
102
|
+
cleanupAll(): void {
|
|
103
|
+
for (const taskId of Array.from(this.worktrees.keys())) {
|
|
104
|
+
this.cleanup(taskId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return all currently active worktrees. */
|
|
109
|
+
listActive(): WorktreeInfo[] {
|
|
110
|
+
return [...this.worktrees.values()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Check whether a worktree exists for the given task. */
|
|
114
|
+
isActive(taskId: string): boolean {
|
|
115
|
+
return this.worktrees.has(taskId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -151,7 +151,18 @@ export class HttpMcpServer {
|
|
|
151
151
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
152
152
|
|
|
153
153
|
if (method === 'POST') {
|
|
154
|
-
|
|
154
|
+
let body: unknown;
|
|
155
|
+
try {
|
|
156
|
+
body = await this.readBody(req);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const statusCode = (err as { statusCode?: number }).statusCode;
|
|
159
|
+
if (statusCode === 413) {
|
|
160
|
+
this.sendJSON(res, 413, { error: 'Request body too large' });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.sendJSON(res, 400, { error: 'Failed to read request body' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
155
166
|
|
|
156
167
|
if (sessionId) {
|
|
157
168
|
const session = this.sessions.get(sessionId);
|
|
@@ -241,10 +252,41 @@ export class HttpMcpServer {
|
|
|
241
252
|
}
|
|
242
253
|
|
|
243
254
|
private readBody(req: IncomingMessage): Promise<unknown> {
|
|
255
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
256
|
+
const BODY_TIMEOUT = 30_000; // 30 seconds
|
|
257
|
+
|
|
244
258
|
return new Promise((resolve, reject) => {
|
|
259
|
+
let size = 0;
|
|
245
260
|
const chunks: Buffer[] = [];
|
|
246
|
-
|
|
261
|
+
let settled = false;
|
|
262
|
+
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
if (!settled) {
|
|
265
|
+
settled = true;
|
|
266
|
+
req.destroy();
|
|
267
|
+
reject(new Error('Request body timeout'));
|
|
268
|
+
}
|
|
269
|
+
}, BODY_TIMEOUT);
|
|
270
|
+
|
|
271
|
+
const cleanup = () => clearTimeout(timer);
|
|
272
|
+
|
|
273
|
+
req.on('data', (chunk: Buffer) => {
|
|
274
|
+
size += chunk.length;
|
|
275
|
+
if (size > MAX_BODY_SIZE) {
|
|
276
|
+
if (!settled) {
|
|
277
|
+
settled = true;
|
|
278
|
+
cleanup();
|
|
279
|
+
req.destroy();
|
|
280
|
+
reject(Object.assign(new Error('Request body too large'), { statusCode: 413 }));
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
chunks.push(chunk);
|
|
285
|
+
});
|
|
247
286
|
req.on('end', () => {
|
|
287
|
+
if (settled) return;
|
|
288
|
+
settled = true;
|
|
289
|
+
cleanup();
|
|
248
290
|
try {
|
|
249
291
|
const text = Buffer.concat(chunks).toString('utf-8');
|
|
250
292
|
resolve(text.length > 0 ? JSON.parse(text) : {});
|
|
@@ -252,7 +294,12 @@ export class HttpMcpServer {
|
|
|
252
294
|
reject(e);
|
|
253
295
|
}
|
|
254
296
|
});
|
|
255
|
-
req.on('error',
|
|
297
|
+
req.on('error', (e) => {
|
|
298
|
+
if (settled) return;
|
|
299
|
+
settled = true;
|
|
300
|
+
cleanup();
|
|
301
|
+
reject(e);
|
|
302
|
+
});
|
|
256
303
|
});
|
|
257
304
|
}
|
|
258
305
|
|
|
@@ -230,11 +230,19 @@ export class WsMcpServer {
|
|
|
230
230
|
|
|
231
231
|
// Set up frame reader
|
|
232
232
|
const maxSize = this.config.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE;
|
|
233
|
+
const maxBufferSize = maxSize; // raw buffer limit matches max message size (1 MB default)
|
|
233
234
|
let buffer = Buffer.alloc(0);
|
|
234
235
|
|
|
235
236
|
socket.on('data', (chunk: Buffer) => {
|
|
236
237
|
buffer = Buffer.concat([buffer, chunk]);
|
|
237
238
|
|
|
239
|
+
// Guard against unbounded buffer growth (e.g. slow-drip DoS with no complete frames)
|
|
240
|
+
if (buffer.length > maxBufferSize) {
|
|
241
|
+
this.sendClose(socket, 1009, 'Buffer exceeded max size');
|
|
242
|
+
socket.destroy();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
// Process all complete frames in the buffer
|
|
239
247
|
while (buffer.length >= 2) {
|
|
240
248
|
const frame = this.parseFrame(buffer);
|
|
@@ -68,6 +68,12 @@ class LinkingMockDB implements PersistenceProvider {
|
|
|
68
68
|
get<T>(sql: string, params?: unknown[]): T | undefined {
|
|
69
69
|
const p = params ?? [];
|
|
70
70
|
if (sql.includes('COUNT(*)')) {
|
|
71
|
+
if (sql.includes('NOT IN')) {
|
|
72
|
+
// Count orphan entries (no links)
|
|
73
|
+
const linkedIds = new Set(this.links.flatMap((l) => [l.source_id, l.target_id]));
|
|
74
|
+
const count = this.entries.filter((e) => !linkedIds.has(e.id)).length;
|
|
75
|
+
return { count } as T;
|
|
76
|
+
}
|
|
71
77
|
const id = p[0] as string;
|
|
72
78
|
const count = this.links.filter((l) => l.source_id === id || l.target_id === id).length;
|
|
73
79
|
return { count } as T;
|
|
@@ -102,6 +108,12 @@ class LinkingMockDB implements PersistenceProvider {
|
|
|
102
108
|
const ids = new Set(p.slice(0, half) as string[]);
|
|
103
109
|
return this.links.filter((l) => ids.has(l.source_id) || ids.has(l.target_id)) as T[];
|
|
104
110
|
}
|
|
111
|
+
if (sql.includes('FROM entries WHERE id IN')) {
|
|
112
|
+
const ids = new Set(p as string[]);
|
|
113
|
+
return this.entries
|
|
114
|
+
.filter((e) => ids.has(e.id))
|
|
115
|
+
.map((e) => ({ id: e.id, title: e.title, type: e.type, domain: e.domain })) as T[];
|
|
116
|
+
}
|
|
105
117
|
if (sql.includes('NOT IN')) {
|
|
106
118
|
const limit = p[0] as number;
|
|
107
119
|
const linkedIds = new Set(this.links.flatMap((l) => [l.source_id, l.target_id]));
|
package/src/vault/linking.ts
CHANGED
|
@@ -145,6 +145,7 @@ export class LinkManager {
|
|
|
145
145
|
/**
|
|
146
146
|
* Walk the link graph from a starting entry up to `depth` hops.
|
|
147
147
|
* BFS — walks both outgoing and incoming links (undirected).
|
|
148
|
+
* Batch-loads all links per frontier level to avoid N+1 queries.
|
|
148
149
|
*/
|
|
149
150
|
traverse(entryId: string, depth: number = 2): LinkedEntry[] {
|
|
150
151
|
const visited = new Set<string>([entryId]);
|
|
@@ -152,9 +153,47 @@ export class LinkManager {
|
|
|
152
153
|
let frontier = [entryId];
|
|
153
154
|
|
|
154
155
|
for (let d = 0; d < depth && frontier.length > 0; d++) {
|
|
156
|
+
// Batch-load all links for entire frontier in one query
|
|
157
|
+
const allLinks = this.getAllLinksForEntries(frontier);
|
|
158
|
+
|
|
159
|
+
// Collect unvisited neighbor IDs
|
|
160
|
+
const neighborMap = new Map<
|
|
161
|
+
string,
|
|
162
|
+
{ link: VaultLink; direction: 'outgoing' | 'incoming' }
|
|
163
|
+
>();
|
|
164
|
+
for (const link of allLinks) {
|
|
165
|
+
// Outgoing: source is in frontier, target is the neighbor
|
|
166
|
+
if (frontier.includes(link.sourceId) && !visited.has(link.targetId)) {
|
|
167
|
+
if (!neighborMap.has(link.targetId)) {
|
|
168
|
+
neighborMap.set(link.targetId, { link, direction: 'outgoing' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Incoming: target is in frontier, source is the neighbor
|
|
172
|
+
if (frontier.includes(link.targetId) && !visited.has(link.sourceId)) {
|
|
173
|
+
if (!neighborMap.has(link.sourceId)) {
|
|
174
|
+
neighborMap.set(link.sourceId, { link, direction: 'incoming' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (neighborMap.size === 0) break;
|
|
180
|
+
|
|
181
|
+
// Batch-load entry metadata for all neighbors in one query
|
|
182
|
+
const neighborIds = [...neighborMap.keys()];
|
|
183
|
+
const metaMap = this.getEntryMetaBatch(neighborIds);
|
|
184
|
+
|
|
155
185
|
const nextFrontier: string[] = [];
|
|
156
|
-
for (const
|
|
157
|
-
|
|
186
|
+
for (const [neighborId, { link, direction }] of neighborMap) {
|
|
187
|
+
visited.add(neighborId);
|
|
188
|
+
nextFrontier.push(neighborId);
|
|
189
|
+
const entry = metaMap.get(neighborId);
|
|
190
|
+
if (!entry) continue;
|
|
191
|
+
result.push({
|
|
192
|
+
...entry,
|
|
193
|
+
linkType: link.linkType,
|
|
194
|
+
linkDirection: direction,
|
|
195
|
+
linkNote: link.note,
|
|
196
|
+
});
|
|
158
197
|
}
|
|
159
198
|
frontier = nextFrontier;
|
|
160
199
|
}
|
|
@@ -162,43 +201,6 @@ export class LinkManager {
|
|
|
162
201
|
return result;
|
|
163
202
|
}
|
|
164
203
|
|
|
165
|
-
/** Collect unvisited outgoing and incoming neighbors for BFS. */
|
|
166
|
-
private collectNeighbors(
|
|
167
|
-
currentId: string,
|
|
168
|
-
visited: Set<string>,
|
|
169
|
-
nextFrontier: string[],
|
|
170
|
-
result: LinkedEntry[],
|
|
171
|
-
): void {
|
|
172
|
-
for (const link of this.getLinks(currentId)) {
|
|
173
|
-
this.visitNeighbor(link.targetId, link, 'outgoing', visited, nextFrontier, result);
|
|
174
|
-
}
|
|
175
|
-
for (const link of this.getBacklinks(currentId)) {
|
|
176
|
-
this.visitNeighbor(link.sourceId, link, 'incoming', visited, nextFrontier, result);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Visit a single neighbor node if not already visited. */
|
|
181
|
-
private visitNeighbor(
|
|
182
|
-
neighborId: string,
|
|
183
|
-
link: VaultLink,
|
|
184
|
-
direction: 'outgoing' | 'incoming',
|
|
185
|
-
visited: Set<string>,
|
|
186
|
-
nextFrontier: string[],
|
|
187
|
-
result: LinkedEntry[],
|
|
188
|
-
): void {
|
|
189
|
-
if (visited.has(neighborId)) return;
|
|
190
|
-
visited.add(neighborId);
|
|
191
|
-
nextFrontier.push(neighborId);
|
|
192
|
-
const entry = this.getEntryMeta(neighborId);
|
|
193
|
-
if (!entry) return;
|
|
194
|
-
result.push({
|
|
195
|
-
...entry,
|
|
196
|
-
linkType: link.linkType,
|
|
197
|
-
linkDirection: direction,
|
|
198
|
-
linkNote: link.note,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
204
|
// ── Bulk Queries ────────────────────────────────────────────────────
|
|
203
205
|
|
|
204
206
|
/** Get all links where either source or target is in the given ID set. */
|
|
@@ -317,20 +319,43 @@ export class LinkManager {
|
|
|
317
319
|
const batchSize = opts?.batchSize ?? 50;
|
|
318
320
|
const start = Date.now();
|
|
319
321
|
|
|
320
|
-
const orphans = this.getOrphans(10000);
|
|
321
322
|
let processed = 0;
|
|
322
323
|
let linksCreated = 0;
|
|
323
324
|
const preview: Array<{ sourceId: string; targetId: string; linkType: string; score: number }> =
|
|
324
325
|
[];
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// Estimate total for progress reporting (single COUNT query)
|
|
328
|
+
let totalEstimate = 0;
|
|
329
|
+
try {
|
|
330
|
+
const countRow = this.provider.get<{ count: number }>(
|
|
331
|
+
`SELECT COUNT(*) as count FROM entries
|
|
332
|
+
WHERE id NOT IN (SELECT source_id FROM vault_links)
|
|
333
|
+
AND id NOT IN (SELECT target_id FROM vault_links)`,
|
|
334
|
+
);
|
|
335
|
+
totalEstimate = countRow?.count ?? 0;
|
|
336
|
+
} catch {
|
|
337
|
+
// fall through with 0
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Process orphans in batches of batchSize instead of loading all at once.
|
|
341
|
+
// After each batch, successfully linked entries are no longer orphans,
|
|
342
|
+
// so the next getOrphans() call returns the next set.
|
|
343
|
+
// For dry-run mode, we must track processed IDs to avoid re-fetching the same orphans.
|
|
344
|
+
const processedIds = new Set<string>();
|
|
345
|
+
// eslint-disable-next-line no-constant-condition
|
|
346
|
+
while (true) {
|
|
347
|
+
const batch = this.getOrphans(batchSize);
|
|
348
|
+
// Filter out already-processed entries (relevant for dry-run where orphan status doesn't change)
|
|
349
|
+
const unprocessed = batch.filter((e) => !processedIds.has(e.id));
|
|
350
|
+
if (unprocessed.length === 0) break;
|
|
351
|
+
|
|
352
|
+
for (const entry of unprocessed) {
|
|
353
|
+
processedIds.add(entry.id);
|
|
329
354
|
const created = this.processOrphan(entry.id, threshold, maxLinks, dryRun, preview);
|
|
330
355
|
linksCreated += created;
|
|
331
356
|
processed++;
|
|
332
357
|
}
|
|
333
|
-
opts?.onProgress?.({ processed, total:
|
|
358
|
+
opts?.onProgress?.({ processed, total: totalEstimate, linksCreated });
|
|
334
359
|
}
|
|
335
360
|
|
|
336
361
|
return {
|
|
@@ -381,6 +406,27 @@ export class LinkManager {
|
|
|
381
406
|
return null;
|
|
382
407
|
}
|
|
383
408
|
}
|
|
409
|
+
|
|
410
|
+
/** Batch-load entry metadata for multiple IDs in a single query. */
|
|
411
|
+
private getEntryMetaBatch(
|
|
412
|
+
entryIds: string[],
|
|
413
|
+
): Map<string, Omit<LinkedEntry, 'linkType' | 'linkDirection' | 'linkNote'>> {
|
|
414
|
+
const result = new Map<string, Omit<LinkedEntry, 'linkType' | 'linkDirection' | 'linkNote'>>();
|
|
415
|
+
if (entryIds.length === 0) return result;
|
|
416
|
+
try {
|
|
417
|
+
const placeholders = entryIds.map(() => '?').join(',');
|
|
418
|
+
const rows = this.provider.all<{ id: string; title: string; type: string; domain: string }>(
|
|
419
|
+
`SELECT id, title, type, domain FROM entries WHERE id IN (${placeholders})`,
|
|
420
|
+
entryIds,
|
|
421
|
+
);
|
|
422
|
+
for (const row of rows) {
|
|
423
|
+
result.set(row.id, row);
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// graceful degradation
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
384
430
|
}
|
|
385
431
|
|
|
386
432
|
// ── Free-standing helpers ─────────────────────────────────────────────
|
|
@@ -111,25 +111,18 @@ export function archive(
|
|
|
111
111
|
const reason = options.reason ?? `Archived: older than ${options.olderThanDays} days`;
|
|
112
112
|
|
|
113
113
|
return provider.transaction(() => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, ?
|
|
125
|
-
FROM entries WHERE id = ?`,
|
|
126
|
-
[reason, id],
|
|
127
|
-
);
|
|
128
|
-
const result = provider.run('DELETE FROM entries WHERE id = ?', [id]);
|
|
129
|
-
archived += result.changes;
|
|
130
|
-
}
|
|
114
|
+
// Bulk INSERT INTO ... SELECT — copies all matching entries to archive in one query
|
|
115
|
+
provider.run(
|
|
116
|
+
`INSERT OR IGNORE INTO entries_archive (id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, archive_reason)
|
|
117
|
+
SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, ?
|
|
118
|
+
FROM entries WHERE updated_at < ?`,
|
|
119
|
+
[reason, cutoff],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Bulk DELETE — removes all archived entries in one query
|
|
123
|
+
const result = provider.run('DELETE FROM entries WHERE updated_at < ?', [cutoff]);
|
|
131
124
|
|
|
132
|
-
return { archived };
|
|
125
|
+
return { archived: result.changes };
|
|
133
126
|
});
|
|
134
127
|
}
|
|
135
128
|
|
|
@@ -299,21 +299,29 @@ export function memoryTopics(
|
|
|
299
299
|
export function memoriesByProject(
|
|
300
300
|
provider: PersistenceProvider,
|
|
301
301
|
): Array<{ project: string; count: number; memories: Memory[] }> {
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
// Single query fetching all non-archived memories, grouped client-side by project
|
|
303
|
+
const allRows = provider.all<Record<string, unknown>>(
|
|
304
|
+
'SELECT * FROM memories WHERE archived_at IS NULL ORDER BY project_path, created_at DESC',
|
|
304
305
|
);
|
|
305
306
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
307
|
+
const projectMap = new Map<string, Memory[]>();
|
|
308
|
+
for (const row of allRows) {
|
|
309
|
+
const memory = rowToMemory(row);
|
|
310
|
+
const project = memory.projectPath;
|
|
311
|
+
if (!projectMap.has(project)) {
|
|
312
|
+
projectMap.set(project, []);
|
|
313
|
+
}
|
|
314
|
+
projectMap.get(project)!.push(memory);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Sort by count descending (matching original behavior)
|
|
318
|
+
return [...projectMap.entries()]
|
|
319
|
+
.map(([project, memories]) => ({
|
|
320
|
+
project,
|
|
321
|
+
count: memories.length,
|
|
322
|
+
memories,
|
|
323
|
+
}))
|
|
324
|
+
.sort((a, b) => b.count - a.count);
|
|
317
325
|
}
|
|
318
326
|
|
|
319
327
|
// ── Helper ──────────────────────────────────────────────────────────────
|
|
@@ -10,6 +10,7 @@ import { Vault } from './vault.js';
|
|
|
10
10
|
import { Brain } from '../brain/brain.js';
|
|
11
11
|
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
12
12
|
|
|
13
|
+
const isCI = !!process.env.CI;
|
|
13
14
|
const DOMAINS = ['design', 'a11y', 'performance', 'security', 'architecture', 'testing', 'ux'];
|
|
14
15
|
const TYPES: IntelligenceEntry['type'][] = ['pattern', 'anti-pattern', 'rule', 'playbook'];
|
|
15
16
|
const SEVERITIES: IntelligenceEntry['severity'][] = ['critical', 'warning', 'suggestion'];
|
|
@@ -76,7 +77,7 @@ describe('Vault Scaling — 10K entries', () => {
|
|
|
76
77
|
const elapsed = performance.now() - start;
|
|
77
78
|
|
|
78
79
|
expect(results.length).toBeGreaterThan(0);
|
|
79
|
-
expect(elapsed).toBeLessThan(50);
|
|
80
|
+
expect(elapsed).toBeLessThan(isCI ? 500 : 50);
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
83
|
|
|
@@ -89,7 +90,7 @@ describe('Vault Scaling — 10K entries', () => {
|
|
|
89
90
|
const elapsed = performance.now() - start;
|
|
90
91
|
|
|
91
92
|
expect(results.length).toBeGreaterThan(0);
|
|
92
|
-
expect(elapsed).toBeLessThan(50);
|
|
93
|
+
expect(elapsed).toBeLessThan(isCI ? 500 : 50);
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
test('list with filters under 200ms at 10K', () => {
|