diff --git a/apps/api/src/companion-discovery-context.test.mjs b/apps/api/src/companion-discovery-context.test.mjs
index 56941844..e3c5bbe2 100644
--- a/apps/api/src/companion-discovery-context.test.mjs
+++ b/apps/api/src/companion-discovery-context.test.mjs
@@ -1,5 +1,6 @@
import { expect, test } from "bun:test";
import { MockLanguageModelV4 } from "ai/test";
+import { CompanionDiscoveryOutputSchema } from "@edgeever/shared";
import { discoveryContext, discoveryInputHash } from "./companion-discovery-context.ts";
import { generateCompanionDiscovery } from "./companion-discovery-runtime.ts";
@@ -31,6 +32,15 @@ test("only exact supplied aliases can resolve to real note IDs", () => {
expect(() => discoveryContext({ ...input, candidates: [candidates[0], candidates[0]] })).toThrow();
});
+test("discovery copy is constrained to compact three-line content", () => {
+ const context = discoveryContext(input);
+ expect(context.instructions).toContain("at most 3 short lines");
+ const base = { kind: "insight", title: "Short title", sourceIds: ["n1", "n2"], targetId: null };
+ expect(CompanionDiscoveryOutputSchema.safeParse({ suggestion: { ...base, body: "One\nTwo\nThree" } }).success).toBe(true);
+ expect(CompanionDiscoveryOutputSchema.safeParse({ suggestion: { ...base, body: "One\nTwo\nThree\nFour" } }).success).toBe(false);
+ expect(CompanionDiscoveryOutputSchema.safeParse({ suggestion: { ...base, body: "x".repeat(181) } }).success).toBe(false);
+});
+
test("analysis fingerprint includes content, real IDs, context and model configuration", async () => {
const request = { ...input, settingsVersion: 1, contextRevision: 0, modelConfiguration: { default_model_id: "model-a" } };
const hash = await discoveryInputHash(request);
diff --git a/apps/api/src/companion-discovery-context.ts b/apps/api/src/companion-discovery-context.ts
index 4586e738..8dd792c3 100644
--- a/apps/api/src/companion-discovery-context.ts
+++ b/apps/api/src/companion-discovery-context.ts
@@ -20,7 +20,8 @@ Every suggestion must reference the current anchor and at least one other suppli
merge: only fragments of the SAME concrete idea, not just similar topics. Sources will move to trash, so prefer null when uncertain.
append: exactly two plainText notes; targetId is the existing longer note; the other must be the anchor, a useful new fragment. Existing text and source are preserved.
insight: explain a specific useful connection to older knowledge, with supporting note IDs; no action is required.
-body must explain the actual connection and benefit, not describe your process. Distinguish inference from evidence.
+Keep the title to one short line. Keep body compact: at most 3 short lines, no bullets or headings, and at most 180 characters total.
+For Simplified Chinese, aim for no more than 90 Chinese characters. body must explain only the actual connection and benefit, not your process. Distinguish inference from evidence.
For merge and insight targetId must be null. Reply in ${args.locale === "zh-CN" ? "Simplified Chinese" : "English"}.`,
prompt: JSON.stringify({ anchorId: aliases.get(args.anchorId), notes: args.candidates.map(note => ({ ...note, id: aliases.get(note.id) })) }),
decode(output: CompanionDiscoveryOutput): CompanionDiscoveryOutput {
diff --git a/apps/web/src/components/CompanionActionCard.tsx b/apps/web/src/components/CompanionActionCard.tsx
index e4233b76..1f0d7837 100644
--- a/apps/web/src/components/CompanionActionCard.tsx
+++ b/apps/web/src/components/CompanionActionCard.tsx
@@ -17,12 +17,10 @@ export function CompanionActionCard({ action, busy, onApply, onDismiss, onOpenNo
return {t(merge ? "companion.actions.merge" : "companion.actions.tag")}
- {action.plan.reason}
+ {action.plan.reason}
- {action.status === "pending" ?
- {t("companion.actions.review")}
-