Browse Source

Merge pull request #940 from mhenke/fix/922-compaction-loop

fix(internal-initiator): treat OpenCode compaction continuation as internal (#922)
Alvin 4 days ago
parent
commit
18fa99d0e1
2 changed files with 53 additions and 1 deletions
  1. 44 0
      src/utils/internal-initiator.test.ts
  2. 9 1
      src/utils/internal-initiator.ts

+ 44 - 0
src/utils/internal-initiator.test.ts

@@ -43,4 +43,48 @@ describe('internal initiator markers', () => {
       }),
     ).toBe(false);
   });
+
+  test('recognizes OpenCode compaction continuation as internal initiator', () => {
+    // OpenCode's compaction sends a synthetic continuation prompt with
+    // metadata.compaction_continue = true but no INTERNAL_INITIATOR_METADATA_KEY.
+    // This should be treated as internal to prevent board injection on
+    // the continuation turn (issue #922).
+    const compactionContinuation = {
+      type: 'text',
+      synthetic: true,
+      text: 'Continue if you have next steps.',
+      metadata: { compaction_continue: true },
+    };
+    expect(isInternalInitiatorPart(compactionContinuation)).toBe(true);
+  });
+
+  test('compaction_continue without synthetic is not internal', () => {
+    expect(
+      isInternalInitiatorPart({
+        type: 'text',
+        text: 'Continue if you have next steps.',
+        metadata: { compaction_continue: true },
+      }),
+    ).toBe(false);
+  });
+
+  test('compaction_continue false or string is not internal', () => {
+    expect(
+      isInternalInitiatorPart({
+        type: 'text',
+        synthetic: true,
+        text: 'Continue if you have next steps.',
+        metadata: { compaction_continue: false },
+      }),
+    ).toBe(false);
+
+    expect(
+      isInternalInitiatorPart({
+        type: 'text',
+        synthetic: true,
+        text: 'Continue if you have next steps.',
+        metadata: { compaction_continue: 'true' },
+      }),
+    ).toBe(false);
+  });
 });

+ 9 - 1
src/utils/internal-initiator.ts

@@ -29,5 +29,13 @@ export function isInternalInitiatorPart(part: unknown): boolean {
     return false;
   }
 
-  return part.metadata[INTERNAL_INITIATOR_METADATA_KEY] === true;
+  return (
+    part.metadata[INTERNAL_INITIATOR_METADATA_KEY] === true ||
+    // OpenCode's compaction continuation emits compaction_continue: true
+    // instead of our internal initiator key; treat it as internal to
+    // prevent board injection on the continuation turn (#922).
+    // Upstream key is not a stable plugin contract — graceful degradation
+    // if renamed: injection resumes, loop returns, no crash.
+    part.metadata['compaction_continue'] === true
+  );
 }