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
5 changes: 0 additions & 5 deletions .test1.py

This file was deleted.

109 changes: 92 additions & 17 deletions apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,24 @@ def _runtime_namespace(self) -> dict[str, Any]:
"profileLabel": profile_id,
}

def _record_namespace(self) -> dict[str, Any]:
"""Namespace used for write-path records.

Hermes delegation hooks can be global and occasionally arrive through
a provider instance whose `profileId` fell back to `default` while
`agent_identity` still carries the real profile label (for example
coder10). For writes, prefer the concrete non-default label so
subagent outcome traces inherit the parent profile instead of leaking
into hermes/default.
"""
ns = dict(self._runtime_namespace())
label = (self._agent_identity or ns.get("profileLabel") or "").strip()
profile_id = str(ns.get("profileId") or "").strip()
if profile_id in ("", "default", "hermes") and label and label not in ("default", "hermes"):
ns["profileId"] = label
ns["profileLabel"] = label
return ns

def _register_tool_call_hook(self) -> None:
if self._hook_registered:
return
Expand Down Expand Up @@ -818,20 +836,25 @@ def sync_turn(
len(thinking),
)
ts_ms = int(time.time() * 1000)
is_feedback_turn = _is_verifier_feedback_prompt(user)
feedback_submitted = False
try:
if user and not self._episode_id:
self._turn_start(user, session_id=session_id or self._session_id)
self._turn_end(
current_trace_id = self._turn_end(
user,
assistant,
tool_calls,
ts_ms,
agent_thinking=thinking,
)
if _is_verifier_feedback_prompt(user):
self._submit_verifier_feedback(user, assistant, ts_ms)
feedback_submitted = True
if is_feedback_turn:
feedback_submitted = self._try_submit_verifier_feedback(
user,
assistant,
ts_ms,
trace_id=current_trace_id,
)
except Exception as err:
if not self._is_transport_closed(err):
logger.warning("MemOS: sync_turn turn.end failed — %s", err)
Expand All @@ -845,21 +868,36 @@ def sync_turn(
self._reconnect_bridge(session_id or self._session_id, timeout=75.0)
if user:
self._turn_start(user, session_id=session_id or self._session_id)
self._turn_end(
current_trace_id = self._turn_end(
user,
assistant,
tool_calls,
ts_ms,
agent_thinking=thinking,
)
if _is_verifier_feedback_prompt(user) and not feedback_submitted:
self._submit_verifier_feedback(user, assistant, ts_ms)
feedback_submitted = True
if is_feedback_turn and not feedback_submitted:
feedback_submitted = self._try_submit_verifier_feedback(
user,
assistant,
ts_ms,
trace_id=current_trace_id,
)
except Exception:
logger.exception(
"MemOS: sync_turn failed after bridge reconnect; "
"memory turn was not persisted"
)
if is_feedback_turn and not feedback_submitted:
# turn.end may time out while the bridge continues lite capture in
# the background. Preserve the user's explicit signal at episode
# scope instead of dropping Decision Repair entirely.
self._try_submit_verifier_feedback(
user,
assistant,
ts_ms,
trace_id="",
fallback=True,
)
if user_content:
self._last_user_text = user_content

Expand All @@ -883,12 +921,16 @@ def on_delegation(
try:
if not self._episode_id and self._last_user_text:
self._turn_start(self._last_user_text, session_id=self._session_id)
namespace = self._record_namespace()
hook_meta = {
"hookKwargs": kwargs,
"namespace": namespace,
}
self._bridge.request(
"subagent.record",
{
"agent": "hermes",
"namespace": namespace,
"sessionId": self._session_id,
"episodeId": self._episode_id or None,
"childSessionId": child_session_id or None,
Expand All @@ -897,6 +939,11 @@ def on_delegation(
"toolCalls": self._extract_child_tool_calls(child_session_id),
"ts": int(time.time() * 1000),
"meta": hook_meta,
"contextHints": {
"agentIdentity": self._agent_identity,
"namespace": namespace,
**self._host_runtime_context(),
},
},
)
except Exception as err:
Expand Down Expand Up @@ -1684,9 +1731,9 @@ def _turn_end(
ts_ms: int,
*,
agent_thinking: str = "",
) -> None:
) -> str:
if not self._bridge:
return
return ""
# Strip private book-keeping fields before sending.
clean_tool_calls = [
{k: v for k, v in tc.items() if k not in {"_id", "_ids"}} for tc in tool_calls
Expand All @@ -1713,16 +1760,44 @@ def _turn_end(
if result and isinstance(result, dict):
trace_ids = result.get("traceIds", [])
if trace_ids and len(trace_ids) > 0:
self._last_trace_id = trace_ids[-1] # Last trace is the current turn
trace_id = trace_ids[-1] # Last trace is the current turn
self._last_trace_id = trace_id
return trace_id
return ""

def _try_submit_verifier_feedback(
self,
user_content: str,
assistant_content: str,
ts_ms: int,
*,
trace_id: str = "",
fallback: bool = False,
) -> bool:
try:
submitted = self._submit_verifier_feedback(
user_content,
assistant_content,
ts_ms,
trace_id=trace_id,
)
if submitted and fallback:
logger.info("MemOS: submitted verifier feedback without trace binding")
return submitted
except Exception as err:
logger.warning("MemOS: verifier feedback submit failed — %s", err)
return False

def _submit_verifier_feedback(
self,
user_content: str,
assistant_content: str,
ts_ms: int,
) -> None:
*,
trace_id: str = "",
) -> bool:
if not self._bridge or not self._episode_id:
return
return False
polarity = _feedback_polarity(user_content)
magnitude = _feedback_magnitude(user_content, polarity)
raw = {
Expand All @@ -1740,10 +1815,10 @@ def _submit_verifier_feedback(
"raw": raw,
"ts": ts_ms,
}
# Include the last trace ID if available
if self._last_trace_id:
payload["traceId"] = self._last_trace_id
self._bridge.request("feedback.submit", payload)
if trace_id:
payload["traceId"] = trace_id
self._bridge.request("feedback.submit", payload, timeout=75.0)
return True


# ─── Discovery entry points ───────────────────────────────────────────────
Expand Down
38 changes: 30 additions & 8 deletions apps/memos-local-plugin/agent-contract/memory-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ export interface MemoryCore {
sharedAt?: number | null;
},
): Promise<TraceDTO | null>;
getPolicy(id: string, namespace?: RuntimeNamespace): Promise<PolicyDTO | null>;
getWorldModel(id: string, namespace?: RuntimeNamespace): Promise<WorldModelDTO | null>;
getPolicy(id: string, namespace?: RuntimeNamespace, opts?: { includeAllNamespaces?: boolean }): Promise<PolicyDTO | null>;
getWorldModel(id: string, namespace?: RuntimeNamespace, opts?: { includeAllNamespaces?: boolean }): Promise<WorldModelDTO | null>;
/**
* List L2 policies ("经验") — newest-first. The viewer uses this
* for the Experiences panel.
Expand All @@ -215,11 +215,17 @@ export interface MemoryCore {
limit?: number;
offset?: number;
q?: string;
ownerAgentKind?: AgentKind;
ownerProfileId?: string;
includeAllNamespaces?: boolean;
}): Promise<PolicyDTO[]>;
/** Total policy rows matching the same filter (no limit/offset). */
countPolicies(input?: {
status?: PolicyDTO["status"];
q?: string;
ownerAgentKind?: AgentKind;
ownerProfileId?: string;
includeAllNamespaces?: boolean;
}): Promise<number>;
/**
* List L3 world models ("世界环境知识") — newest-first.
Expand All @@ -229,9 +235,12 @@ export interface MemoryCore {
offset?: number;
q?: string;
namespace?: RuntimeNamespace;
ownerAgentKind?: AgentKind;
ownerProfileId?: string;
includeAllNamespaces?: boolean;
}): Promise<WorldModelDTO[]>;
/** Total world-model rows matching the same filter. */
countWorldModels(input?: { q?: string }): Promise<number>;
countWorldModels(input?: { q?: string; ownerAgentKind?: AgentKind; ownerProfileId?: string; includeAllNamespaces?: boolean }): Promise<number>;
/** Transition a policy through candidate → active → archived. */
setPolicyStatus(
id: string,
Expand Down Expand Up @@ -316,10 +325,13 @@ export interface MemoryCore {
sessionId?: SessionId;
limit?: number;
offset?: number;
ownerAgentKind?: AgentKind;
ownerProfileId?: string;
includeAllNamespaces?: boolean;
}): Promise<EpisodeListItemDTO[]>;
/** Total episode rows matching the same filter (no limit/offset). */
countEpisodes(input?: { sessionId?: SessionId }): Promise<number>;
timeline(input: { episodeId: EpisodeId; namespace?: RuntimeNamespace }): Promise<TraceDTO[]>;
countEpisodes(input?: { sessionId?: SessionId; ownerAgentKind?: AgentKind; ownerProfileId?: string; includeAllNamespaces?: boolean }): Promise<number>;
timeline(input: { episodeId: EpisodeId; namespace?: RuntimeNamespace; includeAllNamespaces?: boolean }): Promise<TraceDTO[]>;
/**
* Reverse-chronological trace listing for the Memories viewer.
*
Expand All @@ -339,7 +351,16 @@ export interface MemoryCore {
limit?: number;
offset?: number;
sessionId?: SessionId;
ownerAgentKind?: AgentKind;
ownerProfileId?: string;
q?: string;
/**
* Viewer/admin listing mode. Retrieval still respects namespace
* visibility; the local viewer needs to browse every profile stored in
* the same agent DB so users can switch between Hermes profiles /
* OpenClaw agents.
*/
includeAllNamespaces?: boolean;
/**
* When true, paginate by distinct `(episodeId, turnId)` groups so
* one user turn (query + tool sub-steps + reply) counts as one
Expand All @@ -348,7 +369,7 @@ export interface MemoryCore {
groupByTurn?: boolean;
}): Promise<TraceDTO[]>;
/** Total trace rows matching the same filter (no limit/offset). */
countTraces(input?: { sessionId?: SessionId; q?: string; groupByTurn?: boolean }): Promise<number>;
countTraces(input?: { sessionId?: SessionId; ownerAgentKind?: AgentKind; ownerProfileId?: string; q?: string; groupByTurn?: boolean; includeAllNamespaces?: boolean }): Promise<number>;

/**
* Paged listing of the rich api_logs table ({@link ApiLogDTO}).
Expand All @@ -363,9 +384,9 @@ export interface MemoryCore {
}): Promise<{ logs: ApiLogDTO[]; total: number }>;

// ── skills ──
listSkills(input?: { status?: SkillDTO["status"]; limit?: number; namespace?: RuntimeNamespace }): Promise<SkillDTO[]>;
listSkills(input?: { status?: SkillDTO["status"]; limit?: number; namespace?: RuntimeNamespace; ownerAgentKind?: AgentKind; ownerProfileId?: string; includeAllNamespaces?: boolean }): Promise<SkillDTO[]>;
/** Total skill rows matching the same filter (no limit). */
countSkills(input?: { status?: SkillDTO["status"] }): Promise<number>;
countSkills(input?: { status?: SkillDTO["status"]; ownerAgentKind?: AgentKind; ownerProfileId?: string; includeAllNamespaces?: boolean }): Promise<number>;
getSkill(id: SkillId, opts?: {
recordUse?: boolean;
recordTrial?: boolean;
Expand All @@ -375,6 +396,7 @@ export interface MemoryCore {
turnId?: EpochMs;
toolCallId?: string;
namespace?: RuntimeNamespace;
includeAllNamespaces?: boolean;
}): Promise<SkillDTO | null>;
archiveSkill(id: SkillId, reason?: string): Promise<void>;
/**
Expand Down
Loading