Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ sessions/
!App/memmy-agent/src/tools/computer-history/win/.gitkeep
App/backend/src/adapters/outbound/skill-writer/workspace-bridge/memmy-workspace-bridge.mjs
Memory/src/agent-source/integration/workspace-bridge/memmy-workspace-bridge.mjs
/Mac软件打包/
7 changes: 6 additions & 1 deletion App/backend/local-api-contracts/src/memory-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ export const MemoryListItemSchema = z.object({
status: MemoryStatusSchema,
title: NonEmptyStringSchema,
summary: z.string(),
sourceText: z.string().optional(),
generatedTitle: z.string().optional(),
experienceDraft: z.boolean().optional(),
tags: z.array(z.string()),
processing: MemoryProcessingRecordSchema.optional(),
metrics: MemoryMetricsSchema.optional(),
Expand Down Expand Up @@ -265,7 +268,9 @@ export const EpisodeRefSchema = z.object({
skillMemoryIds: z.array(NonEmptyStringSchema).optional(),
linkedSkillId: NonEmptyStringSchema.optional(),
skillStatus: z.string().optional(),
skillReason: z.string().optional()
skillReason: z.string().optional(),
titleGenerated: z.boolean().optional(),
titlePending: z.boolean().optional()
});
export type EpisodeRef = z.infer<typeof EpisodeRefSchema>;

Expand Down
11 changes: 11 additions & 0 deletions App/backend/src/tests/memory-runtime-contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ describe("memory runtime contracts", () => {
})).toThrow();
});

it("keeps waiting source text and generated titles on list items", () => {
const parsed = MemoryListItemSchema.parse({
...memoryListItem(),
sourceText: "请修复自动扫描卡顿",
generatedTitle: "扫描卡顿修复"
});
expect(parsed.sourceText).toBe("请修复自动扫描卡顿");
expect(parsed.generatedTitle).toBe("扫描卡顿修复");
expect(MemoryListItemSchema.parse(memoryListItem()).sourceText).toBeUndefined();
});

it("parses Span memories and Span processing jobs", () => {
expect(() => MemoryListItemSchema.parse(memoryListItem({ kind: "span" }))).not.toThrow();
expect(() => PanelItemsOutputSchema.parse({
Expand Down
30 changes: 24 additions & 6 deletions App/frontend/desktop/src/pages/memory/memories-sub-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { MemoryDrawerDeleteAction } from "./memory-delete-action.js";
import { MemoryAgentSourceTag } from "./memory-agent-source-tag.js";
import { MemoryMarkdown } from "./memory-markdown.js";
import { toMemoryDetailErrorMessage } from "./memory-detail-error.js";
import { cleanMemoryBody, cleanMemoryText, displayMemoryTitle, drawerEyebrow, memoryDisplaySource } from "./memory-display.js";
import { cleanMemoryBody, cleanMemoryText, displayMemoryTitle, drawerEyebrow, independentGeneratedTitle, isMemorySummaryWaiting, memoryDisplaySource, showsGeneratedSummaryRow, waitingPrimaryText } from "./memory-display.js";
import {
clearMemoryPanelCache,
memoryPanelCacheKey,
Expand Down Expand Up @@ -501,6 +501,11 @@ function MemoryListState(input: { props: MemoriesSubPageViewProps }) {
{visibleItems.map((item) => {
const selected = props.selectedMemoryId === item.id || ("detail" in props.state && props.state.detail?.status === "ready" && props.state.detail.data.item.id === item.id);
const processingStatus = memoryProcessingStatus(item);
const waiting = isMemorySummaryWaiting(item.processing);
const primaryText = waiting ? waitingPrimaryText(item) : displayMemoryTitle(item);
const summaryText = !waiting && showsGeneratedSummaryRow(item, waiting)
? cleanMemoryText(item.summary)
: "";
return (
<div
key={item.id}
Expand All @@ -517,7 +522,8 @@ function MemoryListState(input: { props: MemoriesSubPageViewProps }) {
className={`memory-card${selected ? " memory-card--selected" : ""}`}
>
<div className="memory-card__body">
<div className="memory-card__title">{displayMemoryTitle(item)}</div>
<div className="memory-card__title">{primaryText}</div>
{summaryText ? <div className="memory-card__summary">{summaryText}</div> : null}
<div className="memory-card__meta">
<MemoryAgentSourceTag sourceAgent={memoryDisplaySource(item)} label={t("memory.memories.source")} />
<span>{formatDateTime(item.createdAt)}</span>
Expand Down Expand Up @@ -570,19 +576,24 @@ function MemoryDetailPanel(props: {

const readyDetail = props.detail.status === "ready" ? props.detail.data : null;
const eyebrow = readyDetail ? drawerEyebrow(readyDetail.item) : t("memory.memories.detailTitle");
const waiting = readyDetail ? isMemorySummaryWaiting(readyDetail.item.processing) : false;
const drawerTitle = readyDetail && !waiting && readyDetail.item.kind !== "span"
? independentGeneratedTitle(readyDetail.item)
: undefined;

return (
<div className="memory-drawer-backdrop" onClick={props.onClose}>
<button type="button" className="memory-drawer-backdrop__close" tabIndex={-1} aria-hidden="true" onClick={(e) => {
e.stopPropagation();
props.onClose();
}} />
<aside className="memory-drawer memory-drawer--entry" role="dialog" aria-modal="true" aria-labelledby="memory-detail-title" onClick={(e) => e.stopPropagation()}>
<aside className="memory-drawer memory-drawer--entry" role="dialog" aria-modal="true" aria-labelledby={drawerTitle ? "memory-detail-heading" : "memory-detail-title"} onClick={(e) => e.stopPropagation()}>
<header className="memory-drawer__header">
<div>
<div className="memory-drawer__identity">
<span id="memory-detail-title" className="memory-drawer__eyebrow">{eyebrow}</span>
</div>
{drawerTitle ? <h4 id="memory-detail-heading" className="memory-drawer__title">{drawerTitle}</h4> : null}
</div>
<button type="button" className="memory-drawer__close" onClick={props.onClose} aria-label={t("common.close")}>
<X size={16} />
Expand Down Expand Up @@ -622,7 +633,7 @@ function MemoryDetailBody(props: {
const detail = props.detail;
const item = detail.item;
const traceDetail = readTraceDetail(detail);
const summaryText = displayMemorySummaryText(item.summary, t);
const summaryText = displayMemorySummaryText(item.summary, t, item.processing);

if (item.kind === "span") {
const span = recordValue(recordValue(recordValue(item.metadata.properties).internal_info).span);
Expand Down Expand Up @@ -874,7 +885,7 @@ function TraceMemoryDetail(props: {
: detail.steps.flatMap((step) => step.toolCalls);
const turnEvents = buildTraceTurnEvents(detail, toolCalls);
const hasTurnSteps = turnEvents.length > 0;
const summaryText = displayMemorySummaryText(detail.summary || props.item.summary, t);
const summaryText = displayMemorySummaryText(detail.summary || props.item.summary, t, props.item.processing);

return (
<>
Expand Down Expand Up @@ -1075,7 +1086,14 @@ function waitForProcessingPoll(): Promise<void> {
return new Promise((resolve) => window.setTimeout(resolve, 500));
}

function displayMemorySummaryText(value: string | undefined | null, t: (key: MessageKey) => string): string {
function displayMemorySummaryText(
value: string | undefined | null,
t: (key: MessageKey) => string,
processing?: MemoryProcessingRecord
): string {
if (isMemorySummaryWaiting(processing)) {
return t(MEMORY_PROCESSING_STATUS_LABELS.summary);
}
const cleaned = cleanMemoryText(value);
const processingStatus = importProcessingSummaryStatus(cleaned);
return processingStatus ? t(MEMORY_PROCESSING_STATUS_LABELS[processingStatus]) : cleaned;
Expand Down
70 changes: 67 additions & 3 deletions App/frontend/desktop/src/pages/memory/memory-display.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MemoryListItem } from "@memmy/local-api-contracts";
import { displayMemoryId } from "./memory-id.js";

type MemoryDisplayItem = Pick<MemoryListItem, "id" | "title" | "summary" | "memoryLayer"> & {
type MemoryDisplayItem = Pick<MemoryListItem, "id" | "title" | "summary" | "memoryLayer" | "sourceText" | "generatedTitle" | "experienceDraft"> & {
kind?: MemoryListItem["kind"];
body?: string;
metadata?: Record<string, unknown>;
Expand Down Expand Up @@ -50,19 +50,72 @@ export function displayMemoryTitle(item: MemoryDisplayItem, ...candidates: Array
if (topic && !isInternalTitle(topic)) return topic;
}

if (item.kind === "policy" && isExperienceTitleDraft(item.title)) {
return waitingPrimaryText(item);
}

const generatedTitle = independentGeneratedTitle(item);
if (generatedTitle) return generatedTitle;

const summary = readySummary(item.summary);
if (item.kind === "trace" && summary) {
return summary;
}

for (const value of [summary, ...candidates, firstUserQueryLine(item.body), firstReadableBodyLine(item.body), item.title]) {
const cleaned = cleanMemoryText(value);
if (cleaned && !isInternalTitle(cleaned)) return cleaned;
if (cleaned && !isInternalTitle(cleaned) && !isExperienceTitleDraft(cleaned)) return cleaned;
}

return displayMemoryId(item.id);
}

export function isMemorySummaryWaiting(processing?: { state?: string } | null): boolean {
return processing?.state === "summary_pending" || processing?.state === "summarizing";
}

export function isExperienceTitleDraft(value?: string | null): boolean {
return Boolean(value && /^Policy:\s+/i.test(value.trim()));
}

export function isExperienceWaiting(item: MemoryDisplayItem): boolean {
return item.kind === "policy" && (item.experienceDraft === true || isExperienceTitleDraft(item.title));
}

export function experienceDisplayTitle(item: MemoryDisplayItem): string | undefined {
if (item.kind !== "policy" || item.experienceDraft) return undefined;
const generated = independentGeneratedTitle(item);
if (generated) return generated;
const title = cleanMemoryText(item.title);
if (!title || isInternalTitle(title) || isExperienceTitleDraft(title)) return undefined;
return title;
}

export function waitingPrimaryText(item: MemoryDisplayItem): string {
const source = cleanMemoryText(item.sourceText);
if (usableWaitingText(source)) return source;
if (item.kind === "policy") return displayMemoryId(item.id);
return firstUserQueryLine(item.body)
?? firstReadableBodyLine(item.body)
?? displayMemoryId(item.id);
}

export function independentGeneratedTitle(item: MemoryDisplayItem): string | undefined {
const title = cleanMemoryText(item.generatedTitle);
if (!title || isInternalTitle(title) || isPlaceholderTitle(title) || isExperienceTitleDraft(title)) {
return undefined;
}
return title;
}

export function showsGeneratedSummaryRow(item: MemoryDisplayItem, waiting: boolean): boolean {
if (waiting || item.kind === "policy") return false;
const summary = readySummary(item.summary);
if (!summary) return false;
const title = item.kind === "span" ? spanGoal(item.metadata) : independentGeneratedTitle(item);
return Boolean(title && title !== summary);
}

function spanGoal(metadata?: Record<string, unknown>): string | undefined {
const explicitGoal = stringValue(metadata?.spanGoal)?.trim();
if (explicitGoal) return explicitGoal;
Expand Down Expand Up @@ -104,7 +157,7 @@ function firstUserQueryLine(value?: string | null): string | undefined {
let inUserSection = false;

for (const line of lines) {
const role = markdownRoleHeading(line);
const role = markdownRoleHeading(line) ?? plainRoleLabel(line);
if (role) {
inUserSection = role === "user";
continue;
Expand Down Expand Up @@ -133,6 +186,10 @@ function readySummary(value?: string | null): string | undefined {
: undefined;
}

function usableWaitingText(value: string): value is string {
return Boolean(value && !isInternalTitle(value) && !isPlaceholderTitle(value) && !isExperienceTitleDraft(value));
}

function isInternalTitle(value: string): boolean {
return /^(trace|policy|world|world_model|skill)[:_]/i.test(value)
|| /^episode_[a-f0-9]{8,}$/i.test(value)
Expand All @@ -148,6 +205,13 @@ function markdownRoleHeading(value: string): string | undefined {
return match?.[1]?.toLowerCase();
}

function plainRoleLabel(value: string): string | undefined {
const match = value.trim().match(/^(User|Assistant|Agent|System|Tool|Developer):$/i);
if (!match) return undefined;
const role = match[1]?.toLowerCase();
return role === "agent" ? "assistant" : role;
}

function isInternalMetricLine(value: string): boolean {
return /^(Alpha|Value|Priority|RawTurn|TraceStep|Reflection|Signature|Vec Summary|Vec Action):\s*/i.test(value.trim());
}
Expand Down
33 changes: 23 additions & 10 deletions App/frontend/desktop/src/pages/memory/policies-sub-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
import { ChevronRight, Search, Sparkles, X } from "./memory-prototype-icons.js";
import { MemoryDrawerDeleteAction } from "./memory-delete-action.js";
import { toMemoryDetailErrorMessage } from "./memory-detail-error.js";
import { cleanMemoryBody, displayMemoryTitle, drawerEyebrow } from "./memory-display.js";
import { cleanMemoryBody, displayMemoryTitle, drawerEyebrow, experienceDisplayTitle, isExperienceWaiting, waitingPrimaryText } from "./memory-display.js";
import {
MemoryReferenceTags,
type MemoryReferenceOpenRequest,
Expand Down Expand Up @@ -240,7 +240,7 @@ export function PoliciesSubPage(props: PoliciesSubPageProps) {
);
}

function ExperienceState(props: {
export function ExperienceState(props: {
state: RemoteData<PanelItemsOutput>;
detail: DetailState;
onOpenDetail: (item: MemoryListItem) => void;
Expand All @@ -259,16 +259,24 @@ function ExperienceState(props: {
{props.state.status === "ready" && props.state.data.items.length > 0 && (
<>
<div className="memory-list">
{props.state.data.items.map((item) => (
{props.state.data.items.map((item) => {
const waiting = isExperienceWaiting(item);
const title = waiting ? waitingPrimaryText(item) : (experienceDisplayTitle(item) ?? displayMemoryTitle(item));
return (
<button
key={item.id}
type="button"
onClick={() => props.onOpenDetail(item)}
className="memory-card"
>
<div className="memory-card__body">
<div className="memory-card__title">{displayMemoryTitle(item)}</div>
<div className="memory-card__title">{title}</div>
<div className="memory-card__meta">
{waiting && (
<span className="memory-pill memory-pill--processing">
{t("memory.memories.processing.summary")}
</span>
)}
<PolicyStatusPill status={item.status} />
<span>{t("memory.memories.updatedAt")}: {formatDateTime(item.updatedAt)}</span>
{item.tags.slice(0, 4).map((tag) => (
Expand All @@ -280,7 +288,8 @@ function ExperienceState(props: {
<ChevronRight size={16} />
</div>
</button>
))}
);
})}
</div>
<MemoryPagination data={props.state.data} onPageChange={props.onPageChange} />
</>
Expand Down Expand Up @@ -308,7 +317,8 @@ function ExperienceDrawer(props: {
}

const readyDetail = props.detail.status === "ready" ? props.detail.data : null;
const title = readyDetail ? experienceFromDetail(readyDetail).title : t("memory.policies.detailTitle");
const waiting = readyDetail ? isExperienceWaiting(readyDetail.item) : false;
const generatedTitle = readyDetail && !waiting ? experienceDisplayTitle(readyDetail.item) : undefined;
const eyebrow = readyDetail ? drawerEyebrow(readyDetail.item) : t("memory.policies.detailTitle");

return (
Expand All @@ -317,13 +327,13 @@ function ExperienceDrawer(props: {
e.stopPropagation();
props.onClose();
}} />
<aside className="memory-drawer" role="dialog" aria-modal="true" aria-labelledby="memory-policy-title" onClick={(e) => e.stopPropagation()}>
<aside className="memory-drawer" role="dialog" aria-modal="true" aria-labelledby={generatedTitle ? "memory-policy-title" : "memory-policy-eyebrow"} onClick={(e) => e.stopPropagation()}>
<header className="memory-drawer__header">
<div>
<div className="memory-drawer__identity">
<span className="memory-drawer__eyebrow">{eyebrow}</span>
<span id="memory-policy-eyebrow" className="memory-drawer__eyebrow">{eyebrow}</span>
</div>
<h4 id="memory-policy-title" className="memory-drawer__title">{title}</h4>
{generatedTitle ? <h4 id="memory-policy-title" className="memory-drawer__title">{generatedTitle}</h4> : null}
</div>
<button type="button" className="memory-drawer__close" onClick={props.onClose} aria-label={t("common.close")}>
<X size={16} />
Expand Down Expand Up @@ -474,7 +484,10 @@ function experienceFromDetail(detail: GetMemoryOutput): ExperienceView {
);

return {
title: displayMemoryTitle(detail.item, firstString(policy.title, internalInfo.title)),
title: experienceDisplayTitle(detail.item)
?? (isExperienceWaiting(detail.item)
? waitingPrimaryText(detail.item)
: displayMemoryTitle(detail.item, firstString(policy.title, internalInfo.title))),
status: firstString(policy.status, internalInfo.status, info.status, detail.item.status),
trigger: firstString(policy.trigger, internalInfo.trigger, parseBodyField(detail.item.body, "Trigger")),
procedure: firstString(policy.procedure, policy.action, internalInfo.procedure, parseBodyField(detail.item.body, "Procedure")),
Expand Down
Loading
Loading