Skip to content

Commit 2780152

Browse files
Bosheng0422claude
andauthored
Feature/bug fix 0720 (#29)
* 优化 * 修复memory提取模型无法fallback导致提取失效的问题 * feat(memory): let each agent pin a model for memory extraction Extraction always followed the agent's chat model, so a user running an expensive model paid that rate for every distillation pass and had no way to point extraction at something cheaper. - memory.aux_model ('provider/model', empty/absent = AUTO) is settable per agent via the extract-config API and a picker in memory settings - the manager maps the one chosen ref onto both harness extraction tiers (memory_aux_light_model / memory_aux_heavy_model) - refs are validated on write; a ref that goes stale later (provider deleted, model disabled) is dropped with a warning at build time so extraction falls back to the default model instead of failing at call time Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(memory): surface extract_run entries in the journal harness-memory now journals every extraction pass, including empty and failed ones. The journal filter offered 'capture'/'extract' actions that the backend no longer emits, so a user whose extraction ran but found nothing saw an empty list and no evidence it had run at all. Replaces those two filter options with 'extract_run' and renders each row as a plain-language summary built from the entry's structured stats — including a specific message when extraction is skipped for want of a configured model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): sync uv.lock with the editable harness-agent override pyproject already pins orcakit-harness-agent to the local ../harness-agent checkout; the lockfile still resolved it from PyPI at 0.9.10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: drop the local sibling-checkout adaptation from pyproject/README Realigns pyproject.toml, README.md, and uv.lock with origin/main. The [tool.uv.sources] block pointing the four harness packages at sibling directories is a local dev convenience and should not ship on the branch; harness deps resolve from PyPI again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: drop the unrelated pixel-art intro deck from the repo octop-pixel-ppt/index.html landed in 8843d78 alongside the memory dashboard work it has nothing to do with. It is a standalone presentation page, referenced from nowhere in the codebase. Recoverable from history if needed: git show 8843d78:octop-pixel-ppt/index.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add online-deps dev workflow and relax eslint unused-var rule Add make targets (install-online / test-online / run-online / clean-online) that build a dedicated .venv-online where Octop is installed editable while the harness-* siblings resolve from PyPI (--no-sources), for testing local source against published components. Ignore .venv-online in git. Also honour the `_`-prefix convention in @typescript-eslint/no-unused-vars (argsIgnorePattern / varsIgnorePattern / ignoreRestSiblings) so intentionally discarded destructures and placeholder callback args stop erroring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: make provider preset assertions robust to upstream model-id drift tencent-token-plan model ids gained a release-date suffix in published harness-agent (deepseek-v4-flash-202605) and the Kimi minor version moved, breaking exact-match assertions. Assert by model-family prefix instead so the test tracks upstream provider data without churning on every release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(memory-dashboard): 7-day growth, entity summary viewer, rename 对话素材→对话记忆 - Overview: switch the growth chart to the last 7 days and lay the growth and memory-type charts out 50/50 (was 1.6fr : 1fr). - MemoryTree: add a "查看摘要" action on each topic row that opens a drawer rendering the entity's long-form summary page (summary_markdown via LazyMarkdown), with headline, version/updated meta, a 待刷新 badge, and loading / failed / not-yet-generated states. Type the getEntity page payload (EntityPage / EntityDetail) instead of unknown. - Rename the raw-events concept from 对话素材 to 对话记忆 across the overview cards, pipeline stage, hints and settings copy (EN labels → "Conversation memory") so users read it as the first stage of memory rather than loose data. Tests and shared test copy updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * build(deps): require harness-agent>=0.9.11, add harness-memory>=0.9.2 Bump orcakit-harness-agent to >=0.9.11 and declare harness-memory>=0.9.2 as a direct dependency — Octop imports harness_memory directly (proactive picker, memory_portable, memory_client) but only ever pulled it transitively. Regenerate uv.lock against PyPI (no editable local sources): harness-memory 0.8.5 -> 0.9.2, orcakit-harness-agent 0.9.7 -> 0.9.11, harness-gateway 0.8.5, harness-browser 0.7.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d990ca1 commit 2780152

34 files changed

Lines changed: 5573 additions & 3062 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pip-wheel-metadata/
3030

3131
# Virtual environments
3232
.venv/
33+
.venv-online/
3334
venv/
3435
env/
3536
ENV/

Makefile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ help:
5151
@echo " dev-frontend Start Vite dev server only"
5252
@echo " dev-backend Start octop run only"
5353
@echo ""
54+
@echo "Online-deps targets (local Octop source + PyPI harness components):"
55+
@echo " install-online Create .venv-online: Octop editable + harness-* from PyPI"
56+
@echo " test-online pytest against .venv-online (not live)"
57+
@echo " run-online Start octop run from .venv-online"
58+
@echo ""
5459
@echo "Quality targets (backend — CI ship bar):"
5560
@echo " all lint + typecheck + test (backend)"
5661
@echo " lint Ruff check + format check (src, tests)"
@@ -76,6 +81,7 @@ help:
7681
@echo " install-tools Install build + twine for publishing"
7782
@echo " docs-cli Regenerate docs/cli.md from Click commands"
7883
@echo " clean Remove build artifacts and caches"
84+
@echo " clean-online Remove the .venv-online environment"
7985
@echo " version Show current project version"
8086

8187
# ─── Build ───────────────────────────────────────────────────────────────────
@@ -162,6 +168,30 @@ dev-frontend:
162168
dev-backend:
163169
$(RUN) octop run
164170

171+
# ─── Online-deps dev (local Octop source + PyPI harness components) ───────────
172+
# Keeps a dedicated .venv-online alongside .venv. Octop itself is installed
173+
# editable (-e) so source edits are live; the harness-* siblings are pulled
174+
# from PyPI (--no-sources ignores the editable local paths in uv.lock).
175+
# Re-run install-online only when a dependency version changes.
176+
VENV_ONLINE := $(REPO_ROOT)/.venv-online
177+
PY_ONLINE := $(VENV_ONLINE)/bin
178+
179+
.PHONY: install-online
180+
install-online:
181+
@echo "[install-online] Creating $(VENV_ONLINE) (local Octop + PyPI harness deps)..."
182+
uv venv $(VENV_ONLINE) --python 3.12
183+
uv pip install --no-sources --python $(PY_ONLINE)/python -e ".[dev]"
184+
@echo "[install-online] Done. Octop=editable(local), harness-*=PyPI."
185+
186+
.PHONY: test-online
187+
test-online:
188+
@echo "[test-online] pytest against $(VENV_ONLINE) (not live)..."
189+
$(PY_ONLINE)/pytest -m "not live"
190+
191+
.PHONY: run-online
192+
run-online:
193+
$(PY_ONLINE)/octop run
194+
165195
# ─── Quality (backend) ───────────────────────────────────────────────────────
166196

167197
.PHONY: all
@@ -261,6 +291,12 @@ clean:
261291
find $(REPO_ROOT)/src -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
262292
@echo "[clean] Done."
263293

294+
.PHONY: clean-online
295+
clean-online:
296+
@echo "[clean-online] Removing $(VENV_ONLINE)..."
297+
rm -rf $(VENV_ONLINE)
298+
@echo "[clean-online] Done."
299+
264300
.PHONY: version
265301
version:
266302
@$(PYTHON) -c "import pathlib, re; t = pathlib.Path('pyproject.toml').read_text(); m = re.search(r'^version\\s*=\\s*\"([^\"]+)\"', t, re.M); print(m.group(1) if m else 'unknown')"

dashboard/eslint.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ export default tseslint.config(
2020
rules: {
2121
...reactHooks.configs.recommended.rules,
2222
"react-refresh/only-export-components": "off",
23+
// Honour the `_`-prefix convention for intentionally-unused bindings
24+
// (discarded destructures, placeholder callback args).
25+
"@typescript-eslint/no-unused-vars": [
26+
"error",
27+
{
28+
argsIgnorePattern: "^_",
29+
varsIgnorePattern: "^_",
30+
ignoreRestSiblings: true,
31+
},
32+
],
2333
},
2434
},
2535
);

dashboard/src/api/modules/memoryDashboard.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,49 @@ export interface ListAtomsResponse {
6060
has_more: boolean;
6161
}
6262

63+
export interface RawEventItem {
64+
id: string;
65+
host: string;
66+
session_id: string | null;
67+
thread_id: string | null;
68+
user: string | null;
69+
timestamp: string;
70+
event_type: string;
71+
content: string;
72+
payload?: Record<string, unknown>;
73+
}
74+
75+
export interface ListRawEventsResponse {
76+
items: RawEventItem[];
77+
total: number;
78+
has_more: boolean;
79+
}
80+
81+
export interface ListRawEventsBody {
82+
session_id?: string;
83+
thread_id?: string;
84+
event_type?: string;
85+
query?: string;
86+
offset?: number;
87+
limit?: number;
88+
}
89+
90+
export type ExtractTriggerMode = "idle" | "interval";
91+
92+
export interface ExtractConfig {
93+
/** Missing on older Octop API processes; absence keeps the historical enabled default. */
94+
memory_enabled?: boolean;
95+
extract_on_session_end: boolean;
96+
extract_trigger_mode: ExtractTriggerMode;
97+
extract_idle_seconds: number;
98+
extract_interval_seconds: number;
99+
/**
100+
* "provider/model" ref used for extraction / promotion. null/absent = AUTO
101+
* (follow the chat model); send "" to reset back to AUTO.
102+
*/
103+
aux_model?: string | null;
104+
}
105+
63106
export interface EntityItem {
64107
id: string;
65108
entity_type: string;
@@ -70,6 +113,21 @@ export interface EntityItem {
70113
page_dirty?: boolean;
71114
}
72115

116+
/** L3 entity page — the long-form, LLM-regenerated summary for a topic. */
117+
export interface EntityPage {
118+
id: string;
119+
entity_id: string;
120+
summary_markdown: string;
121+
headline: string;
122+
topics: string[];
123+
dirty: boolean;
124+
summary_version: number;
125+
created_at: string;
126+
updated_at: string;
127+
}
128+
129+
export type EntityDetail = EntityItem & { page: EntityPage | null };
130+
73131
export interface ListEntitiesResponse {
74132
items: EntityItem[];
75133
total: number;
@@ -94,6 +152,20 @@ export interface ListEpisodesResponse {
94152
has_more: boolean;
95153
}
96154

155+
/** Structured run stats attached to an ``extract_run`` journal entry. */
156+
export interface ExtractRunStats {
157+
events_considered?: number;
158+
events_extracted?: number;
159+
candidates?: number;
160+
promoted?: number;
161+
merged?: number;
162+
conflicts?: number;
163+
needs_review?: number;
164+
dropped?: number;
165+
llm_calls?: number;
166+
failure_reason?: string | null;
167+
}
168+
97169
export interface JournalItem {
98170
id: string;
99171
timestamp: string;
@@ -105,6 +177,8 @@ export interface JournalItem {
105177
note?: string | null;
106178
/** Short target memory/topic text enriched by the backend for specific action display. */
107179
target_summary?: string | null;
180+
/** Present on ``extract_run`` rows: structured stats for this extraction pass. */
181+
after?: ExtractRunStats | null;
108182
}
109183

110184
export interface ListJournalResponse {
@@ -296,12 +370,15 @@ export const memoryDashboardApi = {
296370
listCandidates: (aid: string, body?: ListCandidatesBody) =>
297371
post<ListCandidatesResponse>(`${base(aid)}/candidates/list`, body),
298372

373+
listRawEvents: (aid: string, body?: ListRawEventsBody) =>
374+
post<ListRawEventsResponse>(`${base(aid)}/raw_events/list`, body),
375+
299376
// single fetches
300377
getAtom: (aid: string, atomId: string) =>
301378
request<AtomItem>(`${base(aid)}/atoms/${encodeURIComponent(atomId)}`),
302379

303380
getEntity: (aid: string, entityId: string) =>
304-
request<EntityItem & { page: unknown | null }>(
381+
request<EntityDetail>(
305382
`${base(aid)}/entities/${encodeURIComponent(entityId)}`,
306383
),
307384

@@ -385,6 +462,16 @@ export const memoryDashboardApi = {
385462
request<TerminalEntityResponse>(
386463
`${base(aid)}/terminal/entities?limit=${limit}`,
387464
),
465+
466+
// extraction-trigger config
467+
getExtractConfig: (aid: string) =>
468+
request<ExtractConfig>(`${base(aid)}/extract-config`),
469+
470+
putExtractConfig: (aid: string, body: Partial<ExtractConfig>) =>
471+
request<ExtractConfig>(`${base(aid)}/extract-config`, {
472+
method: "PUT",
473+
body: JSON.stringify(body),
474+
}),
388475
};
389476

390477
export default memoryDashboardApi;

dashboard/src/locales/en.json

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,16 @@
850850
"rankingsEmpty": "No skills in this ranking yet",
851851
"agentSkillsLib": "{{agent}}'s skills library (/skills/<name>/SKILL.md)",
852852
"noAgentSelected": "Pick an agent in the top switcher first",
853+
"tabs": {
854+
"overview": "Overview",
855+
"profile": "Profile",
856+
"library": "Memory tree",
857+
"episodes": "Emotional journal",
858+
"candidates": "All",
859+
"journal": "Activity",
860+
"proactive": "Proactive care",
861+
"settings": "Settings"
862+
},
853863
"noSkills": "No skills yet",
854864
"noSkillsDesc": "Create a custom skill — the agent will use it at the right moment.",
855865
"totalCount": "{{count}} skill(s)",
@@ -2105,7 +2115,91 @@
21052115
"saveSuccess": "Search configuration saved",
21062116
"applySuccess": "Search configuration applied",
21072117
"saveAndApply": "Save & Apply"
2108-
}
2118+
},
2119+
"pipeline": {
2120+
"title": "Memory pipeline",
2121+
"stageRaw": "Conversation memory",
2122+
"stageRawHint": "Filtered material messages captured from conversations (counts don't map 1:1 to Conversations)",
2123+
"stagePending": "Pending review",
2124+
"stagePendingHint": "Distilled memory candidates",
2125+
"stageAtoms": "Promoted memories",
2126+
"stageAtomsHint": "Memories available for recall",
2127+
"hintNoRaw": "Start chatting and the system will capture material and distill memories automatically.",
2128+
"hintDistilling": "{{n}} pieces of conversation material captured. Distillation runs automatically once a session goes idle; the first memories usually appear after a few exchanges.",
2129+
"hintNormal": "Material is distilled automatically once a session goes idle; results land in Pending Review before becoming promoted memories.",
2130+
"emptyTitle": "Memories are still being distilled",
2131+
"emptyNoRaw": "No memories yet. Start chatting and the system will capture material and distill memories automatically.",
2132+
"emptyWithRaw": "{{n}} pieces of conversation material captured. Distillation runs automatically once a session goes idle; the first memories usually appear after a few exchanges.",
2133+
"emptyPendingSuffix": "{{n}} memory candidates are awaiting confirmation in Pending Review."
2134+
},
2135+
"library": {
2136+
"viewRaw": "Raw material",
2137+
"hintRaw": "Raw conversation material captured before distillation (counts don't map 1:1 to Conversations)"
2138+
},
2139+
"raw": {
2140+
"typeFilter": "Type",
2141+
"typeAll": "All types",
2142+
"typeUser": "User message",
2143+
"typeAssistant": "AI reply",
2144+
"typeToolCall": "Tool call",
2145+
"typeToolResult": "Tool result",
2146+
"searchPlaceholder": "Search material content",
2147+
"detailTitle": "Material detail",
2148+
"content": "Content",
2149+
"capturedAt": "Captured at"
2150+
},
2151+
"extractConfig": {
2152+
"title": "Memory distillation timing",
2153+
"subtitle": "Control when conversation material becomes memory",
2154+
"enableLabel": "Auto-distill memory",
2155+
"modeIdle": "Distill after conversation goes idle",
2156+
"modeIdleHint": "Waits for a quiet conversation — best memory quality (recommended)",
2157+
"modeInterval": "Distill on a fixed interval",
2158+
"modeIntervalHint": "Batch-distills on a schedule; may catch a mid-conversation session",
2159+
"idlePrefix": "After",
2160+
"minutes": "minutes of inactivity",
2161+
"intervalPrefix": "Every",
2162+
"hours": "hours",
2163+
"intervalNote": "Fixed-interval distillation may run before a session ends; the idle mode is recommended for most cases.",
2164+
"disabledHint": "When off, distillation runs only when a session is ended manually.",
2165+
"saved": "Saved — the agent will reload automatically",
2166+
"saveFailed": "Save failed"
2167+
},
2168+
"settings": {
2169+
"title": "Memory settings",
2170+
"description": "Control whether this agent uses memory and when conversations become long-term memory.",
2171+
"storageTitle": "Store memory",
2172+
"storageDescription": "Allow the agent to read existing memories and keep learning from new conversations.",
2173+
"enabled": "On",
2174+
"disabled": "Off",
2175+
"disabledTitle": "The agent will stop using memory",
2176+
"disabledDescription": "The agent will not read existing memories or capture and distill new ones. Existing memories and conversation history are kept and become available again when memory is re-enabled.",
2177+
"distillTitle": "Distillation timing",
2178+
"distillDescription": "Choose when conversation material is organized into recallable long-term memory.",
2179+
"extractModelTitle": "Extraction model",
2180+
"extractModelDescription": "The model used to distill memories. \"Auto\" follows the chat default model; you can also pin a cheaper or faster model for extraction.",
2181+
"extractModelAuto": "Auto (follow chat model)",
2182+
"reloadHint": "Saving reloads the agent automatically. Current conversations are retained.",
2183+
"loadFailed": "Failed to load memory settings",
2184+
"saved": "Saved — the agent will reload automatically",
2185+
"saveFailed": "Save failed"
2186+
},
2187+
"overview": {
2188+
"dashboardTitle": "Memory overview",
2189+
"dashboardSubtitle": "See what the agent remembers and whether memory processing is healthy.",
2190+
"memoryOn": "Memory active",
2191+
"memoryOff": "Memory off",
2192+
"atoms": "Long-term memories",
2193+
"entities": "Key topics",
2194+
"episodes": "Emotional moments",
2195+
"rawEvents": "Conversation memory",
2196+
"candidatesPending": "Needs attention",
2197+
"kindsTitle": "Memory types",
2198+
"kindsEmpty": "No memory type data yet",
2199+
"growthTitle": "Memory growth · 7 days",
2200+
"growthEmpty": "No new memories in the last 7 days"
2201+
},
2202+
"entitySummary": "Topic summary"
21092203
},
21102204
"personalization": {
21112205
"title": "Scenarios",

0 commit comments

Comments
 (0)