Browse Source

fix session goal inheritance

Alvin Unreal 3 months ago
parent
commit
d5b2bbc74b
2 changed files with 63 additions and 19 deletions
  1. 33 3
      src/hooks/session-goal/index.test.ts
  2. 30 16
      src/hooks/session-goal/index.ts

+ 33 - 3
src/hooks/session-goal/index.test.ts

@@ -114,6 +114,37 @@ describe('createSessionGoalHook', () => {
     expect(output.system.join('\n')).not.toContain('Original.');
     expect(output.system.join('\n')).not.toContain('Original.');
   });
   });
 
 
+  test('grandchild sessions inherit the root goal', async () => {
+    const hook = createSessionGoalHook(
+      { directory: '.' } as Parameters<typeof createSessionGoalHook>[0],
+      {} as Parameters<typeof createSessionGoalHook>[1],
+      { getAgentName: () => 'explorer' },
+    );
+    await hook.handleCommandExecuteBefore(
+      { command: 'goal', sessionID: 'root', arguments: 'Root objective.' },
+      { parts: [] },
+    );
+    hook.handleEvent({
+      event: {
+        type: 'session.created',
+        properties: { info: { id: 'child', parentID: 'root' } },
+      },
+    });
+    hook.handleEvent({
+      event: {
+        type: 'session.created',
+        properties: { info: { id: 'grandchild', parentID: 'child' } },
+      },
+    });
+
+    const output = { system: [] as string[] };
+    hook.handleSystemTransform({ sessionID: 'grandchild' }, output);
+
+    expect(output.system.join('\n')).toContain('<parent_goal>');
+    expect(output.system.join('\n')).toContain('Root objective.');
+    expect(output.system.join('\n')).not.toContain('Objective: \n');
+  });
+
   test('child sessions stop inheriting after parent goal is cleared', async () => {
   test('child sessions stop inheriting after parent goal is cleared', async () => {
     const hook = createSessionGoalHook(
     const hook = createSessionGoalHook(
       { directory: '.' } as Parameters<typeof createSessionGoalHook>[0],
       { directory: '.' } as Parameters<typeof createSessionGoalHook>[0],
@@ -171,9 +202,8 @@ describe('createSessionGoalHook', () => {
     );
     );
 
 
     expect(output.parts[0].text).toContain('Set active goal from interview');
     expect(output.parts[0].text).toContain('Set active goal from interview');
-    expect(hook.getGoal('ses_1')?.text).toContain('Feature Goal');
-    expect(hook.getGoal('ses_1')?.text).toContain(
-      'Build the feature with minimal scope.',
+    expect(hook.getGoal('ses_1')?.text).toBe(
+      'From interview: Feature Goal\n\nBuild the feature with minimal scope.',
     );
     );
   });
   });
 
 

+ 30 - 16
src/hooks/session-goal/index.ts

@@ -19,10 +19,6 @@ interface GoalState {
   createdAt: number;
   createdAt: number;
 }
 }
 
 
-interface StoredGoalState extends GoalState {
-  inheritedFrom?: string;
-}
-
 interface SystemTransformOutput {
 interface SystemTransformOutput {
   system: string[];
   system: string[];
 }
 }
@@ -31,6 +27,10 @@ function normalizeGoalText(text: string): string {
   return text.trim().replace(/\s+/g, ' ').slice(0, MAX_GOAL_LENGTH);
   return text.trim().replace(/\s+/g, ' ').slice(0, MAX_GOAL_LENGTH);
 }
 }
 
 
+function trimGoalText(text: string): string {
+  return text.trim().slice(0, MAX_GOAL_LENGTH);
+}
+
 function pushText(
 function pushText(
   output: { parts: Array<{ type: string; text?: string }> },
   output: { parts: Array<{ type: string; text?: string }> },
   text: string,
   text: string,
@@ -62,7 +62,7 @@ async function readInterviewGoal(
     const content = await fs.readFile(sourcePath, 'utf8');
     const content = await fs.readFile(sourcePath, 'utf8');
     const title = extractTitle(content);
     const title = extractTitle(content);
     const summary = extractSummarySection(content);
     const summary = extractSummarySection(content);
-    const text = normalizeGoalText(
+    const text = trimGoalText(
       [title ? `From interview: ${title}` : '', summary]
       [title ? `From interview: ${title}` : '', summary]
         .filter(Boolean)
         .filter(Boolean)
         .join('\n\n'),
         .join('\n\n'),
@@ -74,19 +74,33 @@ async function readInterviewGoal(
 }
 }
 
 
 function resolveGoal(
 function resolveGoal(
-  goals: Map<string, StoredGoalState>,
+  goals: Map<string, GoalState>,
   sessionID: string,
   sessionID: string,
 ): { goal: GoalState; inherited: boolean } | null {
 ): { goal: GoalState; inherited: boolean } | null {
-  const goal = goals.get(sessionID);
-  if (!goal) return null;
-  if (!goal.inheritedFrom) return { goal, inherited: false };
-
-  const parentGoal = goals.get(goal.inheritedFrom);
-  if (!parentGoal) {
-    goals.delete(sessionID);
-    return null;
+  const seen = new Set<string>();
+  let currentSessionID = sessionID;
+  let inherited = false;
+
+  while (true) {
+    if (seen.has(currentSessionID)) {
+      goals.delete(sessionID);
+      return null;
+    }
+    seen.add(currentSessionID);
+
+    const goal = goals.get(currentSessionID);
+    if (!goal) {
+      goals.delete(sessionID);
+      return null;
+    }
+
+    if (!goal.inheritedFrom) {
+      return { goal, inherited };
+    }
+
+    inherited = true;
+    currentSessionID = goal.inheritedFrom;
   }
   }
-  return { goal: parentGoal, inherited: true };
 }
 }
 
 
 export function createSessionGoalHook(
 export function createSessionGoalHook(
@@ -108,7 +122,7 @@ export function createSessionGoalHook(
   ) => void;
   ) => void;
   getGoal: (sessionID: string) => GoalState | undefined;
   getGoal: (sessionID: string) => GoalState | undefined;
 } {
 } {
-  const goals = new Map<string, StoredGoalState>();
+  const goals = new Map<string, GoalState>();
   const outputFolder = config.interview?.outputFolder ?? 'interview';
   const outputFolder = config.interview?.outputFolder ?? 'interview';
 
 
   return {
   return {