Browse Source

fix: compact resumable session file hints

dhaern 3 months ago
parent
commit
33d9e7b5c2

+ 1 - 1
docs/configuration.md

@@ -121,7 +121,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `tmux.main_pane_size` | number | `60` | Legacy alias for `multiplexer.main_pane_size` |
 | `sessionManager.maxSessionsPerAgent` | integer | `2` | Maximum remembered resumable child sessions per specialist type in the current orchestrator session (1–10). See [Session Management](session-management.md) |
 | `sessionManager.readContextMinLines` | integer | `10` | Minimum number of lines read from a file before it appears in resumable-session context (0–1000) |
-| `sessionManager.readContextMaxFiles` | integer | `8` | Maximum number of recent read-context files shown per remembered child session (0–50) |
+| `sessionManager.readContextMaxFiles` | integer | `4` | Maximum number of recent read-context files shown per remembered child session (0–50) |
 | `disabled_mcps` | string[] | `[]` | MCP server IDs to disable globally |
 | `fallback.enabled` | boolean | `false` | Enable model failover on timeout/error |
 | `fallback.timeoutMs` | number | `15000` | Time before aborting and trying next model |

+ 6 - 7
docs/session-management.md

@@ -39,8 +39,7 @@ The orchestrator sees a compact reminder in its system context, for example:
 
 ```text
 ### Resumable Sessions
-- explorer: exp-1 Search routing files
-  Context read by exp-1: src/router.ts (120 lines), src/routes/api.ts (74 lines)
+- explorer: exp-1 Search routing files — files: src/router.ts (120 lines), src/routes/api.ts (74 lines)
 - oracle: ora-1 Review auth architecture
 ```
 
@@ -50,7 +49,7 @@ orchestrator choose the right session to resume for related follow-up work.
 
 To keep the prompt small, read context only shows files where at least 10 lines
 were read, includes line counts, and caps each remembered session to the most
-recent 8 files by default. Both thresholds are configurable.
+recent 4 files by default. Both thresholds are configurable.
 
 On a related follow-up, the orchestrator can reuse that session instead of
 launching a fresh one. If the remembered child session no longer exists, the
@@ -109,7 +108,7 @@ Only add `sessionManager` if you want to change the default limits:
   "sessionManager": {
     "maxSessionsPerAgent": 2,
     "readContextMinLines": 10,
-    "readContextMaxFiles": 8
+    "readContextMaxFiles": 4
   }
 }
 ```
@@ -133,12 +132,12 @@ the prompt focused on substantial file reads.
 
 | Type | Default | Range | Meaning |
 |------|---------|-------|---------|
-| integer | `8` | `0`–`50` | Maximum number of recent read-context files shown per remembered child session |
+| integer | `4` | `0`–`50` | Maximum number of recent read-context files shown per remembered child session |
 
 Set this to `0` to keep session aliases but hide read-context file lists.
 
-Use a higher value if you often run several parallel threads per specialist. Use
-a lower value if you want fewer aliases in the orchestrator context.
+Use a higher value if you need more file hints per specialist thread. Use a
+lower value if you want fewer file hints in the orchestrator context.
 
 ---
 

+ 1 - 1
oh-my-opencode-slim.schema.json

@@ -502,7 +502,7 @@
           "maximum": 1000
         },
         "readContextMaxFiles": {
-          "default": 8,
+          "default": 4,
           "type": "integer",
           "minimum": 0,
           "maximum": 50

+ 1 - 1
src/config/schema.ts

@@ -184,7 +184,7 @@ export type InterviewConfig = z.infer<typeof InterviewConfigSchema>;
 export const SessionManagerConfigSchema = z.object({
   maxSessionsPerAgent: z.number().int().min(1).max(10).default(2),
   readContextMinLines: z.number().int().min(0).max(1000).default(10),
-  readContextMaxFiles: z.number().int().min(0).max(50).default(8),
+  readContextMaxFiles: z.number().int().min(0).max(50).default(4),
 });
 
 export type SessionManagerConfig = z.infer<typeof SessionManagerConfigSchema>;

+ 11 - 9
src/hooks/task-session-manager/index.test.ts

@@ -130,7 +130,7 @@ describe('task-session-manager hook', () => {
     expect(next.args.task_id).toBe('child-1');
   });
 
-  test('keeps file context out of resumable message context', async () => {
+  test('keeps compact file context in resumable message context', async () => {
     const { hook } = createHook();
 
     await hook.event({
@@ -191,10 +191,12 @@ describe('task-session-manager hook', () => {
     const userMessage = messages.messages[0];
     expect(userMessage.parts[0].text).toContain('exp-1 session files');
     expect(userMessage.parts[0].text).not.toContain('Context read by exp-1');
-    expect(userMessage.parts[0].text).not.toContain('src/index.ts');
+    expect(userMessage.parts[0].text).toContain(
+      'files: src/index.ts (12 lines)',
+    );
   });
 
-  test('tracks read context internally without rendering file paths', async () => {
+  test('renders compact tracked read context', async () => {
     const { hook } = createHook();
 
     await hook.event({
@@ -256,7 +258,7 @@ describe('task-session-manager hook', () => {
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 line counts');
     expect(prompt).not.toContain('small.ts');
-    expect(prompt).not.toContain('large.ts');
+    expect(prompt).toContain('large.ts (12 lines)');
     expect(prompt).not.toContain('Context read by');
   });
 
@@ -300,7 +302,7 @@ describe('task-session-manager hook', () => {
 
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 repeat reads');
-    expect(prompt).not.toContain('src/repeat.ts');
+    expect(prompt).toContain('src/repeat.ts (12 lines)');
     expect(prompt).not.toContain('Context read by');
   });
 
@@ -352,10 +354,10 @@ describe('task-session-manager hook', () => {
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 configured caps');
     expect(prompt).not.toContain('small.ts');
-    expect(prompt).not.toContain('medium.ts');
+    expect(prompt).toContain('medium.ts (5 lines)');
     expect(prompt).not.toContain('large.ts');
     expect(prompt).not.toContain('Context read by exp-1:');
-    expect(prompt).not.toContain('(+1 more)');
+    expect(prompt).toContain('(+1 more)');
   });
 
   test('ignores reads from unmanaged child sessions', async () => {
@@ -443,9 +445,9 @@ describe('task-session-manager hook', () => {
     expect(prompt).not.toContain('exp-1 thread 1');
     expect(prompt).not.toContain('file-1.ts');
     expect(prompt).toContain('exp-2 thread 2');
-    expect(prompt).not.toContain('file-2.ts');
+    expect(prompt).toContain('file-2.ts');
     expect(prompt).toContain('exp-3 thread 3');
-    expect(prompt).not.toContain('file-3.ts');
+    expect(prompt).toContain('file-3.ts');
   });
 
   test('drops stale remembered sessions and falls back to fresh', async () => {

+ 1 - 1
src/index.ts

@@ -304,7 +304,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
       maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
       readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
-      readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
+      readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 4,
       shouldManageSession: (sessionID) =>
         sessionAgentMap.get(sessionID) === 'orchestrator',
     });

+ 14 - 13
src/utils/session-manager.test.ts

@@ -46,7 +46,7 @@ describe('SessionManager', () => {
     expect(manager.formatForPrompt('parent-1')).toBeUndefined();
   });
 
-  test('omits read context from resumable prompt', () => {
+  test('includes compact read context in resumable prompt', () => {
     const manager = new SessionManager(2);
 
     manager.remember({
@@ -67,11 +67,12 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1');
     expect(prompt).toContain('exp-1 session manager');
     expect(prompt).not.toContain('Context read by exp-1');
-    expect(prompt).not.toContain('src/multiplexer/session-manager.ts');
-    expect(prompt).not.toContain('src/index.ts');
+    expect(prompt).toContain(
+      'files: src/multiplexer/session-manager.ts (24 lines), src/index.ts (42 lines)',
+    );
   });
 
-  test('does not render tracked read context file paths', () => {
+  test('filters tiny reads and caps compact read context files', () => {
     const manager = new SessionManager(2);
 
     manager.remember({
@@ -92,11 +93,11 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 large context');
     expect(prompt).not.toContain('file-0.ts');
-    expect(prompt).not.toContain('file-9.ts');
-    expect(prompt).not.toContain('(+1 more)');
+    expect(prompt).toContain('file-9.ts (29 lines)');
+    expect(prompt).toContain('(+1 more)');
   });
 
-  test('keeps configurable read context thresholds internal', () => {
+  test('uses configurable read context thresholds', () => {
     const manager = new SessionManager(2, {
       readContextMinLines: 5,
       readContextMaxFiles: 1,
@@ -117,12 +118,12 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 custom thresholds');
     expect(prompt).not.toContain('small.ts');
-    expect(prompt).not.toContain('large.ts');
+    expect(prompt).toContain('large.ts (12 lines)');
     expect(prompt).not.toContain('medium.ts');
-    expect(prompt).not.toContain('(+1 more)');
+    expect(prompt).toContain('(+1 more)');
   });
 
-  test('bounds stored read context files without rendering them', () => {
+  test('bounds stored read context files to the render cap plus overflow marker', () => {
     const manager = new SessionManager(2, {
       readContextMinLines: 1,
       readContextMaxFiles: 2,
@@ -146,9 +147,9 @@ describe('SessionManager', () => {
     expect(remembered.contextFiles).toHaveLength(3);
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 bounded context');
-    expect(prompt).not.toContain('file-9.ts');
-    expect(prompt).not.toContain('file-8.ts');
-    expect(prompt).not.toContain('(+1 more)');
+    expect(prompt).toContain('file-9.ts (10 lines)');
+    expect(prompt).toContain('file-8.ts (10 lines)');
+    expect(prompt).toContain('(+1 more)');
     expect(prompt).not.toContain('file-0.ts');
   });
 });

+ 23 - 2
src/utils/session-manager.ts

@@ -20,7 +20,7 @@ export interface RememberedTaskSession {
 type SessionGroupMap = Map<AgentName, RememberedTaskSession[]>;
 
 const MIN_CONTEXT_FILE_LINES = 10;
-const MAX_CONTEXT_FILES_PER_SESSION = 8;
+const MAX_CONTEXT_FILES_PER_SESSION = 4;
 
 interface SessionManagerOptions {
   readContextMinLines?: number;
@@ -233,7 +233,13 @@ export class SessionManager {
       .map(
         ([agentType, entries]) =>
           `- ${agentType}: ${entries
-            .map((entry) => `${entry.alias} ${entry.label}`)
+            .map((entry) => {
+              const context = formatContextFiles(entry.contextFiles, {
+                minLines: this.readContextMinLines,
+                maxFiles: this.readContextMaxFiles,
+              });
+              return `${entry.alias} ${entry.label}${context ? ` — files: ${context}` : ''}`;
+            })
             .join('; ')}`,
       );
 
@@ -323,3 +329,18 @@ export class SessionManager {
     return this.orderCounter;
   }
 }
+
+function formatContextFiles(
+  files: ContextFile[],
+  options: { minLines: number; maxFiles: number },
+): string {
+  const eligible = files
+    .filter((file) => file.lineCount >= options.minLines)
+    .sort((a, b) => b.lastReadAt - a.lastReadAt);
+  const shown = eligible.slice(0, options.maxFiles);
+  const rest = eligible.length - shown.length;
+  const rendered = shown.map(
+    (file) => `${file.path} (${file.lineCount} lines)`,
+  );
+  return `${rendered.join(', ')}${rest > 0 ? ` (+${rest} more)` : ''}`;
+}