@jmylchreest/aide-plugin 0.0.39 → 0.0.42
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/package.json +1 -1
- package/skills/assess-findings/SKILL.md +167 -0
- package/skills/code-search/SKILL.md +12 -0
- package/skills/git/SKILL.md +10 -0
- package/skills/memorise/SKILL.md +89 -11
- package/skills/patterns/SKILL.md +182 -0
- package/skills/plan-swarm/SKILL.md +5 -0
- package/skills/ralph/SKILL.md +15 -8
- package/skills/review/SKILL.md +3 -0
- package/skills/swarm/SKILL.md +75 -24
- package/src/cli/config.ts +4 -1
- package/src/core/aide-client.ts +15 -12
- package/src/core/mcp-sync.ts +19 -3
- package/src/core/partial-memory.ts +46 -55
- package/src/core/persistence-logic.ts +28 -4
- package/src/core/session-init.ts +10 -6
- package/src/core/session-summary-logic.ts +9 -2
- package/src/core/todo-checker.ts +53 -18
- package/src/core/tool-tracking.ts +3 -5
- package/src/core/types.ts +21 -0
- package/src/lib/aide-downloader.ts +0 -8
- package/src/lib/hook-utils.ts +47 -115
- package/src/lib/hud.ts +2 -2
- package/src/lib/skills-registry.ts +32 -4
- package/src/lib/worktree.ts +11 -37
- package/src/opencode/hooks.ts +27 -1
- package/src/lib/aide-memory.ts +0 -400
package/src/core/types.ts
CHANGED
|
@@ -15,6 +15,27 @@ export interface AideConfig {
|
|
|
15
15
|
/** Auto-export on session end (default: false) */
|
|
16
16
|
autoExport?: boolean;
|
|
17
17
|
};
|
|
18
|
+
findings?: {
|
|
19
|
+
/** Complexity analyser settings */
|
|
20
|
+
complexity?: {
|
|
21
|
+
/** Cyclomatic complexity threshold (default: 10) */
|
|
22
|
+
threshold?: number;
|
|
23
|
+
};
|
|
24
|
+
/** Import coupling analyser settings */
|
|
25
|
+
coupling?: {
|
|
26
|
+
/** Fan-out threshold — max outgoing imports (default: 15) */
|
|
27
|
+
fanOut?: number;
|
|
28
|
+
/** Fan-in threshold — max incoming imports (default: 20) */
|
|
29
|
+
fanIn?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Code clone detection settings */
|
|
32
|
+
clones?: {
|
|
33
|
+
/** Sliding window size in tokens (default: 50) */
|
|
34
|
+
windowSize?: number;
|
|
35
|
+
/** Minimum clone size in lines (default: 6) */
|
|
36
|
+
minLines?: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
export const DEFAULT_CONFIG: AideConfig = {};
|
|
@@ -468,14 +468,6 @@ Then restart Claude Code to use the new version.`;
|
|
|
468
468
|
};
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
/**
|
|
472
|
-
* Synchronous version for simple existence check (no download, no version check)
|
|
473
|
-
* Use ensureAideBinary() for full functionality
|
|
474
|
-
*/
|
|
475
|
-
export function findAideBinarySync(cwd?: string): string | null {
|
|
476
|
-
return findAideBinary(cwd);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
471
|
// --- CLI Mode ---
|
|
480
472
|
// Run as standalone script for postinstall or manual download
|
|
481
473
|
|
package/src/lib/hook-utils.ts
CHANGED
|
@@ -1,80 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared utilities for Claude Code hooks
|
|
2
|
+
* Shared utilities for Claude Code hooks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* readStdin() is the only unique implementation here. All other functions
|
|
5
|
+
* are convenience wrappers around src/core/aide-client.ts that resolve the
|
|
6
|
+
* binary from AIDE_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT automatically.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
findAideBinary as clientFindBinary,
|
|
11
|
+
runAide as clientRunAide,
|
|
12
|
+
setState,
|
|
13
|
+
getState,
|
|
14
|
+
deleteState,
|
|
15
|
+
clearAgentState as clientClearAgentState,
|
|
16
|
+
sanitizeForLog,
|
|
17
|
+
shellEscape,
|
|
18
|
+
} from "../core/aide-client.js";
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
export { sanitizeForLog, shellEscape };
|
|
21
|
+
|
|
22
|
+
/** Maximum stdin payload size: 50 MiB. Prevents unbounded memory allocation. */
|
|
23
|
+
const MAX_STDIN_BYTES = 50 * 1024 * 1024;
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
|
-
* Read JSON input from stdin (used by all hooks)
|
|
26
|
+
* Read JSON input from stdin (used by all hooks).
|
|
27
|
+
* Rejects payloads exceeding MAX_STDIN_BYTES.
|
|
17
28
|
*/
|
|
18
29
|
export async function readStdin(): Promise<string> {
|
|
19
30
|
const chunks: Buffer[] = [];
|
|
31
|
+
let totalBytes = 0;
|
|
20
32
|
for await (const chunk of process.stdin) {
|
|
33
|
+
totalBytes += chunk.length;
|
|
34
|
+
if (totalBytes > MAX_STDIN_BYTES) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`stdin payload exceeds ${MAX_STDIN_BYTES} bytes, rejecting`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
21
39
|
chunks.push(chunk);
|
|
22
40
|
}
|
|
23
41
|
return Buffer.concat(chunks).toString("utf-8");
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
* Get the plugin root directory from environment variables.
|
|
46
|
+
*/
|
|
47
|
+
function getPluginRoot(): string | undefined {
|
|
48
|
+
return process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the aide binary — Claude Code convenience wrapper.
|
|
32
53
|
*
|
|
33
|
-
*
|
|
54
|
+
* Reads AIDE_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT from the environment
|
|
55
|
+
* and delegates to the platform-agnostic aide-client implementation.
|
|
34
56
|
*/
|
|
35
57
|
export function findAideBinary(cwd?: string): string | null {
|
|
36
|
-
|
|
37
|
-
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
|
|
38
|
-
// Resolve symlinks (e.g., src-office -> dtkr4-cnjjf)
|
|
39
|
-
if (pluginRoot) {
|
|
40
|
-
try {
|
|
41
|
-
pluginRoot = realpathSync(pluginRoot);
|
|
42
|
-
} catch (err) {
|
|
43
|
-
debug(SOURCE, `realpath failed for pluginRoot ${pluginRoot}: ${err}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (pluginRoot) {
|
|
47
|
-
const pluginBinary = join(pluginRoot, "bin", "aide");
|
|
48
|
-
if (existsSync(pluginBinary)) {
|
|
49
|
-
return pluginBinary;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// PATH fallback
|
|
54
|
-
try {
|
|
55
|
-
const result = execSync("which aide", { stdio: "pipe", timeout: 2000 })
|
|
56
|
-
.toString()
|
|
57
|
-
.trim();
|
|
58
|
-
if (result) return result;
|
|
59
|
-
} catch (err) {
|
|
60
|
-
debug(SOURCE, `aide not found in PATH: ${err}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return null;
|
|
58
|
+
return clientFindBinary({ cwd, pluginRoot: getPluginRoot() });
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
/**
|
|
67
|
-
*
|
|
62
|
+
* Run an aide command with the auto-discovered binary.
|
|
68
63
|
*/
|
|
69
|
-
export function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.replace(/\$/g, "\\$")
|
|
74
|
-
.replace(/`/g, "\\`")
|
|
75
|
-
.replace(/!/g, "\\!")
|
|
76
|
-
.replace(/\n/g, " ")
|
|
77
|
-
.slice(0, 1000);
|
|
64
|
+
export function runAide(cwd: string, args: string[]): string | null {
|
|
65
|
+
const binary = findAideBinary(cwd);
|
|
66
|
+
if (!binary) return null;
|
|
67
|
+
return clientRunAide(binary, cwd, args);
|
|
78
68
|
}
|
|
79
69
|
|
|
80
70
|
/**
|
|
@@ -88,16 +78,7 @@ export function setMemoryState(
|
|
|
88
78
|
): boolean {
|
|
89
79
|
const binary = findAideBinary(cwd);
|
|
90
80
|
if (!binary) return false;
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const args = ["state", "set", key, value];
|
|
94
|
-
if (agentId) args.push(`--agent=${agentId}`);
|
|
95
|
-
execFileSync(binary, args, { cwd, stdio: "pipe", timeout: 5000 });
|
|
96
|
-
return true;
|
|
97
|
-
} catch (err) {
|
|
98
|
-
debug(SOURCE, `setMemoryState failed for key=${key}: ${err}`);
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
81
|
+
return setState(binary, cwd, key, value, agentId);
|
|
101
82
|
}
|
|
102
83
|
|
|
103
84
|
/**
|
|
@@ -110,22 +91,7 @@ export function getMemoryState(
|
|
|
110
91
|
): string | null {
|
|
111
92
|
const binary = findAideBinary(cwd);
|
|
112
93
|
if (!binary) return null;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const args = ["state", "get", key];
|
|
116
|
-
if (agentId) args.push(`--agent=${agentId}`);
|
|
117
|
-
const output = execFileSync(binary, args, {
|
|
118
|
-
cwd,
|
|
119
|
-
encoding: "utf-8",
|
|
120
|
-
timeout: 5000,
|
|
121
|
-
});
|
|
122
|
-
// Parse output format: "key = value" or "[agent] key = value"
|
|
123
|
-
const match = output.match(/=\s*(.+)$/m);
|
|
124
|
-
return match ? match[1].trim() : null;
|
|
125
|
-
} catch (err) {
|
|
126
|
-
debug(SOURCE, `getMemoryState failed for key=${key}: ${err}`);
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
94
|
+
return getState(binary, cwd, key, agentId);
|
|
129
95
|
}
|
|
130
96
|
|
|
131
97
|
/**
|
|
@@ -138,16 +104,7 @@ export function deleteMemoryState(
|
|
|
138
104
|
): boolean {
|
|
139
105
|
const binary = findAideBinary(cwd);
|
|
140
106
|
if (!binary) return false;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
// For agent-specific keys, we need to construct the full key
|
|
144
|
-
const fullKey = agentId ? `agent:${agentId}:${key}` : key;
|
|
145
|
-
execFileSync(binary, ["state", "delete", fullKey], { cwd, stdio: "pipe" });
|
|
146
|
-
return true;
|
|
147
|
-
} catch (err) {
|
|
148
|
-
debug(SOURCE, `deleteMemoryState failed for key=${key}: ${err}`);
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
107
|
+
return deleteState(binary, cwd, key, agentId);
|
|
151
108
|
}
|
|
152
109
|
|
|
153
110
|
/**
|
|
@@ -156,30 +113,5 @@ export function deleteMemoryState(
|
|
|
156
113
|
export function clearAgentState(cwd: string, agentId: string): boolean {
|
|
157
114
|
const binary = findAideBinary(cwd);
|
|
158
115
|
if (!binary) return false;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
execFileSync(binary, ["state", "clear", `--agent=${agentId}`], {
|
|
162
|
-
cwd,
|
|
163
|
-
stdio: "pipe",
|
|
164
|
-
});
|
|
165
|
-
return true;
|
|
166
|
-
} catch (err) {
|
|
167
|
-
debug(SOURCE, `clearAgentState failed for agent=${agentId}: ${err}`);
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Run an aide command with proper escaping
|
|
174
|
-
*/
|
|
175
|
-
export function runAide(cwd: string, args: string[]): string | null {
|
|
176
|
-
const binary = findAideBinary(cwd);
|
|
177
|
-
if (!binary) return null;
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
return execFileSync(binary, args, { cwd, encoding: "utf-8" });
|
|
181
|
-
} catch (err) {
|
|
182
|
-
debug(SOURCE, `runAide failed: ${args.join(" ")}: ${err}`);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
116
|
+
return clientClearAgentState(binary, cwd, agentId);
|
|
185
117
|
}
|
package/src/lib/hud.ts
CHANGED
|
@@ -97,7 +97,7 @@ const ICONS = {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Get all agent states from aide
|
|
100
|
+
* Get all agent states from aide state store
|
|
101
101
|
*/
|
|
102
102
|
export function getAgentStates(cwd: string): AgentState[] {
|
|
103
103
|
const output = runAide(cwd, ["state", "list"]);
|
|
@@ -165,7 +165,7 @@ export function loadHudConfig(cwd: string): HudConfig {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* Get current session state from aide
|
|
168
|
+
* Get current session state from aide state store
|
|
169
169
|
*/
|
|
170
170
|
export function getSessionState(cwd: string): SessionState {
|
|
171
171
|
const state: SessionState = {
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
copyFileSync,
|
|
32
32
|
unlinkSync,
|
|
33
33
|
} from "fs";
|
|
34
|
-
import { join, basename } from "path";
|
|
34
|
+
import { join, basename, resolve } from "path";
|
|
35
35
|
import { homedir } from "os";
|
|
36
36
|
|
|
37
37
|
export interface SkillMetadata {
|
|
@@ -121,6 +121,30 @@ export function saveRegistry(cwd: string, registry: SkillsRegistry): void {
|
|
|
121
121
|
writeFileSync(join(cwd, REGISTRY_FILE), JSON.stringify(registry, null, 2));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Sanitize a skill name to prevent path traversal.
|
|
126
|
+
* Strips path separators and rejects names that would escape the target directory.
|
|
127
|
+
*/
|
|
128
|
+
function sanitizeSkillName(name: string): string {
|
|
129
|
+
// Take only the basename to strip any directory components
|
|
130
|
+
const safe = basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
131
|
+
if (!safe || safe === "." || safe === "..") {
|
|
132
|
+
throw new Error(`Invalid skill name: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
return safe;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate that a resolved path stays within the expected directory.
|
|
139
|
+
*/
|
|
140
|
+
function assertWithinDir(filePath: string, dir: string): void {
|
|
141
|
+
const resolved = resolve(filePath);
|
|
142
|
+
const resolvedDir = resolve(dir);
|
|
143
|
+
if (!resolved.startsWith(resolvedDir + "/") && resolved !== resolvedDir) {
|
|
144
|
+
throw new Error(`Path traversal detected: ${filePath} escapes ${dir}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
/**
|
|
125
149
|
* Install a skill from skills.sh or a URL
|
|
126
150
|
*
|
|
@@ -201,8 +225,10 @@ export async function installSkill(
|
|
|
201
225
|
version = meta.version || version;
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
//
|
|
228
|
+
// Sanitize name and write skill file
|
|
229
|
+
name = sanitizeSkillName(name);
|
|
205
230
|
const skillPath = join(targetDir, `${name}.md`);
|
|
231
|
+
assertWithinDir(skillPath, targetDir);
|
|
206
232
|
writeFileSync(skillPath, content);
|
|
207
233
|
|
|
208
234
|
// Update registry
|
|
@@ -242,8 +268,10 @@ export function uninstallSkill(cwd: string, name: string): boolean {
|
|
|
242
268
|
return false;
|
|
243
269
|
}
|
|
244
270
|
|
|
245
|
-
//
|
|
246
|
-
const
|
|
271
|
+
// Sanitize name and remove file
|
|
272
|
+
const safeName = sanitizeSkillName(name);
|
|
273
|
+
const skillPath = join(cwd, SKILLS_DIR, `${safeName}.md`);
|
|
274
|
+
assertWithinDir(skillPath, join(cwd, SKILLS_DIR));
|
|
247
275
|
if (existsSync(skillPath)) {
|
|
248
276
|
try {
|
|
249
277
|
unlinkSync(skillPath);
|
package/src/lib/worktree.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* 3. Error handling and edge cases are better tested
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { execFileSync } from "child_process";
|
|
27
27
|
import {
|
|
28
28
|
existsSync,
|
|
29
29
|
mkdirSync,
|
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
statSync,
|
|
35
35
|
} from "fs";
|
|
36
36
|
import { join } from "path";
|
|
37
|
+
import { debug } from "./logger.js";
|
|
38
|
+
|
|
39
|
+
const SOURCE = "worktree";
|
|
37
40
|
|
|
38
41
|
export type WorktreeStatus = "active" | "agent-complete" | "merged";
|
|
39
42
|
|
|
@@ -129,7 +132,7 @@ export function createWorktree(
|
|
|
129
132
|
agentId: string,
|
|
130
133
|
): Worktree | null {
|
|
131
134
|
if (!isGitRepo(cwd)) {
|
|
132
|
-
|
|
135
|
+
debug(SOURCE, "Not a git repository");
|
|
133
136
|
return null;
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -184,7 +187,7 @@ export function createWorktree(
|
|
|
184
187
|
|
|
185
188
|
return worktree;
|
|
186
189
|
} catch (error) {
|
|
187
|
-
|
|
190
|
+
debug(SOURCE, `Failed to create worktree: ${error}`);
|
|
188
191
|
return null;
|
|
189
192
|
}
|
|
190
193
|
}
|
|
@@ -197,7 +200,7 @@ export function removeWorktree(cwd: string, name: string): boolean {
|
|
|
197
200
|
const worktree = state.active.find((w) => w.name === name);
|
|
198
201
|
|
|
199
202
|
if (!worktree) {
|
|
200
|
-
|
|
203
|
+
debug(SOURCE, `Worktree not found: ${name}`);
|
|
201
204
|
return false;
|
|
202
205
|
}
|
|
203
206
|
|
|
@@ -220,7 +223,7 @@ export function removeWorktree(cwd: string, name: string): boolean {
|
|
|
220
223
|
|
|
221
224
|
return true;
|
|
222
225
|
} catch (error) {
|
|
223
|
-
|
|
226
|
+
debug(SOURCE, `Failed to remove worktree: ${error}`);
|
|
224
227
|
return false;
|
|
225
228
|
}
|
|
226
229
|
}
|
|
@@ -233,7 +236,7 @@ export function mergeWorktree(cwd: string, name: string): boolean {
|
|
|
233
236
|
const worktree = state.active.find((w) => w.name === name);
|
|
234
237
|
|
|
235
238
|
if (!worktree) {
|
|
236
|
-
|
|
239
|
+
debug(SOURCE, `Worktree not found: ${name}`);
|
|
237
240
|
return false;
|
|
238
241
|
}
|
|
239
242
|
|
|
@@ -250,7 +253,7 @@ export function mergeWorktree(cwd: string, name: string): boolean {
|
|
|
250
253
|
|
|
251
254
|
return true;
|
|
252
255
|
} catch (error) {
|
|
253
|
-
|
|
256
|
+
debug(SOURCE, `Failed to merge worktree: ${error}`);
|
|
254
257
|
return false;
|
|
255
258
|
}
|
|
256
259
|
}
|
|
@@ -315,7 +318,7 @@ export function registerWorktree(
|
|
|
315
318
|
agentId: string,
|
|
316
319
|
): Worktree | null {
|
|
317
320
|
if (!existsSync(worktreePath)) {
|
|
318
|
-
|
|
321
|
+
debug(SOURCE, `Worktree path does not exist: ${worktreePath}`);
|
|
319
322
|
return null;
|
|
320
323
|
}
|
|
321
324
|
|
|
@@ -452,32 +455,3 @@ export function getWorktreesReadyForMerge(cwd: string): Worktree[] {
|
|
|
452
455
|
const state = loadWorktreeState(cwd);
|
|
453
456
|
return state.active.filter((w) => w.status === "agent-complete");
|
|
454
457
|
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Execute command in a worktree
|
|
458
|
-
* WARNING: This function executes arbitrary shell commands. Only call with trusted input.
|
|
459
|
-
* Used internally for running git commands and build tools in isolated worktrees.
|
|
460
|
-
*/
|
|
461
|
-
export function execInWorktree(
|
|
462
|
-
cwd: string,
|
|
463
|
-
name: string,
|
|
464
|
-
command: string,
|
|
465
|
-
): string | null {
|
|
466
|
-
const state = loadWorktreeState(cwd);
|
|
467
|
-
const worktree = state.active.find((w) => w.name === name);
|
|
468
|
-
|
|
469
|
-
if (!worktree) {
|
|
470
|
-
console.error(`Worktree not found: ${name}`);
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
return execSync(command, {
|
|
476
|
-
cwd: worktree.path,
|
|
477
|
-
encoding: "utf-8",
|
|
478
|
-
});
|
|
479
|
-
} catch (error) {
|
|
480
|
-
console.error(`Command failed in worktree: ${error}`);
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
}
|
package/src/opencode/hooks.ts
CHANGED
|
@@ -372,6 +372,18 @@ async function handleSessionCreated(
|
|
|
372
372
|
if (state.initializedSessions.has(sessionId)) return;
|
|
373
373
|
state.initializedSessions.add(sessionId);
|
|
374
374
|
|
|
375
|
+
// Defensive cap: if sessions leak without cleanup, evict oldest entries.
|
|
376
|
+
// Normal operation has only 1-2 concurrent sessions.
|
|
377
|
+
if (state.initializedSessions.size > 100) {
|
|
378
|
+
const entries = Array.from(state.initializedSessions);
|
|
379
|
+
const keepFrom = Math.floor(entries.length / 2);
|
|
380
|
+
state.initializedSessions.clear();
|
|
381
|
+
for (let i = keepFrom; i < entries.length; i++) {
|
|
382
|
+
state.initializedSessions.add(entries[i]);
|
|
383
|
+
}
|
|
384
|
+
debug(SOURCE, `Evicted ${keepFrom} stale entries from initializedSessions`);
|
|
385
|
+
}
|
|
386
|
+
|
|
375
387
|
state.sessionState = initializeSession(sessionId, state.cwd);
|
|
376
388
|
|
|
377
389
|
// Track this session for per-session context injection
|
|
@@ -380,6 +392,16 @@ async function handleSessionCreated(
|
|
|
380
392
|
createdAt: new Date().toISOString(),
|
|
381
393
|
});
|
|
382
394
|
|
|
395
|
+
// Defensive cap for sessionInfoMap (mirrors initializedSessions cap)
|
|
396
|
+
if (state.sessionInfoMap.size > 100) {
|
|
397
|
+
const entries = Array.from(state.sessionInfoMap.keys());
|
|
398
|
+
const keepFrom = Math.floor(entries.length / 2);
|
|
399
|
+
for (let i = 0; i < keepFrom; i++) {
|
|
400
|
+
state.sessionInfoMap.delete(entries[i]);
|
|
401
|
+
}
|
|
402
|
+
debug(SOURCE, `Evicted ${keepFrom} stale entries from sessionInfoMap`);
|
|
403
|
+
}
|
|
404
|
+
|
|
383
405
|
// Register session as an "agent" in aide state for visibility
|
|
384
406
|
if (state.binary) {
|
|
385
407
|
try {
|
|
@@ -427,7 +449,11 @@ async function handleSessionIdle(
|
|
|
427
449
|
// Check persistence: if ralph/autopilot mode is active, re-prompt the session
|
|
428
450
|
if (state.binary) {
|
|
429
451
|
try {
|
|
430
|
-
const persistResult = checkPersistence(
|
|
452
|
+
const persistResult = checkPersistence(
|
|
453
|
+
state.binary,
|
|
454
|
+
state.cwd,
|
|
455
|
+
sessionId,
|
|
456
|
+
);
|
|
431
457
|
if (persistResult) {
|
|
432
458
|
const activeMode = getActiveMode(state.binary, state.cwd);
|
|
433
459
|
debug(
|