-
Notifications
You must be signed in to change notification settings - Fork 31
feat: per-agent workspace routing via workspaceMapping config #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6d1cb70
8b6c919
026ac1c
481d093
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,11 @@ async function flushMessages( | |
| const isSubagent = isSubagentSession(ctx); | ||
| const parentAgentId = isSubagent ? subagentParentMap.get(ctx.sessionKey ?? "") : undefined; | ||
|
|
||
| await state.ensureInitialized(); | ||
| // Resolve workspace and get the correct Honcho client for this agent | ||
| const workspaceId = state.resolveWorkspace(agentId); | ||
| const honcho = state.getHonchoClient(workspaceId); | ||
|
|
||
| await state.ensureInitialized(workspaceId); | ||
|
Comment on lines
+29
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep The session is now created in 🔧 Suggested guard+ const parentWorkspaceId =
+ isSubagent && parentAgentId ? state.resolveWorkspace(parentAgentId) : null;
const parentPeer =
- isSubagent && parentAgentId && parentAgentId !== agentId
+ isSubagent &&
+ parentAgentId &&
+ parentAgentId !== agentId &&
+ parentWorkspaceId === workspaceId
? await state.getAgentPeer(parentAgentId)
: null;Also applies to: 48-48 🤖 Prompt for AI Agents |
||
| const agentPeer = await state.getAgentPeer(agentId); | ||
| const parentPeer = | ||
| isSubagent && parentAgentId && parentAgentId !== agentId | ||
|
|
@@ -41,7 +45,7 @@ async function flushMessages( | |
| } : {}), | ||
| }; | ||
|
|
||
| const session = await state.honcho.session(sessionKey, { metadata: sessionMeta }); | ||
| const session = await honcho.session(sessionKey, { metadata: sessionMeta }); | ||
| const meta = await session.getMetadata(); | ||
| const existingMeta: Record<string, unknown> = | ||
| meta && typeof meta === "object" ? (meta as Record<string, unknown>) : {}; | ||
|
|
@@ -69,15 +73,47 @@ async function flushMessages( | |
| return 0; | ||
| } | ||
|
|
||
| const newRawMessages = messages.slice(startIndex); | ||
| const extracted = extractMessages(newRawMessages, state.ownerPeer!, agentPeer, state.cfg.noisePatterns); | ||
| // Cap backfill to prevent overwhelming Honcho with stale history. | ||
| // If more messages are unsaved than maxBackfill, skip older ones. | ||
| const unsavedCount = messages.length - startIndex; | ||
| const maxBackfill = state.cfg.maxBackfill; | ||
| let effectiveStartIndex = startIndex; | ||
| if (maxBackfill >= 0 && unsavedCount > maxBackfill) { | ||
| const skipped = unsavedCount - maxBackfill; | ||
| effectiveStartIndex = messages.length - maxBackfill; | ||
| // Advance lastSavedIndex past the skipped messages so they're never retried | ||
| await session.setMetadata({ ...existingMeta, ...sessionMeta, lastSavedIndex: effectiveStartIndex }); | ||
| if (maxBackfill === 0) { | ||
| // No backfill at all — just mark current position and return | ||
| await session.setMetadata({ ...existingMeta, ...sessionMeta, lastSavedIndex: messages.length }); | ||
| return 0; | ||
| } | ||
| api.logger.info?.(`[honcho] Skipped ${skipped} old messages (maxBackfill=${maxBackfill}), saving most recent ${maxBackfill}`); | ||
| } | ||
|
|
||
| const newRawMessages = messages.slice(effectiveStartIndex); | ||
| const ownerPeer = state.getOwnerPeer(workspaceId)!; | ||
| const extracted = extractMessages(newRawMessages, ownerPeer, agentPeer, state.cfg.noisePatterns); | ||
|
|
||
| if (extracted.length === 0) { | ||
| await session.setMetadata({ ...existingMeta, ...sessionMeta, lastSavedIndex: messages.length }); | ||
| return 0; | ||
| } | ||
|
|
||
| await session.addMessages(extracted); | ||
| // Honcho API enforces a 100-message-per-request limit. | ||
| // Batch to stay under that ceiling and advance lastSavedIndex per batch | ||
| // so partial progress is preserved if a later batch fails. | ||
| const BATCH_SIZE = 50; | ||
| let saved = 0; | ||
| for (let i = 0; i < extracted.length; i += BATCH_SIZE) { | ||
| const batch = extracted.slice(i, i + BATCH_SIZE); | ||
| await session.addMessages(batch); | ||
| saved += batch.length; | ||
| // Advance index after each successful batch so we don't re-send on retry | ||
| const progressIndex = startIndex + saved; | ||
| await session.setMetadata({ ...existingMeta, ...sessionMeta, lastSavedIndex: progressIndex }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| await session.setMetadata({ ...existingMeta, ...sessionMeta, lastSavedIndex: messages.length }); | ||
| return extracted.length; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,17 +11,22 @@ export function registerContextHook(api: OpenClawPluginApi, state: PluginState): | |
| const agentId = ctx.agentId ?? state.resolveDefaultAgentId(); | ||
| const isSubagent = isSubagentSession(ctx); | ||
|
|
||
| // Resolve workspace and get the correct Honcho client for this agent | ||
| const workspaceId = state.resolveWorkspace(agentId); | ||
| const honcho = state.getHonchoClient(workspaceId); | ||
|
|
||
| state.turnStartIndex.set(sessionKey, event.messages.length); | ||
|
|
||
| try { | ||
| await state.ensureInitialized(); | ||
| await state.ensureInitialized(workspaceId); | ||
| const agentPeer = await state.getAgentPeer(agentId); | ||
| const ownerPeer = state.getOwnerPeer(workspaceId)!; | ||
|
Comment on lines
11
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Route subagent context through the parent’s workspace. This resolves the workspace from 🤖 Prompt for AI Agents |
||
|
|
||
| const sections: string[] = []; | ||
|
|
||
| if (isSubagent) { | ||
| try { | ||
| const peerCtx = await agentPeer.context({ target: state.ownerPeer! }); | ||
| const peerCtx = await agentPeer.context({ target: ownerPeer }); | ||
| if (peerCtx.peerCard?.length) { | ||
| sections.push(`Key facts:\n${peerCtx.peerCard.map((f: string) => `• ${f}`).join("\n")}`); | ||
| } | ||
|
|
@@ -36,14 +41,14 @@ export function registerContextHook(api: OpenClawPluginApi, state: PluginState): | |
| throw e; | ||
| } | ||
| } else { | ||
| const session = await state.honcho.session(sessionKey, { metadata: { agentId } }); | ||
| const session = await honcho.session(sessionKey, { metadata: { agentId } }); | ||
|
|
||
| let context; | ||
| try { | ||
| context = await session.context({ | ||
| summary: true, | ||
| tokens: 2000, | ||
| peerTarget: state.ownerPeer!, | ||
| peerTarget: ownerPeer, | ||
| peerPerspective: agentPeer, | ||
| }); | ||
| } catch (e: unknown) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fail fast on malformed
workspaceMappingentries.This silently drops bad prefixes/workspace IDs. Because unmatched agents fall back to
workspaceId, a typo here routes traffic back into the shared default workspace—the opposite of the isolation this feature is adding. Trim and reject invalid entries during parse instead of ignoring them.🛠️ Suggested hardening
📝 Committable suggestion
🤖 Prompt for AI Agents