mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-30 09:17:51 +08:00
* feat(integrations): add ZCode memory plugin Add examples/zcode-memory-plugin — a thin ZCode lifecycle adapter that reuses the shared memory-plugin-shared runtime for recall, capture, commit, and MCP proxy. No memory logic is duplicated. Key design decisions (see docs/design/zcode-memory-plugin-design.md): - Vendor shared runtime into scripts/shared/ via sync.mjs (self-contained plugin) - 4 hook events only (SessionStart, UserPromptSubmit, PreToolUse, Stop) — ZCode does not support PreCompact/SessionEnd/SubagentStart/SubagentStop - Output schema: ZCode-canonical keys only (no Claude-Code 'decision: approve') - Config-driven install: hooks + MCP merged into ~/.zcode/cli/config.json - install.sh wiring: detection, TUI, validation, install, uninstall Verified locally: - 22/22 node:test cases pass (turns parser + hook output schema) - sync.test.mjs passes - install → uninstall cycle: hooks/MCP correctly written and cleaned - URI guard denies viking:// paths with MCP redirect - Capture writes to OV session with zc- prefix Closes #3127 Related: #3442, #3544 * chore: remove non-essential files from PR, add .scratch to .gitignore - Remove .scratch/ working notes (local ticket files, not codebase artifacts) - Remove package.json and .gitignore from plugin dir (TRAE/Cursor don't have them) - Add .scratch/ to root .gitignore * fix(zcode): use verified ZCode field names + rollout fallback for capture - Update zcode-turns.mjs to probe responseText/responsePreview (verified from ZCode reverse-engineering in #3127 by @quinn-zenith) instead of the TRAE-inferred last_assistant_message - Add rollout file fallback: when stdin payload lacks user content (the known ZCode limitation), read ~/.zcode/cli/rollout/model-io-sess-*.jsonl to extract the last user+assistant pair from request.messages+response - Fix concurrent session isolation: normalize sessionId→session_id in zcode-hook.mjs before resolveNativeSessionId to prevent cwd-fallback collision when two ZCode windows run in the same directory - Add 2 new test cases for rollout fallback (14 turns tests total, 24 total) - All 24 tests pass * fix(zcode): address maintainer review blockers (config safety, MCP ownership, turnId) Addresses 3 blockers from @huangruiteng's review (CHANGES_REQUESTED): 1. Config safety: distinguish ENOENT from parse errors — malformed config.json now aborts instead of overwriting. Use backup+tmp+rename for atomic writes. 2. MCP ownership: only replace/delete mcp.servers.openviking entries tagged as openviking-memory. User-managed entries with the same name are preserved on install and untouched on uninstall. 3. TurnId-based dedup: rollout entries carry monotonic turnId — now used as the primary dedup key (capturedTurnIds set) instead of stableHash. extractUnseenRolloutTurns scans ALL unseen entries since lastTurnId, not just the last row — recovers missed turns after hook failure. Fail-closed when no turns are found. Also updates DESIGN.md to reflect verified field names (responseText/ responsePreview) and the turnId contract. 27/27 tests pass (was 24). Added 3 new rollout tests: incremental capture with lastTurnId, multi-entry scan, turnId propagation. * docs(zcode): update stale field name references in design spec Update test case descriptions to match verified field names (responseText/responsePreview instead of last_assistant_message) and add rollout fallback + turnId test coverage descriptions. * fix(zcode): dedup key includes role + first-capture returns all turns Fix two bugs found in code review pass 2: 1. Assistant turns silently dropped: user and assistant from the same rollout entry shared a turnId, so dedup via capturedTurnIds dropped the assistant. Fix: dedup key is now ${turnId}:${role}, not turnId alone. Regression test added. 2. First-capture data loss: when no lastKnownTurnId was set, only the last rollout entry was returned, losing prior turns. Fix: first-time capture now returns ALL entries. Also: add backup step to config atomic write (copyFileSync before tmp+rename), fix line width in zcode-turns.mjs, add 2 lifecycle tests (missed Stop recovery, user+assistant same turnId). 29/29 tests pass (was 27). * test(zcode): add concurrent session isolation tests Two new test cases addressing maintainer criterion 4 (concurrent sessions): 1. Two sessions read their own rollout files — verifies session A cannot see session B's content and vice versa (sentinel-based assertion) 2. Independent lastTurnId state per session — verifies incremental capture progresses independently when one session has prior state and another is fresh 31/31 tests pass (was 29). * fix(zcode): correct rollout file path pattern (model-io-<sessionId>) The rollout path used model-io-sess-${sessionId} but ZCode filenames are model-io-<sessionId> where sessionId already includes the sess_ prefix. This caused the rollout fallback to always miss the file and return empty, defeating capture entirely in production. Verified on live two-session ZCode setup: - Session A (sess_8c6ce483): 2 messages, 2 commits - Session B (sess_74759710): 2 messages, 2 commits - No cross-contamination between sessions 31/31 tests pass. Updated all test rollout filename patterns. * docs(zcode): fix stale rollout path in comments and DESIGN.md Comments referenced model-io-sess-<sessionId> but actual pattern is model-io-<sessionId> (fixed in code already, comments were stale). --------- Co-authored-by: woshiguanxiaoliang <woshiguanxiaoliang@noreply.gitcode.com>
58 lines
1.7 KiB
JavaScript
58 lines
1.7 KiB
JavaScript
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
/**
|
|
* Shared OpenViking session-id helpers for memory plugin harnesses.
|
|
*/
|
|
|
|
/**
|
|
* Glob -> RegExp. Minimal implementation: supports `*`, `**`, and literals.
|
|
*/
|
|
function globToRe(glob) {
|
|
let re = "^";
|
|
for (let i = 0; i < glob.length; i++) {
|
|
const c = glob[i];
|
|
if (c === "*") {
|
|
if (glob[i + 1] === "*") { re += ".*"; i++; }
|
|
else re += "[^/]*";
|
|
} else if (/[.+?^${}()|[\]\\]/.test(c)) {
|
|
re += "\\" + c;
|
|
} else {
|
|
re += c;
|
|
}
|
|
}
|
|
re += "$";
|
|
return new RegExp(re);
|
|
}
|
|
|
|
export function isBypassed(cfg, { sessionId, cwd } = {}) {
|
|
if (cfg.bypassSession) return true;
|
|
const patterns = cfg.bypassSessionPatterns || [];
|
|
if (patterns.length === 0) return false;
|
|
const haystacks = [sessionId, cwd].filter(Boolean);
|
|
for (const pat of patterns) {
|
|
const re = globToRe(pat);
|
|
if (haystacks.some((h) => re.test(h))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function safeId(value, replacement = "_") {
|
|
return String(value || "unknown").replace(/[^A-Za-z0-9._-]/g, replacement);
|
|
}
|
|
|
|
export function deriveHarnessSessionId(prefix, sessionId, suffix = "") {
|
|
if (!prefix || typeof prefix !== "string") {
|
|
throw new Error("deriveHarnessSessionId requires a non-empty prefix");
|
|
}
|
|
if (!sessionId || typeof sessionId !== "string") {
|
|
throw new Error("deriveHarnessSessionId requires a non-empty sessionId");
|
|
}
|
|
const base = `${prefix}${sessionId}`;
|
|
if (!suffix) return base;
|
|
const normalized = String(suffix).replace(/:/g, "-").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
return `${base}__${normalized}`;
|
|
}
|
|
|
|
export function deriveCodexSessionId(codexSessionId) {
|
|
return `cx-${safeId(codexSessionId, "_")}`;
|
|
}
|