2026-02-18 01:34:35 +00:00
|
|
|
import fs from "node:fs/promises";
|
|
|
|
|
import os from "node:os";
|
2026-02-13 19:54:22 -04:00
|
|
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
2026-01-22 09:58:07 +00:00
|
|
|
import {
|
|
|
|
|
createAgentSession,
|
2026-02-20 23:21:09 -04:00
|
|
|
DefaultResourceLoader,
|
2026-01-22 09:58:07 +00:00
|
|
|
estimateTokens,
|
|
|
|
|
SessionManager,
|
|
|
|
|
} from "@mariozechner/pi-coding-agent";
|
2026-02-18 01:29:02 +00:00
|
|
|
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
|
2026-02-18 01:29:02 +00:00
|
|
|
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
import {
|
|
|
|
|
ensureContextEnginesInitialized,
|
|
|
|
|
resolveContextEngine,
|
|
|
|
|
} from "../../context-engine/index.js";
|
2026-03-05 22:08:26 -05:00
|
|
|
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
2026-02-22 10:14:55 +01:00
|
|
|
import { generateSecureToken } from "../../infra/secure-random.js";
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
|
Agents: add nested subagent orchestration controls and reduce subagent token waste (#14447)
* Agents: add subagent orchestration controls
* Agents: add subagent orchestration controls (WIP uncommitted changes)
* feat(subagents): add depth-based spawn gating for sub-sub-agents
* feat(subagents): tool policy, registry, and announce chain for nested agents
* feat(subagents): system prompt, docs, changelog for nested sub-agents
* fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback
Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex),
the fallback candidate logic in resolveFallbackCandidates silently appended the
global primary model (opus) as a backstop. On reinjection/steer with a transient
error, the session could fall back to opus which has a smaller context window
and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? []
instead of undefined, preventing the implicit primary backstop.
Bug 2: Active subagents showed 'model n/a' in /subagents list because
resolveModelDisplay only read entry.model/modelProvider (populated after run
completes). Fix: fall back to modelOverride/providerOverride fields which are
populated at spawn time via sessions.patch.
Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could
theoretically escape runEmbeddedPiAgent and be treated as failover candidates
in runWithModelFallback, causing a switch to a model with a smaller context
window. Fix: in runWithModelFallback, detect context overflow errors via
isLikelyContextOverflowError and rethrow them immediately instead of trying the
next model candidate.
* fix(subagents): track spawn depth in session store and fix announce routing for nested agents
* Fix compaction status tracking and dedupe overflow compaction triggers
* fix(subagents): enforce depth block via session store and implement cascade kill
* fix: inject group chat context into system prompt
* fix(subagents): always write model to session store at spawn time
* Preserve spawnDepth when agent handler rewrites session entry
* fix(subagents): suppress announce on steer-restart
* fix(subagents): fallback spawned session model to runtime default
* fix(subagents): enforce spawn depth when caller key resolves by sessionId
* feat(subagents): implement active-first ordering for numeric targets and enhance task display
- Added a test to verify that subagents with numeric targets follow an active-first list ordering.
- Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity.
- Enhanced task display in command responses to prevent truncation of long task descriptions.
- Introduced new utility functions for compacting task text and managing subagent run states.
* fix(subagents): show model for active runs via run record fallback
When the spawned model matches the agent's default model, the session
store's override fields are intentionally cleared (isDefault: true).
The model/modelProvider fields are only populated after the run
completes. This left active subagents showing 'model n/a'.
Fix: store the resolved model on SubagentRunRecord at registration
time, and use it as a fallback in both display paths (subagents tool
and /subagents command) when the session store entry has no model info.
Changes:
- SubagentRunRecord: add optional model field
- registerSubagentRun: accept and persist model param
- sessions-spawn-tool: pass resolvedModel to registerSubagentRun
- subagents-tool: pass run record model as fallback to resolveModelDisplay
- commands-subagents: pass run record model as fallback to resolveModelDisplay
* feat(chat): implement session key resolution and reset on sidebar navigation
- Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar.
- Updated the `renderTab` function to handle session key changes when navigating to the chat tab.
- Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation.
* fix: subagent timeout=0 passthrough and fallback prompt duplication
Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default
- sessions-spawn-tool: default to undefined (not 0) when neither timeout param
is provided; use != null check so explicit 0 passes through to gateway
- agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles
0 → MAX_SAFE_TIMEOUT_MS)
Bug 2: model fallback no longer re-injects the original prompt as a duplicate
- agent.ts: track fallback attempt index; on retries use a short continuation
message instead of the full original prompt since the session file already
contains it from the first attempt
- Also skip re-sending images on fallback retries (already in session)
* feat(subagents): truncate long task descriptions in subagents command output
- Introduced a new utility function to format task previews, limiting their length to improve readability.
- Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately.
- Adjusted related tests to verify that long task descriptions are now truncated in the output.
* refactor(subagents): update subagent registry path resolution and improve command output formatting
- Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically.
- Enhanced the formatting of command output for active and recent subagents, adding separators for better readability.
- Updated related tests to reflect changes in command output structure.
* fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted
The previous fix (75a791106) correctly handled the case where
runTimeoutSeconds was explicitly set to 0 ("no timeout"). However,
when models omit the parameter entirely (which is common since the
schema marks it as optional), runTimeoutSeconds resolved to undefined.
undefined flowed through the chain as:
sessions_spawn → timeout: undefined (since undefined != null is false)
→ gateway agent handler → agentCommand opts.timeout: undefined
→ resolveAgentTimeoutMs({ overrideSeconds: undefined })
→ DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes)
This caused subagents to be killed at exactly 10 minutes even though
the user's intent (via TOOLS.md) was for subagents to run without a
timeout.
Fix: default runTimeoutSeconds to 0 (no timeout) when neither
runTimeoutSeconds nor timeoutSeconds is provided by the caller.
Subagent spawns are long-running by design and should not inherit the
600s agent-command default timeout.
* fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default)
* fix: thread timeout override through getReplyFromConfig dispatch path
getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override,
always falling back to the config default (600s). Add timeoutOverrideSeconds
to GetReplyOptions and pass it through as overrideSeconds so callers of the
dispatch chain can specify a custom timeout (0 = no timeout).
This complements the existing timeout threading in agentCommand and the
cron isolated-agent runner, which already pass overrideSeconds correctly.
* feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling
- Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution.
- Updated the `resolveFallbackCandidates` function to utilize the new normalization logic.
- Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms.
- Introduced a new test case to ensure that the normalization process works as expected for various input formats.
* feat(tests): add unit tests for steer failure behavior in openclaw-tools
- Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails.
- Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected.
- Enhanced the subagent registry with a new function to clear steer restart suppression.
- Updated related components to support the new test scenarios.
* fix(subagents): replace stop command with kill in slash commands and documentation
- Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs.
- Modified related documentation to reflect the change in command usage.
- Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling.
- Enhanced tests to ensure correct behavior of the updated commands and their interactions.
* feat(tests): add unit tests for readLatestAssistantReply function
- Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios.
- Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text.
- Mocked the gateway call to simulate different message histories for comprehensive testing.
* feat(tests): enhance subagent kill-all cascade tests and announce formatting
- Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents.
- Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content.
- Improved the handling of long findings and stats in the announce formatting logic to ensure concise output.
- Refactored related functions to enhance clarity and maintainability in the subagent registry and tools.
* refactor(subagent): update announce formatting and remove unused constants
- Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests.
- Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic.
- Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs.
- Cleaned up unused imports in the commands-subagents file to enhance code clarity.
* feat(tests): enhance billing error handling in user-facing text
- Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context.
- Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages.
- Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output.
- Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements.
* feat(subagent): enhance workflow guidance and auto-announcement clarity
- Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates.
- Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow.
- Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts.
* fix(cron): avoid announcing interim subagent spawn acks
* chore: clean post-rebase imports
* fix(cron): fall back to child replies when parent stays interim
* fix(subagents): make active-run guidance advisory
* fix(subagents): update announce flow to handle active descendants and enhance test coverage
- Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting.
- Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents.
- Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process.
* fix(subagents): enhance announce flow and formatting for user updates
- Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context.
- Refactored the announcement logic to improve clarity and ensure internal context remains private.
- Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates.
- Introduced a new function to build reply instructions based on session context, improving the overall announcement process.
* fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204)
* fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204)
* fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204)
* fix: apply formatting after rebase (#14447) (thanks @tyler6204)
2026-02-14 22:03:45 -08:00
|
|
|
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { resolveSignalReactionLevel } from "../../signal/reaction-level.js";
|
2026-01-16 20:16:35 +00:00
|
|
|
import { resolveTelegramInlineButtonsScope } from "../../telegram/inline-buttons.js";
|
2026-01-25 03:20:09 +00:00
|
|
|
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
|
|
|
|
|
import { resolveUserPath } from "../../utils.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
|
|
|
|
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
2026-01-30 03:15:10 +01:00
|
|
|
import { resolveOpenClawAgentDir } from "../agent-paths.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { resolveSessionAgentIds } from "../agent-scope.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
2026-01-18 06:07:20 +00:00
|
|
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { listChannelSupportedActions, resolveChannelMessageToolHints } from "../channel-tools.js";
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
import { resolveContextWindowInfo } from "../context-window-guard.js";
|
2026-03-07 20:40:34 -05:00
|
|
|
import { ensureCustomApiRegistered } from "../custom-api-registry.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { resolveOpenClawDocsPath } from "../docs-path.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js";
|
2026-03-06 19:07:11 -05:00
|
|
|
import { supportsModelTools } from "../model-tool-support.js";
|
2026-01-30 03:15:10 +01:00
|
|
|
import { ensureOpenClawModelsJson } from "../models-config.js";
|
2026-03-07 20:40:34 -05:00
|
|
|
import { createConfiguredOllamaStreamFn } from "../ollama-stream.js";
|
2026-02-22 09:34:48 +01:00
|
|
|
import { resolveOwnerDisplaySetting } from "../owner-display.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import {
|
|
|
|
|
ensureSessionHeader,
|
|
|
|
|
validateAnthropicTurns,
|
|
|
|
|
validateGeminiTurns,
|
|
|
|
|
} from "../pi-embedded-helpers.js";
|
2026-02-26 21:46:24 +01:00
|
|
|
import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js";
|
2026-01-30 03:15:10 +01:00
|
|
|
import { createOpenClawCodingTools } from "../pi-tools.js";
|
2026-03-08 23:38:14 +00:00
|
|
|
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { resolveSandboxContext } from "../sandbox.js";
|
2026-02-03 05:17:42 +08:00
|
|
|
import { repairSessionFileIfNeeded } from "../session-file-repair.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { guardSessionManager } from "../session-tool-result-guard-wrapper.js";
|
2026-02-12 07:42:05 +08:00
|
|
|
import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
|
2026-02-17 03:09:56 +01:00
|
|
|
import {
|
|
|
|
|
acquireSessionWriteLock,
|
|
|
|
|
resolveSessionLockMaxHoldFromTimeout,
|
|
|
|
|
} from "../session-write-lock.js";
|
2026-02-07 11:32:31 -06:00
|
|
|
import { detectRuntimeShell } from "../shell-utils.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import {
|
|
|
|
|
applySkillEnvOverrides,
|
|
|
|
|
applySkillEnvOverridesFromSnapshot,
|
|
|
|
|
resolveSkillsPromptForRun,
|
|
|
|
|
type SkillSnapshot,
|
|
|
|
|
} from "../skills.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
2026-02-17 03:09:56 +01:00
|
|
|
import {
|
|
|
|
|
compactWithSafetyTimeout,
|
|
|
|
|
EMBEDDED_COMPACTION_TIMEOUT_MS,
|
|
|
|
|
} from "./compaction-safety-timeout.js";
|
2026-02-20 23:21:09 -04:00
|
|
|
import { buildEmbeddedExtensionFactories } from "./extensions.js";
|
2026-01-16 06:57:25 +00:00
|
|
|
import {
|
|
|
|
|
logToolSchemasForGoogle,
|
|
|
|
|
sanitizeSessionHistory,
|
|
|
|
|
sanitizeToolsForGoogle,
|
|
|
|
|
} from "./google.js";
|
2026-01-14 14:31:43 +00:00
|
|
|
import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
|
|
|
|
|
import { log } from "./logger.js";
|
|
|
|
|
import { buildModelAliasLines, resolveModel } from "./model.js";
|
|
|
|
|
import { buildEmbeddedSandboxInfo } from "./sandbox-info.js";
|
2026-01-14 14:31:43 +00:00
|
|
|
import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js";
|
2026-03-03 02:44:04 -05:00
|
|
|
import { resolveEmbeddedRunSkillEntries } from "./skills-runtime.js";
|
2026-02-01 15:06:42 -08:00
|
|
|
import {
|
|
|
|
|
applySystemPromptOverrideToSession,
|
|
|
|
|
buildEmbeddedSystemPrompt,
|
|
|
|
|
createSystemPromptOverride,
|
|
|
|
|
} from "./system-prompt.js";
|
2026-02-21 23:06:44 -08:00
|
|
|
import { collectAllowedToolNames } from "./tool-name-allowlist.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { splitSdkTools } from "./tool-split.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type { EmbeddedPiCompactResult } from "./types.js";
|
2026-02-13 17:34:04 -08:00
|
|
|
import { describeUnknownError, mapThinkingLevel } from "./utils.js";
|
2026-02-14 06:35:43 +11:00
|
|
|
import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
export type CompactEmbeddedPiSessionParams = {
|
2026-01-14 01:08:15 +00:00
|
|
|
sessionId: string;
|
2026-02-13 19:54:22 -04:00
|
|
|
runId?: string;
|
2026-01-14 01:08:15 +00:00
|
|
|
sessionKey?: string;
|
|
|
|
|
messageChannel?: string;
|
|
|
|
|
messageProvider?: string;
|
|
|
|
|
agentAccountId?: string;
|
2026-01-24 22:23:49 +00:00
|
|
|
authProfileId?: string;
|
2026-01-24 05:49:23 +00:00
|
|
|
/** Group id for channel-level tool policy resolution. */
|
|
|
|
|
groupId?: string | null;
|
|
|
|
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
|
|
|
groupChannel?: string | null;
|
|
|
|
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
|
|
|
groupSpace?: string | null;
|
|
|
|
|
/** Parent session key for subagent policy inheritance. */
|
|
|
|
|
spawnedBy?: string | null;
|
2026-02-04 19:49:36 -05:00
|
|
|
/** Whether the sender is an owner (required for owner-only tools). */
|
|
|
|
|
senderIsOwner?: boolean;
|
2026-01-14 01:08:15 +00:00
|
|
|
sessionFile: string;
|
|
|
|
|
workspaceDir: string;
|
|
|
|
|
agentDir?: string;
|
2026-01-30 03:15:10 +01:00
|
|
|
config?: OpenClawConfig;
|
2026-01-14 01:08:15 +00:00
|
|
|
skillsSnapshot?: SkillSnapshot;
|
|
|
|
|
provider?: string;
|
|
|
|
|
model?: string;
|
|
|
|
|
thinkLevel?: ThinkLevel;
|
|
|
|
|
reasoningLevel?: ReasoningLevel;
|
|
|
|
|
bashElevated?: ExecElevatedDefaults;
|
|
|
|
|
customInstructions?: string;
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
tokenBudget?: number;
|
|
|
|
|
force?: boolean;
|
2026-02-15 06:54:12 +08:00
|
|
|
trigger?: "overflow" | "manual";
|
2026-02-13 19:54:22 -04:00
|
|
|
diagId?: string;
|
|
|
|
|
attempt?: number;
|
|
|
|
|
maxAttempts?: number;
|
2026-01-14 01:08:15 +00:00
|
|
|
lane?: string;
|
|
|
|
|
enqueue?: typeof enqueueCommand;
|
|
|
|
|
extraSystemPrompt?: string;
|
|
|
|
|
ownerNumbers?: string[];
|
2026-01-24 19:09:24 -03:00
|
|
|
};
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-02-13 19:54:22 -04:00
|
|
|
type CompactionMessageMetrics = {
|
|
|
|
|
messages: number;
|
|
|
|
|
historyTextChars: number;
|
|
|
|
|
toolResultChars: number;
|
|
|
|
|
estTokens?: number;
|
|
|
|
|
contributors: Array<{ role: string; chars: number; tool?: string }>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-06 03:40:25 +08:00
|
|
|
function hasRealConversationContent(msg: AgentMessage): boolean {
|
|
|
|
|
return msg.role === "user" || msg.role === "assistant" || msg.role === "toolResult";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 19:54:22 -04:00
|
|
|
function createCompactionDiagId(): string {
|
2026-02-22 10:14:55 +01:00
|
|
|
return `cmp-${Date.now().toString(36)}-${generateSecureToken(4)}`;
|
2026-02-13 19:54:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMessageTextChars(msg: AgentMessage): number {
|
|
|
|
|
const content = (msg as { content?: unknown }).content;
|
|
|
|
|
if (typeof content === "string") {
|
|
|
|
|
return content.length;
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(content)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const block of content) {
|
|
|
|
|
if (!block || typeof block !== "object") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const text = (block as { text?: unknown }).text;
|
|
|
|
|
if (typeof text === "string") {
|
|
|
|
|
total += text.length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveMessageToolLabel(msg: AgentMessage): string | undefined {
|
|
|
|
|
const candidate =
|
|
|
|
|
(msg as { toolName?: unknown }).toolName ??
|
|
|
|
|
(msg as { name?: unknown }).name ??
|
|
|
|
|
(msg as { tool?: unknown }).tool;
|
|
|
|
|
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function summarizeCompactionMessages(messages: AgentMessage[]): CompactionMessageMetrics {
|
|
|
|
|
let historyTextChars = 0;
|
|
|
|
|
let toolResultChars = 0;
|
|
|
|
|
const contributors: Array<{ role: string; chars: number; tool?: string }> = [];
|
|
|
|
|
let estTokens = 0;
|
|
|
|
|
let tokenEstimationFailed = false;
|
|
|
|
|
|
|
|
|
|
for (const msg of messages) {
|
|
|
|
|
const role = typeof msg.role === "string" ? msg.role : "unknown";
|
|
|
|
|
const chars = getMessageTextChars(msg);
|
|
|
|
|
historyTextChars += chars;
|
|
|
|
|
if (role === "toolResult") {
|
|
|
|
|
toolResultChars += chars;
|
|
|
|
|
}
|
|
|
|
|
contributors.push({ role, chars, tool: resolveMessageToolLabel(msg) });
|
|
|
|
|
if (!tokenEstimationFailed) {
|
|
|
|
|
try {
|
|
|
|
|
estTokens += estimateTokens(msg);
|
|
|
|
|
} catch {
|
|
|
|
|
tokenEstimationFailed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
messages: messages.length,
|
|
|
|
|
historyTextChars,
|
|
|
|
|
toolResultChars,
|
|
|
|
|
estTokens: tokenEstimationFailed ? undefined : estTokens,
|
|
|
|
|
contributors: contributors.toSorted((a, b) => b.chars - a.chars).slice(0, 3),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function classifyCompactionReason(reason?: string): string {
|
|
|
|
|
const text = (reason ?? "").trim().toLowerCase();
|
|
|
|
|
if (!text) {
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("nothing to compact")) {
|
|
|
|
|
return "no_compactable_entries";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("below threshold")) {
|
|
|
|
|
return "below_threshold";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("already compacted")) {
|
|
|
|
|
return "already_compacted_recently";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("guard")) {
|
|
|
|
|
return "guard_blocked";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("summary")) {
|
|
|
|
|
return "summary_failed";
|
|
|
|
|
}
|
|
|
|
|
if (text.includes("timed out") || text.includes("timeout")) {
|
|
|
|
|
return "timeout";
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
text.includes("400") ||
|
|
|
|
|
text.includes("401") ||
|
|
|
|
|
text.includes("403") ||
|
|
|
|
|
text.includes("429")
|
|
|
|
|
) {
|
|
|
|
|
return "provider_error_4xx";
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
text.includes("500") ||
|
|
|
|
|
text.includes("502") ||
|
|
|
|
|
text.includes("503") ||
|
|
|
|
|
text.includes("504")
|
|
|
|
|
) {
|
|
|
|
|
return "provider_error_5xx";
|
|
|
|
|
}
|
|
|
|
|
return "unknown";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
/**
|
|
|
|
|
* Core compaction logic without lane queueing.
|
|
|
|
|
* Use this when already inside a session/global lane to avoid deadlocks.
|
|
|
|
|
*/
|
|
|
|
|
export async function compactEmbeddedPiSessionDirect(
|
|
|
|
|
params: CompactEmbeddedPiSessionParams,
|
|
|
|
|
): Promise<EmbeddedPiCompactResult> {
|
2026-02-13 19:54:22 -04:00
|
|
|
const startedAt = Date.now();
|
|
|
|
|
const diagId = params.diagId?.trim() || createCompactionDiagId();
|
|
|
|
|
const trigger = params.trigger ?? "manual";
|
|
|
|
|
const attempt = params.attempt ?? 1;
|
|
|
|
|
const maxAttempts = params.maxAttempts ?? 1;
|
|
|
|
|
const runId = params.runId ?? params.sessionId;
|
2026-01-24 19:09:24 -03:00
|
|
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
2026-03-08 23:38:14 +00:00
|
|
|
ensureRuntimePluginsLoaded({
|
|
|
|
|
config: params.config,
|
|
|
|
|
workspaceDir: resolvedWorkspace,
|
|
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
const prevCwd = process.cwd();
|
|
|
|
|
|
2026-03-08 18:47:34 +01:00
|
|
|
// Resolve compaction model: prefer config override, then fall back to caller-supplied model
|
|
|
|
|
const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim();
|
|
|
|
|
let provider: string;
|
|
|
|
|
let modelId: string;
|
|
|
|
|
// When switching provider via override, drop the primary auth profile to avoid
|
|
|
|
|
// sending the wrong credentials (e.g. OpenAI profile token to OpenRouter).
|
|
|
|
|
let authProfileId: string | undefined = params.authProfileId;
|
|
|
|
|
if (compactionModelOverride) {
|
|
|
|
|
const slashIdx = compactionModelOverride.indexOf("/");
|
|
|
|
|
if (slashIdx > 0) {
|
|
|
|
|
provider = compactionModelOverride.slice(0, slashIdx).trim();
|
|
|
|
|
modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;
|
|
|
|
|
// Provider changed — drop primary auth profile so getApiKeyForModel
|
|
|
|
|
// falls back to provider-based key resolution for the override model.
|
|
|
|
|
if (provider !== (params.provider ?? "").trim()) {
|
|
|
|
|
authProfileId = undefined;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
|
|
|
|
modelId = compactionModelOverride;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
|
|
|
|
modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
|
|
|
|
}
|
2026-02-15 19:09:05 +00:00
|
|
|
const fail = (reason: string): EmbeddedPiCompactResult => {
|
2026-02-13 19:54:22 -04:00
|
|
|
log.warn(
|
|
|
|
|
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
|
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
|
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=failed reason=${classifyCompactionReason(reason)} ` +
|
|
|
|
|
`durationMs=${Date.now() - startedAt}`,
|
|
|
|
|
);
|
2026-01-24 19:09:24 -03:00
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
compacted: false,
|
2026-02-13 19:54:22 -04:00
|
|
|
reason,
|
2026-01-24 19:09:24 -03:00
|
|
|
};
|
2026-02-15 19:09:05 +00:00
|
|
|
};
|
|
|
|
|
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
|
|
|
|
|
await ensureOpenClawModelsJson(params.config, agentDir);
|
|
|
|
|
const { model, error, authStorage, modelRegistry } = resolveModel(
|
|
|
|
|
provider,
|
|
|
|
|
modelId,
|
|
|
|
|
agentDir,
|
|
|
|
|
params.config,
|
|
|
|
|
);
|
|
|
|
|
if (!model) {
|
|
|
|
|
const reason = error ?? `Unknown model: ${provider}/${modelId}`;
|
|
|
|
|
return fail(reason);
|
2026-01-24 19:09:24 -03:00
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const apiKeyInfo = await getApiKeyForModel({
|
|
|
|
|
model,
|
|
|
|
|
cfg: params.config,
|
2026-03-08 18:47:34 +01:00
|
|
|
profileId: authProfileId,
|
2026-01-24 19:09:24 -03:00
|
|
|
agentDir,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!apiKeyInfo.apiKey) {
|
|
|
|
|
if (apiKeyInfo.mode !== "aws-sdk") {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`No API key resolved for provider "${model.provider}" (auth mode: ${apiKeyInfo.mode}).`,
|
|
|
|
|
);
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
} else if (model.provider === "github-copilot") {
|
|
|
|
|
const { resolveCopilotApiToken } = await import("../../providers/github-copilot-token.js");
|
|
|
|
|
const copilotToken = await resolveCopilotApiToken({
|
|
|
|
|
githubToken: apiKeyInfo.apiKey,
|
|
|
|
|
});
|
|
|
|
|
authStorage.setRuntimeApiKey(model.provider, copilotToken.token);
|
|
|
|
|
} else {
|
|
|
|
|
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-02-13 19:54:22 -04:00
|
|
|
const reason = describeUnknownError(err);
|
2026-02-15 19:09:05 +00:00
|
|
|
return fail(reason);
|
2026-01-24 19:09:24 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
|
|
|
|
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
|
|
|
|
const sandbox = await resolveSandboxContext({
|
|
|
|
|
config: params.config,
|
|
|
|
|
sessionKey: sandboxSessionKey,
|
|
|
|
|
workspaceDir: resolvedWorkspace,
|
|
|
|
|
});
|
|
|
|
|
const effectiveWorkspace = sandbox?.enabled
|
|
|
|
|
? sandbox.workspaceAccess === "rw"
|
|
|
|
|
? resolvedWorkspace
|
|
|
|
|
: sandbox.workspaceDir
|
|
|
|
|
: resolvedWorkspace;
|
|
|
|
|
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
|
|
|
|
await ensureSessionHeader({
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
cwd: effectiveWorkspace,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let restoreSkillEnv: (() => void) | undefined;
|
|
|
|
|
process.chdir(effectiveWorkspace);
|
|
|
|
|
try {
|
2026-03-03 02:44:04 -05:00
|
|
|
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
config: params.config,
|
|
|
|
|
skillsSnapshot: params.skillsSnapshot,
|
|
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
restoreSkillEnv = params.skillsSnapshot
|
|
|
|
|
? applySkillEnvOverridesFromSnapshot({
|
|
|
|
|
snapshot: params.skillsSnapshot,
|
|
|
|
|
config: params.config,
|
|
|
|
|
})
|
|
|
|
|
: applySkillEnvOverrides({
|
|
|
|
|
skills: skillEntries ?? [],
|
|
|
|
|
config: params.config,
|
2026-01-14 01:08:15 +00:00
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
const skillsPrompt = resolveSkillsPromptForRun({
|
|
|
|
|
skillsSnapshot: params.skillsSnapshot,
|
|
|
|
|
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
|
|
|
|
config: params.config,
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
2026-03-05 22:08:26 -05:00
|
|
|
const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
|
2026-01-24 19:09:24 -03:00
|
|
|
const { contextFiles } = await resolveBootstrapContextForRun({
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
config: params.config,
|
|
|
|
|
sessionKey: params.sessionKey,
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
|
|
|
|
|
});
|
2026-03-07 19:18:48 +00:00
|
|
|
// Apply contextTokens cap to model so pi-coding-agent's auto-compaction
|
|
|
|
|
// threshold uses the effective limit, not the native context window.
|
|
|
|
|
const ctxInfo = resolveContextWindowInfo({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
provider,
|
|
|
|
|
modelId,
|
|
|
|
|
modelContextWindow: model.contextWindow,
|
|
|
|
|
defaultTokens: DEFAULT_CONTEXT_TOKENS,
|
|
|
|
|
});
|
|
|
|
|
const effectiveModel =
|
|
|
|
|
ctxInfo.tokens < (model.contextWindow ?? Infinity)
|
|
|
|
|
? { ...model, contextWindow: ctxInfo.tokens }
|
|
|
|
|
: model;
|
|
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
const runAbortController = new AbortController();
|
2026-01-30 03:15:10 +01:00
|
|
|
const toolsRaw = createOpenClawCodingTools({
|
2026-01-24 19:09:24 -03:00
|
|
|
exec: {
|
|
|
|
|
elevated: params.bashElevated,
|
|
|
|
|
},
|
|
|
|
|
sandbox,
|
2026-03-05 22:08:26 -05:00
|
|
|
messageProvider: resolvedMessageProvider,
|
2026-01-24 19:09:24 -03:00
|
|
|
agentAccountId: params.agentAccountId,
|
2026-03-02 11:28:27 -08:00
|
|
|
sessionKey: sandboxSessionKey,
|
2026-03-02 15:11:51 -08:00
|
|
|
sessionId: params.sessionId,
|
2026-03-02 17:23:08 -08:00
|
|
|
runId: params.runId,
|
2026-01-24 19:09:24 -03:00
|
|
|
groupId: params.groupId,
|
|
|
|
|
groupChannel: params.groupChannel,
|
|
|
|
|
groupSpace: params.groupSpace,
|
|
|
|
|
spawnedBy: params.spawnedBy,
|
2026-02-04 19:49:36 -05:00
|
|
|
senderIsOwner: params.senderIsOwner,
|
2026-01-24 19:09:24 -03:00
|
|
|
agentDir,
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
config: params.config,
|
|
|
|
|
abortSignal: runAbortController.signal,
|
|
|
|
|
modelProvider: model.provider,
|
|
|
|
|
modelId,
|
2026-03-07 19:18:48 +00:00
|
|
|
modelContextWindowTokens: ctxInfo.tokens,
|
2026-01-24 19:09:24 -03:00
|
|
|
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
|
|
|
|
});
|
2026-03-06 19:07:11 -05:00
|
|
|
const tools = sanitizeToolsForGoogle({
|
|
|
|
|
tools: supportsModelTools(model) ? toolsRaw : [],
|
|
|
|
|
provider,
|
|
|
|
|
});
|
2026-02-21 23:06:44 -08:00
|
|
|
const allowedToolNames = collectAllowedToolNames({ tools });
|
2026-01-24 19:09:24 -03:00
|
|
|
logToolSchemasForGoogle({ tools, provider });
|
|
|
|
|
const machineName = await getMachineDisplayName();
|
|
|
|
|
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
|
|
|
|
|
let runtimeCapabilities = runtimeChannel
|
|
|
|
|
? (resolveChannelCapabilities({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
channel: runtimeChannel,
|
|
|
|
|
accountId: params.agentAccountId,
|
|
|
|
|
}) ?? [])
|
|
|
|
|
: undefined;
|
|
|
|
|
if (runtimeChannel === "telegram" && params.config) {
|
|
|
|
|
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
accountId: params.agentAccountId ?? undefined,
|
|
|
|
|
});
|
|
|
|
|
if (inlineButtonsScope !== "off") {
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!runtimeCapabilities) {
|
|
|
|
|
runtimeCapabilities = [];
|
|
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
if (
|
|
|
|
|
!runtimeCapabilities.some((cap) => String(cap).trim().toLowerCase() === "inlinebuttons")
|
|
|
|
|
) {
|
|
|
|
|
runtimeCapabilities.push("inlineButtons");
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
}
|
2026-01-25 03:20:09 +00:00
|
|
|
const reactionGuidance =
|
|
|
|
|
runtimeChannel && params.config
|
|
|
|
|
? (() => {
|
|
|
|
|
if (runtimeChannel === "telegram") {
|
|
|
|
|
const resolved = resolveTelegramReactionLevel({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
accountId: params.agentAccountId ?? undefined,
|
|
|
|
|
});
|
|
|
|
|
const level = resolved.agentReactionGuidance;
|
|
|
|
|
return level ? { level, channel: "Telegram" } : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (runtimeChannel === "signal") {
|
|
|
|
|
const resolved = resolveSignalReactionLevel({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
accountId: params.agentAccountId ?? undefined,
|
|
|
|
|
});
|
|
|
|
|
const level = resolved.agentReactionGuidance;
|
|
|
|
|
return level ? { level, channel: "Signal" } : undefined;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
})()
|
|
|
|
|
: undefined;
|
2026-01-24 19:09:24 -03:00
|
|
|
// Resolve channel-specific message actions for system prompt
|
|
|
|
|
const channelActions = runtimeChannel
|
|
|
|
|
? listChannelSupportedActions({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
channel: runtimeChannel,
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
|
|
|
|
const messageToolHints = runtimeChannel
|
|
|
|
|
? resolveChannelMessageToolHints({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
channel: runtimeChannel,
|
|
|
|
|
accountId: params.agentAccountId,
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
const runtimeInfo = {
|
|
|
|
|
host: machineName,
|
|
|
|
|
os: `${os.type()} ${os.release()}`,
|
|
|
|
|
arch: os.arch(),
|
|
|
|
|
node: process.version,
|
|
|
|
|
model: `${provider}/${modelId}`,
|
2026-02-07 11:32:31 -06:00
|
|
|
shell: detectRuntimeShell(),
|
2026-01-24 19:09:24 -03:00
|
|
|
channel: runtimeChannel,
|
|
|
|
|
capabilities: runtimeCapabilities,
|
|
|
|
|
channelActions,
|
|
|
|
|
};
|
|
|
|
|
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
|
|
|
|
|
const reasoningTagHint = isReasoningTagProvider(provider);
|
|
|
|
|
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
|
|
|
|
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
|
|
|
|
|
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
|
|
|
|
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
|
|
|
|
sessionKey: params.sessionKey,
|
|
|
|
|
config: params.config,
|
|
|
|
|
});
|
|
|
|
|
const isDefaultAgent = sessionAgentId === defaultAgentId;
|
Agents: add nested subagent orchestration controls and reduce subagent token waste (#14447)
* Agents: add subagent orchestration controls
* Agents: add subagent orchestration controls (WIP uncommitted changes)
* feat(subagents): add depth-based spawn gating for sub-sub-agents
* feat(subagents): tool policy, registry, and announce chain for nested agents
* feat(subagents): system prompt, docs, changelog for nested sub-agents
* fix(subagents): prevent model fallback override, show model during active runs, and block context overflow fallback
Bug 1: When a session has an explicit model override (e.g., gpt/openai-codex),
the fallback candidate logic in resolveFallbackCandidates silently appended the
global primary model (opus) as a backstop. On reinjection/steer with a transient
error, the session could fall back to opus which has a smaller context window
and crash. Fix: when storedModelOverride is set, pass fallbacksOverride ?? []
instead of undefined, preventing the implicit primary backstop.
Bug 2: Active subagents showed 'model n/a' in /subagents list because
resolveModelDisplay only read entry.model/modelProvider (populated after run
completes). Fix: fall back to modelOverride/providerOverride fields which are
populated at spawn time via sessions.patch.
Bug 3: Context overflow errors (prompt too long, context_length_exceeded) could
theoretically escape runEmbeddedPiAgent and be treated as failover candidates
in runWithModelFallback, causing a switch to a model with a smaller context
window. Fix: in runWithModelFallback, detect context overflow errors via
isLikelyContextOverflowError and rethrow them immediately instead of trying the
next model candidate.
* fix(subagents): track spawn depth in session store and fix announce routing for nested agents
* Fix compaction status tracking and dedupe overflow compaction triggers
* fix(subagents): enforce depth block via session store and implement cascade kill
* fix: inject group chat context into system prompt
* fix(subagents): always write model to session store at spawn time
* Preserve spawnDepth when agent handler rewrites session entry
* fix(subagents): suppress announce on steer-restart
* fix(subagents): fallback spawned session model to runtime default
* fix(subagents): enforce spawn depth when caller key resolves by sessionId
* feat(subagents): implement active-first ordering for numeric targets and enhance task display
- Added a test to verify that subagents with numeric targets follow an active-first list ordering.
- Updated `resolveSubagentTarget` to sort subagent runs based on active status and recent activity.
- Enhanced task display in command responses to prevent truncation of long task descriptions.
- Introduced new utility functions for compacting task text and managing subagent run states.
* fix(subagents): show model for active runs via run record fallback
When the spawned model matches the agent's default model, the session
store's override fields are intentionally cleared (isDefault: true).
The model/modelProvider fields are only populated after the run
completes. This left active subagents showing 'model n/a'.
Fix: store the resolved model on SubagentRunRecord at registration
time, and use it as a fallback in both display paths (subagents tool
and /subagents command) when the session store entry has no model info.
Changes:
- SubagentRunRecord: add optional model field
- registerSubagentRun: accept and persist model param
- sessions-spawn-tool: pass resolvedModel to registerSubagentRun
- subagents-tool: pass run record model as fallback to resolveModelDisplay
- commands-subagents: pass run record model as fallback to resolveModelDisplay
* feat(chat): implement session key resolution and reset on sidebar navigation
- Added functions to resolve the main session key and reset chat state when switching sessions from the sidebar.
- Updated the `renderTab` function to handle session key changes when navigating to the chat tab.
- Introduced a test to verify that the session resets to "main" when opening chat from the sidebar navigation.
* fix: subagent timeout=0 passthrough and fallback prompt duplication
Bug 1: runTimeoutSeconds=0 now means 'no timeout' instead of applying 600s default
- sessions-spawn-tool: default to undefined (not 0) when neither timeout param
is provided; use != null check so explicit 0 passes through to gateway
- agent.ts: accept 0 as valid timeout (resolveAgentTimeoutMs already handles
0 → MAX_SAFE_TIMEOUT_MS)
Bug 2: model fallback no longer re-injects the original prompt as a duplicate
- agent.ts: track fallback attempt index; on retries use a short continuation
message instead of the full original prompt since the session file already
contains it from the first attempt
- Also skip re-sending images on fallback retries (already in session)
* feat(subagents): truncate long task descriptions in subagents command output
- Introduced a new utility function to format task previews, limiting their length to improve readability.
- Updated the command handler to use the new formatting function, ensuring task descriptions are truncated appropriately.
- Adjusted related tests to verify that long task descriptions are now truncated in the output.
* refactor(subagents): update subagent registry path resolution and improve command output formatting
- Replaced direct import of STATE_DIR with a utility function to resolve the state directory dynamically.
- Enhanced the formatting of command output for active and recent subagents, adding separators for better readability.
- Updated related tests to reflect changes in command output structure.
* fix(subagent): default sessions_spawn to no timeout when runTimeoutSeconds omitted
The previous fix (75a791106) correctly handled the case where
runTimeoutSeconds was explicitly set to 0 ("no timeout"). However,
when models omit the parameter entirely (which is common since the
schema marks it as optional), runTimeoutSeconds resolved to undefined.
undefined flowed through the chain as:
sessions_spawn → timeout: undefined (since undefined != null is false)
→ gateway agent handler → agentCommand opts.timeout: undefined
→ resolveAgentTimeoutMs({ overrideSeconds: undefined })
→ DEFAULT_AGENT_TIMEOUT_SECONDS (600s = 10 minutes)
This caused subagents to be killed at exactly 10 minutes even though
the user's intent (via TOOLS.md) was for subagents to run without a
timeout.
Fix: default runTimeoutSeconds to 0 (no timeout) when neither
runTimeoutSeconds nor timeoutSeconds is provided by the caller.
Subagent spawns are long-running by design and should not inherit the
600s agent-command default timeout.
* fix(subagent): accept timeout=0 in agent-via-gateway path (second 600s default)
* fix: thread timeout override through getReplyFromConfig dispatch path
getReplyFromConfig called resolveAgentTimeoutMs({ cfg }) with no override,
always falling back to the config default (600s). Add timeoutOverrideSeconds
to GetReplyOptions and pass it through as overrideSeconds so callers of the
dispatch chain can specify a custom timeout (0 = no timeout).
This complements the existing timeout threading in agentCommand and the
cron isolated-agent runner, which already pass overrideSeconds correctly.
* feat(model-fallback): normalize OpenAI Codex model references and enhance fallback handling
- Added normalization for OpenAI Codex model references, specifically converting "gpt-5.3-codex" to "openai-codex" before execution.
- Updated the `resolveFallbackCandidates` function to utilize the new normalization logic.
- Enhanced tests to verify the correct behavior of model normalization and fallback mechanisms.
- Introduced a new test case to ensure that the normalization process works as expected for various input formats.
* feat(tests): add unit tests for steer failure behavior in openclaw-tools
- Introduced a new test file to validate the behavior of subagents when steer replacement dispatch fails.
- Implemented tests to ensure that the announce behavior is restored correctly and that the suppression reason is cleared as expected.
- Enhanced the subagent registry with a new function to clear steer restart suppression.
- Updated related components to support the new test scenarios.
* fix(subagents): replace stop command with kill in slash commands and documentation
- Updated the `/subagents` command to replace `stop` with `kill` for consistency in controlling sub-agent runs.
- Modified related documentation to reflect the change in command usage.
- Removed legacy timeoutSeconds references from the sessions-spawn-tool schema and tests to streamline timeout handling.
- Enhanced tests to ensure correct behavior of the updated commands and their interactions.
* feat(tests): add unit tests for readLatestAssistantReply function
- Introduced a new test file for the `readLatestAssistantReply` function to validate its behavior with various message scenarios.
- Implemented tests to ensure the function correctly retrieves the latest assistant message and handles cases where the latest message has no text.
- Mocked the gateway call to simulate different message histories for comprehensive testing.
* feat(tests): enhance subagent kill-all cascade tests and announce formatting
- Added a new test to verify that the `kill-all` command cascades through ended parents to active descendants in subagents.
- Updated the subagent announce formatting tests to reflect changes in message structure, including the replacement of "Findings:" with "Result:" and the addition of new expectations for message content.
- Improved the handling of long findings and stats in the announce formatting logic to ensure concise output.
- Refactored related functions to enhance clarity and maintainability in the subagent registry and tools.
* refactor(subagent): update announce formatting and remove unused constants
- Modified the subagent announce formatting to replace "Findings:" with "Result:" and adjusted related expectations in tests.
- Removed constants for maximum announce findings characters and summary words, simplifying the announcement logic.
- Updated the handling of findings to retain full content instead of truncating, ensuring more informative outputs.
- Cleaned up unused imports in the commands-subagents file to enhance code clarity.
* feat(tests): enhance billing error handling in user-facing text
- Added tests to ensure that normal text mentioning billing plans is not rewritten, preserving user context.
- Updated the `isBillingErrorMessage` and `sanitizeUserFacingText` functions to improve handling of billing-related messages.
- Introduced new test cases for various scenarios involving billing messages to ensure accurate processing and output.
- Enhanced the subagent announce flow to correctly manage active descendant runs, preventing premature announcements.
* feat(subagent): enhance workflow guidance and auto-announcement clarity
- Added a new guideline in the subagent system prompt to emphasize trust in push-based completion, discouraging busy polling for status updates.
- Updated documentation to clarify that sub-agents will automatically announce their results, improving user understanding of the workflow.
- Enhanced tests to verify the new guidance on avoiding polling loops and to ensure the accuracy of the updated prompts.
* fix(cron): avoid announcing interim subagent spawn acks
* chore: clean post-rebase imports
* fix(cron): fall back to child replies when parent stays interim
* fix(subagents): make active-run guidance advisory
* fix(subagents): update announce flow to handle active descendants and enhance test coverage
- Modified the announce flow to defer announcements when active descendant runs are present, ensuring accurate status reporting.
- Updated tests to verify the new behavior, including scenarios where no fallback requester is available and ensuring proper handling of finished subagents.
- Enhanced the announce formatting to include an `expectFinal` flag for better clarity in the announcement process.
* fix(subagents): enhance announce flow and formatting for user updates
- Updated the announce flow to provide clearer instructions for user updates based on active subagent runs and requester context.
- Refactored the announcement logic to improve clarity and ensure internal context remains private.
- Enhanced tests to verify the new message expectations and formatting, including updated prompts for user-facing updates.
- Introduced a new function to build reply instructions based on session context, improving the overall announcement process.
* fix: resolve prep blockers and changelog placement (#14447) (thanks @tyler6204)
* fix: restore cron delivery-plan import after rebase (#14447) (thanks @tyler6204)
* fix: resolve test failures from rebase conflicts (#14447) (thanks @tyler6204)
* fix: apply formatting after rebase (#14447) (thanks @tyler6204)
2026-02-14 22:03:45 -08:00
|
|
|
const promptMode =
|
|
|
|
|
isSubagentSessionKey(params.sessionKey) || isCronSessionKey(params.sessionKey)
|
|
|
|
|
? "minimal"
|
|
|
|
|
: "full";
|
2026-01-30 03:15:10 +01:00
|
|
|
const docsPath = await resolveOpenClawDocsPath({
|
2026-01-24 19:09:24 -03:00
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
argv1: process.argv[1],
|
|
|
|
|
cwd: process.cwd(),
|
|
|
|
|
moduleUrl: import.meta.url,
|
|
|
|
|
});
|
|
|
|
|
const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
|
2026-02-22 09:34:48 +01:00
|
|
|
const ownerDisplay = resolveOwnerDisplaySetting(params.config);
|
2026-01-24 19:09:24 -03:00
|
|
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
defaultThinkLevel: params.thinkLevel,
|
|
|
|
|
reasoningLevel: params.reasoningLevel ?? "off",
|
|
|
|
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
|
|
|
ownerNumbers: params.ownerNumbers,
|
2026-02-22 09:34:48 +01:00
|
|
|
ownerDisplay: ownerDisplay.ownerDisplay,
|
|
|
|
|
ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
|
2026-01-24 19:09:24 -03:00
|
|
|
reasoningTagHint,
|
|
|
|
|
heartbeatPrompt: isDefaultAgent
|
|
|
|
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
|
|
|
|
: undefined,
|
|
|
|
|
skillsPrompt,
|
|
|
|
|
docsPath: docsPath ?? undefined,
|
|
|
|
|
ttsHint,
|
|
|
|
|
promptMode,
|
2026-02-26 11:00:09 +01:00
|
|
|
acpEnabled: params.config?.acp?.enabled !== false,
|
2026-01-24 19:09:24 -03:00
|
|
|
runtimeInfo,
|
2026-01-25 03:20:09 +00:00
|
|
|
reactionGuidance,
|
2026-01-24 19:09:24 -03:00
|
|
|
messageToolHints,
|
|
|
|
|
sandboxInfo,
|
|
|
|
|
tools,
|
|
|
|
|
modelAliasLines: buildModelAliasLines(params.config),
|
|
|
|
|
userTimezone,
|
|
|
|
|
userTime,
|
|
|
|
|
userTimeFormat,
|
|
|
|
|
contextFiles,
|
2026-01-27 21:57:15 -08:00
|
|
|
memoryCitationsMode: params.config?.memory?.citations,
|
2026-01-24 19:09:24 -03:00
|
|
|
});
|
2026-02-01 15:06:42 -08:00
|
|
|
const systemPromptOverride = createSystemPromptOverride(appendPrompt);
|
2026-01-24 19:09:24 -03:00
|
|
|
|
|
|
|
|
const sessionLock = await acquireSessionWriteLock({
|
|
|
|
|
sessionFile: params.sessionFile,
|
2026-02-17 03:09:56 +01:00
|
|
|
maxHoldMs: resolveSessionLockMaxHoldFromTimeout({
|
|
|
|
|
timeoutMs: EMBEDDED_COMPACTION_TIMEOUT_MS,
|
|
|
|
|
}),
|
2026-01-24 19:09:24 -03:00
|
|
|
});
|
|
|
|
|
try {
|
2026-02-03 05:17:42 +08:00
|
|
|
await repairSessionFileIfNeeded({
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
warn: (message) => log.warn(message),
|
|
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
await prewarmSessionFile(params.sessionFile);
|
|
|
|
|
const transcriptPolicy = resolveTranscriptPolicy({
|
|
|
|
|
modelApi: model.api,
|
|
|
|
|
provider,
|
|
|
|
|
modelId,
|
|
|
|
|
});
|
|
|
|
|
const sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), {
|
|
|
|
|
agentId: sessionAgentId,
|
|
|
|
|
sessionKey: params.sessionKey,
|
|
|
|
|
allowSyntheticToolResults: transcriptPolicy.allowSyntheticToolResults,
|
2026-02-21 23:06:44 -08:00
|
|
|
allowedToolNames,
|
2026-01-14 01:08:15 +00:00
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
trackSessionManagerAccess(params.sessionFile);
|
2026-02-26 21:46:24 +01:00
|
|
|
const settingsManager = createPreparedEmbeddedPiSettingsManager({
|
|
|
|
|
cwd: effectiveWorkspace,
|
|
|
|
|
agentDir,
|
2026-02-19 21:41:09 -06:00
|
|
|
cfg: params.config,
|
2026-01-24 19:09:24 -03:00
|
|
|
});
|
2026-02-20 23:21:09 -04:00
|
|
|
// Sets compaction/pruning runtime state and returns extension factories
|
|
|
|
|
// that must be passed to the resource loader for the safeguard to be active.
|
|
|
|
|
const extensionFactories = buildEmbeddedExtensionFactories({
|
2026-01-24 19:09:24 -03:00
|
|
|
cfg: params.config,
|
|
|
|
|
sessionManager,
|
|
|
|
|
provider,
|
|
|
|
|
modelId,
|
|
|
|
|
model,
|
2026-01-14 01:08:15 +00:00
|
|
|
});
|
2026-02-20 23:21:09 -04:00
|
|
|
// Only create an explicit resource loader when there are extension factories
|
|
|
|
|
// to register; otherwise let createAgentSession use its built-in default.
|
|
|
|
|
let resourceLoader: DefaultResourceLoader | undefined;
|
|
|
|
|
if (extensionFactories.length > 0) {
|
|
|
|
|
resourceLoader = new DefaultResourceLoader({
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
agentDir,
|
|
|
|
|
settingsManager,
|
|
|
|
|
extensionFactories,
|
|
|
|
|
});
|
|
|
|
|
await resourceLoader.reload();
|
|
|
|
|
}
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-01-24 19:09:24 -03:00
|
|
|
const { builtInTools, customTools } = splitSdkTools({
|
|
|
|
|
tools,
|
|
|
|
|
sandboxEnabled: !!sandbox?.enabled,
|
|
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-01-31 06:22:19 +00:00
|
|
|
const { session } = await createAgentSession({
|
2026-03-05 22:08:26 -05:00
|
|
|
cwd: effectiveWorkspace,
|
2026-01-24 19:09:24 -03:00
|
|
|
agentDir,
|
|
|
|
|
authStorage,
|
|
|
|
|
modelRegistry,
|
2026-03-07 19:18:48 +00:00
|
|
|
model: effectiveModel,
|
2026-01-24 19:09:24 -03:00
|
|
|
thinkingLevel: mapThinkingLevel(params.thinkLevel),
|
|
|
|
|
tools: builtInTools,
|
|
|
|
|
customTools,
|
|
|
|
|
sessionManager,
|
|
|
|
|
settingsManager,
|
2026-02-20 23:21:09 -04:00
|
|
|
resourceLoader,
|
2026-01-31 06:22:19 +00:00
|
|
|
});
|
2026-02-02 02:04:50 -08:00
|
|
|
applySystemPromptOverrideToSession(session, systemPromptOverride());
|
2026-03-07 20:40:34 -05:00
|
|
|
if (model.api === "ollama") {
|
|
|
|
|
const providerBaseUrl =
|
|
|
|
|
typeof params.config?.models?.providers?.[model.provider]?.baseUrl === "string"
|
|
|
|
|
? params.config.models.providers[model.provider]?.baseUrl
|
|
|
|
|
: undefined;
|
|
|
|
|
ensureCustomApiRegistered(
|
|
|
|
|
model.api,
|
|
|
|
|
createConfiguredOllamaStreamFn({
|
|
|
|
|
model,
|
|
|
|
|
providerBaseUrl,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const prior = await sanitizeSessionHistory({
|
|
|
|
|
messages: session.messages,
|
|
|
|
|
modelApi: model.api,
|
2026-01-14 01:08:15 +00:00
|
|
|
modelId,
|
2026-01-24 19:09:24 -03:00
|
|
|
provider,
|
2026-02-21 23:06:44 -08:00
|
|
|
allowedToolNames,
|
2026-02-18 00:43:31 +01:00
|
|
|
config: params.config,
|
2026-01-24 19:09:24 -03:00
|
|
|
sessionManager,
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
policy: transcriptPolicy,
|
2026-01-14 01:08:15 +00:00
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
const validatedGemini = transcriptPolicy.validateGeminiTurns
|
|
|
|
|
? validateGeminiTurns(prior)
|
|
|
|
|
: prior;
|
|
|
|
|
const validated = transcriptPolicy.validateAnthropicTurns
|
|
|
|
|
? validateAnthropicTurns(validatedGemini)
|
|
|
|
|
: validatedGemini;
|
2026-03-05 22:08:26 -05:00
|
|
|
// Apply validated transcript to the live session even when no history limit is configured,
|
|
|
|
|
// so compaction and hook metrics are based on the same message set.
|
|
|
|
|
session.agent.replaceMessages(validated);
|
|
|
|
|
// "Original" compaction metrics should describe the validated transcript that enters
|
|
|
|
|
// limiting/compaction, not the raw on-disk session snapshot.
|
|
|
|
|
const originalMessages = session.messages.slice();
|
2026-02-12 07:42:05 +08:00
|
|
|
const truncated = limitHistoryTurns(
|
2026-03-05 22:08:26 -05:00
|
|
|
session.messages,
|
2026-01-24 19:09:24 -03:00
|
|
|
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),
|
2026-01-14 01:08:15 +00:00
|
|
|
);
|
2026-02-12 07:42:05 +08:00
|
|
|
// Re-run tool_use/tool_result pairing repair after truncation, since
|
|
|
|
|
// limitHistoryTurns can orphan tool_result blocks by removing the
|
|
|
|
|
// assistant message that contained the matching tool_use.
|
|
|
|
|
const limited = transcriptPolicy.repairToolUseResultPairing
|
|
|
|
|
? sanitizeToolUseResultPairing(truncated)
|
|
|
|
|
: truncated;
|
2026-01-24 19:09:24 -03:00
|
|
|
if (limited.length > 0) {
|
|
|
|
|
session.agent.replaceMessages(limited);
|
2026-01-16 20:16:35 +00:00
|
|
|
}
|
2026-03-05 22:08:26 -05:00
|
|
|
const missingSessionKey = !params.sessionKey || !params.sessionKey.trim();
|
|
|
|
|
const hookSessionKey = params.sessionKey?.trim() || params.sessionId;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
const hookRunner = getGlobalHookRunner();
|
2026-03-05 22:08:26 -05:00
|
|
|
const messageCountOriginal = originalMessages.length;
|
|
|
|
|
let tokenCountOriginal: number | undefined;
|
|
|
|
|
try {
|
|
|
|
|
tokenCountOriginal = 0;
|
|
|
|
|
for (const message of originalMessages) {
|
|
|
|
|
tokenCountOriginal += estimateTokens(message);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
tokenCountOriginal = undefined;
|
|
|
|
|
}
|
|
|
|
|
const messageCountBefore = session.messages.length;
|
|
|
|
|
let tokenCountBefore: number | undefined;
|
|
|
|
|
try {
|
|
|
|
|
tokenCountBefore = 0;
|
|
|
|
|
for (const message of session.messages) {
|
|
|
|
|
tokenCountBefore += estimateTokens(message);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
tokenCountBefore = undefined;
|
|
|
|
|
}
|
|
|
|
|
// TODO(#7175): Consider exposing full message snapshots or pre-compaction injection
|
|
|
|
|
// hooks; current events only report counts/metadata.
|
|
|
|
|
try {
|
|
|
|
|
const hookEvent = createInternalHookEvent("session", "compact:before", hookSessionKey, {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
missingSessionKey,
|
|
|
|
|
messageCount: messageCountBefore,
|
|
|
|
|
tokenCount: tokenCountBefore,
|
|
|
|
|
messageCountOriginal,
|
|
|
|
|
tokenCountOriginal,
|
|
|
|
|
});
|
|
|
|
|
await triggerInternalHook(hookEvent);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("session:compact:before hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
errorStack: err instanceof Error ? err.stack : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
if (hookRunner?.hasHooks("before_compaction")) {
|
2026-03-05 22:08:26 -05:00
|
|
|
try {
|
|
|
|
|
await hookRunner.runBeforeCompaction(
|
|
|
|
|
{
|
|
|
|
|
messageCount: messageCountBefore,
|
|
|
|
|
tokenCount: tokenCountBefore,
|
|
|
|
|
},
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
{
|
2026-03-05 22:08:26 -05:00
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
agentId: sessionAgentId,
|
|
|
|
|
sessionKey: hookSessionKey,
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
messageProvider: resolvedMessageProvider,
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
},
|
2026-03-05 22:08:26 -05:00
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("before_compaction hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
errorStack: err instanceof Error ? err.stack : undefined,
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
});
|
2026-03-05 22:08:26 -05:00
|
|
|
}
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
}
|
2026-02-13 19:54:22 -04:00
|
|
|
const diagEnabled = log.isEnabled("debug");
|
|
|
|
|
const preMetrics = diagEnabled ? summarizeCompactionMessages(session.messages) : undefined;
|
|
|
|
|
if (diagEnabled && preMetrics) {
|
|
|
|
|
log.debug(
|
|
|
|
|
`[compaction-diag] start runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
|
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
|
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} ` +
|
|
|
|
|
`pre.messages=${preMetrics.messages} pre.historyTextChars=${preMetrics.historyTextChars} ` +
|
|
|
|
|
`pre.toolResultChars=${preMetrics.toolResultChars} pre.estTokens=${preMetrics.estTokens ?? "unknown"}`,
|
|
|
|
|
);
|
|
|
|
|
log.debug(
|
|
|
|
|
`[compaction-diag] contributors diagId=${diagId} top=${JSON.stringify(preMetrics.contributors)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 03:40:25 +08:00
|
|
|
if (!session.messages.some(hasRealConversationContent)) {
|
|
|
|
|
log.info(
|
|
|
|
|
`[compaction] skipping — no real conversation messages (sessionKey=${params.sessionKey ?? params.sessionId})`,
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
compacted: false,
|
|
|
|
|
reason: "no real conversation messages",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 19:54:22 -04:00
|
|
|
const compactStartedAt = Date.now();
|
2026-03-05 22:08:26 -05:00
|
|
|
// Measure compactedCount from the original pre-limiting transcript so compaction
|
|
|
|
|
// lifecycle metrics represent total reduction through the compaction pipeline.
|
|
|
|
|
const messageCountCompactionInput = messageCountOriginal;
|
2026-02-15 06:54:12 +08:00
|
|
|
const result = await compactWithSafetyTimeout(() =>
|
|
|
|
|
session.compact(params.customInstructions),
|
|
|
|
|
);
|
2026-01-24 19:09:24 -03:00
|
|
|
// Estimate tokens after compaction by summing token estimates for remaining messages
|
|
|
|
|
let tokensAfter: number | undefined;
|
2026-01-14 01:08:15 +00:00
|
|
|
try {
|
2026-01-24 19:09:24 -03:00
|
|
|
tokensAfter = 0;
|
|
|
|
|
for (const message of session.messages) {
|
|
|
|
|
tokensAfter += estimateTokens(message);
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
// Sanity check: tokensAfter should be less than tokensBefore
|
|
|
|
|
if (tokensAfter > result.tokensBefore) {
|
|
|
|
|
tokensAfter = undefined; // Don't trust the estimate
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// If estimation fails, leave tokensAfter undefined
|
|
|
|
|
tokensAfter = undefined;
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
2026-03-05 22:08:26 -05:00
|
|
|
const messageCountAfter = session.messages.length;
|
|
|
|
|
const compactedCount = Math.max(0, messageCountCompactionInput - messageCountAfter);
|
2026-02-13 19:54:22 -04:00
|
|
|
const postMetrics = diagEnabled ? summarizeCompactionMessages(session.messages) : undefined;
|
|
|
|
|
if (diagEnabled && preMetrics && postMetrics) {
|
|
|
|
|
log.debug(
|
|
|
|
|
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
|
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
|
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=compacted reason=none ` +
|
|
|
|
|
`durationMs=${Date.now() - compactStartedAt} retrying=false ` +
|
|
|
|
|
`post.messages=${postMetrics.messages} post.historyTextChars=${postMetrics.historyTextChars} ` +
|
|
|
|
|
`post.toolResultChars=${postMetrics.toolResultChars} post.estTokens=${postMetrics.estTokens ?? "unknown"} ` +
|
|
|
|
|
`delta.messages=${postMetrics.messages - preMetrics.messages} ` +
|
|
|
|
|
`delta.historyTextChars=${postMetrics.historyTextChars - preMetrics.historyTextChars} ` +
|
|
|
|
|
`delta.toolResultChars=${postMetrics.toolResultChars - preMetrics.toolResultChars} ` +
|
|
|
|
|
`delta.estTokens=${typeof preMetrics.estTokens === "number" && typeof postMetrics.estTokens === "number" ? postMetrics.estTokens - preMetrics.estTokens : "unknown"}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-05 22:08:26 -05:00
|
|
|
// TODO(#9611): Consider exposing compaction summaries or post-compaction injection;
|
|
|
|
|
// current events only report summary metadata.
|
|
|
|
|
try {
|
|
|
|
|
const hookEvent = createInternalHookEvent("session", "compact:after", hookSessionKey, {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
missingSessionKey,
|
|
|
|
|
messageCount: messageCountAfter,
|
|
|
|
|
tokenCount: tokensAfter,
|
|
|
|
|
compactedCount,
|
|
|
|
|
summaryLength: typeof result.summary === "string" ? result.summary.length : undefined,
|
|
|
|
|
tokensBefore: result.tokensBefore,
|
|
|
|
|
tokensAfter,
|
|
|
|
|
firstKeptEntryId: result.firstKeptEntryId,
|
|
|
|
|
});
|
|
|
|
|
await triggerInternalHook(hookEvent);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("session:compact:after hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
errorStack: err instanceof Error ? err.stack : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (hookRunner?.hasHooks("after_compaction")) {
|
|
|
|
|
try {
|
|
|
|
|
await hookRunner.runAfterCompaction(
|
|
|
|
|
{
|
|
|
|
|
messageCount: messageCountAfter,
|
|
|
|
|
tokenCount: tokensAfter,
|
|
|
|
|
compactedCount,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
agentId: sessionAgentId,
|
|
|
|
|
sessionKey: hookSessionKey,
|
|
|
|
|
workspaceDir: effectiveWorkspace,
|
|
|
|
|
messageProvider: resolvedMessageProvider,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("after_compaction hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
errorStack: err instanceof Error ? err.stack : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-14 01:08:15 +00:00
|
|
|
return {
|
2026-01-24 19:09:24 -03:00
|
|
|
ok: true,
|
|
|
|
|
compacted: true,
|
|
|
|
|
result: {
|
|
|
|
|
summary: result.summary,
|
|
|
|
|
firstKeptEntryId: result.firstKeptEntryId,
|
|
|
|
|
tokensBefore: result.tokensBefore,
|
|
|
|
|
tokensAfter,
|
|
|
|
|
details: result.details,
|
|
|
|
|
},
|
2026-01-14 01:08:15 +00:00
|
|
|
};
|
|
|
|
|
} finally {
|
2026-02-14 06:35:43 +11:00
|
|
|
await flushPendingToolResultsAfterIdle({
|
|
|
|
|
agent: session?.agent,
|
|
|
|
|
sessionManager,
|
2026-03-05 19:29:18 -08:00
|
|
|
clearPendingOnTimeout: true,
|
2026-02-14 06:35:43 +11:00
|
|
|
});
|
2026-01-24 19:09:24 -03:00
|
|
|
session.dispose();
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
2026-01-24 19:09:24 -03:00
|
|
|
} finally {
|
|
|
|
|
await sessionLock.release();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-02-13 19:54:22 -04:00
|
|
|
const reason = describeUnknownError(err);
|
2026-02-15 19:09:05 +00:00
|
|
|
return fail(reason);
|
2026-01-24 19:09:24 -03:00
|
|
|
} finally {
|
|
|
|
|
restoreSkillEnv?.();
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compacts a session with lane queueing (session lane + global lane).
|
|
|
|
|
* Use this from outside a lane context. If already inside a lane, use
|
|
|
|
|
* `compactEmbeddedPiSessionDirect` to avoid deadlocks.
|
|
|
|
|
*/
|
|
|
|
|
export async function compactEmbeddedPiSession(
|
|
|
|
|
params: CompactEmbeddedPiSessionParams,
|
|
|
|
|
): Promise<EmbeddedPiCompactResult> {
|
|
|
|
|
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
|
|
|
|
|
const globalLane = resolveGlobalLane(params.lane);
|
|
|
|
|
const enqueueGlobal =
|
|
|
|
|
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
|
|
|
|
|
return enqueueCommandInLane(sessionLane, () =>
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
enqueueGlobal(async () => {
|
2026-03-08 23:38:14 +00:00
|
|
|
ensureRuntimePluginsLoaded({
|
|
|
|
|
config: params.config,
|
|
|
|
|
workspaceDir: params.workspaceDir,
|
|
|
|
|
});
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
ensureContextEnginesInitialized();
|
|
|
|
|
const contextEngine = await resolveContextEngine(params.config);
|
|
|
|
|
try {
|
|
|
|
|
// Resolve token budget from model context window so the context engine
|
|
|
|
|
// knows the compaction target. The runner's afterTurn path passes this
|
|
|
|
|
// automatically, but the /compact command path needs to compute it here.
|
|
|
|
|
const ceProvider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
|
|
|
|
const ceModelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
|
|
|
|
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
|
|
|
|
|
const { model: ceModel } = resolveModel(ceProvider, ceModelId, agentDir, params.config);
|
|
|
|
|
const ceCtxInfo = resolveContextWindowInfo({
|
|
|
|
|
cfg: params.config,
|
|
|
|
|
provider: ceProvider,
|
|
|
|
|
modelId: ceModelId,
|
|
|
|
|
modelContextWindow: ceModel?.contextWindow,
|
|
|
|
|
defaultTokens: DEFAULT_CONTEXT_TOKENS,
|
|
|
|
|
});
|
2026-03-12 13:19:20 +10:00
|
|
|
// When the context engine owns compaction, its compact() implementation
|
|
|
|
|
// bypasses compactEmbeddedPiSessionDirect (which fires the hooks internally).
|
|
|
|
|
// Fire before_compaction / after_compaction hooks here so plugin subscribers
|
|
|
|
|
// are notified regardless of which engine is active.
|
|
|
|
|
const engineOwnsCompaction = contextEngine.info.ownsCompaction === true;
|
|
|
|
|
const hookRunner = engineOwnsCompaction ? getGlobalHookRunner() : null;
|
|
|
|
|
const hookSessionKey = params.sessionKey?.trim() || params.sessionId;
|
|
|
|
|
const { sessionAgentId } = resolveSessionAgentIds({
|
|
|
|
|
sessionKey: params.sessionKey,
|
|
|
|
|
config: params.config,
|
|
|
|
|
});
|
|
|
|
|
const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
|
|
|
|
|
const hookCtx = {
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
agentId: sessionAgentId,
|
|
|
|
|
sessionKey: hookSessionKey,
|
|
|
|
|
workspaceDir: resolveUserPath(params.workspaceDir),
|
|
|
|
|
messageProvider: resolvedMessageProvider,
|
|
|
|
|
};
|
|
|
|
|
// Engine-owned compaction doesn't load the transcript at this level, so
|
|
|
|
|
// message counts are unavailable. We pass sessionFile so hook subscribers
|
|
|
|
|
// can read the transcript themselves if they need exact counts.
|
|
|
|
|
if (hookRunner?.hasHooks("before_compaction")) {
|
|
|
|
|
try {
|
|
|
|
|
await hookRunner.runBeforeCompaction(
|
|
|
|
|
{
|
|
|
|
|
messageCount: -1,
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
},
|
|
|
|
|
hookCtx,
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("before_compaction hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
const result = await contextEngine.compact({
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
tokenBudget: ceCtxInfo.tokens,
|
|
|
|
|
customInstructions: params.customInstructions,
|
|
|
|
|
force: params.trigger === "manual",
|
2026-03-08 17:13:18 +00:00
|
|
|
runtimeContext: params as Record<string, unknown>,
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
});
|
2026-03-12 13:19:20 +10:00
|
|
|
if (result.ok && result.compacted && hookRunner?.hasHooks("after_compaction")) {
|
|
|
|
|
try {
|
|
|
|
|
await hookRunner.runAfterCompaction(
|
|
|
|
|
{
|
|
|
|
|
messageCount: -1,
|
|
|
|
|
compactedCount: -1,
|
|
|
|
|
tokenCount: result.result?.tokensAfter,
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
},
|
|
|
|
|
hookCtx,
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
log.warn("after_compaction hook failed", {
|
|
|
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
feature(context): extend plugin system to support custom context management (#22201)
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
2026-03-06 05:31:59 -08:00
|
|
|
return {
|
|
|
|
|
ok: result.ok,
|
|
|
|
|
compacted: result.compacted,
|
|
|
|
|
reason: result.reason,
|
|
|
|
|
result: result.result
|
|
|
|
|
? {
|
|
|
|
|
summary: result.result.summary ?? "",
|
|
|
|
|
firstKeptEntryId: result.result.firstKeptEntryId ?? "",
|
|
|
|
|
tokensBefore: result.result.tokensBefore,
|
|
|
|
|
tokensAfter: result.result.tokensAfter,
|
|
|
|
|
details: result.result.details,
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
await contextEngine.dispose?.();
|
|
|
|
|
}
|
|
|
|
|
}),
|
2026-01-14 01:08:15 +00:00
|
|
|
);
|
|
|
|
|
}
|