瀏覽代碼

fix: compact resumable session file hints

dhaern 3 月之前
父節點
當前提交
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` |
 | `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.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.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 |
 | `disabled_mcps` | string[] | `[]` | MCP server IDs to disable globally |
 | `fallback.enabled` | boolean | `false` | Enable model failover on timeout/error |
 | `fallback.enabled` | boolean | `false` | Enable model failover on timeout/error |
 | `fallback.timeoutMs` | number | `15000` | Time before aborting and trying next model |
 | `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
 ```text
 ### Resumable Sessions
 ### 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
 - 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
 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
 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
 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
 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": {
   "sessionManager": {
     "maxSessionsPerAgent": 2,
     "maxSessionsPerAgent": 2,
     "readContextMinLines": 10,
     "readContextMinLines": 10,
-    "readContextMaxFiles": 8
+    "readContextMaxFiles": 4
   }
   }
 }
 }
 ```
 ```
@@ -133,12 +132,12 @@ the prompt focused on substantial file reads.
 
 
 | Type | Default | Range | Meaning |
 | 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.
 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
           "maximum": 1000
         },
         },
         "readContextMaxFiles": {
         "readContextMaxFiles": {
-          "default": 8,
+          "default": 4,
           "type": "integer",
           "type": "integer",
           "minimum": 0,
           "minimum": 0,
           "maximum": 50
           "maximum": 50

+ 1 - 1
src/config/schema.ts

@@ -184,7 +184,7 @@ export type InterviewConfig = z.infer<typeof InterviewConfigSchema>;
 export const SessionManagerConfigSchema = z.object({
 export const SessionManagerConfigSchema = z.object({
   maxSessionsPerAgent: z.number().int().min(1).max(10).default(2),
   maxSessionsPerAgent: z.number().int().min(1).max(10).default(2),
   readContextMinLines: z.number().int().min(0).max(1000).default(10),
   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>;
 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');
     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();
     const { hook } = createHook();
 
 
     await hook.event({
     await hook.event({
@@ -191,10 +191,12 @@ describe('task-session-manager hook', () => {
     const userMessage = messages.messages[0];
     const userMessage = messages.messages[0];
     expect(userMessage.parts[0].text).toContain('exp-1 session files');
     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('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();
     const { hook } = createHook();
 
 
     await hook.event({
     await hook.event({
@@ -256,7 +258,7 @@ describe('task-session-manager hook', () => {
     const prompt = messages.messages[0].parts[0].text;
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 line counts');
     expect(prompt).toContain('exp-1 line counts');
     expect(prompt).not.toContain('small.ts');
     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');
     expect(prompt).not.toContain('Context read by');
   });
   });
 
 
@@ -300,7 +302,7 @@ describe('task-session-manager hook', () => {
 
 
     const prompt = messages.messages[0].parts[0].text;
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 repeat reads');
     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');
     expect(prompt).not.toContain('Context read by');
   });
   });
 
 
@@ -352,10 +354,10 @@ describe('task-session-manager hook', () => {
     const prompt = messages.messages[0].parts[0].text;
     const prompt = messages.messages[0].parts[0].text;
     expect(prompt).toContain('exp-1 configured caps');
     expect(prompt).toContain('exp-1 configured caps');
     expect(prompt).not.toContain('small.ts');
     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('large.ts');
     expect(prompt).not.toContain('Context read by exp-1:');
     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 () => {
   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('exp-1 thread 1');
     expect(prompt).not.toContain('file-1.ts');
     expect(prompt).not.toContain('file-1.ts');
     expect(prompt).toContain('exp-2 thread 2');
     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).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 () => {
   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, {
     taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
       maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
       maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
       readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
       readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
-      readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
+      readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 4,
       shouldManageSession: (sessionID) =>
       shouldManageSession: (sessionID) =>
         sessionAgentMap.get(sessionID) === 'orchestrator',
         sessionAgentMap.get(sessionID) === 'orchestrator',
     });
     });

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

@@ -46,7 +46,7 @@ describe('SessionManager', () => {
     expect(manager.formatForPrompt('parent-1')).toBeUndefined();
     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);
     const manager = new SessionManager(2);
 
 
     manager.remember({
     manager.remember({
@@ -67,11 +67,12 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1');
     const prompt = manager.formatForPrompt('parent-1');
     expect(prompt).toContain('exp-1 session manager');
     expect(prompt).toContain('exp-1 session manager');
     expect(prompt).not.toContain('Context read by exp-1');
     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);
     const manager = new SessionManager(2);
 
 
     manager.remember({
     manager.remember({
@@ -92,11 +93,11 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 large context');
     expect(prompt).toContain('exp-1 large context');
     expect(prompt).not.toContain('file-0.ts');
     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, {
     const manager = new SessionManager(2, {
       readContextMinLines: 5,
       readContextMinLines: 5,
       readContextMaxFiles: 1,
       readContextMaxFiles: 1,
@@ -117,12 +118,12 @@ describe('SessionManager', () => {
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 custom thresholds');
     expect(prompt).toContain('exp-1 custom thresholds');
     expect(prompt).not.toContain('small.ts');
     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('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, {
     const manager = new SessionManager(2, {
       readContextMinLines: 1,
       readContextMinLines: 1,
       readContextMaxFiles: 2,
       readContextMaxFiles: 2,
@@ -146,9 +147,9 @@ describe('SessionManager', () => {
     expect(remembered.contextFiles).toHaveLength(3);
     expect(remembered.contextFiles).toHaveLength(3);
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     const prompt = manager.formatForPrompt('parent-1') ?? '';
     expect(prompt).toContain('exp-1 bounded context');
     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');
     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[]>;
 type SessionGroupMap = Map<AgentName, RememberedTaskSession[]>;
 
 
 const MIN_CONTEXT_FILE_LINES = 10;
 const MIN_CONTEXT_FILE_LINES = 10;
-const MAX_CONTEXT_FILES_PER_SESSION = 8;
+const MAX_CONTEXT_FILES_PER_SESSION = 4;
 
 
 interface SessionManagerOptions {
 interface SessionManagerOptions {
   readContextMinLines?: number;
   readContextMinLines?: number;
@@ -233,7 +233,13 @@ export class SessionManager {
       .map(
       .map(
         ([agentType, entries]) =>
         ([agentType, entries]) =>
           `- ${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('; ')}`,
             .join('; ')}`,
       );
       );
 
 
@@ -323,3 +329,18 @@ export class SessionManager {
     return this.orderCounter;
     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)` : ''}`;
+}