Conversation
|
This looks much more maintainable. Thanks for working with this. One thing of note, the alternative #981 writes to a direct buffer with up to 3KiB of output interception. Already as-is seems to not only show the /ctx-stats, doctor, etcetera. And, direct hooks to replace hermes' terminal commands to the sandbox. One clarification: the warning about allowing the plugin to overwrite results refers to Hermes’ The native Sessions DB compatibility and "legacy"
So. Yeah. Much more robust implementation. Thanks again. I will see how well this integrates with my librarian and skill retrieval tools and write back 👍 |
|
Working well within runtimes thus far. CI apparently failing. The Hermes integration itself was not failing. Only The test created this POSIX-only fake executable on every platform: #!/bin/sh
printf '%s' '{"hookSpecificOutput":{"additionalContext":"continuity"}}'Windows Python could not launch that extensionless shell script. The plugin correctly failed open, returned no continuity context, and the embedded Python assertion at line 31 failed. The three GitHub annotations were repetitions of that same failure—not three separate defects. The Node 20 Actions notice was only a warning. (click to expand)import { describe, it, expect } from "vitest";
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { mkdtempSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
import { tmpdir } from "node:os";
import { HermesAdapter } from "../../src/adapters/hermes/index.js";
const ROOT = resolve(__dirname, "../..");
describe("Hermes native adapter", () => {
it("declares hooks with Hermes' public manifest field", () => {
const manifest = readFileSync(resolve(ROOT, "plugin.yaml"), "utf8");
expect(manifest).toContain("provides_hooks:");
expect(manifest).not.toMatch(/^hooks:/m);
});
it("uses Hermes storage and normalizes public hook payloads", () => {
const adapter = new HermesAdapter();
expect(adapter.getSessionDir()).toBe(resolve(adapter.getConfigDir(), "context-mode", "sessions"));
expect(adapter.parsePreToolUseInput({ tool_name: "terminal", args: { command: "pwd" }, session_id: "s" })).toMatchObject({ toolName: "terminal", toolInput: { command: "pwd" }, sessionId: "s" });
expect(adapter.capabilities.canModifyOutput).toBe(true);
expect(adapter.paradigm).toBe("python-plugin");
});
it("honors profile-scoped HERMES_HOME for MCP and hook storage", () => {
const previous = process.env.HERMES_HOME;
const home = mkdtempSync(resolve(tmpdir(), "hermes-home-"));
process.env.HERMES_HOME = home;
try {
const adapter = new HermesAdapter();
expect(adapter.getConfigDir()).toBe(home);
expect(adapter.getSettingsPath()).toBe(resolve(home, "config.yaml"));
expect(adapter.getSessionDir()).toBe(resolve(home, "context-mode", "sessions"));
} finally {
if (previous === undefined) delete process.env.HERMES_HOME;
else process.env.HERMES_HOME = previous;
}
});
it("loads as a real Python plugin, registers public hooks/commands, and fails open", () => {
const binDir = mkdtempSync(resolve(tmpdir(), "context-mode-hermes-"));
const windows = process.platform === "win32";
const stub = resolve(binDir, windows ? "context-mode.cmd" : "context-mode");
writeFileSync(stub, windows ? `@echo off\r
<nul set /p ={"hookSpecificOutput":{"additionalContext":"continuity"}}\r
exit /b 0\r
` : `#!/bin/sh
printf '%s' '{"hookSpecificOutput":{"additionalContext":"continuity"}}'
`);
if (!windows) chmodSync(stub, 0o755);
const harness = String.raw`
import importlib.util, json, pathlib, time
root=pathlib.Path(${JSON.stringify(ROOT)})
spec=importlib.util.spec_from_file_location("context_mode_hermes", root/"__init__.py")
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
class C:
def __init__(self): self.hooks={}; self.commands={}; self.calls=[]; self.delay=False
def register_hook(self,n,f): self.hooks[n]=f
def register_command(self,n,f,*a): self.commands[n]=f
def dispatch_tool(self,n,a,**kw):
self.calls.append((n,a,kw))
if self.delay: time.sleep(0.2)
return json.dumps({"success":True})
c=C(); m.register(c)
assert {"pre_tool_call","post_tool_call","pre_llm_call","on_session_end","on_session_finalize","on_session_reset","transform_tool_result"} <= set(c.hooks)
assert "post_llm_call" not in c.hooks and "on_session_start" not in c.hooks
assert {"ctx-stats","ctx-doctor","ctx-search"} == set(c.commands)
assert c.hooks["transform_tool_result"]("terminal", "small", session_id="s") is None
large="x"*17000
assert c.hooks["transform_tool_result"]("terminal", large, session_id="s") is None
marker=c.hooks["transform_tool_result"]("read_file", large, session_id="s", tool_call_id="call-a")
assert marker and "indexed 17000 bytes" in marker
marker2=c.hooks["transform_tool_result"]("read_file", large, session_id="s", tool_call_id="call-b")
assert marker2 and c.calls[-2][1]["source"] != c.calls[-1][1]["source"]
assert c.hooks["transform_tool_result"]("write_file", large, session_id="s") is None
c.delay=True; m._INDEX_TIMEOUT=0.01
started=time.monotonic()
assert c.hooks["transform_tool_result"]("read_file", large, session_id="s", tool_call_id="slow") is None
assert time.monotonic()-started < 0.1
r=c.hooks["pre_llm_call"](session_id="fresh", user_message="hello", is_first_turn=True, compaction_applied=False)
assert r == {"system_context":"continuity"}
r=c.hooks["pre_llm_call"](session_id="fresh", user_message="next", is_first_turn=False, compaction_applied=True)
assert r == {"system_context":"continuity"}
print("ok")
`;
const run = spawnSync("python3", ["-c", harness], { encoding: "utf8", timeout: 10_000, env: { ...process.env, CONTEXT_MODE_EXECUTABLE: stub } });
expect(run.status, run.stderr).toBe(0);
expect(run.stdout.trim()).toBe("ok");
});
});Updated the test to use:
Production plugin code was untouched. Python officially supports invoking Windows batch files without enabling Verification passed: To push only the CI fix: That should rerun PR #1010 with the actual Windows execution model represented correctly. |
Windows Python cannot launch the POSIX-only extensionless stub, so the plugin failed open and the embedded harness assertion failed. Write a .cmd stub on win32 and keep the existing shell stub elsewhere. Co-authored-by: ildunari <ildunari@users.noreply.github.com>
Continue downstream compatibility for ildunari's upstream PR mksglu#1010. Use the existing scanned subdirectory installer and native search argument metadata.
Resolve closure E1 using context-preserving async offloading, retaining the existing MCP lane and timeout semantics. Prove responsiveness through real gateway dispatch and CLI async result resolution. Continues ildunari upstream PR mksglu#1010.
Summary
Add first-class Hermes Agent support using context-mode's existing MCP server and session pipeline rather than introducing a parallel storage implementation.
This adds:
plugin.yaml+__init__.py) installable withhermes plugins install mksglu/context-mode --enablecontext-mode hook hermes <event>pre_llm_call(compaction_applied=...)signalmcp__context_mode__ctx_indextool, with unique per-tool-call sources and replacement only after confirmed success/ctx-stats,/ctx-doctor, and/ctx-searchHermes commands backed by the same MCP serverThis is intended as a more complete native-integration path alongside the work discussed in #981: the MCP server remains the data plane, while the Hermes plugin is only the lifecycle/control-plane bridge.
Architecture
The integration does not register a Hermes memory provider or context engine. It uses public plugin hooks and
PluginContext.dispatch_toolonly.Safety and failure behavior
pre_tool_call, so context-mode modify decisions become enforceable blocks with routing guidance rather than silently running the original command.tool_call_id(UUID fallback), preventing later calls from overwriting earlier indexed results.$HERMES_HOME/context-mode(normally~/.hermes/context-mode) so named/profile-scoped Hermes instances do not bleed state.Install
Verification
npm run typechecknpm run buildnpm testgit diff --checkPluginManagerdiscovery/load with isolatedHERMES_HOMEpre_llm_callhook bridge created a profile-scoped SessionDB and injected the Hermes routing blockcontext-modectx_index,ctx_search,ctx_stats, andctx_doctorpresentctx_indexsucceededctx_searchretrieved the indexed marker